├── .github └── workflows │ └── Publish.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README-ZH.md ├── README.md ├── analysis_options.yaml ├── app-profile.apk ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ └── contents.xcworkspacedata │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── animation │ │ └── opacity_animation.dart │ ├── item │ │ └── complex_item.dart │ ├── main.dart │ └── page │ │ ├── complex_common_example.dart │ │ ├── complex_list_example.dart │ │ └── opt │ │ ├── common_opt_example.dart │ │ ├── list_opt_example1.dart │ │ ├── list_opt_example2.dart │ │ ├── list_opt_example3.dart │ │ ├── list_opt_example4.dart │ │ └── list_opt_example5.dart ├── pubspec.yaml └── test │ └── widget_test.dart ├── lib ├── keframe.dart └── src │ ├── frame_separate_task.dart │ ├── frame_separate_widget.dart │ ├── layout_proxy.dart │ ├── logcat.dart │ ├── notification.dart │ └── size_cache_widget.dart ├── pubspec.yaml └── test └── keframe_test.dart /.github/workflows/Publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v1 16 | - name: Publish 17 | uses: sakebook/actions-flutter-pub-publisher@v1.3.0 18 | with: 19 | credential: ${{ secrets.CREDENTIALS }} 20 | flutter_package: true 21 | skip_test: true 22 | dry_run: false 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | pubspec.lock 33 | 34 | # Android related 35 | **/android/**/gradle-wrapper.jar 36 | **/android/.gradle 37 | **/android/captures/ 38 | **/android/gradlew 39 | **/android/gradlew.bat 40 | **/android/local.properties 41 | **/android/**/GeneratedPluginRegistrant.java 42 | 43 | # iOS/XCode related 44 | **/ios/**/*.mode1v3 45 | **/ios/**/*.mode2v3 46 | **/ios/**/*.moved-aside 47 | **/ios/**/*.pbxuser 48 | **/ios/**/*.perspectivev3 49 | **/ios/**/*sync/ 50 | **/ios/**/.sconsign.dblite 51 | **/ios/**/.tags* 52 | **/ios/**/.vagrant/ 53 | **/ios/**/DerivedData/ 54 | **/ios/**/Icon? 55 | **/ios/**/Pods/ 56 | **/ios/**/.symlinks/ 57 | **/ios/**/profile 58 | **/ios/**/xcuserdata 59 | **/ios/.generated/ 60 | **/ios/Flutter/App.framework 61 | **/ios/Flutter/Flutter.framework 62 | **/ios/Flutter/Flutter.podspec 63 | **/ios/Flutter/Generated.xcconfig 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 77 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f139b11009aeb8ed2a3a3aa8b0066e482709dde3 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.1 2 | 3 | * add the `builder` to create a widget when it's need and the `child` is deprecated. 4 | 5 | ## 3.0.0 6 | 7 | * adapted 3.0 8 | 9 | ## 2.0.6 10 | 11 | * format code 12 | 13 | ## 2.0.5 14 | 15 | * add export file 16 | 17 | ## 2.0.4 18 | 19 | * improve code style 20 | 21 | ## 2.0.3 22 | 23 | * remove unnecessary task 24 | 25 | ## 2.0.2 26 | 27 | * fix widget doesn't transform 28 | 29 | ## 2.0.1 30 | 31 | * modify notification usage 32 | 33 | ## 2.0.0 34 | 35 | * adapter null-safe version 36 | 37 | ## 1.0.3 38 | 39 | * remove unnecessary task 40 | 41 | ## 1.0.2 42 | 43 | * fix widget doesn't transform 44 | 45 | ## 1.0.1 46 | 47 | * before null-safe version 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright <2022> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README-ZH.md: -------------------------------------------------------------------------------- 1 | # Flutter 流畅度优化组件 Keframe 2 | 3 | ![image](https://user-images.githubusercontent.com/40540394/123200939-50503d80-d4e4-11eb-8db0-afcc3c32423a.png) 4 | 5 | 6 | + [列表流畅度优化](#列表流畅度优化) 7 | + [页面切换流畅度提升:](#页面切换流畅度提升) 8 | + [如何使用?](#如何使用) 9 | - [项目依赖:](#项目依赖) 10 | - [快速上手](#快速上手) 11 | + [构造函数说明:](#构造函数说明) 12 | + [Example 示例说明:](#Example示例说明) 13 | - [1、列表中实际 item 尺寸已知的情况](#1列表中实际-item-尺寸已知的情况) 14 | - [2、列表中实际 item 高度未知的情况](#2列表中实际-item-高度未知的情况) 15 | - [3、预估一屏 item 的数量](#3预估一屏-item-的数量) 16 | - [4、非列表场景](#4非列表场景) 17 | + [分帧的成本](#分帧的成本) 18 | + [优化前后对比演示](#优化前后对比演示) 19 | + [相关原理分析:](#相关原理分析) 20 | 21 | Language: [English](README.md) | 中文简体 22 | 23 | [![null-safe](https://img.shields.io/badge/nullsafe-2.0.6-brightgreen)](https://pub.dev/packages/keframe) 24 | [![null-safe](https://img.shields.io/badge/normal-1.0.3-brightgreen)](https://pub.dev/packages/keframe) 25 | [![GitHub stars](https://img.shields.io/github/stars/LianjiaTech/keframe)](https://github.com/LianjiaTech/keframe/stargazers) 26 | [![GitHub license](https://img.shields.io/github/license/LianjiaTech/keframe)](https://github.com/LianjiaTech/keframe/blob/master/LICENSE) 27 | 28 | 29 | ### 列表流畅度优化 30 | 31 | 这是一个通用的流畅度优化方案,通过分帧渲染优化由构建导致的卡顿,例如页面切换或者复杂列表快速滚动的场景。 32 | 33 | 代码中 [example](app-profile.apk)(可以直接下载运行) 运行在 VIVO X23(骁龙 660),相同操作下优化前后 200 帧采集数据指标对比(gif 在文章最后): 34 | 35 | | 优化前 | 优化后 | 36 | | --- | --- | 37 | | 优化前 | 优化后 | 38 | 39 | 监控工具来自:[fps_monitor](https://github.com/Nayuta403/fps_monitor),指标详细信息:[页面流畅度不再是谜!调试神器开箱即用,Flutter FPS检测工具](https://juejin.cn/post/6947911434424549384) 40 | 41 | - 流畅:一帧耗时低于 18ms 42 | - 良好:一帧耗时在 18ms-33ms 之间 43 | - 轻微卡顿:一帧耗时在 33ms-67ms 之间 44 | - 卡顿:一帧耗时大于 66.7ms 45 | 46 | 采用分帧优化后,卡顿次数从 **平均 33.3 帧出现了一帧**,降低到 **200 帧中仅出现了一帧**,峰值也**从 188ms 降低到 90ms**。卡顿现象大幅减轻,流畅帧占比显著提升,整体表现更流畅。下方是详细数据。 47 | 48 | | | 优化前 | 优化后 | 49 | | -------------------------- | ------- | ------ | 50 | | 平均多少帧出现一帧卡顿 | 33.3 | 200 | 51 | | 平均多少帧出现一帧轻微卡顿 | 8.6 | 66.7 | 52 | | 最大耗时 | 188.0ms | 90.0ms | 53 | | 平均耗时 | 27.0ms | 19.4ms | 54 | | 流畅帧占比 | 40% | 64.5% | 55 | 56 | **** 57 | 58 | ### 页面切换流畅度提升: 59 | 60 | 在打开一个页面或者 Tab 切换时,系统会渲染整个页面并结合动画完成页面切换。对于复杂页面,同样会出现卡顿掉帧。借助分帧组件,将页面的构建逐帧拆解,通过 DevTools 中的性能工具查看。切换时一帧的峰值由 **112.5ms 降低到 30.2 ms**,整体切换过程更加流畅。 61 | 62 | | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c0ce341f0a2d4fceb0ad123fd4834ce2~tplv-k3u1fbpfcp-watermark.image) | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0c571a755ac84f39b52d57a13856a243~tplv-k3u1fbpfcp-watermark.image) | 63 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 64 | 65 | 三位开发者的实际使用效果 [Keframe 优化实践合集](https://juejin.cn/post/6984606303641403406) 66 | 67 | *** 68 | 69 | ### 如何使用? 70 | 71 | #### 项目依赖: 72 | 73 | 在 `pubspec.yaml` 中添加 `keframe` 依赖 74 | 75 | ```yaml 76 | dependencies: 77 | keframe: version 78 | ``` 79 | 80 | 组件仅区分非空安全与空安全版本 81 | 82 | 非空安全使用: `1.0.2` 83 | 84 | 空安全版本使用: `2.0.6` 85 | 86 | #### 快速上手 87 | 88 | 如下图所示 89 | 90 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83d16f3b2a3e45b79fc73d7a52774696~tplv-k3u1fbpfcp-watermark.image) 91 | 92 | 假如现在页面由 A、B、C、D 四部分组成,每部分耗时 10ms,在页面时构建为 40ms。使用分帧组件 `FrameSeparateWidget` 嵌套每一个部分。页面构建时会在第一帧渲染简单的占位,在后续四帧内分别渲染 A、B、C、D。 93 | 94 | 对于列表,在每一个 item 中嵌套 `FrameSeparateWidget`,并将 `ListView` 嵌套在 `SizeCacheWidget` 内。 95 | 96 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ffecd49bf9ba4379984a22ef79663104~tplv-k3u1fbpfcp-watermark.image) 97 | 98 | *** 99 | 100 | ### 构造函数说明: 101 | 102 | FrameSeparateWidget :分帧组件,将嵌套的 widget 单独一帧渲染 103 | 104 | | 类型 | 参数名 | 是否必填 | 含义 | 105 | | ------ | ----------- | -------- | ------------------------------------------------------------ | 106 | | Key | key | 否 | | 107 | | int | index | 否 | 分帧组件 id,使用 SizeCacheWidget 的场景必传,SizeCacheWidget 中维护了 index 对应的 Size 信息 | 108 | | Widget | child | 是 | 实际需要渲染的 widget | 109 | | Widget | placeHolder | 否 | 占位 widget,尽量设置简单的占位,不传默认是 Container() | 110 | 111 | SizeCacheWidget:缓存子节点中,分帧组件嵌套的**实际 widget 的尺寸信息** 112 | 113 | | 类型 | 参数名 | 是否必填 | 含义 | 114 | | ------ | ------------- | -------- | ------------------------------------------------------ | 115 | | Key | key | 否 | | 116 | | Widget | child | 是 | 子节点中如果包含分帧组件,则缓存**实际的 widget 尺寸** | 117 | | int | estimateCount | 否 | 预估屏幕上子节点的数量,提高快速滚动时的响应速度 | 118 | 119 | *** 120 | 121 | ### Example 示例说明: 122 | 123 | 卡顿的页面往往都是由多个复杂 widget 同时渲染导致。通过为复杂的 widget 嵌套分帧组件 `FrameSeparateWidget`。渲染时,分帧组件会在第一帧同时渲染多个 `palceHolder`,之后连续的多帧内依次渲染复杂子项,以此提升页面流畅度。 124 | 125 | 例如 example 中的优化前示例: 126 | 127 | ```dart 128 | ListView.builder( 129 | itemCount: childCount, 130 | itemBuilder: (c, i) => CellWidget( 131 | color: i % 2 == 0 ? Colors.red : Colors.blue, 132 | index: i, 133 | ), 134 | ) 135 | ``` 136 | 137 | 其中 `CellWidget` 高度为 60,内部嵌套了三个 `TextField` 的组件(整体构建耗时在 9ms 左右)。 138 | 139 | 优化仅需为每一个 item 嵌套分帧组件,并为其设置 `placeHolder`(placeHolder 尽量简单,样式与实际 item 接近即可)。 140 | 141 | 在列表情况下,给 ListView 嵌套 `SizeCacheWidget`,同时建议将预加载范围 `cacheExtent` 设置大一点,例如 500(该属性默认为 250),提升慢速滑动时候的体验。 142 | 143 | 例如: 144 | 145 | ```dart 146 | SizeCacheWidget( 147 | child: ListView.builder( 148 | cacheExtent: 500, 149 | itemCount: childCount, 150 | itemBuilder: (c, i) => FrameSeparateWidget( 151 | index: i, 152 | placeHolder: Container( 153 | color: i % 2 == 0 ? Colors.red : Colors.blue, 154 | height: 60, 155 | ), 156 | child: CellWidget( 157 | color: i % 2 == 0 ? Colors.red : Colors.blue, 158 | index: i, 159 | ), 160 | ), 161 | ), 162 | ), 163 | ``` 164 | 165 | 下面是几种场景说明: 166 | 167 | #### 1、列表中实际 item 尺寸已知的情况 168 | 169 | 实际 item 高度已知的情况下(每个 item 高度为 60),将占位设置与实际 item 高度一致即可,查看 example 中 分帧优化 1。 170 | 171 | ```dart 172 | FrameSeparateWidget( 173 | index: i, 174 | placeHolder: Container( 175 | color: i % 2 == 0 ? Colors.red : Colors.blue, 176 | height: 60,// 与实际 item 高度保持一致 177 | ), 178 | child: CellWidget( 179 | color: i % 2 == 0 ? Colors.red : Colors.blue, 180 | index: i, 181 | ), 182 | ) 183 | ``` 184 | 185 | #### 2、列表中实际 item 高度未知的情况 186 | 187 | 现实场景中,列表往往是根据数据下发展示,无法一开始预知 item 的尺寸。 188 | 189 | 例如,example 中 分帧优化 2, `placeHolder` (高度40)与实际 item (高度60)尺寸不一致, 190 | 由于每一个 item 分在不同帧完成渲染,因此会出现列表「抖动」的情况。 191 | 192 | 这时可以给 placeholder 设置一个近似的高度。并且在将 ListView 嵌套在 SizeCacheWidget 中。对于已渲染过的 widget 会强制设置 `palceHolder` 的尺寸,同时将`cacheExtent`调大。这样在来回滑动过程中,已经渲染过的 item 将不会出现跳动情况。 193 | 194 | 例如,example 中 分帧优化 3 195 | 196 | ```dart 197 | SizeCacheWidget( 198 | child: ListView.builder( 199 | cacheExtent: 500, 200 | itemCount: childCount, 201 | itemBuilder: (c, i) => FrameSeparateWidget( 202 | index: i, 203 | placeHolder: Container( 204 | color: i % 2 == 0 ? Colors.red : Colors.blue, 205 | height: 40, 206 | ), 207 | child: CellWidget( 208 | color: i % 2 == 0 ? Colors.red : Colors.blue, 209 | index: i, 210 | ), 211 | ), 212 | ), 213 | ), 214 | ``` 215 | 216 | 实际效果如下: 217 | 218 | Screenrecording_20210611_194905.gif 219 | 220 | 221 | 222 | #### 3、预估一屏 item 的数量 223 | 224 | 如果能粗略估计一屏能展示的实际 item 的最大数量,例如 10。将 SizeCacheWidget 的 `estimateCount ` 属性设置为 10*2。快速滚动场景构建响应更快,并且内存更稳定。例如,example 中的 分帧优化4 225 | 226 | ```dart 227 | ... 228 | SizeCacheWidget( 229 | estimateCount: 20, 230 | child: ListView.builder( 231 | ... 232 | ``` 233 | 234 | 此外,也可以给 item 嵌套透明度/位移等动画,优化视觉上的效果。 235 | 236 | 效果如下图: 237 | 238 | | ![Screenrecording_20210315_133310.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb7d1361ae7842df954bb1c559e2ec54~tplv-k3u1fbpfcp-watermark.image) | ![Screenrecording_20210315_133848.gif](https://user-images.githubusercontent.com/40540394/123905372-c9e4a180-d9a4-11eb-94d0-4190710828f5.gif) | 239 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 240 | 241 | #### 4、非列表场景 242 | 243 | 对于非列表场景,一般不存在流畅度问题,不过在初次进入的时候任然可能出现卡顿。同样的,可以将复杂的模块分到不同帧渲染,避免初次进入的卡顿。例如,我们将为优化例子中底部的操作区域嵌套分帧组件: 244 | 245 | ```dart 246 | FrameSeparateWidget( 247 | child: operateBar(), 248 | index: -1, 249 | ) 250 | ``` 251 | 252 | **** 253 | 254 | 255 | 256 | ### 分帧的成本 257 | 258 | 当然分帧方案也非十全十美,在我看来主要有两点成本: 259 | 260 | 1、额外的构建开销:整个构建过程的构建消耗由「n * widget消耗 」变成了「n *( widget + 占位)消耗 + 系统调度 n 帧消耗」。可以看出,额外的开销主要由占位的复杂度决定。如果占位只是简单的 Container,测试后发现整体构建耗时大概提升在 15 % 左右。这种额外开销对于当下的移动设备而言,成本几乎可以不计。 261 | 262 | 2、视觉上的变化:如同上面的演示,组件会将 item 分帧渲染,页面在视觉上出现占位变成实际 widget 的过程。但其实由于列表存在缓存区域(建议将缓存区调大),在高端机或正常滑动情况下用户并无感知。而在中低端设备上快速滑动能感觉到切换的过程,但比严重顿挫要好。 263 | 264 | *** 265 | ### 如果项目对你有所帮助,并且你愿意分享。可以发送你优化前后的对比到我的邮箱 762579473@qq.com 非常感谢! 266 | ### 如果有任何问题,欢迎与我联系;如果对你有所启发,不要忘了 start ✨✨✨✨ Thanks~ 267 | ### 优化前后对比演示 268 | 269 | 注:gif 帧率只有20 270 | 271 | | 优化前 | 优化后 | 272 | | --- | --- | 273 | | ![优化前](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2f20f593cc144b72a1df4bdae57a165c~tplv-k3u1fbpfcp-watermark.image) | ![优化后](https://user-images.githubusercontent.com/40540394/123905087-3c08b680-d9a4-11eb-9485-4cdf21c38ad2.gif) | 274 | 275 | *** 276 | 277 | ### 相关原理分析: 278 | 279 | [ListView流畅度翻倍!!Flutter卡顿分析和通用优化方案](https://juejin.cn/post/6940134891606507534) 280 | 281 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter fluency optimization component "Keframe" 2 | 3 | ![image](https://user-images.githubusercontent.com/40540394/123203124-6eb83800-d4e8-11eb-9fd8-8b53e79c258f.png) 4 | 5 | + [Page switching fluency improved:](#page-switching-fluency-improved) 6 | + [How to use](#how-to-use) 7 | - [Project depend on:](#project-depend-on) 8 | - [Quick learning](#quick-learning) 9 | + [Constructor Description:](#constructor-description) 10 | + [Example explanation:](#example-explanation-) 11 | - [1. The actual item size in the list is known](#1-the-actual-item-size-in-the-list-is-known) 12 | - [2. The actual item height in the list is unknown](#2-the-actual-item-height-in-the-list-is-unknown) 13 | - [3. Estimate the number of items in a screen](#3-estimate-the-number-of-items-in-a-screen) 14 | - [4. Non-list scenarios](#4-non-list-scenarios) 15 | + [The cost of framing](#the-cost-of-framing) 16 | + [Before and after optimization demo](#before-and-after-optimization-demo) 17 | 18 | Language: English | [中文简体](README-ZH.md) 19 | 20 | [![null-safe](https://img.shields.io/badge/nullsafe-2.0.6-brightgreen)](https://pub.dev/packages/keframe) 21 | [![null-safe](https://img.shields.io/badge/normal-1.0.3-brightgreen)](https://pub.dev/packages/keframe) 22 | [![GitHub stars](https://img.shields.io/github/stars/LianjiaTech/keframe)](https://github.com/LianjiaTech/keframe/stargazers) 23 | [![GitHub license](https://img.shields.io/github/license/LianjiaTech/keframe)](https://github.com/LianjiaTech/keframe/blob/master/LICENSE) 24 | 25 | Optimize for lag caused by builds, such as page switches or rapid scrolling of complex lists, through frame-splitting rendering. 26 | 27 | The following is [Example](app-profile.apk)(you can download it directly) running in VIVO X23 (Snapdragon 660). Comparison of collected data indicators before and after optimization of 200 frames under the same operation (The demo is at the end of the article) 28 | 29 | | Before optimization | after optimization | 30 | | --- | --- | 31 | | 优化前 | 优化后 | 32 | 33 | Monitoring tools from: [fps_monitor](https://github.com/Nayuta403/fps_monitor) 34 | 35 | - Fluency: FPS greater than 55, which means less than 18ms per frame 36 | - Good: FPS between 30-55, i.e. 18ms-33ms per frame 37 | - Slight lag: FPS between 15-30, i.e. 33ms-67ms per frame 38 | - Caton: FPS less than 15, which means a frame time greater than 66.7ms 39 | 40 | After using frame splitting optimization, the number of lag decreased from an average of 33.3 frames to only one in 200 frames, and the slight lag decreased from 188ms to 90ms. The phenomenon of lag is greatly reduced, the proportion of fluid frames is significantly increased, and the overall performance is smoother. Below are the details. 41 | 42 | | | Before optimization | after optimization | 43 | | ------------------------------------------------ | ------------------- | ------------------ | 44 | | The average number of frames appears a lag | 33.3 | 200 | 45 | | The average number of frames with a slight lag | 8.6 | 66.7 | 46 | | Most time consuming | 188.0ms | 90.0ms | 47 | | The average time | 27.0ms | 19.4ms | 48 | | Fluency frame ratio | 40% | 64.5% | 49 | 50 | **** 51 | 52 | ### Page switching fluency improved: 53 | 54 | When opening a page or Tab switch, the system will render the entire page and complete the page switch with animation. For complex pages, there will also be a frame lag. 55 | 56 | With the framing component, the page build is disassembled frame by frame and viewed through the performance tool in DevTools. During switching, the peak value of a frame is reduced from **112.5ms to 30.2ms**, and the overall switching process is more smooth. 57 | 58 | | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c0ce341f0a2d4fceb0ad123fd4834ce2~tplv-k3u1fbpfcp-watermark.image) | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0c571a755ac84f39b52d57a13856a243~tplv-k3u1fbpfcp-watermark.image) | 59 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 60 | 61 | *** 62 | 63 | ### How to use 64 | 65 | #### Project depend on: 66 | 67 | Add a dependency on `keframe` to `pubspec.yaml` 68 | 69 | ```yaml 70 | dependencies: 71 | keframe: version 72 | ``` 73 | 74 | Components distinguish only the normal and null-safe versions 75 | 76 | The normal version uses : `1.0.3` 77 | 78 | The null-safe version uses :`2.0.6` 79 | 80 | 81 | #### Quick learning 82 | 83 | As shown in the figure below : 84 | 85 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83d16f3b2a3e45b79fc73d7a52774696~tplv-k3u1fbpfcp-watermark.image) 86 | 87 | Suppose the page now consists of four parts A, B, C and D, each of which takes 10ms and is built as 40ms at page time. 88 | 89 | Use the frameseparateWidget component to nest each section. A simple placeholder is rendered on the first frame of the page, and A, B, C, and D are rendered on the next four frames. 90 | 91 | For the list, nested `FrameSeparateWidget` in each item and nested `ListView` in `SizeCacheWidget`. 92 | 93 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ffecd49bf9ba4379984a22ef79663104~tplv-k3u1fbpfcp-watermark.image) 94 | 95 | *** 96 | 97 | ### Constructor Description: 98 | 99 | FrameSeparateWidget :A frame-splitting component that renders nested widgets in a single frame. 100 | 101 | | type | name | required | describe | 102 | | ------ | ----------- | -------- | ------------------------------------------------------------ | 103 | | Key | key | no | | 104 | | int | index | no | frame component id, using SizeCacheWidget scenario will pass, and to maintain the index in the SizeCacheWidget corresponding Size information | 105 | | Widget | child | yes | the actual need to render the widget | 106 | | Widget | placeHolder | no | placeholder widget, try to set up a simple placeholder, not the default is the Container () | 107 | 108 | SizeCacheWidget:Cache size information for **actual widgets nested by framing components in child nodes **. 109 | 110 | | type | name | required | describe | 111 | | ------ | ------------- | -------- | ------------------------------------------------------------ | 112 | | Key | key | no | | 113 | | Widget | child | yes | if include framing component in the child nodes, the **cache is the actual widget size** | 114 | | int | estimateCount | no | estimates the number of child nodes on the screen, can enhance the response speed of the fast scroll | 115 | 116 | *** 117 | 118 | ### Example explanation: 119 | 120 | Caton's pages are often rendered by multiple complex widgets at the same time. By nested `FrameSeparateWidget` for complex widgets. When rendering, the Framing Component renders multiple `palceHolder` simultaneously in the first frame, and then renders complex sub-items in successive frames to improve page fluency. 121 | 122 | For example: 123 | 124 | ```dart 125 | ListView.builder( 126 | itemCount: childCount, 127 | itemBuilder: (c, i) => CellWidget( 128 | color: i % 2 == 0 ? Colors.red : Colors.blue, 129 | index: i, 130 | ), 131 | ) 132 | ``` 133 | 134 | The height of `cellWidget` is 60, and three components of `TextField` are nested inside (the overall construction time is about 9ms). 135 | 136 | Optimization simply involves nested framing components for each item and setting `placeHolder` for each item (placeHolder should be as simple as possible and should look like the actual item). 137 | 138 | In the `ListView` case, nested `SizeCacheWidget` is recommended, and the pre-load `cacheExtent` is recommended to be larger, such as 500 (the default is 250), to improve the slow sliding experience. 139 | 140 | For example: 141 | 142 | ```dart 143 | SizeCacheWidget( 144 | child: ListView.builder( 145 | cacheExtent: 500, 146 | itemCount: childCount, 147 | itemBuilder: (c, i) => FrameSeparateWidget( 148 | index: i, 149 | placeHolder: Container( 150 | color: i % 2 == 0 ? Colors.red : Colors.blue, 151 | height: 60, 152 | ), 153 | child: CellWidget( 154 | color: i % 2 == 0 ? Colors.red : Colors.blue, 155 | index: i, 156 | ), 157 | ), 158 | ), 159 | ), 160 | ``` 161 | 162 | Here are a few scenarios: 163 | 164 | #### 1. The actual item size in the list is known 165 | 166 | If the actual item height is known (each item height is 60), just set the placeholder to match the actual item height. See frame optimization 1 in Example. 167 | 168 | ```dart 169 | FrameSeparateWidget( 170 | index: i, 171 | placeHolder: Container( 172 | color: i % 2 == 0 ? Colors.red : Colors.blue, 173 | height: 60,// Keep the same height as the actual item 174 | ), 175 | child: CellWidget( 176 | color: i % 2 == 0 ? Colors.red : Colors.blue, 177 | index: i, 178 | ), 179 | ) 180 | ``` 181 | 182 | #### 2. The actual item height in the list is unknown 183 | 184 | In the real world, lists are often presented based on data, and there is no way to predict the size of an item at first. 185 | 186 | For example, in example frame optimization 2, `placeHolder` (height 40) is not the same size as the actual item (height 60). 187 | Because each item is rendered in a different frame, list `jitter` occurs. 188 | 189 | You can set some approximate height for the placeholder. And nested the ListView in the SizeCacheWidget. 190 | 191 | For widgets that have been rendered, the `palceHolder` size is forced and the `cacheExtent` is increased. This way, the rendered item will not jump as it slides back and forth. 192 | 193 | For example, framing optimization 3 in example 194 | 195 | ```dart 196 | SizeCacheWidget( 197 | child: ListView.builder( 198 | cacheExtent: 500, 199 | itemCount: childCount, 200 | itemBuilder: (c, i) => FrameSeparateWidget( 201 | index: i, 202 | placeHolder: Container( 203 | color: i % 2 == 0 ? Colors.red : Colors.blue, 204 | height: 40, 205 | ), 206 | child: CellWidget( 207 | color: i % 2 == 0 ? Colors.red : Colors.blue, 208 | index: i, 209 | ), 210 | ), 211 | ), 212 | ), 213 | ``` 214 | 215 | The actual effect is as follows: 216 | 217 | Screenrecording_20210611_194905.gif 218 | 219 | 220 | 221 | #### 3. Estimate the number of items in a screen 222 | 223 | If you can roughly estimate the maximum number of actual items that can be displayed on a screen, say 10. Set the `SizeCacheWidge` `estimateCount` property to 10*2. Fast scrolling scene builds are more responsive and more memory stable. 224 | 225 | For example, framing optimization 4 in example 226 | 227 | ```dart 228 | ... 229 | SizeCacheWidget( 230 | estimateCount: 20, 231 | child: ListView.builder( 232 | ... 233 | ``` 234 | 235 | In addition, you can also nest animations such as transparency/displacement on items to optimize the visual effect. 236 | 237 | The actual effect is as follows: 238 | 239 | | ![Screenrecording_20210315_133310.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb7d1361ae7842df954bb1c559e2ec54~tplv-k3u1fbpfcp-watermark.image) | ![Screenrecording_20210315_133848.gif](https://user-images.githubusercontent.com/40540394/123905372-c9e4a180-d9a4-11eb-94d0-4190710828f5.gif) | 240 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 241 | 242 | #### 4. Non-list scenarios 243 | 244 | For non-list scenarios, there are generally no fluency issues, but there can still be a lag on first entry. 245 | 246 | Similarly, complex modules can be rendered in different frames to avoid first-time entry delays. 247 | 248 | For example, we will nest the framing component for the bottom action area in the optimization example: 249 | 250 | ```dart 251 | FrameSeparateWidget( 252 | child: operateBar(), 253 | index: -1, 254 | ) 255 | ``` 256 | 257 | **** 258 | 259 | ### The cost of framing 260 | 261 | Of course, the framing scheme is not perfect. In my opinion, there are two main costs: 262 | 263 | 1. Extra build cost: The build cost of the entire build process changed from "N * widget cost" to "N * (widget + placeholder) cost + The system scheduling N frame cost". As you can see, the additional overhead is mainly due to the complexity of the placeholder. If the placeholders were simple Containers, the overall build time would probably increase by 10-20% after testing.This extra overhead is negligible for today's mobile devices. 264 | 265 | 2. Visual change: As demonstrated above, the component will render the item in frames, and the page will visually occupy the space to become the actual widget. 266 | But in fact, because the list exists in the cache area (it is recommended to increase the cacheExtent), the user does not feel it in the high-end machine or normal sliding situation. 267 | On lower devices, a quick swipe can feel the transition, but it's better than a heavy stumble. 268 | 269 | *** 270 | ### If you have any questions, please feel free to contact me. If this inspires you, dont forget start ✨✨✨✨ Thanks 271 | 272 | ### Before and after optimization demo 273 | 274 | | Before optimization | after optimization | 275 | | --- | --- | 276 | | ![优化前](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2f20f593cc144b72a1df4bdae57a165c~tplv-k3u1fbpfcp-watermark.image) | ![优化后](https://user-images.githubusercontent.com/40540394/123905087-3c08b680-d9a4-11eb-9485-4cdf21c38ad2.gif) | 277 | 278 | ## Contributors 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | [![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors) 291 | 292 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/main/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are other similar analysis options files in the flutter repos, 11 | # which should be kept in sync with this file: 12 | # 13 | # - analysis_options.yaml (this file) 14 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 15 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 16 | # - https://github.com/flutter/packages/blob/master/analysis_options.yaml 17 | # 18 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 19 | # Android Studio, and the `flutter analyze` command. 20 | 21 | analyzer: 22 | strong-mode: 23 | implicit-casts: false 24 | implicit-dynamic: false 25 | errors: 26 | # treat missing required parameters as a warning (not a hint) 27 | missing_required_param: warning 28 | # treat missing returns as a warning (not a hint) 29 | missing_return: warning 30 | # allow having TODO comments in the code 31 | todo: ignore 32 | # allow self-reference to deprecated members (we do this because otherwise we have 33 | # to annotate every member in every test, assert, etc, when we deprecate something) 34 | deprecated_member_use_from_same_package: ignore 35 | # TODO(ianh): https://github.com/flutter/flutter/issues/74381 36 | # Clean up existing unnecessary imports, and remove line to ignore. 37 | unnecessary_import: ignore 38 | # Turned off until null-safe rollout is complete. 39 | unnecessary_null_comparison: ignore 40 | exclude: 41 | - "bin/cache/**" 42 | # Ignore protoc generated files 43 | - "dev/conductor/lib/proto/*" 44 | 45 | linter: 46 | rules: 47 | # these rules are documented on and in the same order as 48 | # the Dart Lint rules page to make maintenance easier 49 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 50 | - always_declare_return_types 51 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 52 | - always_require_non_null_named_parameters 53 | # - always_use_package_imports # we do this commonly 54 | - annotate_overrides 55 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 56 | - avoid_bool_literals_in_conditional_expressions 57 | # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 58 | # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023 59 | - avoid_classes_with_only_static_members 60 | - avoid_double_and_int_checks 61 | - avoid_dynamic_calls 62 | - avoid_empty_else 63 | - avoid_equals_and_hash_code_on_mutable_classes 64 | - avoid_escaping_inner_quotes 65 | - avoid_field_initializers_in_const_classes 66 | - avoid_function_literals_in_foreach_calls 67 | - avoid_implementing_value_types 68 | - avoid_init_to_null 69 | - avoid_js_rounded_ints 70 | # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to 71 | - avoid_null_checks_in_equality_operators 72 | # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it 73 | - avoid_print 74 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 75 | - avoid_redundant_argument_values 76 | - avoid_relative_lib_imports 77 | - avoid_renaming_method_parameters 78 | - avoid_return_types_on_setters 79 | # - avoid_returning_null # still violated by some pre-nnbd code that we haven't yet migrated 80 | - avoid_returning_null_for_future 81 | - avoid_returning_null_for_void 82 | # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives 83 | - avoid_setters_without_getters 84 | - avoid_shadowing_type_parameters 85 | - avoid_single_cascade_in_expression_statements 86 | - avoid_slow_async_io 87 | - avoid_type_to_string 88 | - avoid_types_as_parameter_names 89 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 90 | - avoid_unnecessary_containers 91 | - avoid_unused_constructor_parameters 92 | - avoid_void_async 93 | # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere 94 | - await_only_futures 95 | - camel_case_extensions 96 | - camel_case_types 97 | - cancel_subscriptions 98 | # - cascade_invocations # doesn't match the typical style of this repo 99 | - cast_nullable_to_non_nullable 100 | # - close_sinks # not reliable enough 101 | # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 102 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 103 | - control_flow_in_finally 104 | # - curly_braces_in_flow_control_structures # not required by flutter style 105 | - depend_on_referenced_packages 106 | - deprecated_consistency 107 | # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) 108 | - directives_ordering 109 | # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic 110 | - empty_catches 111 | - empty_constructor_bodies 112 | - empty_statements 113 | - eol_at_end_of_file 114 | - exhaustive_cases 115 | - file_names 116 | - flutter_style_todos 117 | - hash_and_equals 118 | - implementation_imports 119 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 120 | - iterable_contains_unrelated_type 121 | # - join_return_with_assignment # not required by flutter style 122 | - leading_newlines_in_multiline_strings 123 | - library_names 124 | - library_prefixes 125 | - library_private_types_in_public_api 126 | # - lines_longer_than_80_chars # not required by flutter style 127 | - list_remove_unrelated_type 128 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/linter/issues/453 129 | - missing_whitespace_between_adjacent_strings 130 | - no_adjacent_strings_in_list 131 | - no_default_cases 132 | - no_duplicate_case_values 133 | - no_logic_in_create_state 134 | # - no_runtimeType_toString # ok in tests; we enable this only in packages/ 135 | - non_constant_identifier_names 136 | - noop_primitive_operations 137 | - null_check_on_nullable_type_parameter 138 | - null_closures 139 | # - omit_local_variable_types # opposite of always_specify_types 140 | # - one_member_abstracts # too many false positives 141 | - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al 142 | - overridden_fields 143 | - package_api_docs 144 | - package_names 145 | - package_prefixed_library_names 146 | # - parameter_assignments # we do this commonly 147 | - prefer_adjacent_string_concatenation 148 | - prefer_asserts_in_initializer_lists 149 | # - prefer_asserts_with_message # not required by flutter style 150 | - prefer_collection_literals 151 | - prefer_conditional_assignment 152 | - prefer_const_constructors 153 | - prefer_const_constructors_in_immutables 154 | - prefer_const_declarations 155 | - prefer_const_literals_to_create_immutables 156 | # - prefer_constructors_over_static_methods # far too many false positives 157 | - prefer_contains 158 | # - prefer_double_quotes # opposite of prefer_single_quotes 159 | - prefer_equal_for_default_values 160 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 161 | - prefer_final_fields 162 | - prefer_final_in_for_each 163 | - prefer_final_locals 164 | # - prefer_final_parameters # we should enable this one day when it can be auto-fixed (https://github.com/dart-lang/linter/issues/3104), see also parameter_assignments 165 | - prefer_for_elements_to_map_fromIterable 166 | - prefer_foreach 167 | - prefer_function_declarations_over_variables 168 | - prefer_generic_function_type_aliases 169 | - prefer_if_elements_to_conditional_expressions 170 | - prefer_if_null_operators 171 | - prefer_initializing_formals 172 | - prefer_inlined_adds 173 | # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants 174 | - prefer_interpolation_to_compose_strings 175 | - prefer_is_empty 176 | - prefer_is_not_empty 177 | - prefer_is_not_operator 178 | - prefer_iterable_whereType 179 | # - prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018 180 | - prefer_null_aware_operators 181 | # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere 182 | - prefer_relative_imports 183 | - prefer_single_quotes 184 | - prefer_spread_collections 185 | - prefer_typing_uninitialized_variables 186 | - prefer_void_to_null 187 | - provide_deprecation_message 188 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 189 | - recursive_getters 190 | # - require_trailing_commas # blocked on https://github.com/dart-lang/sdk/issues/47441 191 | - sized_box_for_whitespace 192 | - slash_for_doc_comments 193 | - sort_child_properties_last 194 | - sort_constructors_first 195 | # - sort_pub_dependencies # prevents separating pinned transitive dependencies 196 | - sort_unnamed_constructors_first 197 | - test_types_in_equals 198 | - throw_in_finally 199 | - tighten_type_of_initializing_formals 200 | # - type_annotate_public_apis # subset of always_specify_types 201 | - type_init_formals 202 | # - unawaited_futures # too many false positives, especially with the way AnimationController works 203 | - unnecessary_await_in_return 204 | - unnecessary_brace_in_string_interps 205 | - unnecessary_const 206 | - unnecessary_constructor_name 207 | # - unnecessary_final # conflicts with prefer_final_locals 208 | - unnecessary_getters_setters 209 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 210 | - unnecessary_new 211 | - unnecessary_null_aware_assignments 212 | - unnecessary_null_checks 213 | - unnecessary_null_in_if_null_operators 214 | - unnecessary_nullable_for_final_variable_declarations 215 | - unnecessary_overrides 216 | - unnecessary_parenthesis 217 | # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint 218 | - unnecessary_statements 219 | - unnecessary_string_escapes 220 | - unnecessary_string_interpolations 221 | - unnecessary_this 222 | - unrelated_type_equality_checks 223 | - unsafe_html 224 | - use_build_context_synchronously 225 | - use_full_hex_values_for_flutter_colors 226 | - use_function_type_syntax_for_parameters 227 | # - use_if_null_to_convert_nulls_to_bools # blocked on https://github.com/dart-lang/sdk/issues/47436 228 | - use_is_even_rather_than_modulo 229 | - use_key_in_widget_constructors 230 | - use_late_for_private_fields_and_variables 231 | - use_named_constants 232 | - use_raw_strings 233 | - use_rethrow_when_possible 234 | - use_setters_to_change_properties 235 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 236 | - use_test_throws_matchers 237 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 238 | - valid_regexps 239 | - void_checks 240 | -------------------------------------------------------------------------------- /app-profile.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/app-profile.apk -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | pubspec.lock 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f139b11009aeb8ed2a3a3aa8b0066e482709dde3 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 31 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.example.example" 42 | minSdkVersion 21 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 67 | } 68 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import androidx.annotation.NonNull; 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | } 10 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.4.32' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 13 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 15 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 16 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 18 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 19 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXCopyFilesBuildPhase section */ 23 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 24 | isa = PBXCopyFilesBuildPhase; 25 | buildActionMask = 2147483647; 26 | dstPath = ""; 27 | dstSubfolderSpec = 10; 28 | files = ( 29 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 30 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 31 | ); 32 | name = "Embed Frameworks"; 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXCopyFilesBuildPhase section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 39 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 40 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 41 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 42 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 43 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 44 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 45 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 46 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 47 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 48 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 52 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 61 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXFrameworksBuildPhase section */ 66 | 67 | /* Begin PBXGroup section */ 68 | 9740EEB11CF90186004384FC /* Flutter */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 3B80C3931E831B6300D905FE /* App.framework */, 72 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 73 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 74 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 75 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 76 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 77 | ); 78 | name = Flutter; 79 | sourceTree = ""; 80 | }; 81 | 97C146E51CF9000F007C117D = { 82 | isa = PBXGroup; 83 | children = ( 84 | 9740EEB11CF90186004384FC /* Flutter */, 85 | 97C146F01CF9000F007C117D /* Runner */, 86 | 97C146EF1CF9000F007C117D /* Products */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | 97C146EF1CF9000F007C117D /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 97C146EE1CF9000F007C117D /* Runner.app */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 97C146F01CF9000F007C117D /* Runner */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 102 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 103 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 104 | 97C147021CF9000F007C117D /* Info.plist */, 105 | 97C146F11CF9000F007C117D /* Supporting Files */, 106 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 107 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 108 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 109 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 110 | ); 111 | path = Runner; 112 | sourceTree = ""; 113 | }; 114 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | ); 118 | name = "Supporting Files"; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | 97C146ED1CF9000F007C117D /* Runner */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 127 | buildPhases = ( 128 | 9740EEB61CF901F6004384FC /* Run Script */, 129 | 97C146EA1CF9000F007C117D /* Sources */, 130 | 97C146EB1CF9000F007C117D /* Frameworks */, 131 | 97C146EC1CF9000F007C117D /* Resources */, 132 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 133 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 134 | ); 135 | buildRules = ( 136 | ); 137 | dependencies = ( 138 | ); 139 | name = Runner; 140 | productName = Runner; 141 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 142 | productType = "com.apple.product-type.application"; 143 | }; 144 | /* End PBXNativeTarget section */ 145 | 146 | /* Begin PBXProject section */ 147 | 97C146E61CF9000F007C117D /* Project object */ = { 148 | isa = PBXProject; 149 | attributes = { 150 | LastUpgradeCheck = 1020; 151 | ORGANIZATIONNAME = "The Chromium Authors"; 152 | TargetAttributes = { 153 | 97C146ED1CF9000F007C117D = { 154 | CreatedOnToolsVersion = 7.3.1; 155 | LastSwiftMigration = 1100; 156 | }; 157 | }; 158 | }; 159 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 160 | compatibilityVersion = "Xcode 3.2"; 161 | developmentRegion = en; 162 | hasScannedForEncodings = 0; 163 | knownRegions = ( 164 | en, 165 | Base, 166 | ); 167 | mainGroup = 97C146E51CF9000F007C117D; 168 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 169 | projectDirPath = ""; 170 | projectRoot = ""; 171 | targets = ( 172 | 97C146ED1CF9000F007C117D /* Runner */, 173 | ); 174 | }; 175 | /* End PBXProject section */ 176 | 177 | /* Begin PBXResourcesBuildPhase section */ 178 | 97C146EC1CF9000F007C117D /* Resources */ = { 179 | isa = PBXResourcesBuildPhase; 180 | buildActionMask = 2147483647; 181 | files = ( 182 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 183 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 184 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 185 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | /* End PBXResourcesBuildPhase section */ 190 | 191 | /* Begin PBXShellScriptBuildPhase section */ 192 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 193 | isa = PBXShellScriptBuildPhase; 194 | buildActionMask = 2147483647; 195 | files = ( 196 | ); 197 | inputPaths = ( 198 | ); 199 | name = "Thin Binary"; 200 | outputPaths = ( 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | shellPath = /bin/sh; 204 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 205 | }; 206 | 9740EEB61CF901F6004384FC /* Run Script */ = { 207 | isa = PBXShellScriptBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | ); 211 | inputPaths = ( 212 | ); 213 | name = "Run Script"; 214 | outputPaths = ( 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | shellPath = /bin/sh; 218 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 219 | }; 220 | /* End PBXShellScriptBuildPhase section */ 221 | 222 | /* Begin PBXSourcesBuildPhase section */ 223 | 97C146EA1CF9000F007C117D /* Sources */ = { 224 | isa = PBXSourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 228 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | /* End PBXSourcesBuildPhase section */ 233 | 234 | /* Begin PBXVariantGroup section */ 235 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 236 | isa = PBXVariantGroup; 237 | children = ( 238 | 97C146FB1CF9000F007C117D /* Base */, 239 | ); 240 | name = Main.storyboard; 241 | sourceTree = ""; 242 | }; 243 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 244 | isa = PBXVariantGroup; 245 | children = ( 246 | 97C147001CF9000F007C117D /* Base */, 247 | ); 248 | name = LaunchScreen.storyboard; 249 | sourceTree = ""; 250 | }; 251 | /* End PBXVariantGroup section */ 252 | 253 | /* Begin XCBuildConfiguration section */ 254 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 255 | isa = XCBuildConfiguration; 256 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 257 | buildSettings = { 258 | ALWAYS_SEARCH_USER_PATHS = NO; 259 | CLANG_ANALYZER_NONNULL = YES; 260 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 261 | CLANG_CXX_LIBRARY = "libc++"; 262 | CLANG_ENABLE_MODULES = YES; 263 | CLANG_ENABLE_OBJC_ARC = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_EMPTY_BODY = YES; 271 | CLANG_WARN_ENUM_CONVERSION = YES; 272 | CLANG_WARN_INFINITE_RECURSION = YES; 273 | CLANG_WARN_INT_CONVERSION = YES; 274 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 275 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 276 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 277 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 278 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 279 | CLANG_WARN_STRICT_PROTOTYPES = YES; 280 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 281 | CLANG_WARN_UNREACHABLE_CODE = YES; 282 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 283 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 284 | COPY_PHASE_STRIP = NO; 285 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 286 | ENABLE_NS_ASSERTIONS = NO; 287 | ENABLE_STRICT_OBJC_MSGSEND = YES; 288 | GCC_C_LANGUAGE_STANDARD = gnu99; 289 | GCC_NO_COMMON_BLOCKS = YES; 290 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 291 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 292 | GCC_WARN_UNDECLARED_SELECTOR = YES; 293 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 294 | GCC_WARN_UNUSED_FUNCTION = YES; 295 | GCC_WARN_UNUSED_VARIABLE = YES; 296 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 297 | MTL_ENABLE_DEBUG_INFO = NO; 298 | SDKROOT = iphoneos; 299 | SUPPORTED_PLATFORMS = iphoneos; 300 | TARGETED_DEVICE_FAMILY = "1,2"; 301 | VALIDATE_PRODUCT = YES; 302 | }; 303 | name = Profile; 304 | }; 305 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 306 | isa = XCBuildConfiguration; 307 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 308 | buildSettings = { 309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 310 | CLANG_ENABLE_MODULES = YES; 311 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 312 | ENABLE_BITCODE = NO; 313 | FRAMEWORK_SEARCH_PATHS = ( 314 | "$(inherited)", 315 | "$(PROJECT_DIR)/Flutter", 316 | ); 317 | INFOPLIST_FILE = Runner/Info.plist; 318 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 319 | LIBRARY_SEARCH_PATHS = ( 320 | "$(inherited)", 321 | "$(PROJECT_DIR)/Flutter", 322 | ); 323 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 324 | PRODUCT_NAME = "$(TARGET_NAME)"; 325 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 326 | SWIFT_VERSION = 5.0; 327 | VERSIONING_SYSTEM = "apple-generic"; 328 | }; 329 | name = Profile; 330 | }; 331 | 97C147031CF9000F007C117D /* Debug */ = { 332 | isa = XCBuildConfiguration; 333 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 334 | buildSettings = { 335 | ALWAYS_SEARCH_USER_PATHS = NO; 336 | CLANG_ANALYZER_NONNULL = YES; 337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 338 | CLANG_CXX_LIBRARY = "libc++"; 339 | CLANG_ENABLE_MODULES = YES; 340 | CLANG_ENABLE_OBJC_ARC = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 356 | CLANG_WARN_STRICT_PROTOTYPES = YES; 357 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 358 | CLANG_WARN_UNREACHABLE_CODE = YES; 359 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 360 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 361 | COPY_PHASE_STRIP = NO; 362 | DEBUG_INFORMATION_FORMAT = dwarf; 363 | ENABLE_STRICT_OBJC_MSGSEND = YES; 364 | ENABLE_TESTABILITY = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu99; 366 | GCC_DYNAMIC_NO_PIC = NO; 367 | GCC_NO_COMMON_BLOCKS = YES; 368 | GCC_OPTIMIZATION_LEVEL = 0; 369 | GCC_PREPROCESSOR_DEFINITIONS = ( 370 | "DEBUG=1", 371 | "$(inherited)", 372 | ); 373 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 374 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 375 | GCC_WARN_UNDECLARED_SELECTOR = YES; 376 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 377 | GCC_WARN_UNUSED_FUNCTION = YES; 378 | GCC_WARN_UNUSED_VARIABLE = YES; 379 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 380 | MTL_ENABLE_DEBUG_INFO = YES; 381 | ONLY_ACTIVE_ARCH = YES; 382 | SDKROOT = iphoneos; 383 | TARGETED_DEVICE_FAMILY = "1,2"; 384 | }; 385 | name = Debug; 386 | }; 387 | 97C147041CF9000F007C117D /* Release */ = { 388 | isa = XCBuildConfiguration; 389 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 390 | buildSettings = { 391 | ALWAYS_SEARCH_USER_PATHS = NO; 392 | CLANG_ANALYZER_NONNULL = YES; 393 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 394 | CLANG_CXX_LIBRARY = "libc++"; 395 | CLANG_ENABLE_MODULES = YES; 396 | CLANG_ENABLE_OBJC_ARC = YES; 397 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 398 | CLANG_WARN_BOOL_CONVERSION = YES; 399 | CLANG_WARN_COMMA = YES; 400 | CLANG_WARN_CONSTANT_CONVERSION = YES; 401 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 402 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 403 | CLANG_WARN_EMPTY_BODY = YES; 404 | CLANG_WARN_ENUM_CONVERSION = YES; 405 | CLANG_WARN_INFINITE_RECURSION = YES; 406 | CLANG_WARN_INT_CONVERSION = YES; 407 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 408 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 409 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 410 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 411 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 412 | CLANG_WARN_STRICT_PROTOTYPES = YES; 413 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 414 | CLANG_WARN_UNREACHABLE_CODE = YES; 415 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 416 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 417 | COPY_PHASE_STRIP = NO; 418 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 419 | ENABLE_NS_ASSERTIONS = NO; 420 | ENABLE_STRICT_OBJC_MSGSEND = YES; 421 | GCC_C_LANGUAGE_STANDARD = gnu99; 422 | GCC_NO_COMMON_BLOCKS = YES; 423 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 424 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 425 | GCC_WARN_UNDECLARED_SELECTOR = YES; 426 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 427 | GCC_WARN_UNUSED_FUNCTION = YES; 428 | GCC_WARN_UNUSED_VARIABLE = YES; 429 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 430 | MTL_ENABLE_DEBUG_INFO = NO; 431 | SDKROOT = iphoneos; 432 | SUPPORTED_PLATFORMS = iphoneos; 433 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 434 | TARGETED_DEVICE_FAMILY = "1,2"; 435 | VALIDATE_PRODUCT = YES; 436 | }; 437 | name = Release; 438 | }; 439 | 97C147061CF9000F007C117D /* Debug */ = { 440 | isa = XCBuildConfiguration; 441 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 442 | buildSettings = { 443 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 444 | CLANG_ENABLE_MODULES = YES; 445 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 446 | ENABLE_BITCODE = NO; 447 | FRAMEWORK_SEARCH_PATHS = ( 448 | "$(inherited)", 449 | "$(PROJECT_DIR)/Flutter", 450 | ); 451 | INFOPLIST_FILE = Runner/Info.plist; 452 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 453 | LIBRARY_SEARCH_PATHS = ( 454 | "$(inherited)", 455 | "$(PROJECT_DIR)/Flutter", 456 | ); 457 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 458 | PRODUCT_NAME = "$(TARGET_NAME)"; 459 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 460 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 461 | SWIFT_VERSION = 5.0; 462 | VERSIONING_SYSTEM = "apple-generic"; 463 | }; 464 | name = Debug; 465 | }; 466 | 97C147071CF9000F007C117D /* Release */ = { 467 | isa = XCBuildConfiguration; 468 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 469 | buildSettings = { 470 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 471 | CLANG_ENABLE_MODULES = YES; 472 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 473 | ENABLE_BITCODE = NO; 474 | FRAMEWORK_SEARCH_PATHS = ( 475 | "$(inherited)", 476 | "$(PROJECT_DIR)/Flutter", 477 | ); 478 | INFOPLIST_FILE = Runner/Info.plist; 479 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 480 | LIBRARY_SEARCH_PATHS = ( 481 | "$(inherited)", 482 | "$(PROJECT_DIR)/Flutter", 483 | ); 484 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 485 | PRODUCT_NAME = "$(TARGET_NAME)"; 486 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 487 | SWIFT_VERSION = 5.0; 488 | VERSIONING_SYSTEM = "apple-generic"; 489 | }; 490 | name = Release; 491 | }; 492 | /* End XCBuildConfiguration section */ 493 | 494 | /* Begin XCConfigurationList section */ 495 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 496 | isa = XCConfigurationList; 497 | buildConfigurations = ( 498 | 97C147031CF9000F007C117D /* Debug */, 499 | 97C147041CF9000F007C117D /* Release */, 500 | 249021D3217E4FDB00AE95B9 /* Profile */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 506 | isa = XCConfigurationList; 507 | buildConfigurations = ( 508 | 97C147061CF9000F007C117D /* Debug */, 509 | 97C147071CF9000F007C117D /* Release */, 510 | 249021D4217E4FDB00AE95B9 /* Profile */, 511 | ); 512 | defaultConfigurationIsVisible = 0; 513 | defaultConfigurationName = Release; 514 | }; 515 | /* End XCConfigurationList section */ 516 | }; 517 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 518 | } 519 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LianjiaTech/keframe/fb467cc49e86fc928acc22cbed4f70dcc79b96dc/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /example/lib/animation/opacity_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class OpacityTansWidget extends StatefulWidget { 4 | const OpacityTansWidget({Key key, this.child}) : super(key: key); 5 | 6 | final Widget child; 7 | 8 | @override 9 | OpacityState createState() => OpacityState(); 10 | } 11 | 12 | class OpacityState extends State 13 | with TickerProviderStateMixin { 14 | AnimationController controller; 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | 20 | controller = AnimationController( 21 | duration: const Duration(milliseconds: 500), 22 | lowerBound: 0.5, 23 | vsync: this); 24 | controller.forward(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return FadeTransition( 30 | opacity: controller, 31 | child: widget.child, 32 | ); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | controller.dispose(); 38 | super.dispose(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/lib/item/complex_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CellWidget extends StatefulWidget { 4 | const CellWidget({Key key, this.color, this.index}) : super(key: key); 5 | 6 | final Color color; 7 | final int index; 8 | 9 | @override 10 | CellWidgetState createState() => CellWidgetState(); 11 | } 12 | 13 | class CellWidgetState extends State { 14 | double height = 60; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Container( 19 | padding: const EdgeInsets.only(left: 20, right: 20), 20 | height: height, 21 | color: (widget.index % 3 == 0) 22 | ? Colors.lightGreen[300] 23 | : (widget.index % 3 == 1) 24 | ? Colors.lightGreen[500] 25 | : Colors.lightGreen[700], 26 | child: Row( 27 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 28 | children: [ 29 | SizedBox( 30 | width: 30, 31 | height: 30, 32 | child: Text('${widget.index}'), 33 | ), 34 | Container( 35 | width: 30, 36 | height: 30, 37 | color: Colors.black26, 38 | child: const TextField(), 39 | ), 40 | Container( 41 | width: 30, 42 | height: 30, 43 | color: Colors.black26, 44 | child: const TextField(), 45 | ), 46 | Container( 47 | width: 30, 48 | height: 30, 49 | color: Colors.black26, 50 | child: const TextField(), 51 | ), 52 | Container( 53 | width: 30, 54 | height: 30, 55 | color: Colors.black26, 56 | child: const TextField(), 57 | ), 58 | ], 59 | ), 60 | ); 61 | } 62 | 63 | @override 64 | void dispose() { 65 | super.dispose(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/scheduler.dart'; 5 | import 'package:fps_monitor/util/collection_util.dart'; 6 | import 'package:fps_monitor/widget/custom_widget_inspector.dart'; 7 | 8 | import 'page/complex_common_example.dart'; 9 | import 'page/complex_list_example.dart'; 10 | import 'page/opt/common_opt_example.dart'; 11 | import 'page/opt/list_opt_example1.dart'; 12 | import 'page/opt/list_opt_example2.dart'; 13 | import 'page/opt/list_opt_example3.dart'; 14 | import 'page/opt/list_opt_example4.dart'; 15 | import 'page/opt/list_opt_example5.dart'; 16 | 17 | void main() { 18 | kFpsInfoMaxSize = 200; 19 | runApp(const MyApp()); 20 | } 21 | 22 | GlobalKey globalKey = GlobalKey(); 23 | 24 | class MyApp extends StatelessWidget { 25 | const MyApp({Key key}) : super(key: key); 26 | 27 | // This widget is the root of your application. 28 | @override 29 | Widget build(BuildContext context) { 30 | SchedulerBinding.instance.addPostFrameCallback( 31 | (Duration t) => overlayState = globalKey.currentState.overlay); 32 | 33 | return MaterialApp( 34 | navigatorKey: globalKey, 35 | title: 'Keframe example', 36 | theme: ThemeData( 37 | primarySwatch: Colors.blue, 38 | ), 39 | builder: (BuildContext ctx, Widget child) { 40 | if (kIsWeb) return child; 41 | return CustomWidgetInspector( 42 | child: child, 43 | ); 44 | }, 45 | home: const Home(), 46 | ); 47 | } 48 | } 49 | 50 | class Home extends StatelessWidget { 51 | const Home({Key key}) : super(key: key); 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | return Scaffold( 56 | appBar: AppBar( 57 | title: const Text('Keframe example'), 58 | ), 59 | body: ListView( 60 | children: [ 61 | const ListTile( 62 | title: Text('before optimization(优化前):', 63 | style: TextStyle(fontWeight: FontWeight.bold)), 64 | ), 65 | ListTile( 66 | title: const Text('Complex ListView(复杂列表)'), 67 | onTap: () => open(context, const ComplexListExample()), 68 | ), 69 | ListTile( 70 | title: const Text('Complex common(非列表复杂页面)'), 71 | onTap: () => open(context, const ComplexCommonExample()), 72 | ), 73 | const ListTile( 74 | title: Text( 75 | 'After the optimization(分帧优化后):', 76 | style: TextStyle(fontWeight: FontWeight.bold), 77 | ), 78 | ), 79 | ListTile( 80 | title: const Text('Optimization ListView(列表分帧优化)1'), 81 | subtitle: const Text( 82 | 'Given the actual height of the item widget(已知实际 Widget 高度的情况)'), 83 | onTap: () => open(context, const ComplexListOptExample1()), 84 | ), 85 | ListTile( 86 | title: const Text('Optimization ListView(列表分帧优化) 2'), 87 | subtitle: const Text( 88 | 'Unknown height of the actual item widget (jitter)(实际 Widget 高度未知的情况(出现抖动))'), 89 | onTap: () => open(context, const ComplexListOptExample2()), 90 | ), 91 | ListTile( 92 | title: const Text('Optimization ListView(列表分帧优化)3'), 93 | subtitle: const Text( 94 | 'Unknown height of the actual Widget (no jitter for secondary rendering)(实际 Widget 高度未知的情况(二次渲染不抖动))'), 95 | onTap: () => open(context, const ComplexListOptExample3()), 96 | ), 97 | ListTile( 98 | title: const Text('Optimization ListView(列表分帧优化)4'), 99 | subtitle: const Text( 100 | 'If you can predict the number of list items on a screen, rendering performance is better( 如果可以预估一屏上的列表项数,渲染性能会更优)'), 101 | onTap: () => open(context, const ComplexListOptExample4()), 102 | ), 103 | ListTile( 104 | title: const Text('Optimization ListView(列表分帧优化)5'), 105 | subtitle: const Text( 106 | 'By nesting gradients, shifts and other animations, let the frame change more smoothly(通过嵌套渐变、位移等动画,让分帧变化更流畅)'), 107 | onTap: () => open(context, const ComplexListOptExample5()), 108 | ), 109 | ListTile( 110 | title: const Text('Optimization common(非复杂列表优化)1'), 111 | subtitle: const Text( 112 | 'Non - list page, optimize the transition process(非列表页面,优化转场过程)'), 113 | onTap: () => open(context, const ComplexCommonOptExample()), 114 | ) 115 | ], 116 | ), 117 | ); 118 | } 119 | 120 | void open(BuildContext context, Widget widget) { 121 | Navigator.of(context).push( 122 | CupertinoPageRoute(builder: (BuildContext context) => widget)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /example/lib/page/complex_common_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/item/complex_item.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ComplexCommonExample extends StatelessWidget { 5 | const ComplexCommonExample({Key key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Scaffold( 10 | resizeToAvoidBottomInset: true, 11 | appBar: AppBar( 12 | title: const Text('Complex common'), 13 | ), 14 | body: Column(children: [ 15 | rowItem(), 16 | rowItem(), 17 | rowItem(), 18 | rowItem(), 19 | rowItem(), 20 | rowItem(), 21 | const Text( 22 | '说明:实际业务中,对于非列表的复杂页面。卡顿可能发生在打开页面的转场动画中,你可以仔细观察下这个页面打开的打开过程。'), 23 | const Text( 24 | 'Note: In actual business, for non - list complex pages. Jank may occur during a transition animation that opens a page, so you can watch the opening process of the page carefully'), 25 | ]), 26 | ); 27 | } 28 | 29 | Widget rowItem() { 30 | return Row( 31 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 32 | children: const [ 33 | // It's the complex widget in your business 34 | CellWidget( 35 | index: 0, 36 | ), 37 | CellWidget( 38 | index: 1, 39 | ) 40 | ], 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/lib/page/complex_list_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | 4 | import '../item/complex_item.dart'; 5 | 6 | class ComplexListExample extends StatefulWidget { 7 | const ComplexListExample({Key key}) : super(key: key); 8 | 9 | @override 10 | ComplexListExampleState createState() => ComplexListExampleState(); 11 | } 12 | 13 | class ComplexListExampleState extends State { 14 | int childCount = 100; 15 | 16 | ScrollController scrollController; 17 | double scrollPos = 1500; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | scrollController = ScrollController(); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | resizeToAvoidBottomInset: true, 29 | appBar: AppBar( 30 | title: const Text('Complex ListView'), 31 | ), 32 | body: Column( 33 | children: [ 34 | Expanded( 35 | child: ListView.builder( 36 | controller: scrollController, 37 | itemCount: childCount, 38 | itemBuilder: (BuildContext c, int i) => CellWidget( 39 | color: i.isEven ? Colors.red : Colors.blue, 40 | index: i, 41 | ), 42 | ), 43 | ), 44 | operateBar() 45 | ], 46 | ), 47 | ); 48 | } 49 | 50 | Widget operateBar() { 51 | return SizedBox( 52 | height: 100, 53 | child: Column( 54 | crossAxisAlignment: CrossAxisAlignment.start, 55 | children: [ 56 | Expanded( 57 | child: Wrap( 58 | children: [ 59 | ElevatedButton( 60 | onPressed: () { 61 | childCount += 20; 62 | setState(() {}); 63 | }, 64 | child: const Text( 65 | 'setState increase 20 items', 66 | style: TextStyle(fontSize: 14), 67 | )), 68 | ElevatedButton( 69 | onPressed: () { 70 | setState(() {}); 71 | scrollController.animateTo(scrollPos, 72 | duration: Duration( 73 | milliseconds: scrollPos == 0 ? 1500 : 600), 74 | curve: Curves.easeInOut); 75 | scrollPos = scrollPos >= 6000 ? 0 : scrollPos + 1500; 76 | }, 77 | child: Text( 78 | 'Scroll to $scrollPos offset', 79 | style: const TextStyle(fontSize: 14), 80 | )), 81 | ], 82 | ), 83 | ) 84 | ], 85 | ), 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/lib/page/opt/common_opt_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/item/complex_item.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:keframe/keframe.dart'; 4 | 5 | class ComplexCommonOptExample extends StatelessWidget { 6 | const ComplexCommonOptExample({Key key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | resizeToAvoidBottomInset: true, 12 | appBar: AppBar( 13 | title: const Text('Optimization complex common'), 14 | ), 15 | body: Column(children: [ 16 | rowItem(), 17 | rowItem(), 18 | rowItem(), 19 | rowItem(), 20 | rowItem(), 21 | rowItem(), 22 | const Text('说明:直接给复杂组件嵌套 FrameSeparateWidget,转场动画更加流畅'), 23 | const Text( 24 | 'Note: Directly to complex components nested FrameSeparateWidget, transition animation more smooth'), 25 | ]), 26 | ); 27 | } 28 | 29 | Widget rowItem() { 30 | return Row( 31 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 32 | children: [ 33 | // It's the complex widget in your business 34 | FrameSeparateWidget( 35 | placeHolder: Container( 36 | color: Colors.lightGreen[300], 37 | height: 60, 38 | ), 39 | child: const CellWidget( 40 | index: 0, 41 | ), 42 | ), 43 | FrameSeparateWidget( 44 | placeHolder: Container( 45 | color: Colors.lightGreen[300], 46 | height: 60, 47 | ), 48 | child: const CellWidget( 49 | index: 1, 50 | ), 51 | ) 52 | ], 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/lib/page/opt/list_opt_example1.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'package:keframe/keframe.dart'; 4 | 5 | import '../../item/complex_item.dart'; 6 | 7 | class ComplexListOptExample1 extends StatefulWidget { 8 | const ComplexListOptExample1({Key key}) : super(key: key); 9 | 10 | @override 11 | ComplexListOptExample1State createState() => ComplexListOptExample1State(); 12 | } 13 | 14 | class ComplexListOptExample1State extends State { 15 | int childCount = 100; 16 | 17 | ScrollController scrollController; 18 | double scrollPos = 1500; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | scrollController = ScrollController(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | resizeToAvoidBottomInset: true, 30 | appBar: AppBar( 31 | title: const Text('Optimization example(Optimization example) 1'), 32 | ), 33 | body: Column( 34 | children: [ 35 | Expanded( 36 | child: ListView.builder( 37 | cacheExtent: 500, 38 | controller: scrollController, 39 | itemCount: childCount, 40 | itemBuilder: (BuildContext c, int i) => FrameSeparateWidget( 41 | index: i, 42 | placeHolder: Container( 43 | color: (i % 3 == 0) 44 | ? Colors.lightGreen[300] 45 | : (i % 3 == 1) 46 | ? Colors.lightGreen[500] 47 | : Colors.lightGreen[700], 48 | height: 60, 49 | ), 50 | child: CellWidget( 51 | color: i.isEven ? Colors.red : Colors.blue, 52 | index: i, 53 | ), 54 | ), 55 | ), 56 | ), 57 | FrameSeparateWidget( 58 | index: -1, 59 | child: operateBar(), 60 | ) 61 | ], 62 | ), 63 | ); 64 | } 65 | 66 | Widget operateBar() { 67 | return SizedBox( 68 | height: 150, 69 | child: Column( 70 | crossAxisAlignment: CrossAxisAlignment.start, 71 | children: [ 72 | Expanded( 73 | child: Wrap( 74 | children: [ 75 | ElevatedButton( 76 | onPressed: () { 77 | childCount += 20; 78 | setState(() {}); 79 | }, 80 | child: const Text( 81 | 'setState increase 20 items', 82 | style: TextStyle(fontSize: 14), 83 | )), 84 | ElevatedButton( 85 | onPressed: () { 86 | scrollController.animateTo(scrollPos, 87 | duration: Duration( 88 | milliseconds: scrollPos == 0 ? 1500 : 600), 89 | curve: Curves.easeInOut); 90 | scrollPos = scrollPos >= 6000 ? 0 : scrollPos + 1500; 91 | setState(() {}); 92 | }, 93 | child: Text( 94 | 'Scroll to $scrollPos offset', 95 | style: const TextStyle(fontSize: 14), 96 | )), 97 | const Text( 98 | '''Note: Nest the FrameSeparateWidget in the ListView.builder and in the action area at the bottom. If you know the width and height of the actual item, it's best to keep the placeholder consistent. If the actual item itself is complex, you can nest frame components in multiple layers within the item. This scenario suggests setting the ListView's cacheExtent to a larger size, such as 500'''), 99 | const Text( 100 | '说明:将分帧组件嵌套在 builder 以及底部的操作区域。如果已知实际 item 的宽高信息,最好让 placeholder 保持一致。如果实际的 item 本身比较复杂,可以在 item 中多层嵌套分帧组件。这种场景建议将 ListView 的 cacheExtent 设置大一点如 500'), 101 | ], 102 | ), 103 | ) 104 | ], 105 | ), 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /example/lib/page/opt/list_opt_example2.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/rendering.dart'; 4 | import 'package:keframe/keframe.dart'; 5 | 6 | import '../../item/complex_item.dart'; 7 | import 'list_opt_example3.dart'; 8 | 9 | class ComplexListOptExample2 extends StatefulWidget { 10 | const ComplexListOptExample2({Key key}) : super(key: key); 11 | 12 | @override 13 | ComplexListOptExample2State createState() => ComplexListOptExample2State(); 14 | } 15 | 16 | class ComplexListOptExample2State extends State { 17 | int childCount = 100; 18 | 19 | ScrollController scrollController; 20 | double scrollPos = 1500; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | scrollController = ScrollController(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Scaffold( 31 | resizeToAvoidBottomInset: true, 32 | appBar: AppBar( 33 | title: const Text('Optimization example 2'), 34 | ), 35 | body: Column( 36 | children: [ 37 | Expanded( 38 | child: ListView.builder( 39 | cacheExtent: 500, 40 | controller: scrollController, 41 | itemCount: childCount, 42 | itemBuilder: (BuildContext c, int i) => FrameSeparateWidget( 43 | index: i, 44 | placeHolder: Container( 45 | color: i.isEven ? Colors.red : Colors.blue, 46 | height: 40, 47 | ), 48 | child: CellWidget( 49 | color: i.isEven ? Colors.red : Colors.blue, 50 | index: i, 51 | ), 52 | ), 53 | ), 54 | ), 55 | operateBar() 56 | ], 57 | ), 58 | ); 59 | } 60 | 61 | Widget operateBar() { 62 | logcat('operateBar build $scrollPos'); 63 | return SizedBox( 64 | height: 220, 65 | child: Column( 66 | crossAxisAlignment: CrossAxisAlignment.start, 67 | children: [ 68 | Wrap( 69 | children: [ 70 | ElevatedButton( 71 | onPressed: () { 72 | childCount += 20; 73 | setState(() {}); 74 | }, 75 | child: const Text( 76 | 'setState increase 20 items', 77 | style: TextStyle(fontSize: 14), 78 | )), 79 | ElevatedButton( 80 | onPressed: () { 81 | scrollController.animateTo(scrollPos, 82 | duration: 83 | Duration(milliseconds: scrollPos == 0 ? 1500 : 600), 84 | curve: Curves.easeInOut); 85 | scrollPos = scrollPos >= 6000 ? 0 : scrollPos + 1500; 86 | setState(() {}); 87 | }, 88 | child: Text( 89 | 'Scroll to $scrollPos offset', 90 | style: const TextStyle(fontSize: 14), 91 | )), 92 | ], 93 | ), 94 | const Text( 95 | '说明:不确定实际 item 高度的时候,由于 placeHolder 和实际 item 高度不一致,所以出现列表抖动,参考下一个案例'), 96 | const Text( 97 | 'Note: When the actual item height is not certain, due to the placeHolder and the actual item height is inconsistent. Therefore, list jitter will occur. Refer to the next case for solution:'), 98 | ElevatedButton( 99 | onPressed: () { 100 | Navigator.of(context).push(CupertinoPageRoute( 101 | builder: (BuildContext context) => 102 | const ComplexListOptExample3())); 103 | }, 104 | child: const Text( 105 | 'Jump to next case', 106 | style: TextStyle(fontSize: 14), 107 | )) 108 | ], 109 | ), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /example/lib/page/opt/list_opt_example3.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'package:keframe/keframe.dart'; 4 | 5 | import '../../item/complex_item.dart'; 6 | 7 | class ComplexListOptExample3 extends StatefulWidget { 8 | const ComplexListOptExample3({Key key}) : super(key: key); 9 | 10 | @override 11 | ComplexListOptExample3State createState() => ComplexListOptExample3State(); 12 | } 13 | 14 | class ComplexListOptExample3State extends State { 15 | int childCount = 100; 16 | 17 | ScrollController scrollController; 18 | double scrollPos = 1500; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | scrollController = ScrollController(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | resizeToAvoidBottomInset: true, 30 | appBar: AppBar( 31 | title: const Text('Optimization example 3'), 32 | ), 33 | body: Column( 34 | children: [ 35 | Expanded( 36 | child: SizeCacheWidget( 37 | child: ListView.builder( 38 | cacheExtent: 500, 39 | controller: scrollController, 40 | itemCount: childCount, 41 | itemBuilder: (BuildContext c, int i) => FrameSeparateWidget( 42 | index: i, 43 | placeHolder: Container( 44 | color: i.isEven ? Colors.red : Colors.blue, 45 | height: 40, 46 | ), 47 | child: CellWidget( 48 | color: i.isEven ? Colors.red : Colors.blue, 49 | index: i, 50 | ), 51 | ), 52 | ), 53 | ), 54 | ), 55 | FrameSeparateWidget( 56 | index: -1, 57 | child: operateBar(), 58 | ) 59 | ], 60 | ), 61 | ); 62 | } 63 | 64 | Widget operateBar() { 65 | return SizedBox( 66 | height: 200, 67 | child: Column( 68 | crossAxisAlignment: CrossAxisAlignment.start, 69 | children: [ 70 | Wrap( 71 | children: [ 72 | ElevatedButton( 73 | onPressed: () { 74 | childCount += 20; 75 | setState(() {}); 76 | }, 77 | child: const Text( 78 | 'setState increase 20 items', 79 | style: TextStyle(fontSize: 14), 80 | )), 81 | ElevatedButton( 82 | onPressed: () { 83 | scrollController.animateTo(scrollPos, 84 | duration: 85 | Duration(milliseconds: scrollPos == 0 ? 1500 : 600), 86 | curve: Curves.easeInOut); 87 | scrollPos = scrollPos >= 6000 ? 0 : scrollPos + 1500; 88 | setState(() {}); 89 | }, 90 | child: Text( 91 | 'Scroll to $scrollPos offset', 92 | style: const TextStyle(fontSize: 14), 93 | )), 94 | ], 95 | ), 96 | const Text( 97 | 'Note: Set an approximate height to the placeholder when you are not sure of the actual item height. We also nested the ListView in the SizeCacheWidget. Refer to lazy loading schemes such as H5 to set the size of the placeholders for the rendered widgets. During scrolling, rendered items will not jump. '), 98 | const Text( 99 | '说明:当不确定实际 item 高度的时候,给 placeholder 设置一个近似的高度。并且在将 ListView 嵌套在 SizeCacheWidget 中。参考一些延迟加载方案,如 H5 的做法,对于已渲染过的 widget 设置占位的尺寸。在滚动过程中,已经渲染过的 item 将不会出现跳动情况。') 100 | ], 101 | ), 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /example/lib/page/opt/list_opt_example4.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'package:keframe/keframe.dart'; 4 | 5 | import '../../item/complex_item.dart'; 6 | 7 | class ComplexListOptExample4 extends StatefulWidget { 8 | const ComplexListOptExample4({Key key}) : super(key: key); 9 | 10 | @override 11 | ComplexListOptExample4State createState() => ComplexListOptExample4State(); 12 | } 13 | 14 | class ComplexListOptExample4State extends State { 15 | int childCount = 100; 16 | 17 | ScrollController scrollController; 18 | double scrollPos = 1500; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | scrollController = ScrollController(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | resizeToAvoidBottomInset: true, 30 | appBar: AppBar( 31 | title: const Text('Optimization example 4'), 32 | ), 33 | body: Column( 34 | children: [ 35 | Expanded( 36 | child: SizeCacheWidget( 37 | estimateCount: 20, 38 | child: ListView.builder( 39 | controller: scrollController, 40 | itemCount: childCount, 41 | itemBuilder: (BuildContext c, int i) => FrameSeparateWidget( 42 | index: i, 43 | placeHolder: Container( 44 | color: i.isEven ? Colors.red : Colors.blue, 45 | height: 60, 46 | ), 47 | child: CellWidget( 48 | color: i.isEven ? Colors.red : Colors.blue, 49 | index: i, 50 | ), 51 | ), 52 | ), 53 | ), 54 | ), 55 | operateBar() 56 | ], 57 | ), 58 | ); 59 | } 60 | 61 | Widget operateBar() { 62 | return SizedBox( 63 | height: 200, 64 | child: Column( 65 | crossAxisAlignment: CrossAxisAlignment.start, 66 | children: [ 67 | Wrap( 68 | children: [ 69 | ElevatedButton( 70 | onPressed: () { 71 | childCount += 20; 72 | setState(() {}); 73 | }, 74 | child: const Text( 75 | 'setState increase 20 items', 76 | style: TextStyle(fontSize: 14), 77 | )), 78 | ElevatedButton( 79 | onPressed: () { 80 | scrollController.animateTo(scrollPos, 81 | duration: 82 | Duration(milliseconds: scrollPos == 0 ? 1500 : 600), 83 | curve: Curves.easeInOut); 84 | scrollPos = scrollPos >= 6000 ? 0 : scrollPos + 1500; 85 | setState(() {}); 86 | }, 87 | child: Text( 88 | 'Scroll to $scrollPos offset', 89 | style: const TextStyle(fontSize: 14), 90 | )), 91 | ], 92 | ), 93 | const Text( 94 | 'Note: If the maximum number of list items on a screen can be roughly estimated as 10, set the estimateCount for SizeCacheWidget to 10*2. Fast scroll scenario builds are more responsive and memory stable'), 95 | const Text( 96 | '说明:如果能粗略估计一屏上列表项的最大数量如 10,将 SizeCacheWidget 的 estimateCount 设置为 10*2。快速滚动场景构建响应更快,并且内存更稳定'), 97 | ], 98 | ), 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /example/lib/page/opt/list_opt_example5.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'package:keframe/keframe.dart'; 4 | 5 | import '../../animation/opacity_animation.dart'; 6 | import '../../item/complex_item.dart'; 7 | 8 | class ComplexListOptExample5 extends StatefulWidget { 9 | const ComplexListOptExample5({Key key}) : super(key: key); 10 | 11 | @override 12 | ComplexListOptExample5State createState() => ComplexListOptExample5State(); 13 | } 14 | 15 | class ComplexListOptExample5State extends State { 16 | int childCount = 100; 17 | 18 | ScrollController scrollController; 19 | double scrollPos = 1500; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | scrollController = ScrollController(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Scaffold( 30 | resizeToAvoidBottomInset: true, 31 | appBar: AppBar( 32 | title: const Text('Optimization example 5'), 33 | ), 34 | body: Column( 35 | children: [ 36 | Expanded( 37 | child: ListView.builder( 38 | cacheExtent: 500, 39 | controller: scrollController, 40 | itemCount: childCount, 41 | itemBuilder: (BuildContext c, int i) => FrameSeparateWidget( 42 | index: i, 43 | placeHolder: Container( 44 | color: Colors.white, 45 | height: 60, 46 | ), 47 | child: OpacityTansWidget( 48 | child: CellWidget( 49 | color: i.isEven ? Colors.red : Colors.blue, 50 | index: i, 51 | ), 52 | ), 53 | ), 54 | ), 55 | ), 56 | FrameSeparateWidget( 57 | index: -1, 58 | child: operateBar(), 59 | ) 60 | ], 61 | ), 62 | ); 63 | } 64 | 65 | Widget operateBar() { 66 | return SizedBox( 67 | height: 150, 68 | child: Column( 69 | crossAxisAlignment: CrossAxisAlignment.start, 70 | children: [ 71 | Expanded( 72 | child: Wrap( 73 | children: [ 74 | ElevatedButton( 75 | onPressed: () { 76 | childCount += 20; 77 | setState(() {}); 78 | }, 79 | child: const Text( 80 | 'setState increase 20 items', 81 | style: TextStyle(fontSize: 14), 82 | )), 83 | ElevatedButton( 84 | onPressed: () { 85 | scrollController.animateTo(scrollPos, 86 | duration: Duration( 87 | milliseconds: scrollPos == 0 ? 1500 : 600), 88 | curve: Curves.easeInOut); 89 | scrollPos = scrollPos >= 6000 ? 0 : scrollPos + 1500; 90 | setState(() {}); 91 | }, 92 | child: Text( 93 | 'Scroll to $scrollPos offset', 94 | style: const TextStyle(fontSize: 14), 95 | )), 96 | const Text( 97 | 'Note: Combine with gradient, displacement and other animations to make the replacement process more smooth'), 98 | const Text('说明:结合渐变,位移等动画,可以让替换过程更加流畅') 99 | ], 100 | ), 101 | ) 102 | ], 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | keframe: 23 | path: ../ 24 | fps_monitor: ^1.12.13-1 25 | 26 | # The following adds the Cupertino Icons font to your application. 27 | # Use with the CupertinoIcons class for iOS style icons. 28 | cupertino_icons: ^0.1.2 29 | 30 | dev_dependencies: 31 | flutter_test: 32 | sdk: flutter 33 | lints: ^1.0.1 34 | 35 | 36 | # For information on the generic Dart part of this file, see the 37 | # following page: https://dart.dev/tools/pub/pubspec 38 | 39 | # The following section is specific to Flutter. 40 | flutter: 41 | 42 | # The following line ensures that the Material Icons font is 43 | # included with your application, so that you can use the icons in 44 | # the material Icons class. 45 | uses-material-design: true 46 | 47 | # To add assets to your application, add an assets section, like this: 48 | # assets: 49 | # - images/a_dot_burr.jpeg 50 | # - images/a_dot_ham.jpeg 51 | 52 | # An image asset can refer to one or more resolution-specific "variants", see 53 | # https://flutter.dev/assets-and-images/#resolution-aware. 54 | 55 | # For details regarding adding assets from package dependencies, see 56 | # https://flutter.dev/assets-and-images/#from-packages 57 | 58 | # To add custom fonts to your application, add a fonts section here, 59 | # in this "flutter" section. Each entry in this list should have a 60 | # "family" key with the font family name, and a "fonts" key with a 61 | # list giving the asset and other descriptors for the font. For 62 | # example: 63 | # fonts: 64 | # - family: Schyler 65 | # fonts: 66 | # - asset: fonts/Schyler-Regular.ttf 67 | # - asset: fonts/Schyler-Italic.ttf 68 | # style: italic 69 | # - family: Trajan Pro 70 | # fonts: 71 | # - asset: fonts/TrajanPro.ttf 72 | # - asset: fonts/TrajanPro_Bold.ttf 73 | # weight: 700 74 | # 75 | # For details regarding fonts from package dependencies, 76 | # see https://flutter.dev/custom-fonts/#from-packages 77 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/keframe.dart: -------------------------------------------------------------------------------- 1 | library keframe; 2 | 3 | export 'src/frame_separate_task.dart'; 4 | export 'src/frame_separate_widget.dart'; 5 | export 'src/layout_proxy.dart'; 6 | export 'src/logcat.dart'; 7 | export 'src/notification.dart'; 8 | export 'src/size_cache_widget.dart'; 9 | -------------------------------------------------------------------------------- /lib/src/frame_separate_task.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | import 'dart:developer'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/scheduler.dart'; 6 | import 'logcat.dart'; 7 | 8 | /// Copyright 2020 ke.com. All rights reserved. 9 | /// @date 5/7/21 11:10 AM 10 | /// @desc Split the frame queue and add tasks to the queue through the ScheduleTask. 11 | 12 | /// By default, there is no limit to the size of the queue, so limit the size after setting maxTaskSize. 13 | /// 14 | /// Reference [SchedulerBinding], each task has a priority, there are three categories: Idle Animation Touch. 15 | /// Each frame takes the first task (FIFO) out of the queue and schedules it until the queue is empty. 16 | /// Use the [SchedulingStrategy] policy to determine whether the task should be executed: 17 | /// 18 | /// If the policy is successful, the task is executed 19 | /// If the policy fails, a scheduling attempt is made after the next frame is rendered 20 | class FrameSeparateTaskQueue { 21 | FrameSeparateTaskQueue._(); 22 | 23 | bool _hasRequestedAnEventLoopCallback = false; 24 | int maxTaskSize = 0; 25 | 26 | static FrameSeparateTaskQueue? _instance; 27 | 28 | static FrameSeparateTaskQueue? get instance { 29 | _instance ??= FrameSeparateTaskQueue._(); 30 | return _instance; 31 | } 32 | 33 | SchedulingStrategy schedulingStrategy = defaultSchedulingStrategy; 34 | 35 | final Queue> _taskQueue = ListQueue(); 36 | 37 | int get taskLength => _taskQueue.length; 38 | 39 | Future handleEventLoopCallback() async { 40 | if (_taskQueue.isEmpty) return false; 41 | final TaskEntry entry = _taskQueue.first; 42 | if (schedulingStrategy( 43 | priority: entry.priority, scheduler: SchedulerBinding.instance)) { 44 | try { 45 | _taskQueue.removeFirst(); 46 | entry.run(); 47 | } catch (exception, exceptionStack) { 48 | StackTrace? callbackStack; 49 | assert(() { 50 | callbackStack = entry.debugStack; 51 | return true; 52 | }()); 53 | FlutterError.reportError(FlutterErrorDetails( 54 | exception: exception, 55 | stack: exceptionStack, 56 | library: 'scheduler library', 57 | context: ErrorDescription('during a task callback'), 58 | informationCollector: (callbackStack == null) 59 | ? null 60 | : () sync* { 61 | yield DiagnosticsStackTrace( 62 | '\nThis exception was thrown in the context of a scheduler callback. ' 63 | 'When the scheduler callback was _registered_ (as opposed to when the ' 64 | 'exception was thrown), this was the stack', 65 | callbackStack, 66 | ); 67 | }, 68 | )); 69 | } 70 | return _taskQueue.isNotEmpty; 71 | } 72 | return true; 73 | } 74 | 75 | /// Ensures that the scheduler services a task scheduled by [scheduleTask]. 76 | Future _ensureEventLoopCallback() async { 77 | assert(_taskQueue.isNotEmpty); 78 | if (_hasRequestedAnEventLoopCallback) return; 79 | _hasRequestedAnEventLoopCallback = true; 80 | Timer.run(() { 81 | _removeIgnoreTasks(); 82 | _runTasks(); 83 | }); 84 | } 85 | 86 | /// Scheduled by _ensureEventLoopCallback. 87 | Future _runTasks() async { 88 | _hasRequestedAnEventLoopCallback = false; 89 | await SchedulerBinding.instance.endOfFrame; 90 | if (await handleEventLoopCallback()) _ensureEventLoopCallback(); 91 | } 92 | 93 | void shuffleTask(bool Function(TaskEntry taskEntry) condition) { 94 | _taskQueue.removeWhere((TaskEntry e) => condition(e)); 95 | } 96 | 97 | void _removeIgnoreTasks() { 98 | while (_taskQueue.isNotEmpty) { 99 | if (!_taskQueue.first.canIgnore()) { 100 | break; 101 | } 102 | _taskQueue.removeFirst(); 103 | } 104 | } 105 | 106 | Future scheduleTask( 107 | TaskCallback task, Priority priority, ValueGetter canIgnore, 108 | {String? debugLabel, Flow? flow, int? id}) { 109 | final TaskEntry entry = 110 | TaskEntry(task, priority.value, canIgnore, debugLabel, flow, id: id); 111 | _addTask(entry); 112 | _ensureEventLoopCallback(); 113 | return entry.completer.future; 114 | } 115 | 116 | void _addTask(TaskEntry _taskEntry) { 117 | if (maxTaskSize != 0 && _taskQueue.length >= maxTaskSize) { 118 | logcat('remove Task'); 119 | _taskQueue.removeFirst(); 120 | } 121 | _taskQueue.add(_taskEntry); 122 | } 123 | 124 | void resetMaxTaskSize() { 125 | maxTaskSize = 0; 126 | } 127 | } 128 | 129 | class TaskEntry { 130 | TaskEntry( 131 | this.task, this.priority, this.canIgnore, this.debugLabel, this.flow, 132 | {this.id}) { 133 | // ignore: prefer_asserts_in_initializer_lists 134 | assert(() { 135 | debugStack = StackTrace.current; 136 | return true; 137 | }()); 138 | completer = Completer(); 139 | } 140 | 141 | final TaskCallback task; 142 | final int priority; 143 | final String? debugLabel; 144 | final Flow? flow; 145 | final int? id; 146 | final ValueGetter canIgnore; 147 | StackTrace? debugStack; 148 | late Completer completer; 149 | 150 | void run() { 151 | if (!kReleaseMode) { 152 | Timeline.timeSync( 153 | debugLabel ?? 'Scheduled Task', 154 | () { 155 | completer.complete(task()); 156 | }, 157 | flow: flow != null ? Flow.step(flow!.id) : null, 158 | ); 159 | } else { 160 | completer.complete(task()); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /lib/src/frame_separate_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | 4 | import 'frame_separate_task.dart'; 5 | import 'layout_proxy.dart'; 6 | import 'logcat.dart'; 7 | import 'size_cache_widget.dart'; 8 | 9 | /// Copyright 2020 ke.com. All rights reserved. 10 | /// @date 5/7/21 11:48 AM 11 | /// @desc Framing component, which renders the child node in a separate frame 12 | /// after the placeholder is rendered in the first frame 13 | class FrameSeparateWidget extends StatefulWidget { 14 | const FrameSeparateWidget({ 15 | Key? key, 16 | this.index, 17 | this.placeHolder, 18 | @Deprecated( 19 | 'Migrate to builder. ' 20 | 'This feature was deprecated after 3.0.1', 21 | ) 22 | required this.child, 23 | this.builder, 24 | }) : super(key: key); 25 | 26 | factory FrameSeparateWidget.builder({ 27 | Key? key, 28 | int? index, 29 | Widget? placeHolder, 30 | required WidgetBuilder builder, 31 | }) => 32 | FrameSeparateWidget( 33 | index: index, 34 | key: key, 35 | placeHolder: placeHolder, 36 | builder: builder, 37 | child: const SizedBox.shrink(), 38 | ); 39 | 40 | /// The widget below this widget in the tree. 41 | @Deprecated( 42 | 'Migrate to builder. ' 43 | 'This feature was deprecated after 3.0.1', 44 | ) 45 | final Widget child; 46 | 47 | /// The placeholder widget sets components that are as close to the actual widget as possible 48 | final Widget? placeHolder; 49 | 50 | /// Identifies its own ID, used in a scenario where size information is stored 51 | final int? index; 52 | 53 | /// Signature for a function that creates a widget. 54 | /// The builder has a higher priority, prioritize using builder before child. 55 | final WidgetBuilder? builder; 56 | 57 | @override 58 | FrameSeparateWidgetState createState() => FrameSeparateWidgetState(); 59 | } 60 | 61 | class FrameSeparateWidgetState extends State { 62 | Widget? result; 63 | 64 | @override 65 | void initState() { 66 | super.initState(); 67 | result = widget.placeHolder ?? 68 | Container( 69 | height: 20, 70 | ); 71 | final Map? size = SizeCacheWidget.of(context)?.itemsSizeCache; 72 | Size? itemSize; 73 | if (size != null && size.containsKey(widget.index)) { 74 | itemSize = size[widget.index]; 75 | } 76 | if (itemSize != null) { 77 | result = SizedBox( 78 | width: itemSize.width, 79 | height: itemSize.height, 80 | child: result, 81 | ); 82 | } 83 | transformWidget(); 84 | } 85 | 86 | @override 87 | void didUpdateWidget(FrameSeparateWidget oldWidget) { 88 | super.didUpdateWidget(oldWidget); 89 | transformWidget(); 90 | } 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | return ItemSizeInfoNotifier(index: widget.index, child: result); 95 | } 96 | 97 | void transformWidget() { 98 | SchedulerBinding.instance.addPostFrameCallback((Duration t) { 99 | FrameSeparateTaskQueue.instance!.scheduleTask(() { 100 | if (mounted) { 101 | setState(() { 102 | result = widget.builder?.call(context) ?? widget.child; 103 | }); 104 | } 105 | }, Priority.animation, () => !mounted, id: widget.index); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/src/layout_proxy.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | 4 | import 'notification.dart'; 5 | 6 | /// Copyright 2020 ke.com. All rights reserved. 7 | /// @date 5/7/21 11:53 AM 8 | /// @desc Pass up the Size information of the child node 9 | class ItemSizeInfoNotifier extends SingleChildRenderObjectWidget { 10 | const ItemSizeInfoNotifier({ 11 | Key? key, 12 | required this.index, 13 | required Widget? child, 14 | }) : super(key: key, child: child); 15 | final int? index; 16 | 17 | @override 18 | InitialRenderSizeChangedWithCallback createRenderObject( 19 | BuildContext context) { 20 | return InitialRenderSizeChangedWithCallback( 21 | onLayoutChangedCallback: (size) { 22 | LayoutInfoNotification(index, size).dispatch(context); 23 | }); 24 | } 25 | } 26 | 27 | class InitialRenderSizeChangedWithCallback extends RenderProxyBox { 28 | InitialRenderSizeChangedWithCallback({ 29 | RenderBox? child, 30 | required this.onLayoutChangedCallback, 31 | }) : super(child); 32 | 33 | final Function(Size size) onLayoutChangedCallback; 34 | 35 | Size? _oldSize; 36 | 37 | @override 38 | void performLayout() { 39 | super.performLayout(); 40 | if (size != _oldSize) onLayoutChangedCallback(size); 41 | _oldSize = size; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/logcat.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | /// You can customize the component's log tag to make it easy to quickly define your own log 4 | const String bkFrame = 'BKFrame'; 5 | 6 | void logcat(String info) { 7 | debugPrint('$bkFrame $info'); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/notification.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Copyright 2020 ke.com. All rights reserved. 4 | /// @date 5/7/21 2:12 PM 5 | class LayoutInfoNotification extends Notification { 6 | LayoutInfoNotification(this.index, this.size); 7 | 8 | final Size size; 9 | final int? index; 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/size_cache_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'frame_separate_task.dart'; 3 | 4 | import 'logcat.dart'; 5 | import 'notification.dart'; 6 | 7 | /// Copyright 2020 ke.com. All rights reserved. 8 | /// @date 5/7/21 2:13 PM 9 | /// @desc > Cache child node information 10 | class SizeCacheWidget extends StatefulWidget { 11 | const SizeCacheWidget({Key? key, required this.child, this.estimateCount = 0}) 12 | : super(key: key); 13 | final Widget child; 14 | 15 | /// Estimate the number of children on the screen, which is used to set the size of the frame queue 16 | /// Optimizes the list of items on the current screen for delayed response in fast scrolling scenarios 17 | final int estimateCount; 18 | 19 | static SizeCacheWidgetState? of(BuildContext context) { 20 | return context.findAncestorStateOfType(); 21 | } 22 | 23 | @override 24 | SizeCacheWidgetState createState() => SizeCacheWidgetState(); 25 | } 26 | 27 | class SizeCacheWidgetState extends State { 28 | /// Stores the Size of the child node's report 29 | Map itemsSizeCache = {}; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | setSeparateFramingTaskQueue(); 35 | } 36 | 37 | @override 38 | void didUpdateWidget(SizeCacheWidget oldWidget) { 39 | super.didUpdateWidget(oldWidget); 40 | setSeparateFramingTaskQueue(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return Builder( 46 | builder: (BuildContext ctx) { 47 | return NotificationListener( 48 | onNotification: (LayoutInfoNotification notification) { 49 | saveLayoutInfo(notification.index, notification.size); 50 | return true; 51 | }, 52 | child: widget.child, 53 | ); 54 | }, 55 | ); 56 | } 57 | 58 | void saveLayoutInfo(int? index, Size size) { 59 | itemsSizeCache[index] = size; 60 | } 61 | 62 | void setSeparateFramingTaskQueue() { 63 | if (widget.estimateCount != 0) { 64 | FrameSeparateTaskQueue.instance!.maxTaskSize = widget.estimateCount; 65 | } 66 | } 67 | 68 | @override 69 | void dispose() { 70 | FrameSeparateTaskQueue.instance!.resetMaxTaskSize(); 71 | super.dispose(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: keframe 2 | description: Helps you improve the fluency of Flutter's app for any scenario 3 | version: 3.0.1 4 | homepage: https://github.com/LianjiaTech/keframe 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | flutter: '>=3.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | flutter: 19 | -------------------------------------------------------------------------------- /test/keframe_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | --------------------------------------------------------------------------------