├── .gitignore ├── LICENSE ├── README.md ├── Workflow.md ├── articles ├── .gitkeep ├── Dart 进阶 深入理解 Function & Closure.md ├── Flutter 深入理解BuildContext.md ├── Flutter 性能优化——如何避免应用 jank.md ├── Flutter 深入浅出Key.md ├── Flutter 状态管理指南篇——Provider.md ├── Flutter 通过 ServiceLocator 实现无 context 导航.md ├── Flutter Widget - Container 布局详解 └── Flutter 如何优雅的嵌入现有应用.md ├── pic ├── add_dependency.png ├── async_func.png ├── change_color_01.png ├── change_color_02.png ├── change_color_03.png ├── complex_state.png ├── container_layout-bigger.png ├── container_layout-constraints.png ├── container_layout-fixed_height_narrower.png ├── container_layout-fixed_height_wider.png ├── container_layout-force_wider.png ├── container_layout-higher.png ├── container_layout-smaller.png ├── container_layout-tree.png ├── container_layout-wider.png ├── docs_development_add-to-app_index_01.gif ├── docs_development_add-to-app_index_02.gif ├── fix_bug_provider.png ├── get_it.png ├── graphics_pipline.png ├── hybrid-ios-embed-xcode.png ├── hybrid-ios-framework-search-paths.png ├── jank.gif ├── load_balancer.png ├── load_balancer_init.png ├── provider_error.png ├── simple_state.png ├── state_management_demo.png ├── switcher.png ├── sync_func.png ├── thrio │ ├── thrio-architecture.png │ ├── thrio-pop.png │ ├── thrio-popTo.png │ ├── thrio-push.png │ ├── thrio-remove.png │ └── thrio.png └── tip_how_to_find_flutter_page_01.png ├── tips ├── .gitkeep ├── tip_how_to_ask_for_help.md ├── tip_how_to_control_image_cache.md ├── tip_how_to_find_flutter_page.md └── tips-publication │ └── tips_191123.md └── translations ├── .gitkeep ├── build-modes-in-flutter.md ├── dart-awesome-cheat-sheet-for-flutter-devs.md ├── docs_development_add-to-app_index.md ├── dont-use-to-prefix-your-routes-in-flutter.md └── 在既有iOS项目中添加Flutter module.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Awesome Tips 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter-Tips 2 | 知识小集 x Flutter 3 | 4 | ## 目录 5 | 6 | - `./tips` 小技巧 7 | - `./articles` 原创文章 8 | - `./translations` 翻译文章 9 | - `./pic` 图片资源 10 | 11 | ## 成员 12 | 13 | 知识小集 x Flutter 成员名单 14 | 15 | | [南峰子_老驴](https://weibo.com/touristdiary) | | [Lefex](https://github.com/lefex) 16 | ------------- | ------------- | ------------- | ------------- 17 | | [talisk](https://weibo.com/talisk) | | [BillFu](https://github.com/fuleibill) 18 | | [小德-kurt](https://github.com/koudle) | | [刘彦博](https://github.com/Realank) 19 | | [XinLei](https://github.com/Vadaski) || 20 | 21 | ## 参与本项目 22 | 23 | 请从这份详细的 [协作流程指南文档](./Workflow.md) 开始。 24 | -------------------------------------------------------------------------------- /Workflow.md: -------------------------------------------------------------------------------- 1 | # 协作流程 2 | 3 | ## 项目发布流程 4 | 5 | 1. Fork 本项目 6 | 2. 在 Issue 区发布任务并添加 TAG(原创 / 最佳实践 / 开发 Tips / 翻译) 7 | 8 | ## 项目认领流程 9 | 10 | 1. Fork 本项目 11 | 2. 在 Issue 区认领任务 12 | 3. 完成任务 13 | 4. 发 Pull Request 14 | 5. (可选)到 PR 区协助发表评论和建议 15 | 16 | ## 发布任务 17 | 18 | 1. 查阅 Issue 区,搜索已发布任务,确保无重复内容 19 | 2. 预估任务完成所需时间,并添加 TAG 20 | 3. 发布任务(issue) 21 | 22 | ## 认领任务 23 | 24 | 1. 查阅 issue 区,查看待认领任务。 25 | 2. 确认自己的时间和内容预期后,留言提出要认领这个任务,并给出预计完成时间。 26 | 27 | ## 提交(commit 和 Pull Request) 28 | 29 | 在确认自己文章内容正确并自查完成后,按照 git commit 规范进行提交,提交需要注意以下事项: 30 | 31 | - 请确保细粒度的提交,尽可能做到一个 commit 只作用于一个文档 32 | - 确保 commit message 清晰明了 33 | - 请 @ 一位成员,作为校对者 34 | - 校对完毕后,确认无问题合并该 PR 35 | 36 | 将相关 commit 提交至自己 Fork 的 Git 仓库后,发起 Pull Request。PR 过程中注意以下事项: 37 | 38 | 1. 一次 PR 只能包含一个文件 39 | 2. PR 标题格式样例: 40 | 41 | > `[翻译完成] 路径/文件名.md(请复制完整路径名,如 dart.cn/src/docs/***.md)` 42 | 43 | 3. PR Message 内容格式: 44 | 45 | > 完成[翻译 / 原创 / Tips etc.] 文件名.md 46 | > 47 | > 完成 Issue #Issue编号 48 | 49 | 4. 图片资源请统一存放在 `./pic` 文件夹中,并使用相对路径进行引用 50 | -------------------------------------------------------------------------------- /articles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/articles/.gitkeep -------------------------------------------------------------------------------- /articles/Dart 进阶 深入理解 Function & Closure.md: -------------------------------------------------------------------------------- 1 | # Dart 进阶 | 深入理解 Function & Closure 2 | 3 | # 前言 4 | 在最初设计 Dart 的时候,参考了 `JavaScript` 许多特性。无论是在异步处理,还是语法上,都能看到它的影子。熟悉 Dart 的同学应该明白,在 Dart 中一切皆为对象。不仅 `int`、`bool` 是通过 core library 提供的类创建出的对象,连函数也被看作是对象。(本文中可能会出现 **函数** / **方法** 二者仅叫法不同)而本文将带你深入理解 Dart 的函数 (Function)&闭包(Closure)。 5 | 6 | ## 什么是 Closure(闭包) 7 | 如果你从未听说过闭包,没关系,本节将会从零开始引入闭包这个概念。在正式介绍闭包之前,我们需要先来了解一下 **Lexical scoping**。 8 | 9 | ### 詞法作用域 Lexical scoping 10 | 也许你对这个词很陌生,但是它却是最熟悉的陌生人。我们先来看下面一段代码。 11 | ``` dart 12 | void main() { 13 | var a = 0; 14 | var a = 1; // Error:The name 'a' is already defined 15 | } 16 | ``` 17 | 你肯定已经发现了,我们在该段代码中犯了一个明显的错误。那就是定义了两次变量 `a`,而编译器也会提示我们,a 这个变量名已经被定义了。 18 | 19 | 这是由于,我们的变量都有它的 **词法作用域** ,在同一个词法作用域中仅允许存在一个名称为 `a` 的变量,且在编译期就能够提示语法错误。 20 | 21 | 这很好理解,如果一个 **Lexical scoping** 中存在两个同名变量 `a`,那么我们访问的时候从语法上就无法区分到底你是想要访问哪一个 `a` 了。 22 | 23 | > 上述代码中,我们在 `main` 函数的词法作用域中定义了两次 a 24 | 25 | 仅需稍作修改 26 | ``` dart 27 | void main() { 28 | var a = 1; 29 | print(a); // => 1 30 | } 31 | 32 | var a = 0; 33 | ``` 34 | 我们就能够正常打印出 `a` 的值为 1。 35 | 简单的解释,` var a = 0;` 是该 **dart 文件**的 **Lexical scoping** 中定义的变量,而 `var a = 1;` 是在 main 函数的 **Lexical scoping** 中定义的变量,二者不是一个空间,所以不会产生冲突。 36 | 37 | ### Function is Object 38 | 首先,要证明方法(函数)是一个对象这很简单。 39 | ``` dart 40 | print( (){} is Object ); // true 41 | ``` 42 | `(){}` 为一个匿名函数,我们可以看到输出为 `true`。 43 | 44 | 知道了 Function is Object 还不够,我们应该如何看待它呢。 45 | ``` dart 46 | void main() { 47 | var name = 'Vadaski'; 48 | 49 | var printName = (){ 50 | print(name); 51 | }; 52 | } 53 | ``` 54 | 可以很清楚的看到,我们可以在 `main` 函数内定义了一个新的方法,而且还能够将这个方法赋值给一个变量 `printName`。 55 | 56 | 但是如果你运行这段代码,你将看不到任何输出,这是为什么呢。 57 | 58 | 实际上我们在这里定义了 `printName` 之后,并没有真正的去执行它。我们知道,要执行一个方法,需要使用 `XXX()` 才能真正执行。 59 | 60 | ``` dart 61 | void main() { 62 | var name = 'Vadaski'; 63 | 64 | var printName = (){ 65 | print(name); 66 | }; 67 | 68 | printName(); // Vadaski 69 | } 70 | ``` 71 | 上面这个例子非常常见,在 `printName` 内部访问到了外部定义的变量 `name`。也就是说,一个 Lexical scoping **内部** 是能够访问到 **外部** Lexical scoping 中定义的变量的。 72 | 73 | ### Function + Lexical scoping 74 | **内部**访问**外部**定义的变量是 ok 的,很容易就能够想到,外部是否可以访问内部定义的变量呢。 75 | 76 | 如果是正常访问的话,就像下面这样。 77 | 78 | ``` dart 79 | void main() { 80 | 81 | var printName = (){ 82 | var name = 'Vadaski'; 83 | }; 84 | printName(); 85 | 86 | print(name); // Error:Undefined name 'name' 87 | } 88 | ``` 89 | 90 | 这里出现了**未定义该变量**的错误警告,可以看出 `printName` 中定义的变量,对于 `main` 函数中的变量是不可见的。Dart 和 JavaScript 一样具有链式作用域,也就是说,**子作用域**可以访问**父(甚至是祖先)作用域**中的变量,而反过来不行。 91 | 92 | #### 访问规则 93 | 94 | 从上面的例子我们可以看出,**Lexical scoping** 实际上是以链式存在的。一个 scope 中可以开一个新的 scope,而不同 scope 中是可以允许重名变量的。那么我们在某个 scope 中访问一个变量,究竟是基于什么规则来访问变量的呢。 95 | 96 | ``` dart 97 | void main() { 98 | var a = 1; 99 | firstScope(){ 100 | var a = 2; 101 | print('$a in firstScope'); //2 in firstScope 102 | } 103 | print('$a in mainScope'); //1 in mainScope 104 | firstScope(); 105 | } 106 | ``` 107 | 108 | 在上面这个例子中我们可以看到,在 main 和 firstScope 中都定义了变量 a。我们在 `firstScope` 中 print,输出了 `2 in firstScope` 而在 main 中 print 则会输出 `1 in mainScope` 。 109 | 110 | 我们已经可以总结出规律了:**近者优先**。 111 | 112 | 如果你在某个 scope 中访问一个变量,它首先会看当前 scope 中是否已经定义该变量,如果已经定义,那么就使用该变量。如果当前 scope 没找到该变量,那么它就会在它的上一层 scope 中寻找,以此类推,直到最初的 scope。如果所有 scope 链上都不存在该变量,则会提示 `Error:Undefined name 'name'`。 113 | 114 | > Tip: Dart scope 中的变量是静态确定的,如何理解呢? 115 | > 116 | > ``` dart 117 | > void main() { 118 | > print(a); // Local variable 'a' can't be referenced before it is declared 119 | > var a; 120 | > } 121 | > var a = 0; 122 | > ``` 123 | > 124 | > 我们可以看到,虽然在 main 的父 scope 中存在变量 a,且已经赋值,但是我们在 main 的 scope 中也定义了变量 a。因为是静态确定的,所以在 print 的时候会优先使用当前 scope 中定义的 a,而这时候 a 的定义在 print 之后,同样也会导致编译器错误:Local variable 'a' can't be referenced before it is declared。 125 | 126 | ### Closure 的定义 127 | 有了上面这些知识,我们现在可以来看看 Closure 的定义了。 128 | > A closure is a function object that has access to variables in its lexical scope, even when the function is used outside of its original scope. 129 | 130 | > 闭包 即一个函数对象,即使函数对象的调用在它原始作用域之外,依然能够访问在它词法作用域内的变量。 131 | 132 | 简要概括 Closure 的话,它就是**有状态**的函数。 133 | ### 函数状态 134 | #### 无状态函数 135 | 通常我们执行一个函数,它都是**无状态**的。你可能会产生疑问,函数还有状态吗?我们还是看一个例子。 136 | ``` dart 137 | void main() { 138 | printNumber(); // 10 139 | printNumber(); // 10 140 | } 141 | 142 | void printNumber(){ 143 | int num = 0; 144 | for(int i = 0; i < 10; i++){ 145 | num++; 146 | } 147 | print(num); 148 | } 149 | ``` 150 | 上面的代码很好预测,它将会输出两次 10,我们多次调用一个函数的时候,它还是会得到一样的输出。 151 | 152 | 但是,当我们理解 Function is Object 之后,我们应该如何从 Object 的角度来看待函数的执行呢。 153 | 154 | 显然 `printNumber();` 创建了一个 Function 对象,但是我们没有将它赋值给任何变量,下次一个 `printNumber();` 实际上创建了一个新的 Function,两个对象都执行了一遍方法体,所以得到了相同的输出。 155 | 156 | #### 有状态函数 157 | 无状态函数很好理解,我们现在可以来看看有状态的函数了。 158 | ``` dart 159 | void main() { 160 | var numberPrinter = (){ 161 | int num = 0; 162 | return (){ 163 | for(int i = 0; i < 10; i++){ 164 | num++; 165 | } 166 | print(num); 167 | }; 168 | }; 169 | 170 | var printNumber = numberPrinter(); 171 | printNumber(); // 10 172 | printNumber(); // 20 173 | } 174 | ``` 175 | 上面这段代码同样执行了两次 `printNumber();`,然而我们却得到了不同的输出 10,20。好像有点 **状态** 的味道了呢。 176 | 177 | 但看上去似乎还是有些难以理解,让我们一层一层来看。 178 | 179 | ``` dart 180 | var numberPrinter = (){ 181 | int num = 0; 182 | /// execute function 183 | }; 184 | ``` 185 | 首先我们定义了一个 Function 对象,然后把交给 `numberPrinter` 管理。在创建出来的这个 Function 的 **Lexical scoping** 中定义了一个 num 变量,并赋值为 0。 186 | > 注意:这时候该方法并不会立刻执行,而是等调用了 `numberPrinter()` 的时候才执行。所以这时候 num 是不存在的。 187 | 188 | ``` dart 189 | return (){ 190 | for(int i = 0; i < 10; i++){ 191 | num++; 192 | } 193 | print(num); 194 | }; 195 | ``` 196 | 然后返回了一个 Function。这个 Function 能够拿到其父级 scope 中的 num ,并让其增加 10,然后打印 `num` 的值。 197 | 198 | ``` dart 199 | var printNumber = numberPrinter(); 200 | ``` 201 | 然后我们通过调用 numberPrinter(),创建了该 Function 对象,**这就是一个 Closure!** 这个对象**真正执行**我们刚才定义的 `numberPrinter`,并且在它的内部的 scope 中就定义了一个 int 类型的 `num`。然后返回了一个方法给 `printNumber`。 202 | 203 | > 实际上返回的 匿名 Function 又是另一个闭包了。 204 | 205 | 然后我们执行第一次 `printNumber()`,这时候将会获得闭包储存的 num 变量,执行下面的内容。 206 | ``` dart 207 | // num: 0 208 | for(int i = 0; i < 10; i++){ 209 | num++; 210 | } 211 | print(num); 212 | ``` 213 | 最开始 printNumber 的 scope 中储存的 num 为 0,所以经过 10 次自增,num 的值为 10,最后 `print` 打印了 10。 214 | 215 | 而第二次执行 `printNumber()` 我们使用的还是同一个 `numberPrinter` 对象,这个对象在第一次执行完毕后,其 num 已经为 10,所以第二次执行后,是从 10 开始自增,那么最后 `print` 的结果自然就是 20 了。 216 | 217 | 在整个调用过程中,printNumber 作为一个 closure,它保存了内部 num 的状态,只要 printNumber 不被回收,那么其内部的所有对象都不会被 GC 掉。 218 | 219 | > 所以我们也需要注意到闭包可能会造成内存泄漏,或带来内存压力问题。 220 | 221 | ### 到底啥是闭包 222 | 再回过头来理解一下,我们对于闭包的定义就应该好理解了。 223 | 224 | > 闭包 即一个函数对象,即使函数对象的调用在它原始作用域之外,依然能够访问在它词法作用域内的变量。 225 | 226 | 在刚才的例子中,我们的 num 是在 `numberPrinter` 内部定义的,可是我们可以通过返回的 Function 在外部访问到了这个变量。而我们的 `printNumber` 则一直保存了 `num`。 227 | 228 | ## 分阶段看闭包 229 | 在我们使用闭包的时候,我将它看为三个阶段。 230 | ### 定义阶段 231 | 这个阶段,我们定义了 Function 作为闭包,但是却没有真正执行它。 232 | ``` dart 233 | void main() { 234 | var numberPrinter = (){ 235 | int num = 0; 236 | return (){ 237 | print(num); 238 | }; 239 | }; 240 | ``` 241 | 这时候,由于我们只是定义了闭包,而没有执行,所以 num 对象是不存在的。 242 | 243 | ### 创建阶段 244 | ``` dart 245 | var printNumber = numberPrinter(); 246 | ``` 247 | 这时候,我们真正执行了 nu mberPrinter 闭包的内容,并返回执行结果,num 被创建出来。这时候,只要 printNumber 不被 GC,那么 num 也会一直存在。 248 | 249 | ### 访问阶段 250 | ``` dart 251 | printNumber(); 252 | printNumber(); 253 | ``` 254 | 然后我们可以通过某种方式访问 numberPrinter 闭包中的内容。(本例中间接访问了 num) 255 | 256 | 以上三个阶段仅方便理解,不是严谨描述。 257 | ## Closure 的应用 258 | 如果仅是理解概念,那么我们看了可能也就忘了。来点实在的,到底 Closure 可以怎么用? 259 | 260 | ### 在传递对象的位置执行方法 261 | 比如说我们有一个 Text Widget 的内容有些问题,直接给我们 show 了一个 Error Widget。这时候,我想打印一下这个内容看看到底发生了啥,你可以这样做。 262 | 263 | ``` dart 264 | Text((){ 265 | print(data); 266 | return data; 267 | }()) 268 | ``` 269 | 是不是很神奇,竟然还有这种操作。 270 | > Tip 立即执行闭包内容:我们这里通过闭包的语法 `(){}()` 立刻执行闭包的内容,并把我们的 data 返回。 271 | 272 | 虽然 Text 这里仅允许我们传一个 String,但是我依然可以执行 `print` 方法。 273 | 274 | 另一个 case 是,如果我们想要仅在 debug 模式下执行某些语句,也可以通过 closure 配合断言来实现。 275 | 276 | ``` dart 277 | assert(() { 278 | child.owner._debugElementWasRebuilt(child);// execute some code 279 | return true; 280 | }()); 281 | ``` 282 | 283 | 解释一下,首先 assert 断言仅在 debug 模式下才会开启,所以断言内的内容可以仅在 debug 模式才得以执行。 284 | 285 | 然后我们知道,Function( ) 调用就会执行,所以这里我们通过匿名闭包 `(){}()` 立刻执行了闭包中的内容,并返回 true 给断言,让它不会挂掉。从而达到了仅在 debug 模式下执行该闭包内的语句。 286 | 287 | ### 实现策略模式 288 | 通过 closure 我们可以很方便实现策略模式。 289 | ``` dart 290 | void main(){ 291 | var res = exec(select('sum'),1 ,2); 292 | print(res); 293 | } 294 | 295 | Function select(String opType){ 296 | if(opType == 'sum') return sum; 297 | if(opType == 'sub') return sub; 298 | return (a, b) => 0; 299 | } 300 | 301 | int exec(NumberOp op, int a, int b){ 302 | return op(a,b); 303 | } 304 | 305 | int sum(int a, int b) => a + b; 306 | int sub(int a, int b) => a - b; 307 | 308 | typedef NumberOp = Function (int a, int b); 309 | ``` 310 | 通过 select 方法,可以动态选择我们要执行的具体方法。你可以在 https://dartpad.cn/143c33897a0eac7e2d627b01983b7307 运行这段代码。 311 | 312 | ### 实现 Builder 模式 / 懒加载 313 | 如果你有 Flutter 经验,那么你应该使用过 `ListView.builder`,它很好用对不对。我们只向 builder 属性传一个方法,`ListView` 就可以根据这个 `builder` 来构建它的每一个 item。实际上,这也是 closure 的一种体现。 314 | 315 | ``` dart 316 | ListView.builder({ 317 | //... 318 | @required IndexedWidgetBuilder itemBuilder, 319 | //... 320 | }) 321 | 322 | typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index); 323 | ``` 324 | Flutter 通过 typedef 定义了一种 Function,它接收 `BuildContext` 和 `int` 作为参数,然后会返回一个 Widget。对这样的 Function 我们将它定义为 `IndexedWidgetBuilder` 然后将它内部的 Widget 返回出来。这样外部的 scope 也能够访问 `IndexedWidgetBuilder` 的 scope 内部定义的 Widget,从而实现了 builder 模式。 325 | 326 | > 同样,ListView 的懒加载(延迟执行)也是闭包很重要的一个特性哦~ 327 | 328 | ## 牛刀小试 329 | 在学习了 closure 以后,我们来道题检验一下你是否真正理解了吧。 330 | ``` dart 331 | main(){ 332 | var counter = Counter(0); 333 | fun1(){ 334 | var innerCounter = counter; 335 | Counter incrementCounter(){ 336 | print(innerCounter.value); 337 | innerCounter.increment(); 338 | return innerCounter; 339 | } 340 | return incrementCounter; 341 | } 342 | 343 | var myFun = fun1(); 344 | print(myFun() == counter); 345 | print(myFun() == counter); 346 | } 347 | 348 | class Counter{ 349 | int value; 350 | Counter(int value) 351 | : this.value = value; 352 | 353 | increment(){ 354 | value++; 355 | } 356 | } 357 | ``` 358 | 359 | 上面这段代码会输出什么呢? 360 | 361 | 如果你已经想好了答案,就来看看是否正确吧 => https://dartpad.cn/75e338c727ae608cd31d389f7557a0f1 362 | 363 | 也欢迎大家在底下评论区一起讨论~ 364 | 365 | ## 写在最后 366 | 本文非常感谢 [@Realank Liu](https://juejin.im/user/5b5ae02df265da0f9e58a9a7) 的 Review 以及宝贵的建议~ 367 | 368 | 时隔半年来迟迟的更新,不知道是否对大家有点帮助呢~ Closure 在实现 Flutter 的诸多功能上都发挥着重要的作用,可以说它已经深入你编程的日常,默默帮助我们更好地编写 Dart 代码,作为一名不断精进的 Dart 开发者,是时候用起来啦~之后的文章中,我会逐渐转向 Dart,给大家带来更深入的内容,敬请期待! 369 | 370 | 如果您对本文还有任何疑问或者文章的建议,欢迎在下方评论区以及我的邮箱xinlei966@gmail.com 与我联系,我会及时回复! 371 | 372 | 后续我的博文将首发 [xinlei.dev](https://www.xinlei.dev),欢迎关注! -------------------------------------------------------------------------------- /articles/Flutter 深入理解BuildContext.md: -------------------------------------------------------------------------------- 1 | # Flutter | 深入理解BuildContext 2 | 3 | # 前言 4 | 最近看到一些刚接触 Flutter 的同学在进行页面跳转的时候,出现了这个问题。 5 | ``` 6 | flutter: Navigator operation requested with a context that does not include a Navigator. 7 | flutter: The context used to push or pop routes from the Navigator must be that of a widget that is a 8 | flutter: descendant of a Navigator widget. 9 | ``` 10 | 代码是这样的 11 | ``` dart 12 | import 'package:flutter/material.dart'; 13 | 14 | void main() => runApp(MyApp()); 15 | 16 | class MyApp extends StatelessWidget { 17 | @override 18 | Widget build(BuildContext context) { 19 | return MaterialApp( 20 | home: Scaffold( 21 | body: Center( 22 | child: FlatButton( 23 | onPressed: () { 24 | Navigator.of(context).push( 25 | MaterialPageRoute(builder: (context) => SecondPage())); 26 | }, 27 | child: Text('跳转')), 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | 34 | class SecondPage extends StatelessWidget { 35 | @override 36 | Widget build(BuildContext context) { 37 | return Scaffold( 38 | appBar: AppBar(), 39 | ); 40 | } 41 | } 42 | ``` 43 | 一眼看上去好像没什么问题,解决方式也很简单,把 home 部分作为一个新的 Widget 拆出来就可以了。 44 | ``` dart 45 | class MyApp extends StatelessWidget { 46 | @override 47 | Widget build(BuildContext context) { 48 | return MaterialApp( 49 | home: FirstPage(), 50 | ); 51 | } 52 | } 53 | 54 | class FirstPage extends StatelessWidget { 55 | @override 56 | Widget build(BuildContext context) { 57 | return Scaffold( 58 | body: Center( 59 | child: FlatButton( 60 | onPressed: () { 61 | Navigator.of(context).push( 62 | MaterialPageRoute(builder: (context) => SecondPage())); 63 | }, 64 | child: Text('跳转')), 65 | ), 66 | ); 67 | } 68 | } 69 | ``` 70 | 但是刚开始遇到这些东西的时候一定是很懵逼的。BuildContext 是什么鬼,为什么每次我们需要在 build 函数的时候传入一个 BuildContext?为什么我的 Navigator 操作会出现当前的 context 找不到 Navigator 的情况,为什么拆成新的 widget 就好了? 71 | 72 | 所以今天想顺着这个问题跟大家分享一下如何在 Flutter 中理解和使用 BuildContext。其中还会涉及到一些 widget 构建流程的地方,在正式开始之前先简单解释一下这几个概念。 73 | 74 | ### 什么是 Navigator,MaterialApp 做了什么 75 | 我们经常会在应用中打开许多页面,当我们返回的时候,它会先后退到上一个打开的页面,然后一层一层后退,没错这就是一个堆栈。而在 Flutter 中,则是由 Navigator 来负责管理维护这些页面堆栈。 76 | ``` dart 77 | //压一个新的页面到屏幕上 78 | Navigator.of(context).push 79 | //把路由顶层的页面移除 80 | Navigator.of(context).pop 81 | ``` 82 | 通常我们我们在构建应用的时候并没有手动去创建一个 Navigator,也能进行页面导航,这又是为什么呢。 83 | 84 | 没错,这个 Navigator 正是 MaterialApp 为我们提供的。但是如果 home,routes,onGenerateRoute 和 onUnknownRoute 都为 null,并且 builder 不为 null,MaterialApp 则不会创建任何 Navigator。 85 | 86 | 知道了 Navigator 和 MaterialApp 发挥的作用之后,我们再来看看 BuildContext。 87 | 88 | ## BuildContext 89 | 每次我们在编写界面部分代码的时候,都是在 build 函数中进行操作。而 build 函数则需要默认传入一个 BuildContext。我们来看看这到底是啥。 90 | ``` dart 91 | abstract class BuildContext { 92 | /// The current configuration of the [Element] that is this [BuildContext]. 93 | Widget get widget; 94 | 95 | /// The [BuildOwner] for this context. The [BuildOwner] is in charge of 96 | /// managing the rendering pipeline for this context. 97 | BuildOwner get owner; 98 | //... 99 | ``` 100 | 我们可以看到 BuildContext 其实是一个抽象类,但是每次 build 函数传进来的是什么呢。我们来看看构建视图的时候到底发生了什么。 101 | 102 | ### Flutter 如何构建视图 103 | 在 Flutter 中,Everything is Widget,我们通过构造函数嵌套 Widget 来编写 UI 界面。实际上,Widget 并不是真正要显示在屏幕上的东西,只是一个配置信息,它永远是 immutable 的,并且可以在多处重复使用。那真正持有“屏幕上的视图”/“UI控件”树是是什么呢?Element tree! 104 | 105 | 那我们来看一下,在构建视图的时候究竟发生了什么。这里以 Stateless Widget 为例。 106 | ``` dart 107 | abstract class StatelessWidget extends Widget { 108 | const StatelessWidget({ Key key }) : super(key: key); 109 | @override 110 | StatelessElement createElement() => StatelessElement(this); 111 | //... 112 | ``` 113 | 当要把这个 widget 装进视图树的时候,首先会去 createElement,并将当前 widget 传给 Element。 114 | 115 | 我们再来看一看这个 StatelessElement 是什么 116 | ``` dart 117 | class StatelessElement extends ComponentElement { 118 | /// Creates an element that uses the given widget as its configuration. 119 | StatelessElement(StatelessWidget widget) : super(widget); 120 | 121 | @override 122 | StatelessWidget get widget => super.widget; 123 | 124 | @override 125 | Widget build() => widget.build(this); 126 | 127 | @override 128 | void update(StatelessWidget newWidget) { 129 | super.update(newWidget); 130 | assert(widget == newWidget); 131 | _dirty = true; 132 | rebuild(); 133 | } 134 | } 135 | ``` 136 | 我们可以看到,通过将 widget 传入 StatelessElement 的构造函数,StatelessElement 保留了 widget 的引用,并且将会调用 build 方法。 137 | 138 | 而这个 build 方法真正调用的则是 widget 的 build 方法,并将 this,也就是该 StatelessElement 对象传入。我们知道,build 方法需要传入的是一个 BuildContext,为什么传进去了 StatelessElement?于是我们继续看。 139 | ``` dart 140 | class StatelessElement extends ComponentElement 141 | //... 142 | abstract class ComponentElement extends Element 143 | //... 144 | abstract class Element extends DiagnosticableTree implements BuildContext 145 | ``` 146 | 实际上是 Element 类实现了 BuildContext,并由 ComponentElement -> StatelessElement 继承。 147 | 148 | 所以我们现在再来看官方对于 BuildContext 的解释: 149 | 150 | **BuildContext**objects are actually **Element** objects. The **BuildContext**interface is used to discourage direct manipulation of **Element** objects. 151 | 152 | 153 | BuildContext 对象实际上就是 Element 对象,BuildContext 接口用于阻止对 Element 对象的直接操作。 154 | 155 | Cool!我们现在终于知道这个 BuildContext 是哪里来的了。让我们再来梳理一下,flutter 构建视图究竟做了什么。 156 | 157 | ### 视图树装载过程 158 | #### StatelessWidget 159 | - 首先它会调用 StatelessWidget的 createElement 方法,并根据这个 widget 生成 StatelesseElement 对象。 160 | - 将这个 StatelesseElement 对象挂载到 element 树上。 161 | - StatelesseElement 对象调用 widget 的 build 方法,并将 element 自身作为 BuildContext 传入。 162 | 163 | #### StatefulWidget 164 | - 首先同样也是调用 StatefulWidget 的 createElement 方法,并根据这个 widget 生成 StatefulElement 对象,并保留 widget 引用。 165 | - 将这个 StatefulElement 挂载到 Element 树上。 166 | - 根据 widget的 createState 方法创建 State。 167 | - StatefulElement 对象调用 state 的 build 方法,并将 element 自身作为 BuildContext 传入。 168 | 169 | 所以我们在 build 函数中所使用的 context,正是当前 widget 所创建的 Element 对象。 170 | 171 | ## of(context)方法 172 | 在 flutter 中我们经常会使用到这样的代码 173 | ``` dart 174 | //打开一个新的页面 175 | Navigator.of(context).push 176 | //打开Scaffold的Drawer 177 | Scaffold.of(context).openDrawer 178 | //获取display1样式文字主题 179 | Theme.of(context).textTheme.display1 180 | ``` 181 | 那么这个 of(context) 到底是个什么呢。我们这里以 Navigator 打开新页面为例。 182 | ``` dart 183 | static NavigatorState of( 184 | BuildContext context, { 185 | bool rootNavigator = false, 186 | bool nullOk = false, 187 | }) { 188 | //关键代码-----------------------------------------v 189 | 190 | final NavigatorState navigator = rootNavigator 191 | ? context.rootAncestorStateOfType(const TypeMatcher()) 192 | : context.ancestorStateOfType(const TypeMatcher()); 193 | 194 | //关键代码----------------------------------------^ 195 | assert(() { 196 | if (navigator == null && !nullOk) { 197 | throw FlutterError( 198 | 'Navigator operation requested with a context that does not include a Navigator.\n' 199 | 'The context used to push or pop routes from the Navigator must be that of a ' 200 | 'widget that is a descendant of a Navigator widget.' 201 | ); 202 | } 203 | return true; 204 | }()); 205 | return navigator; 206 | } 207 | ``` 208 | 可以看到,关键代码部分通过 context.rootAncestorStateOfType 向上遍历 Element tree,并找到最近匹配的 NavigatorState。也就是说 of 实际上是对 context 跨组件获取数据的一个封装。 209 | 210 | 而我们的 Navigator 的 push 操作就是通过找到的 NavigatorState 来完成的。 211 | 212 | 不仅如此,BuildContext 还有许多方法可以跨组件获取对象 213 | ``` 214 | ancestorInheritedElementForWidgetOfExactType(Type targetType) → InheritedElement 215 | 216 | ancestorRenderObjectOfType(TypeMatcher matcher) → RenderObject 217 | 218 | ancestorStateOfType(TypeMatcher matcher) → State 219 | 220 | ancestorWidgetOfExactType(Type targetType) → Widget 221 | 222 | findRenderObject() → RenderObject 223 | 224 | inheritFromElement(InheritedElement ancestor, { Object aspect }) → InheritedWidget 225 | 226 | inheritFromWidgetOfExactType(Type targetType, { Object aspect }) → InheritedWidget 227 | 228 | rootAncestorStateOfType(TypeMatcher matcher) → State 229 | 230 | visitAncestorElements(bool visitor(Element element)) → void 231 | 232 | visitChildElements(ElementVisitor visitor) → void 233 | ``` 234 | 需要注意的是,在 State 中 initState 阶段是无法跨组件拿数据的,只有在 didChangeDependencies 之后才可以使用这些方法。 235 | 236 | ## 回顾问题 237 | 我们现在再来看看之前遇到的 当前 context 不包含 Navigator 这个问题是不是很简单了呢。 238 | ``` dart 239 | class MyApp extends StatelessWidget { 240 | @override 241 | Widget build(BuildContext context) { 242 | return MaterialApp( 243 | home: Scaffold( 244 | body: Center( 245 | child: FlatButton( 246 | onPressed: () { 247 | Navigator.of(context).push( 248 | MaterialPageRoute(builder: (context) => SecondPage())); 249 | }, 250 | child: Text('跳转')), 251 | ), 252 | ), 253 | ); 254 | } 255 | } 256 | ``` 257 | 当我们在 build 函数中使 Navigator.of(context) 的时候,这个 context 实际上是通过 MyApp 这个 widget 创建出来的 Element 对象,而 of 方法向上寻找祖先节点的时候(MyApp的祖先节点)并不存在 MaterialApp,也就没有它所提供的 Navigator。 258 | 259 | 所以当我们把 Scaffold 部分拆成另外一个 widget 的时候,我们在 FirstPage 的 build 函数中,获得了 FirstPage 的 BuildContext,然后向上寻找发现了 MaterialApp,并找到它提供的 Navigator,于是就可以愉快进行页面跳转了。 260 | 261 | ## 参考资料 262 | - [Flutter Widgets101](https://www.youtube.com/watch?v=wE7khGHVkYY&index=2&list=PLOU2XLYxmsIJyiwUPCou_OVTpRIn_8UMd):Flutter 团队官方视频,介绍了 statelessWidget 与 StatefulWidget 究竟是怎么被创建的,推荐观看。 263 | 264 | # 写在最后 265 | 文章若有不对之处还请各位高手指出,欢迎在下方评论区以及我的邮箱1652219550a@gmail.com留言,我会在24小时内与您联系! 266 | -------------------------------------------------------------------------------- /articles/Flutter 性能优化——如何避免应用 jank.md: -------------------------------------------------------------------------------- 1 | # Flutter | 性能优化——如何避免应用 jank 2 | 3 | ## 前言 4 | 流畅的用户体验一直是每一位开发者的不断追求,为了让自己的应用是否能给用户带来持续的高帧率渲染体验,我们自然想要极力避免发生 jank(卡顿,不流畅)。 5 | 6 | 本文将会解释为什么即使在 Flutter 高性能的渲染能力下,应用还是可能会出现 jank,以及我们应该如何处理这些情况。这是 Flutter 性能分析系列的第一篇文章,后续将会持续剖析 Flutter 中渲染流程以及性能优化。 7 | 8 | ## 什么时候会产生 jank? 9 | 我见过许多开发者在刚上手了 Flutter 之后,尝试开发了一些应用,然而并没有取得比较好的性能表现。例如在长列表加载的时候可能会出现明显卡顿的情况(当然这并不常见)。当你对这种情况没有头绪的时候,可能会误以为是 Flutter 的渲染还不够高效,然而大概率是你的 **姿势不对**。我们来看一个小例子。 10 | 11 | ![](../pic/jank.gif) 12 | 13 | 在屏幕中心有一个一直旋转的 FlutterLogo,当我们点击按钮后,开始计算 0 + 1 + ... +1000000000。这里可以很明显的感受到明显的卡顿。为什么会出现这种情况呢? 14 | 15 | ### Flutter Rendering Pipeline 16 | 17 | ![](../pic/graphics_pipline.png) 18 | 19 | Flutter 由 GPU 的 vsync 信号驱动,每一次信号都会走一个完整的 pipeline(我们现在并不需要关心整个流程的具体细节),而通常我们开发者会接触到的部分就是使用 dart 代码,经过 build -> layout -> paint 最后生成一个 layer,整个过程都在 dart UI 线程中完成。Flutter 需要在每秒60次,也就是 16.67 ms 通过 vsync 进行一次 pipline,而整个过程是一次**同步处理**。如果我们的整个 pipline 耗时超过 16.67 ms,就会出现掉帧。 20 | 21 | 在 Android 中我们是不能在 主线程(UI线程)中进行耗时操作的,如果做一些比较繁重的操作,比如网络请求、数据库操作等相关操作,就会导致 UI 线程卡住,触发 ANR。所以我们需要把这些操作放在子线程去做,通过 handler/looper/message queue 三板斧把结果传给主线程。而 dart 天生是单线程模式,为什么我们能够轻松的做这些任务,而不需要另开一个线程呢? 22 | 23 | 熟悉 dart 的同学肯定了解 event loop 机制了,通过异步处理我们可以把一个耗时方法延迟执行,首先保证我们的同步方法能够按时执行(这也是为什么 setState 中只能进行同步操作的缘故)。而整个 pipline 是一次同步的任务,所以异步任务就会等待 pipline 执行结束再执行,这样就不会因为进行耗时操作卡住 UI。 24 | 25 | ![](../pic/async_func.png) 26 | 27 | 但是单线程毕竟也有它的局限,但是当我们有一些比较重的同步处理任务,例如解析大量 json(这是一个同步操作),或是处理图片这样的操作,很可能处理时间会超过一个 vsync 时间,这样 Flutter 就不能及时将 layer 送到 GPU 线程,导致应用 jank。 28 | 29 | ![](../pic/sync_func.png) 30 | 31 | 在上面这个例子中,我们通过计算 0 + 1 + ... +1000000000 来模拟一个耗时的 json 解析操作,由于它是一个同步的行为,所以它的计算不会被暂停。我们这个复杂的计算任务耗时超过了一次 sync 时间,所以产生了明显的 jank。 32 | 33 | ``` dart 34 | int doSomeHeavyWork() { 35 | int res = 0; 36 | for (int i = 0; i <= 1000000000; i++) { 37 | res += i; 38 | } 39 | return res; 40 | } 41 | ``` 42 | 43 | ## 如何解决 44 | 既然 dart 单线程无法解决这样的问题,我们很容易就会想到使用多线程解决这个问题。在 dart 中,它的线程概念被称为 isolate。 45 | 46 | 它与我们之前理解的 Thread 概念有所不同,各个 isolate 之间是无法共享内存空间,isolate 之间有自己的 event loop。我们只能通过 Port 传递消息,然后在另一个 isolate 中处理然后将结果传递回来,这样我们的 UI 线程就有更多余力处理 pipeline,而不会被卡住。更多概念性的描述请参考 isolate [API文档](https://api.dartlang.org/stable/2.5.2/dart-isolate/Isolate-class.html)。 47 | 48 | ### 创建一个 isolate 49 | 我们可以通过 `Isolate.spawn` 创建一个 isolate。 50 | 51 | ``` dart 52 | static Future spawn(void entryPoint(T message),T message); 53 | ``` 54 | 当我们调用 `Isolate.spawn` 的时候,它将会返回一个对 isolate 的引用的 Future。我们可以通过这个 isolate 来控制创建出的 Isolate,例如 pause、resume、kill 等等。 55 | 56 | - entryPoint:这里传入我们想要在其他 isolate 中执行的方法,入参是一个任意类型的 message。entryPoint 只能是顶层方法或静态方法,且返回值为 void。 57 | - message:创建 Isolate 第一个调用方法的入参,可以是任意值。 58 | 59 | 但是在此之前我们必须要创建两个 isolate 之间沟通的桥梁。 60 | 61 | #### ReceivePort / SendPort 62 | 在两个 isolate 之间,我们必须通过 port 来传递 message。ReceivePort 与 SendPort 就像是一部单向通信电话。ReceivePort 自带一部 SendPort,当我们创建 isolate 的时候,就把 ReceivePort 的 SendPort 丢给创建出来的 isolate。当新的 isolate 完成了计算任务时,通过这个 sendPort 去 send message。 63 | 64 | ``` dart 65 | static void _methodRunAnotherIsolate(dynamic message) { 66 | if (message is SendPort) { 67 | message.send('Isolate Created!'); 68 | } 69 | } 70 | ``` 71 | 这里假设先有一个需要在其他 isolate 中执行的方法,入参是一个 SendPort。需要注意的是,这里的方法**只能是顶层方法或静态方法**,所以我们这里使用了 static 修饰,并让其变成一个私有方法 `_`。它的返回值也只能是 void,你可能会问,那我们如何获得结果呢? 72 | 73 | 还记得我们刚才创建的 ReceivePort 吗。是的,现在我们就需要监听这个 ReceivePort 来获得 sendPort 传递的 message。 74 | 75 | ``` dart 76 | createIsolate() async { 77 | ReceivePort receivePort = ReceivePort(); 78 | try { 79 | // create isolate 80 | isolate = 81 | await Isolate.spawn(_methodRunAnotherIsolate, receivePort.sendPort); 82 | 83 | // listen message from another isolate 84 | receivePort.listen((dynamic message) { 85 | print(message.toString()); 86 | }); 87 | } catch (e) { 88 | print(e.toString()); 89 | } finally { 90 | isolate.addOnExitListener(receivePort.sendPort, 91 | response: "isolate has been killed"); 92 | } 93 | isolate?.kill(); 94 | } 95 | ``` 96 | 97 | 我们先创建出 ReceivePort,然后在 `Isolate.spawn` 的时候将 `receivePort.sendPort` 作为 message 传入新的 isolate。 98 | 99 | 然后监听 receivePort,并打印收听到的 message。这里需要注意的是,我们需要手动调用 `isolate?.kill()` 来关闭这个 isolate。 100 | 101 | 输出结果: 102 | > flutter: Isolate Created! 103 | 104 | > flutter: isolate has been killed 105 | 106 | 实际上这里不写 isolate?.kill() 也会在 gc 时自动销毁 isolate。 107 | 108 | 这时候你可能会问,我们的 `entryPoint` 只允许有一个入参,如果我们想要执行的方法需要传入其他参数怎么办呢。 109 | 110 | #### 定义协议 111 | 其实很简单,我们定义一个协议就行了。比如像下面这样我们定义一个 `SpawnMessageProtocol` 作为 message。 112 | 113 | ``` dart 114 | class SpawnMessageProtocol{ 115 | final SendPort sendPort; 116 | final String url; 117 | SpawnMessageProtocol(this.sendPort, this.url); 118 | } 119 | ``` 120 | 121 | 协议中包含 SendPort 即可。 122 | 123 | ### 更方便的 Compute 124 | 刚才我们使用的 `Isolate.spawn` 创建 Isolate 自然会觉得太过复杂,有没有一种更好的方式呢。实际上 Flutter 已经为我们封装了一些实用方法,让我们能够更加自然地使用多线程进行处理。这里我们先创建一个需要在其他 isolate 中运行的方法。 125 | ``` dart 126 | static int _doSomething(int i) { 127 | return i + 1; 128 | } 129 | ``` 130 | 然后使用 compute 在另一个 isolate 中执行该方法,并返回结果。 131 | ``` dart 132 | runComputeIsolate() async{ 133 | int i = await compute(_doSomething, 8); 134 | print(i); 135 | } 136 | ``` 137 | 仅仅一行代码我们就能够让 `_doSomething` 运行在另一个 isolate 中,并返回结果。这种方式对使用者来说几乎没有负担,基本上和写异步代码是一样的。 138 | 139 | ## 代价是什么 140 | 对于我们来说,其实是把多线程当做一种计算资源来使用的。我们可以通过创建新的 isolate 计算 heavy work,从而减轻 UI 线程的负担。但是这样做的代价是什么呢? 141 | 142 | ### 时间 143 | 通常来说,当我们使用多线程计算的时候,整个计算的时间会比单线程要多,额外的耗时是什么呢? 144 | 145 | - 创建 Isolate 146 | - Copy Message 147 | 148 | 当我们按照上面的代码执行一段多线程代码时,经历了 isolate 的创建以及销毁过程。下面是一种我们在解析 json 中这样编写代码可能的方式。 149 | 150 | ``` dart 151 | static BSModel toBSModel(String json){} 152 | 153 | parsingModelList(List jsonList) async{ 154 | for(var model in jsonList){ 155 | BSModel m = await compute(toBSModel, model); 156 | } 157 | } 158 | ``` 159 | 160 | 在解析 json 的时候,我们可能通过 compute 把解析任务放在新的 isolate 中完成,然后把值传过来。这时候我们会发现,整个解析会变得异常的慢。这是由于我们每次创建 `BSModel` 的时候都经历了一次 isolate 的创建以及销毁过程。这将会耗费约 50-150ms 的时间。 161 | 162 | 在这之中,我们传递 data 也经历了 Network -> Main Isolate -> New Isolate (result) -> Main Isolate,多出来两次 copy 的操作。如果我们是在 Main 线程之外的 isolate 下载的数据,那么就可以直接在该线程进行解析,最后只需要传回 Main Isolate 即可,省下了一次 copy 操作。(Network -> New Isolate (result)-> Main Isolate) 163 | 164 | ### 空间 165 | Isolate 实际上是比较重的,每当我们创建出来一个新的 Isolate 至少需要 2MB 左右的空间甚至更多,取决于我们具体 isolate 的用途。 166 | 167 | #### OOM 风险 168 | 我们可能会使用 message 传递 data 或 file。而实际上我们传递的 message 是经历了一次 copy 过程的,这其实就可能存在着 OOM 的风险。 169 | 170 | 如果说我们想要返回一个 2GB 的 data,在 iPhone X(3GB ram)上,我们是无法完成 message 的传递操作的。 171 | 172 | ## Tips 173 | 上面已经介绍了使用 isolate 进行多线程操作会有一些额外的 cost,那么是否可以通过一些手段减少这些消耗呢。我个人建议从两个方向上入手。 174 | - 减少 isolate 创建所带来的消耗。 175 | - 减少 message copy 次数,以及大小。 176 | 177 | ### 使用 LoadBalancer 178 | 如何减少 isolate 创建所带来的消耗呢。自然一个想法就是能否创建一个线程池,初始化到那里。当我们需要使用的时候再拿来用就好了。 179 | 180 | 实际上 dart team 已经为我们写好一个非常实用的 package,其中就包括 `LoadBalancer`。 181 | 182 | 我们现在 pubspec.yaml 中添加 isolate 的依赖。 183 | 184 | ``` yaml 185 | isolate: ^2.0.2 186 | ``` 187 | 188 | 然后我们可以通过 `LoadBalancer` 创建出指定个数的 isolate。 189 | 190 | ``` dart 191 | Future loadBalancer = LoadBalancer.create(2, IsolateRunner.spawn); 192 | ``` 193 | 194 | 这段代码将会创建出一个 isolate 线程池,并自动实现了负载均衡。 195 | 196 | 由于 dart 天生支持顶层函数,我们可以在 dart 文件中直接创建这个 `LoadBalancer`。下面我们再来看看应该如何使用 `LoadBalancer` 中的 isolate。 197 | 198 | ``` dart 199 | int useLoadBalancer() async { 200 | final lb = await loadBalancer; 201 | int res = await lb.run(_doSomething, 1); 202 | return res; 203 | } 204 | ``` 205 | 206 | 我们关注的只有 `Future run(FutureOr function(P argument), argument,` 方法。我们还是需要传入一个 `function` 在某个 isolate 中运行,并传入其参数 `argument`。run 方法将会返回我们执行方法的返回值。 207 | 208 | 整体和 compute 使用感觉上差不多,但是当我们多次使用额外的 isolate 的时候,不再需要重复创建了。 209 | 210 | 并且 `LoadBalancer` 还支持 runMultiple,可以让一个方法在多线程中执行。具体使用请查看 api。 211 | 212 | `LoadBalancer` 经过测试,它会在第一次使用其 isolate 的时候初始化线程池。 213 | 214 | ![](../pic/load_balancer.png) 215 | 216 | 当应用打开后,即使我们在顶层函数中调用了 LoadBalancer.create,但是还是只会有一个 Isolate。 217 | 218 | ![](../pic/load_balancer_init.png) 219 | 220 | 当我们调用 run 方法时,才真正创建出了实际的 isolate。 221 | 222 | ## 写在最后 223 | 写这篇文章的缘故其实是前两天法空大佬在做图片处理的时候刚好遇到了这个问题,他最后还是调用原生的库解决的,不过我还是写一篇,给之后遇到这个问题的同学一种参考方案。 224 | 225 | 当然 Flutter 中性能调优远不止这一种情况,build / layout / paint 每一个过程其实都有很多能够优化的细节,这个会在之后性能优化系列跟大家慢慢分享。 226 | 227 | 这次的内容就是这样了,如果您对本文还有任何疑问或者文章的建议,欢迎在下方评论区以及我的邮箱1652219550a@gmail.com与我联系,我会及时回复! -------------------------------------------------------------------------------- /articles/Flutter 深入浅出Key.md: -------------------------------------------------------------------------------- 1 | # Flutter | 深入浅出Key 2 | 3 | ## 前言 4 | 在开发 Flutter 的过程中你可能会发现,一些小部件的构造函数中都有一个可选的参数——Key。刚接触的同学或许会对这个概念感到很迷茫,感到不知所措。 5 | 6 | 在这篇文章中我们会深入浅出的介绍什么是 Key,以及应该使用 key 的具体场景。 7 | 8 | ## 什么是Key 9 | 在 Flutter 中我们经常与状态打交道。我们知道 Widget 可以有 Stateful 和 Stateless 两种。Key 能够帮助开发者在 Widget tree 中保存状态,在一般的情况下,我们并不需要使用 Key。那么,究竟什么时候应该使用 Key呢。 10 | 11 | 我们来看看下面这个例子。 12 | ``` dart 13 | class StatelessContainer extends StatelessWidget { 14 | final Color color = RandomColor().randomColor(); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Container( 19 | width: 100, 20 | height: 100, 21 | color: color, 22 | ); 23 | } 24 | } 25 | ``` 26 | 27 | 这是一个很简单的 Stateless Widget,显示在界面上的就是一个 100 * 100 的有颜色的 Container。 28 | RandomColor 能够为这个 Widget 初始化一个随机颜色。 29 | 30 | 我们现在将这个Widget展示到界面上。 31 | 32 | ``` dart 33 | class Screen extends StatefulWidget { 34 | @override 35 | _ScreenState createState() => _ScreenState(); 36 | } 37 | 38 | class _ScreenState extends State { 39 | List widgets = [ 40 | StatelessContainer(), 41 | StatelessContainer(), 42 | ]; 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return Scaffold( 47 | body: Center( 48 | child: Row( 49 | mainAxisAlignment: MainAxisAlignment.center, 50 | children: widgets, 51 | ), 52 | ), 53 | floatingActionButton: FloatingActionButton( 54 | onPressed: switchWidget, 55 | child: Icon(Icons.undo), 56 | ), 57 | ); 58 | } 59 | 60 | switchWidget(){ 61 | widgets.insert(0, widgets.removeAt(1)); 62 | setState(() {}); 63 | } 64 | } 65 | ``` 66 | 67 | 这里在屏幕中心展示了两个 StatelessContainer 小部件,当我们点击 floatingActionButton 时,将会执行 switchWidget 并交换它们的顺序。 68 | 69 | ![](../pic/change_color_01.png) 70 | 看上去并没有什么问题,交换操作被正确执行了。现在我们做一点小小的改动,将这个 StatelessContainer 升级为 StatefulContainer。 71 | 72 | ``` dart 73 | class StatefulContainer extends StatefulWidget { 74 | StatefulContainer({Key key}) : super(key: key); 75 | @override 76 | _StatefulContainerState createState() => _StatefulContainerState(); 77 | } 78 | 79 | class _StatefulContainerState extends State { 80 | final Color color = RandomColor().randomColor(); 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | return Container( 85 | width: 100, 86 | height: 100, 87 | color: color, 88 | ); 89 | } 90 | } 91 | ``` 92 | 在 StatefulContainer 中,我们将定义 Color 和 build 方法都放进了 State 中。 93 | 94 | 现在我们还是使用刚才一样的布局,只不过把 StatelessContainer 替换成 StatefulContainer,看看会发生什么。 95 | 96 | ![](../pic/change_color_02.png) 97 | 98 | 这时,无论我们怎样点击,都再也没有办法交换这两个Container的顺序了,而 switchWidget 确实是被执行了的。 99 | 100 | 为了解决这个问题,我们在两个 Widget 构造的时候给它传入一个 UniqueKey。 101 | ``` dart 102 | class _ScreenState extends State { 103 | List widgets = [ 104 | StatefulContainer(key: UniqueKey(),), 105 | StatefulContainer(key: UniqueKey(),), 106 | ]; 107 | ··· 108 | ``` 109 | 110 | 然后这两个 Widget 又可以正常被交换顺序了。 111 | 112 | 看到这里大家肯定心中会有疑问,为什么 Stateful Widget 无法正常交换顺序,加上了 Key 之后就可以了,在这之中到底发生了什么? 为了弄明白这个问题,我们将涉及 Widget 的 diff 更新机制。 113 | 114 | ### Widget 更新机制 115 | 116 | 在之前的文章中,我们介绍了 **Widget** 和 **Element** 的关系。若你还对 **Element** 的概念感到很模糊的话,请先阅读 [Flutter | 深入理解BuildContext](https://juejin.im/post/5c665cb651882562914ec153)。 117 | 118 | 下面来来看Widget的源码。 119 | 120 | ``` dart 121 | @immutable 122 | abstract class Widget extends DiagnosticableTree { 123 | const Widget({ this.key }); 124 | final Key key; 125 | ··· 126 | static bool canUpdate(Widget oldWidget, Widget newWidget) { 127 | return oldWidget.runtimeType == newWidget.runtimeType 128 | && oldWidget.key == newWidget.key; 129 | } 130 | } 131 | ``` 132 | 我们知道 Widget 只是一个配置且无法修改,而 Element 才是真正被使用的对象,并可以修改。 133 | 134 | 当新的 Widget 到来时将会调用 canUpdate 方法,来确定这个 Element是否需要更新。 135 | 136 | canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否需要更新。若 canUpdate 方法返回 true 说明不需要替换 Element,直接更新 Widget 就可以了。 137 | 138 | #### StatelessContainer 比较过程 139 | 140 | 在 StatelessContainer 中,我们并没有传入 key ,所以只比较它们的 **runtimeType**。这里 runtimeType 一致,canUpdate 方法返回 true,两个 Widget 被交换了位置,StatelessElement 调用新持有 Widget 的 build 方法重新构建,在屏幕上两个 Widget 便被正确的交换了顺序。 141 | 142 | #### StatefulContainer 比较过程 143 | 144 | 而在 StatefulContainer 的例子中,我们将 color 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 的引用的是 Stateful Element。 145 | 146 | 当我们没有给 Widget 任何 key 的时候,将会只比较这两个 Widget 的 runtimeType 。由于两个 Widget 的属性和方法都相同,canUpdate 方法将会返回 true,于是更新 StatefulWidget 的位置,这两个 Element 将不会交换位置。但是原有 Element 只会从它持有的 state 实例中 build 新的 widget。因为 element 没变,它持有的 state 也没变。所以颜色不会交换。这里变换 StatefulWidget 的位置是没有作用的。 147 | 148 | 而我们给 Widget 一个 key 之后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key。并返回 false。(这里 runtimeType 相同,key 不同) 149 | 150 | 此时 RenderObjectElement 会用新 Widget 的 key 在老 Element 列表里面查找,找到匹配的则会更新 Element 的位置并更新对应 renderObject 的位置,对于这个例子来讲就是交换了 Element 的位置并交换了对应 renderObject 的位置。都交换了,那么颜色自然也就交换了。 151 | 152 | **这里感谢ad6623对之前错误描述的指出。** 153 | ### 比较范围 154 | 为了提升性能 Flutter 的比较算法(diff)是有范围的,它并不是对第一个 StatefulWidget 进行比较,而是对某一个层级的 Widget 进行比较。 155 | 156 | ``` dart 157 | ··· 158 | class _ScreenState extends State { 159 | List widgets = [ 160 | Padding( 161 | padding: const EdgeInsets.all(8.0), 162 | child: StatefulContainer(key: UniqueKey(),), 163 | ), 164 | Padding( 165 | padding: const EdgeInsets.all(8.0), 166 | child: StatefulContainer(key: UniqueKey(),), 167 | ), 168 | ]; 169 | ··· 170 | ``` 171 | 172 | 在这个例子中,我们将两个带 key 的 StatefulContainer 包裹上 Padding 组件,然后点击交换按钮,会发生下面这件奇妙的事情。 173 | 174 | ![](../pic/change_color_03.png) 175 | 176 | 两个 Widget 的 Element 并不是交换顺序,而是被重新创建了。 177 | 178 | 在 Flutter 的比较过程中它下到 Row 这个层级,发现它是一个 MultiChildRenderObjectWidget(多子部件的 Widget)。然后它会对所有 children 层逐个进行扫描。 179 | 180 | 在Column这一层级,padding 部分的 runtimeType 并没有改变,且不存在 Key。然后再比较下一个层级。由于内部的 StatefulContainer 存在 key,且现在的层级在 padding 内部,该层级没有多子 Widget。runtimeType 返回 flase,Flutter 的将会认为这个 Element 需要被替换。然后重新生成一个新的 Element 对象装载到 Element 树上替换掉之前的 Element。第二个 Widget 同理。 181 | 182 | 所以为了解决这个问题,我们需要将 key 放到 Column 的 children 这一层级。 183 | 184 | ``` dart 185 | ··· 186 | class _ScreenState extends State { 187 | List widgets = [ 188 | Padding( 189 | key: UniqueKey(), 190 | padding: const EdgeInsets.all(8.0), 191 | child: StatefulContainer(), 192 | ), 193 | Padding( 194 | key: UniqueKey(), 195 | padding: const EdgeInsets.all(8.0), 196 | child: StatefulContainer(), 197 | ), 198 | ]; 199 | ··· 200 | ``` 201 | 现在我们又可以愉快的玩耍了(交换 Widget 顺序)了。 202 | 203 | #### 扩展内容 204 | slot 能够描述子级在其父级列表中的位置。多子部件 Widget 例如 Row,Column 都为它的子级提供了一系列 slot。 205 | 206 | 在调用 Element.updateChild 的时候有一个细节,若新老 Widget 的实例相同,注意这里是**实例相同**而不是类型相同, slot 不同的时候,Flutter 所做的仅仅是更新 slot,也就给他换个位置。因 为 Widget 是不可变的,实例相同意味着显示的配置相同,所以要做的仅仅是挪个地方而已。 207 | 208 | ``` dart 209 | abstract class Element extends DiagnosticableTree implements BuildContext { 210 | ··· 211 | dynamic get slot => _slot; 212 | dynamic _slot; 213 | ··· 214 | @protected 215 | Element updateChild(Element child, Widget newWidget, dynamic newSlot) { 216 | ··· 217 | if (child != null) { 218 | if (child.widget == newWidget) { 219 | if (child.slot != newSlot) 220 | updateSlotForChild(child, newSlot); 221 | return child; 222 | } 223 | if (Widget.canUpdate(child.widget, newWidget)) { 224 | if (child.slot != newSlot) 225 | updateSlotForChild(child, newSlot); 226 | child.update(newWidget); 227 | assert(child.widget == newWidget); 228 | assert(() { 229 | child.owner._debugElementWasRebuilt(child); 230 | return true; 231 | }()); 232 | return child; 233 | } 234 | deactivateChild(child); 235 | assert(child._parent == null); 236 | } 237 | return inflateWidget(newWidget, newSlot); 238 | } 239 | ``` 240 | 更新机制表 241 | | | 新WIDGET为空 | 新 Widget不为空 | 242 | | :-------------: | :------------------------ | :----------------------------------------------------------- | 243 | | **child为空** | 返回null。 | 返回新的 Element | 244 | | **child不为空** | 移除旧的widget,返回null. | 若旧的child Element 可以更新(canUpdate)则更新并将其返回,否则返回一个新的 Element. | 245 | 246 | ## Key 的种类 247 | ### Key 248 | ``` dart 249 | @immutable 250 | abstract class Key { 251 | const factory Key(String value) = ValueKey; 252 | 253 | @protected 254 | const Key.empty(); 255 | } 256 | ``` 257 | 默认创建 Key 将会通过工厂方法根据传入的 value 创建一个 ValueKey。 258 | 259 | Key 派生出两种不同用途的 Key:LocalKey 和 GlobalKey。 260 | ### Localkey 261 | LocalKey 直接继承至 Key,它应用于拥有相同父 Element 的小部件进行比较的情况,也就是上述例子中,有一个多子 Widget 中需要对它的子 widget 进行移动处理,这时候你应该使用Localkey。 262 | 263 | Localkey 派生出了许多子类 key: 264 | 265 | - ValueKey : ValueKey('String') 266 | - ObjectKey : ObjectKey(Object) 267 | - UniqueKey : UniqueKey() 268 | 269 | Valuekey 又派生出了 PageStorageKey : PageStorageKey('value') 270 | ### GlobalKey 271 | ``` dart 272 | @optionalTypeArgs 273 | abstract class GlobalKey> extends Key { 274 | ··· 275 | static final Map _registry = {}; 276 | static final Set _debugIllFatedElements = HashSet(); 277 | static final Map _debugReservations = {}; 278 | ··· 279 | BuildContext get currentContext ··· 280 | Widget get currentWidget ··· 281 | T get currentState ··· 282 | ``` 283 | GlobalKey 使用了一个静态常量 Map 来保存它对应的 Element。 284 | 285 | 你可以通过 GlobalKey 找到持有该GlobalKey的 **Widget**,**State** 和 **Element**。 286 | 287 | 注意:GlobalKey 是非常昂贵的,需要谨慎使用。 288 | 289 | ## 什么时候需要使用 Key 290 | ### ValueKey 291 | 如果您有一个 Todo List 应用程序,它将会记录你需要完成的事情。我们假设每个 Todo 事情都各不相同,而你想要对每个 Todo 进行滑动删除操作。 292 | 293 | 这时候就需要使用 ValueKey! 294 | 295 | ```dart 296 | return TodoItem( 297 | key: ValueKey(todo.task), 298 | todo: todo, 299 | onDismissed: (direction){ 300 | _removeTodo(context, todo); 301 | }, 302 | ); 303 | ``` 304 | ### ObjectKey 305 | 如果你有一个生日应用,它可以记录某个人的生日,并用列表显示出来,同样的还是需要有一个滑动删除操作。 306 | 307 | 我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和生日组合起来的 Object 将具有唯一性。 308 | 309 | 这时候你需要使用 ObjectKey! 310 | 311 | ### UniqueKey 312 | 如果组合的 Object 都无法满足唯一性的时候,你想要确保每一个 Key 都具有唯一性。那么,你可以使用 UniqueKey。它将会通过该对象生成一个具有唯一性的 hash 码。 313 | 314 | 不过这样做,每次 Widget 被构建时都会去重新生成一个新的 UniqueKey,失去了一致性。也就是说你的小部件还是会改变。(还不如不用😂) 315 | 316 | ### PageStorageKey 317 | 当你有一个滑动列表,你通过某一个 Item 跳转到了一个新的页面,当你返回之前的列表页面时,你发现滑动的距离回到了顶部。这时候,给 Sliver 一个 PageStorageKey!它将能够保持 Sliver 的滚动状态。 318 | 319 | ### GlobalKey 320 | GlobalKey 能够跨 Widget 访问状态。 321 | 在这里我们有一个 Switcher 小部件,它可以通过 changeState 改变它的状态。 322 | ``` dart 323 | class SwitcherScreenState extends State { 324 | bool isActive = false; 325 | 326 | @override 327 | Widget build(BuildContext context) { 328 | return Scaffold( 329 | body: Center( 330 | child: Switch.adaptive( 331 | value: isActive, 332 | onChanged: (bool currentStatus) { 333 | isActive = currentStatus; 334 | setState(() {}); 335 | }), 336 | ), 337 | ); 338 | } 339 | 340 | changeState() { 341 | isActive = !isActive; 342 | setState(() {}); 343 | } 344 | } 345 | ``` 346 | 但是我们想要在外部改变该状态,这时候就需要使用 GlobalKey。 347 | ``` dart 348 | class _ScreenState extends State { 349 | final GlobalKey key = GlobalKey(); 350 | 351 | @override 352 | Widget build(BuildContext context) { 353 | return Scaffold( 354 | body: SwitcherScreen( 355 | key: key, 356 | ), 357 | floatingActionButton: FloatingActionButton(onPressed: () { 358 | key.currentState.changeState(); 359 | }), 360 | ); 361 | } 362 | } 363 | ``` 364 | 这里我们通过定义了一个 GlobalKey 并传递给 SwitcherScreen。然后我们便可以通过这个 key 拿到它所绑定的 SwitcherState 并在外部调用 changeState 改变状态了。 365 | 366 | ![](../pic/switcher.png) 367 | 368 | ## 参考资料 369 | - [何时使用密钥 - Flutter小部件 101 第四集](https://www.youtube.com/watch?v=kn0EOS-ZiIc&feature=youtu.be) 370 | - [widgets-intro#keys](https://flutter.dev/docs/development/ui/widgets-intro#keys) 371 | 372 | # 写在最后 373 | 这篇文章的灵感来自于 **何时使用密钥 - Flutter小部件 101 第四集**, 强烈建议大家观看这个系列视频,你会对 Flutter 如何构建视图更加清晰。也希望这篇文章对你有所帮助! 374 | 375 | 文章若有不对之处还请各位高手指出,欢迎在下方评论区以及我的邮箱1652219550a@gmail.com留言,我会在24小时内与您联系! -------------------------------------------------------------------------------- /articles/Flutter 状态管理指南篇——Provider.md: -------------------------------------------------------------------------------- 1 | # Flutter | 状态管理指南篇——Provider 2 | 3 | # 前言 4 | 5 | 2019 Google I/O 大会,官方在 [Pragmatic State Management in Flutter (Google I/O'19)](https://www.youtube.com/watch?v=d_m5csmrf7I&list=PLjxrf2q8roU2no7yROrcQSVtwbYyxAGZV&index=3) 主题演讲上正式介绍了 由社区作者 [Remi Rousselet](https://github.com/rrousselGit) 与 Flutter Team 共同编写的 [Provider](https://github.com/rrousselGit/provider) 代替 Provide 成为官方推荐的状态管理方式之一。 6 | 7 | 本文将基于最新 Provider v-3.0 进行介绍,除了讲解其使用方式之外,我认为更重要的是 Provider 不同“提供”方式的适用场景及使用原则。以及在使用状态管理时候**需要遵守的原则**,在编写 Flutter App 的过程中减轻你的思考负担。希望本文能给你带来一些有价值的参考。(提前打个预防针,本文篇幅较长,建议马住在看。) 8 | 9 | 推荐阅读时间:**1小时** 10 | 11 | # What's the problem 12 | 在正式介绍 Provider 之前允许我再啰嗦两句,为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。 13 | 14 | 如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 **数据** 映射成 **视图** 就可以了。你可能并不需要状态管理,就像下面这样。 15 | 16 | ![](../pic/simple_state.png) 17 | 18 | 但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。 19 | 20 | ![](../pic/complex_state.png) 21 | 22 | 这是什么鬼。我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。 23 | 24 | Flutter 实际上在一开始就为我们提供了一种状态管理方式,那就是 **StatefulWidget**。但是我们很快发现,它正是造成上述原因的**罪魁祸首**。 25 | 26 | 在 State 属于某一个特定的 Widget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,我们增加非常多可怕的垃圾代码。 27 | 28 | 这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。 29 | 30 | # What is Provider 31 | Provider 从名字上就很容易理解,它就是用于提供数据,无论是在**单个页面**还是在**整个** app 都有它自己的解决方案,我们可以很方便的管理状态。可以说,Provider 的目标就是**完全替代** StatefulWidget。 32 | 33 | 说了很多还是很抽象,我们先一起做一个最简单的例子。 34 | 35 | # How to do 36 | 这里我们还是用这个 Counter App 为例,给大家介绍如何在两个独立的页面中共享计数器(counter)的状态应该怎么做,具体长这样。 37 | 38 | ![](../pic/state_management_demo.png) 39 | 40 | 两个页面中心字体共用了同一个字体大小。第二个页面的按钮将会让数字增加,第一个页面的数字将会同步增加。 41 | 42 | ## 第一步:添加依赖 43 | 在pubspec.yaml中添加Provider的依赖。 44 | 45 | ![](../pic/add_dependency.png) 46 | 47 | - 实际添加请参考:https://pub.dev/packages/provider#-installing-tab- 48 | - 由于版本冲突添加失败请参考: https://juejin.im/post/5b8958d351882542b03e6d57 49 | 50 | ## 第二步:创建数据 Model 51 | 这里的 Model 实际上就是我们的状态,它不仅储存了我们的数据模型,而且还包含了更改数据的方法,并暴露出它想要暴露出的数据。 52 | 53 | ``` dart 54 | import 'package:flutter/material.dart'; 55 | 56 | class CounterModel with ChangeNotifier { 57 | int _count = 0; 58 | int get value => _count; 59 | 60 | void increment() { 61 | _count++; 62 | notifyListeners(); 63 | } 64 | } 65 | ``` 66 | 这个类意图非常清晰,我们的数据就是一个 int 类型的 `_count`,下划线代表私有。通过 `get value` 把 `_count` 值暴露出来。并提供 `increment` 方法用于更改数据。 67 | 68 | 这里使用了 mixin 混入了 `ChangeNotifier`,这个类能够帮驻我们自动管理所有听众。当调用 `notifyListeners()` 时,它会通知所有听众进行刷新。 69 | 70 | 如果你对 mixin 这个概念还不是很清楚的话,可以看我之前翻译的这篇 [【译】Dart | 什么是Mixin](https://juejin.im/post/5bb204d3e51d450e4f38e2f6)。 71 | 72 | ## 第三步:创建顶层共享数据 73 | 我们在 main 方法中初始化全局数据。 74 | ``` dart 75 | void main() { 76 | final counter = CounterModel(); 77 | final textSize = 48; 78 | 79 | runApp( 80 | Provider.value( 81 | value: textSize, 82 | child: ChangeNotifierProvider.value( 83 | value: counter, 84 | child: MyApp(), 85 | ), 86 | ), 87 | ); 88 | } 89 | ``` 90 | 通过 `Provider.value` 能够管理一个恒定的数据,并提供给子孙节点使用。我们只需要将数据在其 value 属性中声明即可。在这里我们将 `textSize` 传入。 91 | 92 | 而 `ChangeNotifierProvider.value` 不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有听众刷新。(通过之前我们说过的 `notifyListeners`) 93 | 94 | 此处的 `` 范型可省略。但是我建议大家还是进行声明,这会使你的应用更加健壮。 95 | 96 | 除了上述几个属性之外 `Provider.value` 还提供了 `UpdateShouldNotify` Function,用于控制刷新时机。 97 | 98 | `typedef UpdateShouldNotify = bool Function(T previous, T current);` 99 | 100 | 我们可以在这里传入一个方法 `(T previous, T current){...}` ,并获得前后两个 Model 的实例,然后通过比较两个 Model 以自定义刷新规则,返回 bool 表示是否需要刷新。默认为 previous != current 则刷新。 101 | 102 | 当然,key 属性是肯定有的,常规操作。如果你还不太清楚的话,建议阅读我之前的这篇文章 [Flutter | 深入浅出Key] (https://juejin.im/post/5ca2152f6fb9a05e1a7a9a26。) 103 | 104 | 为了让各位思维连贯,我还是在这里放上这个平淡无奇的 MyApp Widget 代码。😑 105 | ``` dart 106 | class MyApp extends StatelessWidget { 107 | @override 108 | Widget build(BuildContext context) { 109 | return MaterialApp( 110 | theme: ThemeData.dark(), 111 | home: FirstScreen(), 112 | ); 113 | } 114 | } 115 | ``` 116 | 117 | ## 第四步:在子页面中获取状态 118 | 在这里我们有两个页面,FirstScreen 和 SecondScreen。我们先来看 FirstScreen 的代码。 119 | 120 | ### Provider.of(context) 121 | 122 | ``` dart 123 | class FirstScreen extends StatelessWidget { 124 | @override 125 | Widget build(BuildContext context) { 126 | final _counter = Provider.of(context); 127 | final textSize = Provider.of(context).toDouble(); 128 | 129 | return Scaffold( 130 | appBar: AppBar( 131 | title: Text('FirstPage'), 132 | ), 133 | body: Center( 134 | child: Text( 135 | 'Value: ${_counter.value}', 136 | style: TextStyle(fontSize: textSize), 137 | ), 138 | ), 139 | floatingActionButton: FloatingActionButton( 140 | onPressed: () => Navigator.of(context) 141 | .push(MaterialPageRoute(builder: (context) => SecondPage())), 142 | child: Icon(Icons.navigate_next), 143 | ), 144 | ); 145 | } 146 | } 147 | ``` 148 | 149 | 获取顶层数据最简单的方法就是 `Provider.of(context);` 这里的范型 `` 指定了获取 **FirstScreen** 向上寻找最近的储存了 T 的祖先节点的数据。 150 | 151 | 我们通过这个方法获取了顶层的 CounterModel 及 textSize。并在 Text 组件中进行使用。 152 | 153 | floatingActionButton 用来点击跳转到 SecondScreen 页面,和我们的主题无关。 154 | 155 | ### Consumer 156 | 看到这里你可能会想,两个页面都是获取顶层状态,代码不都一样吗,弄啥捏。🤨 别忙着跳到下一节,我们来看另外一种获取状态的方式,这将会影响你的 app performance。 157 | 158 | ``` dart 159 | class SecondPage extends StatelessWidget { 160 | @override 161 | Widget build(BuildContext context) { 162 | return Scaffold( 163 | appBar: AppBar( 164 | title: Text('Second Page'), 165 | ), 166 | body: Consumer2( 167 | builder: (context, CounterModel counter, int textSize, _) => Center( 168 | child: Text( 169 | 'Value: ${counter.value}', 170 | style: TextStyle( 171 | fontSize: textSize.toDouble(), 172 | ), 173 | ), 174 | ), 175 | ), 176 | floatingActionButton: Consumer( 177 | builder: (context, CounterModel counter, child) => FloatingActionButton( 178 | onPressed: counter.increment, 179 | child: child, 180 | ), 181 | child: Icon(Icons.add), 182 | ), 183 | ); 184 | } 185 | } 186 | ``` 187 | 这里我们要介绍的是第二种方式,使用 Consumer 获取祖先节点中的数据。 188 | 189 | 在这个页面中,我们有两处使用到了公共 Model。 190 | - 应用中心的文字:使用 CounterModel 在 Text 中展示文字,以及通过 textSize 定义自身的大小。一共使用到了两个 Model。 191 | - 浮动按钮:使用 CounterModel 的 `increment` 方法触发计数器的值增加。使用到了一个 Model。 192 | 193 | #### Single Model Consumer 194 | 我们先看 floatingActionButton,使用了一个 Consumer 的情况。 195 | 196 | Consumer 使用了 [**Builder**](https://en.wikipedia.org/wiki/Builder_pattern) 模式,收到更新通知就会通过 builder 重新构建。`Consumer` 代表了它要获取哪一个祖先中的 Model。 197 | 198 | Consumer 的 builder 实际上就是一个 Function,它接收三个参数 `(BuildContext context, T model, Widget child)`。 199 | - context: context 就是 build 方法传进来的 BuildContext 在这里就不细说了,如果有兴趣可以看我之前这篇文章 [Flutter | 深入理解BuildContext](https://juejin.im/post/5c665cb651882562914ec153)。 200 | - T:T也很简单,就是获取到的最近一个祖先节点中的数据模型。 201 | - child:它用来构建那些与 Model 无关的部分,在多次运行 builder 中,child 不会进行重建。 202 | 203 | 然后它会返回一个通过这三个参数映射的 Widget 用于构建自身。 204 | 205 | 在这个浮动按钮的例子中,我们通过 **Consumer** 获取到了顶层的 `CounterModel` 实例。并在浮动按钮 onTap 的 callback 中调用其 `increment` 方法。 206 | 207 | 而且我们成功抽离出 **Consumer** 中不变的部分,也就是浮动按钮中心的 `Icon` 并将其作为 child 参数传入 builder 方法中。 208 | 209 | #### Consumer2 210 | 现在我们再来看中心的文字部分。这时候你可能会有疑惑了,刚才我们讲的 Consumer 获取的只有一个 Model,而现在 Text 组件不仅需要 CounterModel 用以显示计数器,而且还需要获得 textSize 以调整字体大小,咋整捏。 211 | 212 | 遇到这种情况你可以使用 `Consumer2`。使用方式基本上和 `Consumer` 一致,只不过范型改为了两个,并且 builder 方法也变成了 `Function(BuildContext context, A value, B value2, Widget child)`。 213 | 214 | 我勒个去...假如我要获得 100 个 Model,那岂不是得搞个 Consumer100 (???黑人问号.jpg) 215 | 216 | 然而并没有 😏。 217 | 218 | 从源码里面可以看到,作者只为我们搞到了 `Consumer6`。emmmmm.....还要要求更多就只有自力更生喽。 219 | 220 | 顺手帮作者修复了一个 clerical error。 221 | 222 | ![](../pic/fix_bug_provider.png) 223 | 224 | #### 区别 225 | 我们来看 Consumer 的内部实现。 226 | ``` dart 227 | @override 228 | Widget build(BuildContext context) { 229 | return builder( 230 | context, 231 | Provider.of(context), 232 | child, 233 | ); 234 | } 235 | ``` 236 | 可以发现,`Consumer` 就是通过 `Provider.of(context)` 来实现的。但是从实现来讲 `Provider.of(context)` 比 `Consumer` 简单好用太多,为啥我要搞得那么复杂捏。 237 | 238 | 实际上 `Consumer` 非常有用,它的经典之处在于能够在复杂项目中,**极大地缩小你的控件刷新范围**。`Provider.of(context)` 将会把调用了该方法的 context 作为听众,并在 `notifyListeners` 的时候通知其刷新。 239 | 240 | 举个例子来说,我们的 FirstScreen 使用了 `Provider.of(context)` 来获取数据,SecondScreen 则没有。 241 | - 你在 FirstScreen 中的 build 方法中添加一个 `print('first screen rebuild');` 242 | - 然后在 SecondScreen 中的 build 方法中添加一个 `print('second screen rebuild');` 243 | - 点击第二个页面的浮动按钮,那么你会在控制台看到这句输出。 244 | 245 | **first screen rebuild** 246 | 247 | 首先这证明了 `Provider.of(context)` 会导致调用的 context 页面范围的刷新。 248 | 249 | 那么第二个页面刷新没有呢? 刷新了,但是只刷新了 `Consumer` 的部分,甚至连浮动按钮中的 `Icon` 的不刷新我们都给控制了。你可以在 `Consumer` 的 builder 方法中验证,这里不再啰嗦 250 | 251 | 假如你在你的应用的 **页面级别** 的 Widget 中,使用了 `Provider.of(context)`。会导致什么后果已经显而易见了,每当其状态改变的时候,你都会重新刷新整个页面。虽然你有 Flutter 的自动优化算法给你撑腰,但你肯定**无法获得最好的性能**。 252 | 253 | 所以在这里我建议各位尽量使用 `Consumer` 而不是 `Provider.of(context)` 获取顶层数据。 254 | 255 | 以上便是一个最简单的使用 Provider 的例子。 256 | # You also need to know 257 | ## 合理选择使用 Provides 的构造方法 258 | 在上面这个例子中👆,我们选择了使用 `XProvider.value` 的构造方法来创建祖先节点中的 提供者。除了这种方式,我们还可以使用默认构造方法。 259 | ``` dart 260 | Provider({ 261 | Key key, 262 | @required ValueBuilder builder, 263 | Disposer dispose, 264 | Widget child, 265 | }) : this._( 266 | key: key, 267 | delegate: BuilderStateDelegate(builder, dispose: dispose), 268 | updateShouldNotify: null, 269 | child: child, 270 | ); 271 | ``` 272 | 常规的 key/child 属性我们不在这里啰嗦。我们先来看这个看上去相对教复杂一点的 builder。 273 | 274 | ### ValueBuilder 275 | 相比起 `.value` 构造方式中直接传入一个 value 就 ok,这里的 builder 要求我们传入一个 ValueBuilder。WTF? 276 | 277 | `typedef ValueBuilder = T Function(BuildContext context);` 278 | 279 | 其实很简单,就是传入一个 Function 返回一个数据而已。在上面这个例子中,你可以替换成这样。 280 | 281 | ``` dart 282 | Provider( 283 | builder: (context) => textSize, 284 | ... 285 | ) 286 | ``` 287 | 288 | 由于是 Builder 模式,这里默认需要传入 context,实际上我们的 Model(textSize)与 context 并没有关系,所以你完全可以这样写。 289 | 290 | ``` dart 291 | Provider( 292 | builder: (_) => textSize, 293 | ... 294 | ) 295 | ``` 296 | ### Disposer 297 | 现在我们知道了 builder,那这个 dispose 方法又用来做什么的呢。实际上这才是 Provider 的点睛之笔。 298 | 299 | `typedef Disposer = void Function(BuildContext context, T value);` 300 | 301 | dispose 属性需要一个 `Disposer`,而这个其实也是一个回调。 302 | 303 | 如果你之前使用过 BLoC 的话,相信你肯定遇到过一个头疼的问题。我应该在什么时候释放资源呢? BloC 使用了观察者模式,它旨在替代 StatefulWidget。然而大量的流使用完毕之后必须 close 掉,以释放资源。 304 | 305 | 然而 Stateless Widget 并没有给我们类似于 dispose 之类的方法,这便是 BLoC 的硬伤。你不得不为了释放资源而使用 StatefulWidget,这与我们的本意相违。而 Provider 则为我们解决了这一点。 306 | 307 | 当 Provider 所在节点被移除的时候,它就会启动 `Disposer`,然后我们便可以在这里释放资源。 308 | 309 | 举个例子,假如我们有这样一个 BLoC。 310 | ``` dart 311 | class ValidatorBLoC { 312 | StreamController _validator = StreamController.broadcast(); 313 | 314 | get validator => _validator.stream; 315 | 316 | validateAccount(String text) { 317 | //Processing verification text ... 318 | } 319 | 320 | dispose() { 321 | _validator.close(); 322 | } 323 | } 324 | ``` 325 | 326 | 这时候我们想要在某个页面提供这个 BLoC 但是又不想使用 StatefulWidget。这时候我们可以在页面顶层套上这个 Provider。 327 | 328 | ``` dart 329 | Provider( 330 | builder:(_) => ValidatorBLoC(), 331 | dispose:(_, ValidatorBLoC bloc) => bloc.dispose(), 332 | } 333 | ) 334 | ``` 335 | 这样就完美解决了数据释放的问题!🤩 336 | 337 | 现在我们可以放心的结合 BLoC 一起使用了,很赞有没有。但是现在你可能又有疑问了,在使用 Provider 的时候,我应该选择哪种构造方法呢。 338 | 339 | 我的推荐是,**简单模型**就选择 `Provider.value`,好处是可以精确控制刷新时机。而需要对资源进行释放处理等**复杂模型**的时候,`Provider()` 默认构造方式绝对是你的最佳选择。 340 | 341 | 其他几种 Provider 也遵循该模式,需要的时候可以自行查看源码。 342 | 343 | ## 我该使用哪种 Provider 344 | 如果你在 Provider 中提供了可监听对象(Listenable 或者 Stream)及其子类的话,那么你会得到下面这个异常警告。 345 | 346 | ![](../pic/provider_error.png) 347 | 348 | 你可以将本文中所使用到的 CounterModel 放入 Provider 进行提供(记得 hot restart 而不是 hot reload),那么你就能看到上面这个 FlutterError 了。 349 | 350 | 你也可以在 main 方法中通过下面这行代码来禁用此提示。 351 | `Provider.debugCheckInvalidValueType = null;` 352 | 353 | 这是由于 Provider 只能提供恒定的数据,不能通知依赖它的子部件刷新。提示也说的很清楚了,假如你想使用一个会发生 change 的 Provider,请使用下面的 Provider。 354 | - ListenableProvider 355 | - ChangeNotifierProvider 356 | - ValueListenableProvider 357 | - StreamProvider 358 | 359 | 你可能会在这里产生一个疑问,不是说(Listenable 或者 Stream)才不行吗,为什么我们的 CounterModel 混入的是 ChangeNotifier 但是还是出现了这个 FlutterError 呢。 360 | 361 | `class ChangeNotifier implements Listenable ` 362 | 363 | 我们再来看上面的这几个 Provider 有什么异同。先关注 `ListenableProvider / ChangeNotifierProvider` 这两个类。 364 | 365 | ListenableProvider 提供(provide)的对象是**继承**了 Listenable 抽象类的子类。由于无法混入,所以通过继承来获得 Listenable 的能力,同时必须实现其 `addListener / removeListener` 方法,手动管理收听者。显然,这样太过复杂,我们通常都不需要这样做。 366 | 367 | 而混入了 `ChangeNotifier` 的类自动帮我们实现了听众管理,所以 ListenableProvider 同样也可以接收混入了 ChangeNotifier 的类。 368 | 369 | ChangeNotifierProvider 则更为简单,它能够对子节点提供一个 **继承** / **混入** / **实现** 了 ChangeNotifier 的类。通常我们只需要在 Model 中 `with ChangeNotifier` ,然后在需要刷新状态的时候调用 `notifyListeners` 即可。 370 | 371 | 那么 **ChangeNotifierProvider** 和 **ListenableProvider** 究竟区别在哪呢,**ListenableProvider** 不是也可以提供(provide)混入了 ChangeNotifier 的 Model 吗。 372 | 373 | 还是那个你需要思考的问题。你在这里的 Model 究竟是一个简单模型还是复杂模型。这是因为 ChangeNotifierProvider 会在你需要的时候,自动调用其 _disposer 方法。 374 | 375 | `static void _disposer(BuildContext context, ChangeNotifier notifier) => 376 | notifier?.dispose();` 377 | 378 | 我们可以在 Model 中重写 ChangeNotifier 的 dispose 方法,来释放其资源。这对于复杂 Model 的情况下十分有用。 379 | 380 | 现在你应该已经十分清楚 `ListenableProvider / ChangeNotifierProvider` 的区别了。下面我们来看 ValueListenableProvider。 381 | 382 | ValueListenableProvider 用于提供实现了 **继承** / **混入** / **实现** 了 ValueListenable 的 Model。它实际上是专门用于处理只有一个单一变化数据的 ChangeNotifier。 383 | 384 | `class ValueNotifier extends ChangeNotifier implements ValueListenable` 385 | 386 | 通过 ValueListenable 处理的类**不再需要**数据更新的时候调用 `notifyListeners`。 387 | 388 | 好了,终于只剩下最后一个 `StreamProvider` 了。 389 | 390 | `StreamProvider` 专门用作提供(provide)一条 Single Stream。我在这里仅对其核心属性进行讲解。 391 | 392 | - `T initialData`:你可以通过这个属性声明这条流的初始值。 393 | - `ErrorBuilder catchError`:这个属性用来捕获流中的 error。在这条流 addError 了之后,你会能够通过 ` T Function(BuildContext context, Object error)` 回调来处理这个异常数据。实际开发中它非常有用。 394 | - `updateShouldNotify`:和之前的回调一样,这里不再赘述。 395 | 396 | 除了这三个构造方法都有的属性以外,StreamProvider 还有三种不同的构造方法。 397 | - `StreamProvider(...)`:默认构造方法用作创建一个 Stream 并收听它。 398 | - `StreamProvider.controller(...)`:通过 builder 方式创建一个 `StreamController`。并且在 StreamProvider 被移除时,自动释放 StreamController。 399 | - `StreamProvider.value(...)`:监听一个已有的 Stream 并将其 value 提供给子孙节点。 400 | 401 | 除了上面这五种已经提到过的 Provider,还有一种 FutureProvider,它提供了一个 Future 给其子孙节点,并在 Future 完成时,通知依赖的子孙节点进行刷新,这里不再详细介绍,需要的话自行查看 api 文档。 402 | ## 优雅地处理多个 Provider 403 | 在我们之前的例子中,我们使用了嵌套的方式来组合多个 Provider。这样看上去有些傻瓜(我就是有一百个 Model 🙃)。 404 | 405 | 这时候我们就可以使用一个非常 sweet 的组件 —— `MultiProvider`。 406 | 407 | 这时候我们刚才那个例子就可以改成这样。 408 | ```dart 409 | void main() { 410 | final counter = CounterModel(); 411 | final textSize = 48; 412 | 413 | runApp( 414 | MultiProvider( 415 | providers: [ 416 | Provider.value(value: textSize), 417 | ChangeNotifierProvider.value(value: counter) 418 | ], 419 | child: MyApp(), 420 | ), 421 | ); 422 | } 423 | ``` 424 | 425 | 我们的代码瞬间清晰很多,而且与刚才的嵌套做法**完全等**价。 426 | 427 | # Tips 428 | ## 保证 build 方法无副作用 429 | build 无副作用也通常被人叫做,build 保持 pure,二者是一个意思。 430 | 431 | 通常我们经常会看到,为了获取顶层数据我们会在 build 方法中调用 XXX.of(context) 方法。你必须**非常小心**,你的 build 函数不应该产生任何副作用,包括新的对象(Widget 以外),请求网络,或作出一个映射视图以外的操作等。 432 | 433 | 这是因为,你的根本无法控制什么时候你的 build 函数将会被调用。我可以说**随时**。每当你的 build 函数被调用,那么都会产生一个副作用。这将会发生非常恐怖的事情。🤯 434 | 435 | 我这样说你肯定会感到比较抽象,我们来举一个例子。 436 | 437 | 假如你有一个 `ArticleModel` 这个 Model 的作用是 **通过网络** 获取一页 List 数据,并用 ListView 显示在页面上。 438 | 439 | 这时候,我们假设你在 build 函数中做了下面这些事情。 440 | 441 | ``` dart 442 | @override 443 | Widget build(BuildContext context) { 444 | final articleModel = Provider.of(context); 445 | mainCategoryModel.getPage(); // By requesting data from the server 446 | return XWidget(...); 447 | } 448 | ``` 449 | 450 | 我们在 build 函数中获得了祖先节点中的 articleModel,随后调用了 getPage 方法。 451 | 452 | 这时候会发生什么事情呢,当我们请求成功获得了结果的时候,根据之前我们已经介绍过的,调用了 `Provider.of(context);` 会重新运行其 build。这样 getPage 就又被执行了一次。 453 | 454 | 而你的 Model 中每次请求 getPage 都会导致 Model 中保存的当前请求页自增(第一次请求第一页的数据,第二次请求第二页的数据以此类推),那么每次 build 都会导致新的一次数据请求,并在新的数据 get 的时候请求下一页的数据。你的服务器挂掉那是迟早的事情。(come on baby! 455 | 456 | > 由于 didChangeDependence 方法也会随着依赖改变而被调用,所以也需要保证它没有副作用。具体解释参见下面单页面数据初始化。 457 | 458 | 所以你应该严格遵守这项原则,否则会导致一系列糟糕的后果。 459 | 460 | 那么怎么解决数据初始化这个问题呢,请看 Q&A 部分。 461 | 462 | ## 不要所有状态都放在全局 463 | 第二个小贴士是不要把你的所有状态都放在顶层。开发者为了图方便省事,再接触了状态管理之后经常喜欢把所有东西都放在顶层 MaterialApp 之上。这样看上去就很方便共享数据了,我要数据就直接去获取。 464 | 465 | 不要这么做。严格区分你的全局数据与局部数据,资源不用了就要释放!否则将会严重影响你的应用 performance。 466 | 467 | ## 尽量在 Model 中使用私有变量“_” 468 | 这可能是我们每个人在新手阶段都会出现的疑问。为什么要用私有变量呢,我在任何地方都能够操作成员不是很方便吗。 469 | 470 | 一个应用需要大量开发人员参与,你写的代码也许在几个月之后被另外一个开发看到了,这时候假如你的变量没有被保护的话,也许同样是让 count++,他会用 countController.sink.add(++_count) 这种原始方法,而不是调用你已经封装好了的 increment 方法。 471 | 472 | 虽然两种方式的效果完全一样,但是第二种方式将会让我们的business logic零散的混入其他代码中。久而久之项目中就会大量充斥着这些垃圾代码增加项目代码耦合程度,非常不利于代码的维护以及阅读。 473 | 474 | 所以,请务必使用私有变量保护你的 Model。 475 | ## 控制你的刷新范围 476 | 在 Flutter 中,**组合**大于**继承**的特性随处可见。常见的 Widget 实际上都是由更小的 Widget 组合而成,直到基本组件为止。为了使我们的应用拥有更高的性能,控制 Widget 的刷新范围便显得至关重要。 477 | 478 | 我们已经通过前面的介绍了解到了,在 Provider 中获取 Model 的方式会影响刷新范围。所有,请尽量使用 Consumer 来获取祖先 Model,以维持最小刷新范围。 479 | 480 | # Q&A 481 | 在这里对一些大家可能会有疑问的常见问题做一个回答,如果你还有这之外的疑问的话,欢迎在下方评论区一起讨论。 482 | ## Provider 是如何做到状态共享的 483 | 这个问题实际上得分两步。 484 | ### 获取顶层数据 485 | 实际上在祖先节点中共享数据这件事我们已经在之前的文章中接触过很多次了,都是通过系统的 InheritedWidget 进行实现的。 486 | 487 | Provider 也不例外,在所有 Provider 的 build 方法中,返回了一个 InheritedProvider。 488 | 489 | `class InheritedProvider extends InheritedWidget` 490 | 491 | Flutter 通过在每个 Element 上维护一个 `InheritedWidget` 哈希表来向下传递 Element 树中的信息。通常情况下,多个 492 | Element 引用相同的哈希表,并且该表仅在 Element 引入新的 `InheritedWidget` 时改变。 493 | 494 | 所以寻找祖先节点的时间复杂度为 O(1) 😎 495 | ### 通知刷新 496 | 通知刷新这一步实际上在讲各种 Provider 的时候已经讲过了,其实就是使用了 Listener 模式。Model 中维护了一堆听众,然后 notifiedListener 通知刷新。(空间换时间🤣 497 | 498 | ## 为什么全局状态需要放在顶层 MaterialApp 之上 499 | 这个问题需要结合 Navigator 以及 BuildContext 来回答,在之前的文章中 [Flutter | 深入理解BuildContext](https://juejin.im/post/5c665cb651882562914ec153) 已经解释过了,这里不再赘述。 500 | 501 | ## 我应该在哪里进行数据初始化 502 | 对于数据初始化这个问题,我们必须要分类讨论。 503 | ### 全局数据 504 | 当我们需要获取全局顶层数据(就像之前 CounterApp 例子一样)并需要做一些会产生额外结果的时候,main 函数是一个很好的选择。 505 | 506 | 我们可以在 main 方法中创建 Model 并进行初始化的工作,这样就只会执行一次。 507 | ### 单页面 508 | 如果我们的数据只是在这个页面中需要使用,那么你有这两种方式可以选择。 509 | #### StatefulWidget 510 | 这里订正一个错误,感谢 @晓杰的V笑 以及 @fantasy525 在讨论中帮我指出。 511 | 512 | 在之前文章的版本中我推荐大家在 State 的 didChangeDependence 中进行数据初始化。这里其实是使用 BLoC 延续下来的习惯。因为使用了 InheritWidget 之后,只有在 State 的 didChangeDependence 阶段进行 Inherit 初始化,initState 阶段是拿不到数据的。而由于 BLoC 是使用的 Stream,数据直接走 Stream 进来,由 StreamBuilder 去 listen,这样 State 的依赖一直都只是这个 Stream 对象而已,不会再次触发 didChangeDependence 方法。那 Provider 有何不同呢。 513 | 514 | ``` dart 515 | /// If [listen] is `true` (default), later value changes will trigger a new 516 | /// [State.build] to widgets, and [State.didChangeDependencies] for 517 | /// [StatefulWidget]. 518 | ``` 519 | 源码中的注释解释了,如果这个 `Provider.of(context)` listen 了的话,那么当 notifyListeners 的时候,就会触发 context 所对应的 State 的 [State.build] 和 [State.didChangeDependencies] 方法。也就是说,如果你使用了非 Provider 提供的数据,例如 ChangeNotifierProvider 这样会改变依赖的类,并且获取数据时 `Provider.of(context, listen: true)` 选择 listen (默认就为 listen)的话,数据刷新时会重新运行 didChangeDependencies 和 build 两个方法。这样一来对 didChangeDependencies 也会产生副作用。假如在这里请求了数据,当数据到来的时候,又回触发下一次请求,最终无限请求下去。 520 | 521 | 这里除了副作用以外还有一点,假如数据改变是一个同步行为,例如这里的 counter.increment 这样的方法,在 didChangeDependencies 中调用的话,就会造成下面这个错误。 522 | ``` 523 | The following assertion was thrown while dispatching notifications for CounterModel: 524 | flutter: setState() or markNeedsBuild() called during build. 525 | flutter: This ChangeNotifierProvider widget cannot be marked as needing to build because the 526 | flutter: framework is already in the process of building widgets. A widget can be marked as needing to be 527 | flutter: built during the build phase only if one of its ancestors is currently building. This exception is 528 | flutter: allowed because the framework builds parent widgets before children, which means a dirty descendant 529 | flutter: will always be built. Otherwise, the framework might not visit this widget during this build phase. 530 | ``` 531 | 这里和和 Flutter 的构建算法有关。简单来说,就是不能够在 State 的 build 期间调用 setState() 或者 markNeedsBuild(),在我们这里 didChangeDependence 的时候调用了此方法,导致出现这个错误。异步数据则会由于 event loop 的缘故不会立即执行。想要深入了解的同学可以看闲鱼技术的这篇文章:[Flutter快速上车之Widget](https://juejin.im/post/5b8ce76f51882542c0626887)。 532 | 533 | 感觉处处都是坑啊,那该怎么初始化呢。目前我找到的办法是这样,首先 要保证初始化数据不能够产生副作用,我们需要找一个在 State 声明周期内**一定**只会运行一次的方法。initState 就是为此而生的。但是 initState 不是无法获取到 Inherit 吗。但是我们现在本身就在页面顶层啊,页面级别的 Model 就在顶层被创建,现在根本就不需要 Inherit。 534 | 535 | ``` dart 536 | class _HomeState extends State { 537 | final _myModel = MyModel(); 538 | 539 | @override 540 | void initState() { 541 | super.initState(); 542 | _myModel.init(); 543 | } 544 | } 545 | ``` 546 | 页面级别的 Model 数据都在页面顶层 Widget 创建并初始化即可。 547 | 548 | 我们还需要考虑一种情况,假如这个操作是一个同步操作应该如何处理,就如我们之前举的 CounterModel.increment 这个操作一样。 549 | 550 | ``` dart 551 | void initState() { 552 | super.initState(); 553 | WidgetsBinding.instance.addPostFrameCallback((callback){ 554 | Provider.of(context).increment(); 555 | }); 556 | } 557 | ``` 558 | 我们通过 addPostFrameCallback 回调中在第一帧 build 结束时调用 increment 方法,这样就不会出现构建错误了。 559 | 560 | ##### provider 作者 Remi 给出了另外一种方式 561 | 562 | > This code is relatively unsafe. There's more than one reason for didChangeDependencies to be called. 563 | 564 | > You probably want something similar to: 565 | ``` dart 566 | MyCounter counter; 567 | 568 | @override 569 | void didChangeDependencies() { 570 | final counter = Provider.of(context); 571 | if (conter != this.counter) { 572 | this.counter = counter; 573 | counter.increment(); 574 | } 575 | } 576 | ``` 577 | > This should trigger increment only once. 578 | 579 | 也就是说初始化数据之前判断一下这个数据是否已经存在。 580 | 581 | #### cascade 582 | 你也可以在使用 dart 的级连语法 `..do()` 直接在页面的 StatelessWidget 成员变量声明时进行初始化。 583 | 584 | ``` dart 585 | class FirstScreen extends StatelessWidget { 586 | CounterModel _counter = CounterModel()..increment(); 587 | double _textSize = 48; 588 | ... 589 | } 590 | ``` 591 | 使用这种方式需要注意,当这个 StatelessWidget 重新运行 build 的时候,状态会丢失。这种情况在 TabBarView 中的子页面切换过程中就可能会出现。 592 | 593 | 所以建议还是使用第一种,在 State 中初始化数据。 594 | 595 | 596 | ## 我需要担心性能问题吗 597 | 是的,无论 Flutter 再怎么努力优化,Provider 考虑的情况再多,我们总是有办法让应用卡爆 😂(开个玩笑) 598 | 599 | 仅当我们不遵守其行为规范的时候,会出现这样的情况。性能会因为你的各种不当操作而变得很糟糕。我的建议是:遵守其规范,做任何事情都考虑对性能的影响,要知道 Flutter 把更新算法可是优化到了 O(N)。 600 | 601 | Provider 仅仅是对 InheritedWidget 的一个升级,你不必担心引入 Provider 会对应用造成性能问题。 602 | 603 | ## 为什么选择 Provider 604 | Provider 不仅做到了提供数据,而且它拥有着一套完整的解决方案,覆盖了你会遇到的绝大多数情况。就连 BLoC 未解决的那个棘手的 dispose 问题,和 ScopedModel 的侵入性问题,它也都解决了。 605 | 606 | 然而它就是完美的吗,并不是,至少现在来说。Flutter Widget 构建模式很容易在 UI 层面上组件化,但是仅仅使用 Provider,Model 和 View 之间还是容易产生依赖。 607 | 608 | 我们只有通过手动将 Model 转化为 ViewModel 这样才能消除掉依赖关系,所以假如各位有组件化的需求,还需要另外处理。 609 | 610 | 不过对于大多数情况来说,Provider 足以优秀,它能够让你开发出**简单**、**高性能**、**层次清晰** 的应用。 611 | 612 | ## 我应该如何选择状态管理 613 | 介绍了这么多状态管理,你可能会发现,一些状态管理之间职责并不冲突。例如 BLoC 可以结合 RxDart 库变得很强大,很好用。而 BLoC 也可以结合 Provider / ScopedModel 一起使用。那我应该选择哪种状态管理方式呢。 614 | 615 | 我的建议是遵守以下几点: 616 | 1. 使用状态管理的目的是为了让编写代码变得更简单,任何会增加你的应用复杂度的状态管理,统统都不要用。 617 | 2. 选择自己能够 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 这些状态管理方式都有一定上手难度,不要选自己无法理解的状态管理方式。 618 | 3. 在做最终决定之前,敲一敲 demo,真正感受各个状态管理方式给你带来的 好处/坏处 然后再做你的决定。 619 | 620 | 希望能够帮助到你。 621 | 622 | # 源码浅析 623 | ## Flutter 中的 Builder 模式 624 | 在 Provider 中,各种 Provider 的原始构造方法都有一个 builder 参数,这里一般就用 `(_) => XXXModel()` 就行了。感觉有点多次一举,为什么不能像 `.value()` 构造方法那样简洁呢。 625 | 626 | 实际上,Provider 为了帮我们管理 Model,使用到了 delegation pattern。 627 | 628 | builder 声明的 ValueBuilder 最终被传入代理类 `BuilderStateDelegate` / `SingleValueDelegate`。 然后通过代理类才实现的 Model 生命周期管理。 629 | 630 | ``` dart 631 | class BuilderStateDelegate extends ValueStateDelegate { 632 | BuilderStateDelegate(this._builder, {Disposer dispose}) 633 | : assert(_builder != null), 634 | _dispose = dispose; 635 | 636 | final ValueBuilder _builder; 637 | final Disposer _dispose; 638 | 639 | T _value; 640 | @override 641 | T get value => _value; 642 | 643 | @override 644 | void initDelegate() { 645 | super.initDelegate(); 646 | _value = _builder(context); 647 | } 648 | 649 | @override 650 | void didUpdateDelegate(BuilderStateDelegate old) { 651 | super.didUpdateDelegate(old); 652 | _value = old.value; 653 | } 654 | 655 | @override 656 | void dispose() { 657 | _dispose?.call(context, value); 658 | super.dispose(); 659 | } 660 | } 661 | ``` 662 | 这里就仅放 BuilderStateDelegate,其余的请自行查看源码。 663 | 664 | ## 如何实现 MultiProvider 665 | ``` dart 666 | Widget build(BuildContext context) { 667 | var tree = child; 668 | for (final provider in providers.reversed) { 669 | tree = provider.cloneWithChild(tree); 670 | } 671 | return tree; 672 | } 673 | ``` 674 | MultiProvider 实际上就是通过每一个 provider 都实现了的 cloneWithChild 方法把自己一层一层包裹起来。 675 | 676 | ```dart 677 | MultiProvider( 678 | providers:[ 679 | AProvider, 680 | BProvider, 681 | CProvider, 682 | ], 683 | child: child, 684 | ) 685 | ``` 686 | 687 | 等价于 688 | ``` dart 689 | AProvider( 690 | child: BProvider( 691 | child: CProvider( 692 | child: child, 693 | ), 694 | ), 695 | ) 696 | ``` 697 | 698 | 699 | 700 | 如果您对Provider还有任何疑问或者文章的建议,欢迎在下方评论区以及我的邮箱1652219550a@gmail.com与我联系,我会及时回复! -------------------------------------------------------------------------------- /articles/Flutter 通过 ServiceLocator 实现无 context 导航.md: -------------------------------------------------------------------------------- 1 | # Flutter | 通过 ServiceLocator 实现无 context 导航 2 | 3 | # 前言 4 | 最近在开发过程中看到很多同学问过这个问题。我想要在网络请求失败的时候弹出一个统一的处理页面告诉用户检查网络连接。由于这个行为可以发生在任何页面,我们当然不希望在每一个页面之中都要重新实现一遍这个逻辑,那样耦合就太高了,这时候我们的第一反应是在网络请求后某个部分统一处理这部分逻辑。 5 | 6 | 看上去没什么问题,但是如果你做过这个需求话,你就会发现:当我们实现跳转提示页面的时候,需要使用到 `Navigator` 这个组件。回想一下我们一般是如何进行跳转的。 7 | 8 | `Navigator.of(context).pushNamed('/errorPage');` 9 | 10 | 我们发现,要实现跳转到 ErrorPage 这个操作,我们缺少了一个重要的元素 `BuildContext`。`Navigator.of(context)` 操作其实是在祖先节点中寻找最近的一个 `NavigatorState`。而这里的 `BuildContext` 就是寻找的起点。 所以很多同学都卡在这里了,那我们就来解决这个问题。 11 | 12 | 在正式开始本文之前你需要已经理解下面几个概念: 13 | 14 | - BuildContext :[Flutter | 深入理解BuildContext](https://juejin.im/post/5c665cb651882562914ec153) 15 | - Key : [Flutter | 深入浅出Key](https://juejin.im/post/5ca2152f6fb9a05e1a7a9a26) 16 | 17 | ## 理解导航原理 18 | >### 什么是Navigator,MaterialApp做了什么 19 | >我们经常会在应用中打开许多页面,当我们返回的时候,它会先后退到上一个打开的页面,然后一层一层后退,没错这就是一个堆栈。而在Flutter中,则是由Navigator来负责管理维护这些页面堆栈。 20 | ``` dart 21 | 压一个新的页面到屏幕上 22 | Navigator.of(context).push 23 | 把路由顶层的页面移除 24 | Navigator.of(context).pop 25 | ``` 26 | >通常我们我们在构建应用的时候并没有手动去创建一个 Navigator,也能进行页面导航,这又是为什么呢。 27 | > 28 | >没错,这个 Navigator 正是 MaterialApp 为我们提供的。但是如果 home,routes,onGenerateRoute 和 onUnknownRoute 都为 null,并且 builder 不为 null,MaterialApp 则不会创建任何 Navigator。 29 | 30 | 既然我们的 `Navigator.of(context)` 实际上就是在获取 MaterialApp 提供的 `NavigatorState` 实例。而 `BuildContext` 跟当前 Element 有关,要统一控制实际上相当复杂。我们是否可以使用另外一种方式来获取 `Navigator`,这样就可以不再受 BuildContext 的约束了。 31 | 32 | ## 获取 Navigator 实例 33 | 要获取某个 Widget 我们在之前的文章中介绍了可以使用 `GlobalKey` 来实现。那我们应该如何获取到 `Navigator` 呢? 34 | ```dart 35 | class _AppState extends State { 36 | GlobalKey _navigatorKey = GlobalKey(debugLabel: 'navigate'); 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return MaterialApp( 41 | navigatorKey: _navigatorKey, 42 | home: HomeScreen(), 43 | ); 44 | } 45 | } 46 | ``` 47 | 48 | 由于 MaterialApp 封装了 Navigator,并且将 Navigator 的 key 属性作为 navigatorKey 暴露出来,我们只需要绑定一个 GlobalKey 就行了。 49 | 50 | 但是现在问题又来了,我们假如想要在外部使用这个 GlobalKey 好像还是不太方便。我们的 Navigator 可能在多处需要使用,假如直接依赖的话每一处都包含了用于创建、定位和管理依赖项的重复代码。假如我们现在仅仅只是想进行网络调试的测试,由于依赖了 Navigator 相关的代码,想要进行测试非常困难。 51 | 52 | 这时候就需要 ServiceLocator 来帮助我们进行解耦。 53 | # ServiceLocator 54 | 这是一种经典的设计模式,主要目的是将类与依赖解耦,让类在编译的时候并知道依赖相的具体实现。从而提升其隔离性和可测试性。 55 | 56 | ## get_it 57 | 而今天我们要介绍的是一个来自 Flutter Community 和 Thomas Burkhart 制作的库 [get_it](https://pub.dev/packages/get_it)。它是一个轻量级 ServiceLocator 库,仅仅用到了 99 行代码(包括注释)。建议有时间都去阅读一下。 58 | 59 | ### 简单上手 60 | get_it 非常简单,使用就分两步。 61 | - 注册服务 62 | - 依赖注入 63 | 64 | #### 注册服务 65 | 首先创建出一个 GetIt 容器对象。 66 | 67 | ``` dart 68 | GetIt getIt = new GetIt(); 69 | ``` 70 | 然后把需要注册的服务在容器中注册。 71 | ``` dart 72 | getIt.registerSingleton(new AppModelImplementation()); 73 | getIt.registerLazySingleton(() =>new RestAPIImplementation()); 74 | ``` 75 | #### 依赖注入 76 | 在需要使用到这个依赖的地方我们还是通过这个容器来获取依赖。 77 | 78 | `var myAppModel = getIt();` 79 | 80 | 你也可以使用 `var myAppModel = getIt.get();` 这个方法,效果是一样的。 81 | 82 | 由于 dart 支持全局变量,我们就把容器直接写在一个 Dart 文件中就好了。是不是很简单呢? 83 | 84 | 这样我们的服务就是在容器中创建的,在实际依赖的时候,我们可以只依赖于接口,然后通过容器注入(DI)实现了该接口的实际对象,达到了解耦的效果。 85 | 86 | ## 实现 NavigateService 87 | 现在我们来看看该如何使用 get_it 实现一个 NavigateService。 88 | ### 添加依赖 89 | 90 | ![](../pic/get_it.png) 91 | 92 | - 实际添加请参考:https://pub.dev/packages/get_it#-installing-tab- 93 | - 由于版本冲突添加失败请参考: https://juejin.im/post/5b8958d351882542b03e6d57 94 | 95 | ### 创建全局 Locater 96 | 我们在项目中新建一个 service_locator.dart 文件。然后在这个文件中创建一个全局 GetIt 实例。 97 | 98 | ``` dart 99 | import 'package:get_it/get_it.dart'; 100 | 101 | final GetIt getIt = GetIt(); 102 | void setupLocator(){} 103 | ``` 104 | 这里先写上 setupLocator 方法,之后会在这里进行服务注册。 105 | 106 | ### 创建 NavigateService 107 | 108 | 我们把导航相关的功能封装成 Service,方便之后使用。 109 | ``` dart 110 | import 'package:flutter/material.dart'; 111 | 112 | class NavigateService { 113 | final GlobalKey key = GlobalKey(debugLabel: 'navigate_key'); 114 | 115 | NavigatorState get navigator => key.currentState; 116 | 117 | get pushNamed => navigator.pushNamed; 118 | get push => navigator.push; 119 | } 120 | ``` 121 | 通过 key.currentState 获取到 NavigatorState 实例。 122 | 123 | 我这里简单暴露了导航的 push 和 pushName 功能,你可以根据自己的功能来进行扩展。 124 | 125 | ### 注册服务 126 | 现在就需要在容器中注册这个服务,回到 service_locator.dart。 127 | ``` dart 128 | void setupLocator(){ 129 | getIt.registerSingleton(NavigateService()); 130 | } 131 | ``` 132 | 通过调用 registerSingleton,我们在容器中注册了一个单例模式使用的 NavigateService。之后我们所有需要注册的 Service 都在这里注册一遍即可。 133 | 134 | ### 容器初始化 135 | 刚刚已经写好了注册函数,现在就需要在我们的 Flutter 应用运行时初始化一次,main 函数是一个不错的选择。 136 | 137 | ``` dart 138 | void main() { 139 | setupLocator(); 140 | runApp(App()); 141 | } 142 | ``` 143 | 这样在我们程序运行的时候就能够把服务都初始化到容器中。 144 | 145 | ### 依赖注入 146 | 刚才我们说了,要想获得 Navigator 需要在 MaterialApp 的 navigatorKey 绑定一个 GlobalKey。所以我们现在通过容器注入服务,来绑定这个 GlobalKey。 147 | 148 | ``` dart 149 | class App extends StatelessWidget { 150 | @override 151 | Widget build(BuildContext context) { 152 | return MaterialApp( 153 | navigatorKey: getIt().key, 154 | routes: {'/ErrorScreen': (_) => ErrorScreen()}, 155 | home: HomeScreen(), 156 | ); 157 | } 158 | } 159 | ``` 160 | 上面通过 getIt() 注入了 NavigateService 的依赖。这个 getIt 就是我们的全局实例。 161 | 162 | 然后添加了一个命名路由。这里我把 HomeScreen 和 ErrorScreen 的代码放在下面。 163 | 164 | ``` dart 165 | class HomeScreen extends StatelessWidget { 166 | @override 167 | Widget build(BuildContext context) { 168 | return Scaffold( 169 | floatingActionButton: FloatingActionButton(onPressed: () { 170 | getIt().pushNamed('/ErrorScreen'); 171 | }), 172 | ); 173 | } 174 | } 175 | 176 | class ErrorScreen extends StatelessWidget { 177 | @override 178 | Widget build(BuildContext context) { 179 | return Container( 180 | alignment: Alignment.center, 181 | color: Colors.red, 182 | child: Text('Error'), 183 | ); 184 | } 185 | } 186 | ``` 187 | 188 | 在 HomeScreen 中点击一下 FloatingActionButton 就会通过注入的 NavigateService 跳转到 ErrorScreen。 189 | 190 | 在进行跳转时,我们可以看到并没有使用 context。 191 | 192 | ` getIt().pushNamed('/ErrorScreen');` 193 | 194 | 这样你就可以在你想要的地方恰当的处理一些全局导航操作了。它的一个巨大的好处在于你不仅可以在 Widget 中使用,而且可以在任何地方使用容器中的服务。 195 | 196 | ## get_it 详解 197 | ### 不同的注册方式 198 | GetIt 提供了多种注册方式,这将会影响这些对象的生命周期。目前有三种: 199 | - 工厂模式:`void registerFactory(FactoryFunc func)` 每次都会返回新的实例。 200 | - 单例模式:`void registerSingleton(T instance)` 每次返回同一实例。 这种模式需要手动初始化,就像我们上面例子中那样。 201 | - 单例模式(懒加载): `void registerLazySingleton(FactoryFunc func)` 这种方式只有第一次注入依赖的时候,才会初始化服务,并且每次返回相同实例。 202 | 203 | ### 覆盖注册 204 | 205 | 如果你在容器中注册了两次同一服务的话,默认情况下会在调试模式中得到一个断言,就像下面这样。 206 | ``` dart 207 | void setupLocator(){ 208 | getIt.registerSingleton(NavigateService()); 209 | getIt.registerSingleton(NavigateService()); 210 | } 211 | ``` 212 | > Failed assertion: line 53 pos 12: 'allowReassignment || !_factories.containsKey(T)': Type NavigateService is already registered 213 | 214 | get_it 会认为你可能是写错了,所以提醒你这里注册了**两次**相同服务。如果你真的必须覆盖注册,那么你可以通过设置属性 `allowReassignment == true` 来关闭此断言。 215 | 216 | ### 重置容器 217 | 如果你想要重置所有容器,可以调用 `reset()` 方法。一般在做测试的时候会用到。 218 | 219 | # Q&A 220 | ## ServiceLocator 与 Dependency Injection & Inversion of Control 的关系 221 | 222 | 我们在上面看到,当我们使用 ServiceLocator 之后,实现了控制反转(Ioc)。服务不再由使用者创建,而是通过容器注入。这样我们可以不再依赖于具体的实现,而是依赖于一层薄薄的的接口。这样调用者不再知道服务具体实现细节,可以很轻松的使用 mock 数据进行替换。ServiceLocator 其实就是一种特殊的控制反转。 223 | 224 | Dependency Injection 实际上和 ServiceLocator 解决的是同样的问题。但是它又与DI的实现原理上有所不同。由于 Flutter 为了减少打包后应用体积禁用了 dart 的反射包,所以你不知道神奇注入对象的来源,这样一来大多数依赖于反射的 DI 包也就没法用了。 225 | 226 | ## 获取服务的性能 227 | 我们可以从 get_it 的源码中看到,这个 ServiceLocator 就是用一个 map 在储存数据。 228 | 229 | ``` dart 230 | final _factories = new Map>(); 231 | ``` 232 | 所以获取服务的性能是 O(1)。 233 | 234 | # 写在最后 235 | 本文参考了以下资料: 236 | - [Navigate without context in Flutter with a Navigation Service](https://www.filledstacks.com/snippet/navigate-without-context-in-flutter-with-a-navigation-service) 237 | - [One to find them all: How to use Service Locators with Flutter](https://www.burkharts.net/apps/blog/one-to-find-them-all-how-to-use-service-locators-with-flutter/) 238 | 239 | 感兴趣的同学可以去阅读一下大师的文章。 240 | 241 | 这次介绍的库非常轻量,你可以很快速的上手它。这里你可能会觉得它与 InheritWidget 有些相似。虽然都在解决模型依赖问题,get_it 不仅能够在 Widget tree 中进行使用,而且能够解决模型间的依赖问题。大家可以根据自己项目的情况来选择使用。 242 | 243 | 如果文章中还存在任何问题还请指正!欢迎在下方评论区以及我的邮箱1652219550a@gmail.com 一起讨论,我会及时回复! 244 | -------------------------------------------------------------------------------- /articles/Flutter Widget - Container 布局详解: -------------------------------------------------------------------------------- 1 | 在 Flutter 中,号称一切皆 Widget,手势是 Widget,动画是 Widget,UI 更是 Widget,今天我们就来说说 Widget 里比较特殊的一个,Container。 2 | 3 | ### 1. 介绍 4 | 5 | *Container 初用起来很简单,但是里面的逻辑又有些复杂,我也不敢说完全吃透,所以本文还是以总结网上各种文章为主,再加上自己的理解,如果有不对的地方,请一定指出* 6 | 7 | 在 Flutter 中,所有的功能都被分散成单一功能的 Widget,比如居中有 Center,边框有 Padding,文字是 Text,手势是 GestureDetector,他们各自维护一个功能,但是我们商业 App 的 UI 都很精美,如果要实现一个很好的布局,需要嵌套非常多的布局 Widget,所以 Container 应运而生: 8 | 9 | > A convenience widget that combines common painting, positioning, and sizing widgets. 10 | 11 | 官方文档一语道破了 Container 复杂的原因,它是一个便利部件,融合了绘图、定位和尺寸部件,据我所知,它可以设置大小,背景颜色,边框,圆角,阴影,渐变。而且大小可以有很多种情况,时而依赖于父部件,时而依赖于子部件,时而依赖于自己,所以我们稍后会重点说一说 Container 的布局(尺寸规则,宽高)。 12 | 13 | ### 2. Widget渲染流程 14 | 15 | ![](../pic/container_layout-tree.png) 16 | 17 | Flutter 是树状渲染结构,首先从根结点开始渲染,从上到下传递约束,直到最终的叶子节点(没有子节点了),然后叶子节点根据约束确定自身大小,然后将大小返回给上级结点,然后上一级根据叶子节点的尺寸,决定自己的大小,再返回上一级,最终根节点确定了大小。之后,根结点逐级往下摆放子节点的位置(根据子节点及子节点兄弟节点的大小和偏移量)。 18 | 19 | ### 3. Container渲染流程 20 | 21 | 根据官网的介绍,Container 会首先使用设置的 padding 来围绕子部件,然后对 padding 的大小添加额外的约束(如果非空),然后 Container 被外部的空白区域(margin)包围。在绘制过程中,Container 首先应用变换(transform),然后绘制装饰(decoration)来填充区域,接着绘制子部件,最后绘制前景装饰(foregroundDecoration),同时填充该区域。 22 | 23 | decoration 和 foregroundDecoration 是填充配置,前者是在子部件之下,后者是子部件之上,可以设置填充颜色,边框,填充形状,阴影,渐变色,背景图片等。 24 | 25 | 如果 Container 的约束是有限制的,那么没有子部件的 Container 会尝试尽可能大,如果 Container 的约束是没有限制的(unbounded),它就会尽可能小。 26 | 27 | 有子部件的 Container,根据子部件确定自己的大小。 28 | 29 | ### 4. 有无限制约束 30 | 31 | 到底什么是有限制约束和无限制约束呢,各种部件又都是哪种约束呢? 32 | 33 | 在 Flutter 中,Widget 由底层的 RenderBox 渲染盒渲染,父组件向渲染盒提供约束条件,然后渲染盒用这些约束调整自己的尺寸,约束(Constraint)由最大和最小的宽/高组成,尺寸(Size)由特定的宽/高组成。 34 | 35 | ![](../pic/container_layout-constraints.png) 36 | 37 | 通常来说,有三种处理约束的盒子: 38 | - 尽可能大:Center 和 ListView 等 39 | - 和子部件一样大:Transform 和 Opacity 等 40 | - 特定大小:Image 和 Text 等 41 | - 特殊情况:Row 和 Column 由其给定的约束决定,Container 由其构造函数的参数决定 42 | 43 | 但约束有时会变得紧凑,意思是它没有留给渲染盒子自行决定尺寸的余地(例如最大宽度和最小宽度相等,那允许的宽度是个固定值),例如 App 这个 Widget,它的约束被设置为固定的应用程序内容大小(也就是整个屏幕)。而在 Flutter 中,很多的 widget,尤其是只能有一个子部件的 widget,会传递自己的约束到子部件。也就是说,如果你在 App 根渲染树里嵌套了一系列 widget,他们将会因为紧凑的约束,一级级地贴着。 44 | 45 | 但是有些 widget会使约束宽松,意思是最大的约束保留,但是最小的约束移除了,比如 Center。 46 | 47 | #### 4.1 无限制约束 48 | 49 | - 在某些情况下,赋给 widget 的约束是无限制(unbounded)的,或者说是无限(infinite)的。也就是说,最大宽度和最大高度,都是 double.INFINITY。 50 | - 一个尝试尽可能大的 widget,遇到无限制约束的时候,是不会起作用的,因为它不知道到底该有多大,在 debug 模式下,就会抛出异常。 51 | - 最常见的拥有无限制约束的情况,就是嵌入在弹性盒子里面,比如 Row,Column 或者可以滚动的区域(ListView 或者其他 ScrollView 子类)。 52 | 53 | 需要指出的是,ListView 会在其交叉方向扩张到父部件边界,例如一个纵向滚动的列表,在横向会尽量和父部件一样宽。 54 | 当你在横向滚动列表里,嵌入一个纵向滚动列表的时候,纵向列表会尽可能宽,也就是无限宽,因为横向列表是无尽宽的,这就会异常。 55 | 56 | 另外,弹性盒子(Row 和 Column)在有限制和无限制约束的情况下,表现出来的行为也不同。 57 | - 在有限制的约束时,他们会在其方向上尽可能大。 58 | - 在无限制约束时,他们会在其方向上适应他们子组件(包住子组件)。这种情况下,你不能在弹性盒子里用Expanded,因为这是将无法确定部件大小。 59 | 60 | ### 5. Container 布局(尺寸规则) 61 | 62 | 因为 Container 集合了其他部件的功能,所以它的布局有些复杂,简而言之,按照顺序,Container 会: 63 | 64 | - 遵循对齐规则 65 | - 为子部件调整自身大小 66 | - 遵循宽高和约束 67 | - 然后 Container 尝试尽可能小。 68 | 69 | #### 5.1 来看看官方文档的解释(这个解释看不懂就算了,有点啰嗦): 70 | 71 | - 如果 Container 没有子部件,没有宽高,没有约束,并且父部件提供了无限制约束(unbounded constraints),Container 会尽可能小。 72 | - 如果 Container 没有子部件,没有对齐规则,但是提供了高度、宽度或者约束,那么 Container 会在遵循宽、高、约束和父部件约束的情况下,尽可能小。 73 | - 如果 Container 没有子部件,没有宽高,没有约束,没有对齐,但是父部件提供了有限制约束,那么 Container 会扩张以适应(fit)父部件约束 74 | - 如果 Container 有一个对齐规则,并且父部件提供了无限制约束,那么 Container 会尝试调整自己来包围子部件 75 | - 如果 Container 有一个对齐规则,而且父部件提供了有限制约束,那么 Container 会尝试扩张以适应(fit)父部件,然后根据对齐方式,将子部件置于其内 76 | - 另外,Container 有子部件,但是 Container 没有宽高、约束、对齐规则,那么 Container 会传递父部件的约束到其子部件,然后调整自身来匹配子部件。 77 | - margin 和 padding 也会影响布局,decoration 会隐性增加 padding(比如设置 border)。 78 | - 默认是尽可能大 79 | 80 | #### 5.2 我们来总结一下: 81 | 82 | | maxWidth | maxHeight | 约束 | 有无子组件 | 布局规则 83 | |----|----|----|----|----| 84 | |有值|有值|有限制|无|尽可能大| 85 | |有值|无值|高度无限制,宽度有限制|无|高度尽可能小,宽度尽可能大| 86 | |无值|有值|高度有限制,宽度无限制|无|高度尽可能大,宽度尽可能小| 87 | |无值|无值|无限制|无|尽可能小| 88 | |都行|都行|都行|有|在满足约束的前提下尽可能小| 89 | 90 | 91 | #### 5.3 例子 92 | 93 | 没有子组件,有约束,尽可能大↓ 94 | ![没有子组件,有约束,尽可能大](../pic/container_layout-bigger.png) 95 | 96 | 没有子组件,父组件约束最大宽度是屏幕宽度,最大高度是无限,自己的约束最小宽度是100,最小高度是100,则高度尽可能小到100,宽度尽可能大到屏幕宽度↓ 97 | 98 | ![](../pic/container_layout-wider.png) 99 | 100 | 父组件最大约束是屏幕宽高,自己是固定宽度100,则宽度是100,高度尽可能大到屏幕高度↓ 101 | 102 | ![](../pic/container_layout-higher.png) 103 | 104 | 自己没有设置约束,有子组件,所以自己包住子组件↓ 105 | 106 | ![](../pic/container_layout-smaller.png) 107 | 108 | 自己设置固定高度100,宽度没有约束,有子组件,则高度为100,宽度包住子组件↓ 109 | 110 | ![](../pic/container_layout-fixed_height_narrower.png) 111 | 112 | 有子组件,自己的高度是固定100,宽度设置为无限,则高度为100,宽度是父组件的约束屏幕宽度↓ 113 | ![](../pic/container_layout-fixed_height_wider.png) 114 | 有子组件,设置最小宽高为无限,则大小为屏幕大小↓ 115 | 116 | ![](../pic/container_layout-force_wider.png) 117 | 118 | ### 6. 总结 119 | 120 | Container 应该是 Flutter 中最灵活的布局 widget,大家一定要善用 Container,用巧妙的方式处理布局,否则可能会让代码可读性变差,难以维护 121 | 122 | ### 7. 参考文献 123 | 124 | [Flutter 快速上车之 Widget](https://juejin.im/post/5b8ce76f51882542c0626887) 125 | 126 | [Container class](https://api.flutter.dev/flutter/widgets/Container-class.html) 127 | 128 | [Layouts in Flutter](https://flutter.dev/docs/development/ui/layout) 129 | 130 | [Dealing with box constraints](https://flutter.dev/docs/development/ui/layout/box-constraints) 131 | 132 | [Flutter — Container Cheat Sheet](https://medium.com/jlouage/container-de5b0d3ad184) 133 | 134 | [Widgets: Container](https://flutterdoc.com/widgets-container-d8eee21ad2f4) 135 | 136 | [What is a Container in Flutter?](https://flutteracademy.com/posts/what-is-a-container-in-flutter/) 137 | 138 | [Container Widget with example Flutter Tutorial](https://www.youtube.com/watch?v=wj1ZGQ-Jc04) 139 | 140 | [Container Widget In Flutter](https://www.youtube.com/watch?v=4KVCaP69GV8) 141 | 142 | [Understanding Flutter Layout (Box)Constraints](https://proandroiddev.com/understanding-flutter-layout-box-constraints-292cc0d5e807) 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /articles/Flutter 如何优雅的嵌入现有应用.md: -------------------------------------------------------------------------------- 1 | # 如何将 Flutter 优雅的嵌入现有应用 2 | 3 | ![thrio logo](../pic/thrio/thrio.png) 4 | 5 | ## 为什么写 thrio 6 | 7 | 在早期 Flutter 发布的时候,谷歌虽然提供了 iOS 和 Android App 上的 Flutter 嵌入方案,但主要针对的是纯 Flutter 的情形,混合开发支持的并不友好。 8 | 9 | 所谓的纯 RN、纯 weex 应用的生命周期都不存在,所以也不会存在一个纯 Flutter 的 App 的生命周期,因为我们总是有需要复用现有模块。 10 | 11 | 所以我们需要一套足够完整的 Flutter 嵌入原生 App 的路由解决方案,所以我们自己造了个轮子 [thrio](https://github.com/hellobike/thrio) ,现已开源,遵循 MIT 协议。 12 | 13 | ## thrio 的设计原则 14 | 15 | - 原则一,dart 端最小改动接入 16 | - 原则二,原生端最小侵入 17 | - 原则三,三端保持一致的 API 18 | 19 | thrio 所有功能的设计,都会遵守这三个原则。下面会逐步对功能层面一步步展开进行说明,后面也会有原理性的解析。 20 | 21 | ## thrio 的页面路由 22 | 23 | 以 dart 中的 `Navigator` 为主要参照,提供以下路由能力: 24 | 25 | - push,打开一个页面并放到路由栈顶 26 | - pop,关闭路由栈顶的页面 27 | - popTo,关闭到某一个页面 28 | - remove,删除任意页面 29 | 30 | Navigator 中的 API 几乎都可以通过组合以上方法实现,`replace` 方法暂未提供。 31 | 32 | 不提供 iOS 中存在的 `present` 功能,因为会导致原生路由栈被覆盖,维护复杂度会非常高,如确实需要可以通过修改转场动画实现。 33 | 34 | ### 页面的索引 35 | 36 | 要路由,我们需要对页面建立索引,通常情况下,我们只需要给每个页面设定一个 `url` 就可以了,如果每个页面都只打开一次的话,不会有任何问题。但是当一个页面被打开多次之后,仅仅通过 url 是无法定位到明确的页面实例的,所以在 `thrio` 中我们增加了页面索引的概念,具体在 API 中都会以 `index` 来表示,同一个 url 第一个打开的页面的索引为 `1` ,之后同一个 `url` 的索引不断累加。 37 | 38 | 如此,唯一定位一个页面的方式为 `url` + `index`,在 dart 中 `route` 的 `name` 就是由 `'$url.$index'` 组合而成。 39 | 40 | 很多时候,使用者不需要关注 `index`,只有当需要定位到多开的 `url` 的页面中的某一个时才需要关注 `index`。最简单获取 `index` 的方式为 `push` 方法的回调返回值。 41 | 42 | ### 页面的 push 43 | 44 | 1. dart 端打开页面 45 | 46 | ```dart 47 | ThrioNavigator.push(url: 'flutter1'); 48 | // 传入参数 49 | ThrioNavigator.push(url: 'native1', params: { '1': {'2': '3'}}); 50 | // 是否动画,目前在内嵌的dart页面中动画无法取消,原生iOS页面有效果 51 | ThrioNavigator.push(url: 'native1', animated:true); 52 | // 接收锁打开页面的关闭回调 53 | ThrioNavigator.push( 54 | url: 'biz2/flutter2', 55 | params: {'1': {'2': '3'}}, 56 | poppedResult: (params) => ThrioLogger.v('biz2/flutter2 popped: $params'), 57 | ); 58 | ``` 59 | 60 | 2. iOS 端打开页面 61 | 62 | ```objc 63 | [ThrioNavigator pushUrl:@"flutter1"]; 64 | // 接收所打开页面的关闭回调 65 | [ThrioNavigator pushUrl:@"biz2/flutter2" poppedResult:^(id _Nonnull params) { 66 | ThrioLogV(@"biz2/flutter2 popped: %@", params); 67 | }]; 68 | ``` 69 | 70 | 3. Android 端打开页面 71 | 72 | ```kotlin 73 | ThrioNavigator.push(this, "biz1/flutter1", 74 | mapOf("k1" to 1), 75 | false, 76 | poppedResult = { 77 | Log.e("Thrio", "native1 popResult call params $it") 78 | } 79 | ) 80 | ``` 81 | 82 | 4. 连续打开页面 83 | 84 | - dart 端只需要 await push,就可以连续打开页面 85 | - 原生端需要等待 push 的 result 回调返回才能打开第二个页面 86 | 87 | 5. 获取所打开页面关闭后的回调参数 88 | 89 | - 三端都可以通过闭包 poppedResult 来获取 90 | 91 | ### 页面的 pop 92 | 93 | 1. dart 端关闭顶层页面 94 | 95 | ```dart 96 | // 默认动画开启 97 | ThrioNavigator.pop(); 98 | // 不开启动画,原生和dart页面都生效 99 | ThrioNavigator.pop(animated: false); 100 | // 关闭当前页面,并传递参数给push这个页面的回调 101 | ThrioNavigator.pop(params: 'popped flutter1'), 102 | ``` 103 | 104 | 2. iOS 端关闭顶层页面 105 | 106 | ```objc 107 | // 默认动画开启 108 | [ThrioNavigator pop]; 109 | // 关闭动画 110 | [ThrioNavigator popAnimated:NO]; 111 | // 关闭当前页面,并传递参数给push这个页面的回调 112 | [ThrioNavigator popParams:@{@"k1": @3}]; 113 | ``` 114 | 115 | 3. Android 端关闭顶层页面 116 | 117 | ```kotlin 118 | ThrioNavigator.pop(this, params, animated) 119 | ``` 120 | 121 | ### 页面的 popTo 122 | 123 | 1. dart 端关闭到页面 124 | 125 | ```dart 126 | // 默认动画开启 127 | ThrioNavigator.popTo(url: 'flutter1'); 128 | // 不开启动画,原生和dart页面都生效 129 | ThrioNavigator.popTo(url: 'flutter1', animated: false); 130 | ``` 131 | 132 | 2. iOS 端关闭到页面 133 | 134 | ```objc 135 | // 默认动画开启 136 | [ThrioNavigator popToUrl:@"flutter1"]; 137 | // 关闭动画 138 | [ThrioNavigator popToUrl:@"flutter1" animated:NO]; 139 | ``` 140 | 141 | 3. Android 端关闭到页面 142 | 143 | ```kotlin 144 | ThrioNavigator.popTo(context, url, index) 145 | ``` 146 | 147 | ### 页面的 remove 148 | 149 | 1. dart 端关闭特定页面 150 | 151 | ```dart 152 | ThrioNavigator.remove(url: 'flutter1'); 153 | // 只有当页面是顶层页面时,animated参数才会生效 154 | ThrioNavigator.remove(url: 'flutter1', animated: true); 155 | ``` 156 | 157 | 2. iOS 端关闭特定页面 158 | 159 | ```objc 160 | [ThrioNavigator removeUrl:@"flutter1"]; 161 | // 只有当页面是顶层页面时,animated参数才会生效 162 | [ThrioNavigator removeUrl:@"flutter1" animated:NO]; 163 | ``` 164 | 165 | 3. Android 端关闭特定页面 166 | 167 | ```kotlin 168 | ThrioNavigator.remove(context, url, index) 169 | ``` 170 | 171 | ## thrio 的页面通知 172 | 173 | 页面通知一般来说并不在路由的范畴之内,但我们在实际开发中却经常需要使用到,由此产生的各种模块化框架一个比一个复杂。 174 | 175 | 那么问题来了,这些模块化框架很难在三端互通,所有的这些模块化框架提供的能力无非最终是一个页面通知的能力,而且页面通知我们可以非常简单的在三端打通。 176 | 177 | 鉴于此,页面通知作为 thrio 的一个必备能力被引入了 thrio。 178 | 179 | ### 发送页面通知 180 | 181 | 1. dart 端给特定页面发通知 182 | 183 | ```dart 184 | ThrioNavigator.notify(url: 'flutter1', name: 'reload'); 185 | ``` 186 | 187 | 2. iOS 端给特定页面发通知 188 | 189 | ```objc 190 | [ThrioNavigator notifyUrl:@"flutter1" name:@"reload"]; 191 | ``` 192 | 193 | 3. Android 端给特定页面发通知 194 | 195 | ```kotlin 196 | ThrioNavigator.notify(url, index, params) 197 | ``` 198 | 199 | ### 接收页面通知 200 | 201 | 1. dart 端接收页面通知 202 | 203 | 使用 `NavigatorPageNotify` 这个 `Widget` 来实现在任何地方接收当前页面收到的通知。 204 | 205 | ```dart 206 | NavigatorPageNotify( 207 | name: 'page1Notify', 208 | onPageNotify: (params) => 209 | ThrioLogger.v('flutter1 receive notify: $params'), 210 | child: Xxxx()); 211 | ``` 212 | 213 | 2. iOS 端接收页面通知 214 | 215 | `UIViewController`实现协议`NavigatorPageNotifyProtocol`,通过 `onNotify` 来接收页面通知 216 | 217 | ```objc 218 | - (void)onNotify:(NSString *)name params:(NSDictionary *)params { 219 | ThrioLogV(@"native1 onNotify: %@, %@", name, params); 220 | } 221 | ``` 222 | 223 | 3. Android 端接收页面通知 224 | 225 | `Activity`实现协议`OnNotifyListener`,通过 `onNotify` 来接收页面通知 226 | 227 | ```kotlin 228 | class Activity : AppCompatActivity(), OnNotifyListener { 229 | override fun onNotify(name: String, params: Any?) { 230 | } 231 | } 232 | ``` 233 | 234 | 因为 Android activity 在后台可能会被销毁,所以页面通知实现了一个懒响应的行为,只有当页面呈现之后才会收到该通知,这也符合页面需要刷新的场景。 235 | 236 | ## thrio 的模块化 237 | 238 | 模块化在 thrio 里面只是一个非核心功能,仅仅为了实现原则二而引入原生端。 239 | 240 | thrio 的模块化能力由一个类提供,`ThrioModule`,很小巧,主要提供了 `Module` 的注册链和初始化链,让代码可以根据路由 url 进行文件分级分类。 241 | 242 | 注册链将所有模块串起来,字母块由最近的父一级模块注册,新增模块的耦合度最低。 243 | 244 | 初始化链将所有模块需要初始化的代码串起来,同样是为了降低耦合度,在初始化链上可以就近注册模块的页面的构造器,页面路由观察者,页面生命周期观察者等,也可以在多引擎模式下提前启动某一个引擎。 245 | 246 | 模块间通信的能力由页面通知实现。 247 | 248 | ```dart 249 | mixin ThrioModule { 250 | /// A function for registering a module, which will call 251 | /// the `onModuleRegister` function of the `module`. 252 | /// 253 | void registerModule(ThrioModule module); 254 | 255 | /// A function for module initialization that will call 256 | /// the `onPageRegister`, `onModuleInit` and `onModuleAsyncInit` 257 | /// methods of all modules. 258 | /// 259 | void initModule(); 260 | 261 | /// A function for registering submodules. 262 | /// 263 | void onModuleRegister() {} 264 | 265 | /// A function for registering a page builder. 266 | /// 267 | void onPageRegister() {} 268 | 269 | /// A function for module initialization. 270 | /// 271 | void onModuleInit() {} 272 | 273 | /// A function for module asynchronous initialization. 274 | /// 275 | void onModuleAsyncInit() {} 276 | 277 | /// Register an page builder for the router. 278 | /// 279 | /// Unregistry by calling the return value `VoidCallback`. 280 | /// 281 | VoidCallback registerPageBuilder(String url, NavigatorPageBuilder builder); 282 | 283 | /// Register observers for the life cycle of Dart pages. 284 | /// 285 | /// Unregistry by calling the return value `VoidCallback`. 286 | /// 287 | /// Do not override this method. 288 | /// 289 | VoidCallback registerPageObserver(NavigatorPageObserver pageObserver); 290 | 291 | /// Register observers for route action of Dart pages. 292 | /// 293 | /// Unregistry by calling the return value `VoidCallback`. 294 | /// 295 | /// Do not override this method. 296 | /// 297 | VoidCallback registerRouteObserver(NavigatorRouteObserver routeObserver); 298 | } 299 | ``` 300 | 301 | ## thrio 的页面生命周期 302 | 303 | 原生端可以获得所有页面的生命周期,Dart 端只能获取自身页面的生命周期 304 | 305 | 1. dart 端获取页面的生命周期 306 | 307 | ```dart 308 | class Module with ThrioModule, NavigatorPageObserver { 309 | @override 310 | void onPageRegister() { 311 | registerPageObserver(this); 312 | } 313 | 314 | @override 315 | void didAppear(RouteSettings routeSettings) {} 316 | 317 | @override 318 | void didDisappear(RouteSettings routeSettings) {} 319 | 320 | @override 321 | void onCreate(RouteSettings routeSettings) {} 322 | 323 | @override 324 | void willAppear(RouteSettings routeSettings) {} 325 | 326 | @override 327 | void willDisappear(RouteSettings routeSettings) {} 328 | } 329 | ``` 330 | 331 | 2. iOS 端获取页面的生命周期 332 | 333 | ```objc 334 | @interface Module1 : ThrioModule 335 | 336 | @end 337 | 338 | @implementation Module1 339 | 340 | - (void)onPageRegister { 341 | [self registerPageObserver:self]; 342 | } 343 | 344 | - (void)onCreate:(NavigatorRouteSettings *)routeSettings { } 345 | 346 | - (void)willAppear:(NavigatorRouteSettings *)routeSettings { } 347 | 348 | - (void)didAppear:(NavigatorRouteSettings *)routeSettings { } 349 | 350 | - (void)willDisappear:(NavigatorRouteSettings *)routeSettings { } 351 | 352 | - (void)didDisappear:(NavigatorRouteSettings *)routeSettings { } 353 | 354 | @end 355 | 356 | ``` 357 | 358 | ## thrio 的页面路由观察者 359 | 360 | 原生端可以观察所有页面的路由行为,dart 端只能观察 dart 页面的路由行为 361 | 362 | 1. dart 端获取页面的路由行为 363 | 364 | ```dart 365 | class Module with ThrioModule, NavigatorRouteObserver { 366 | @override 367 | void onModuleRegister() { 368 | registerRouteObserver(this); 369 | } 370 | 371 | @override 372 | void didPop( 373 | RouteSettings routeSettings, 374 | RouteSettings previousRouteSettings, 375 | ) {} 376 | 377 | @override 378 | void didPopTo( 379 | RouteSettings routeSettings, 380 | RouteSettings previousRouteSettings, 381 | ) {} 382 | 383 | @override 384 | void didPush( 385 | RouteSettings routeSettings, 386 | RouteSettings previousRouteSettings, 387 | ) {} 388 | 389 | @override 390 | void didRemove( 391 | RouteSettings routeSettings, 392 | RouteSettings previousRouteSettings, 393 | ) {} 394 | } 395 | ``` 396 | 397 | 2. iOS 端获取页面的路由行为 398 | 399 | ```objc 400 | @interface Module2 : ThrioModule 401 | 402 | @end 403 | 404 | @implementation Module2 405 | 406 | - (void)onPageRegister { 407 | [self registerRouteObserver:self]; 408 | } 409 | 410 | - (void)didPop:(NavigatorRouteSettings *)routeSettings 411 | previousRoute:(NavigatorRouteSettings * _Nullable)previousRouteSettings { 412 | } 413 | 414 | - (void)didPopTo:(NavigatorRouteSettings *)routeSettings 415 | previousRoute:(NavigatorRouteSettings * _Nullable)previousRouteSettings { 416 | } 417 | 418 | - (void)didPush:(NavigatorRouteSettings *)routeSettings 419 | previousRoute:(NavigatorRouteSettings * _Nullable)previousRouteSettings { 420 | } 421 | 422 | - (void)didRemove:(NavigatorRouteSettings *)routeSettings 423 | previousRoute:(NavigatorRouteSettings * _Nullable)previousRouteSettings { 424 | } 425 | 426 | @end 427 | 428 | ``` 429 | 430 | ## thrio 的额外功能 431 | 432 | ### iOS 显隐当前页面的导航栏 433 | 434 | 原生的导航栏在 dart 上一般情况下是不需要的,但切换到原生页面又需要把原生的导航栏置回来,thrio 不提供的话,使用者较难扩展,我之前在目前一个主流的 Flutter 接入库上进行此项功能的扩展,很不流畅,所以这个功能最好的效果还是 thrio 直接内置,切换到 dart 页面默认会隐藏原生的导航栏,切回原生页面也会自动恢复。另外也可以手动隐藏原生页面的导航栏。 435 | 436 | ```objc 437 | viewController.thrio_hidesNavigationBar = NO; 438 | ``` 439 | 440 | ### 支持页面关闭前弹窗确认的功能 441 | 442 | 如果用户正在填写一个表单,你可能经常会需要弹窗确认是否关闭当前页面的功能。 443 | 444 | 在 dart 中,有一个 `Widget` 提供了该功能,thrio 完好的保留了这个功能。 445 | 446 | ```dart 447 | WillPopScope( 448 | onWillPop: () async => true, 449 | child: Container(), 450 | ); 451 | ``` 452 | 453 | 在 iOS 中,thrio 提供了类似的功能,返回 `NO` 表示不会关闭,一旦设置会将侧滑返回手势禁用 454 | 455 | ```objc 456 | viewController.thrio_willPopBlock = ^(ThrioBoolCallback _Nonnull result) { 457 | result(NO); 458 | }; 459 | ``` 460 | 461 | 关于 `FlutterViewController` 的侧滑返回手势,Flutter 默认支持的是纯 Flutter 应用,仅支持单一的 `FlutterViewController` 作为整个 App 的容器,内部已经将 `FlutterViewController` 的侧滑返回手势去掉。但 thrio 要解决的是 Flutter 与原生应用的无缝集成,所以必须将侧滑返回的手势加回来。 462 | 463 | ## thrio 的设计解析 464 | 465 | 目前开源 Flutter 嵌入原生的库,主要的还是通过切换 FlutterEngine 上的原生容器来实现的,这是 Flutter 原本提供的原生容器之上最小改动而实现,需要小心处理好容器切换的时序,否则在页面导航时会产生崩溃。基于 Flutter 提供的这个功能, thrio 构建了三端一致的页面管理 API。 466 | 467 | ### dart 的核心类 468 | 469 | dart 端只管理 dart 页面 470 | 471 | 1. 基于 `RouteSettings` 进行扩展,复用现有的字段 472 | 473 | - name = url.index 474 | - isInitialRoute = !isNested 475 | - arguments = params 476 | 477 | 2. 基于 `MaterialPageRoute` 扩展的 `NavigatorPageRoute` 478 | 479 | - 主要提供页面描述和转场动画的是否配置的功能 480 | 481 | 2. 基于 `Navigator` 扩展,封装 `NavigatorWidget`,提供以下方法 482 | 483 | ```dart 484 | Future push(RouteSettings settings, { 485 | bool animated = true, 486 | NavigatorParamsCallback poppedResult, 487 | }); 488 | 489 | Future pop(RouteSettings settings, {bool animated = true}); 490 | 491 | Future popTo(RouteSettings settings, {bool animated = true}); 492 | 493 | Future remove(RouteSettings settings, {bool animated = false}); 494 | 495 | ``` 496 | 497 | 3. 封装 `ThrioNavigator` 路由 API 498 | 499 | ```dart 500 | abstract class ThrioNavigator { 501 | /// Push the page onto the navigation stack. 502 | /// 503 | /// If a native page builder exists for the `url`, open the native page, 504 | /// otherwise open the flutter page. 505 | /// 506 | static Future push({ 507 | @required String url, 508 | params, 509 | bool animated = true, 510 | NavigatorParamsCallback poppedResult, 511 | }); 512 | 513 | /// Send a notification to the page. 514 | /// 515 | /// Notifications will be triggered when the page enters the foreground. 516 | /// Notifications with the same `name` will be overwritten. 517 | /// 518 | static Future notify({ 519 | @required String url, 520 | int index, 521 | @required String name, 522 | params, 523 | }); 524 | 525 | /// Pop a page from the navigation stack. 526 | /// 527 | static Future pop({params, bool animated = true}) 528 | 529 | static Future popTo({ 530 | @required String url, 531 | int index, 532 | bool animated = true, 533 | }); 534 | 535 | /// Remove the page with `url` in the navigation stack. 536 | /// 537 | static Future remove({ 538 | @required String url, 539 | int index, 540 | bool animated = true, 541 | }); 542 | } 543 | ``` 544 | 545 | ### iOS 的核心类 546 | 547 | 1. `NavigatorRouteSettings` 对应于 dart 的 `RouteSettings` 类,并提供相同数据结构 548 | 549 | ```objc 550 | 551 | @interface NavigatorRouteSettings : NSObject 552 | 553 | @property (nonatomic, copy, readonly) NSString *url; 554 | 555 | @property (nonatomic, strong, readonly) NSNumber *index; 556 | 557 | @property (nonatomic, assign, readonly) BOOL nested; 558 | 559 | @property (nonatomic, copy, readonly, nullable) id params; 560 | 561 | @end 562 | 563 | ``` 564 | 565 | 2. `NavigatorPageRoute` 对应于 dart 的 `NavigatorPageRoute` 类 566 | 567 | - 存储通知、页面关闭回调、NavigatorRouteSettings 568 | - route 的双向链表 569 | 570 | 3. 基于 `UINavigationController` 扩展,功能类似 dart 的 `NavigatorWidget` 571 | 572 | - 提供一些列的路由内部接口 573 | - 并能兼容非 thrio 体系内的页面 574 | 575 | 4. 基于 `UIViewController` 扩展 576 | 577 | - 提供 `FlutterViewController` 容器上的 dart 页面的管理功能 578 | - 提供 popDisable 等功能 579 | 580 | 5. 封装 `ThrioNavigator` 路由 API 581 | 582 | ```objc 583 | @interface ThrioNavigator : NSObject 584 | 585 | /// Push the page onto the navigation stack. 586 | /// 587 | /// If a native page builder exists for the url, open the native page, 588 | /// otherwise open the flutter page. 589 | /// 590 | + (void)pushUrl:(NSString *)url 591 | params:(id)params 592 | animated:(BOOL)animated 593 | result:(ThrioNumberCallback)result 594 | poppedResult:(ThrioIdCallback)poppedResult; 595 | 596 | /// Send a notification to the page. 597 | /// 598 | /// Notifications will be triggered when the page enters the foreground. 599 | /// Notifications with the same name will be overwritten. 600 | /// 601 | + (void)notifyUrl:(NSString *)url 602 | index:(NSNumber *)index 603 | name:(NSString *)name 604 | params:(id)params 605 | result:(ThrioBoolCallback)result; 606 | 607 | /// Pop a page from the navigation stack. 608 | /// 609 | + (void)popParams:(id)params 610 | animated:(BOOL)animated 611 | result:(ThrioBoolCallback)result; 612 | 613 | /// Pop the page in the navigation stack until the page with `url`. 614 | /// 615 | + (void)popToUrl:(NSString *)url 616 | index:(NSNumber *)index 617 | animated:(BOOL)animated 618 | result:(ThrioBoolCallback)result; 619 | 620 | /// Remove the page with `url` in the navigation stack. 621 | /// 622 | + (void)removeUrl:(NSString *)url 623 | index:(NSNumber *)index 624 | animated:(BOOL)animated 625 | result:(ThrioBoolCallback)result; 626 | 627 | @end 628 | ``` 629 | 630 | ### dart 与 iOS 路由栈的结构 631 | 632 | ![thrio-architecture](../pic/thrio/thrio-architecture.png) 633 | 634 | 1. 一个应用允许启动多个 Flutter 引擎,可让每个引擎运行的代码物理隔离,按需启用,劣势是启动多个 Flutter 引擎可能导致资源消耗过多而引起问题; 635 | 2. 一个 Flutter 引擎通过切换可以匹配到多个 FlutterViewController,这是 Flutter 优雅嵌入原生应用的前提条件 636 | 3. 一个 FlutterViewController 可以内嵌多个 Dart 页面,有效减少单个 FlutterViewController 只打开一个 Dart 页面导致的内存消耗过多问题,关于内存消耗的问题,后续会有提到。 637 | 638 | ### dart 与 iOS push 的时序图 639 | 640 | ![thrio-push](../pic/thrio/thrio-push.png) 641 | 642 | 1. 所有路由操作最终汇聚于原生端开始,如果始于 dart 端,则通过 channel 调用原生端的 API 643 | 2. 通过 `url+index` 定位到页面 644 | 3. 如果页面是原生页面,则直接进行相关操作 645 | 4. 如果页面是 Flutter 容器,则通过 channel 调用 dart 端对应的路由 API 646 | 5. 接 4 步,如果 dart 端对应的路由 API 操作完成后回调,如果成功,则执行原生端的路由栈同步,如果失败,则回调入口 API 的 result 647 | 6. 接 4 不,如果 dart 端对应的路由 API 操作成功,则通过 route channel 调用原生端对应的 route observer,通过 page channel 调用原生端对应的 page observer。 648 | 649 | ### dart 与 iOS pop 的时序图 650 | 651 | ![thrio-pop](../pic/thrio/thrio-pop.png) 652 | 653 | 1. pop 的流程与 push 基本一致; 654 | 2. pop 需要考虑页面是否可关闭的问题; 655 | 3. 但在 iOS 中,侧滑返回手势会导致问题, `popViewControllerAnimated:` 会在手势开始的时候调用,导致 dart 端的页面已经被 pop 掉,但如果手势被放弃了,则导致两端的页面栈不一致,thrio 已经解决了这个问题,具体流程稍复杂,源码可能更好的说明。 656 | 657 | ### dart 与 iOS popTo 的时序图 658 | 659 | ![thrio-popTo](../pic/thrio/thrio-popTo.png) 660 | 661 | 1. popTo 的流程与 push 基本一致; 662 | 2. 但在多引擎模式下,popTo 需要处理多引擎的路由栈同步的问题; 663 | 3. 另外在 Dart 端,popTo 实际上是多个 pop 或者 remove 构成的,最终产生多次的 didPop 或 didRemove 行为,需要将多个 pop 或 remove 组合起来形成一个 didPopTo 行为。 664 | 665 | ### dart 与 iOS remove 的时序图 666 | 667 | ![thrio-remove](../pic/thrio/thrio-remove.png) 668 | 669 | 1. remove 的流程与 push 基本一致。 670 | -------------------------------------------------------------------------------- /pic/add_dependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/add_dependency.png -------------------------------------------------------------------------------- /pic/async_func.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/async_func.png -------------------------------------------------------------------------------- /pic/change_color_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/change_color_01.png -------------------------------------------------------------------------------- /pic/change_color_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/change_color_02.png -------------------------------------------------------------------------------- /pic/change_color_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/change_color_03.png -------------------------------------------------------------------------------- /pic/complex_state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/complex_state.png -------------------------------------------------------------------------------- /pic/container_layout-bigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/container_layout-bigger.png -------------------------------------------------------------------------------- /pic/container_layout-constraints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/container_layout-constraints.png -------------------------------------------------------------------------------- /pic/container_layout-fixed_height_narrower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/container_layout-fixed_height_narrower.png -------------------------------------------------------------------------------- /pic/container_layout-fixed_height_wider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/container_layout-fixed_height_wider.png -------------------------------------------------------------------------------- /pic/container_layout-force_wider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/container_layout-force_wider.png -------------------------------------------------------------------------------- /pic/container_layout-higher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/container_layout-higher.png -------------------------------------------------------------------------------- /pic/container_layout-smaller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/container_layout-smaller.png -------------------------------------------------------------------------------- /pic/container_layout-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/container_layout-tree.png -------------------------------------------------------------------------------- /pic/container_layout-wider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/container_layout-wider.png -------------------------------------------------------------------------------- /pic/docs_development_add-to-app_index_01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/docs_development_add-to-app_index_01.gif -------------------------------------------------------------------------------- /pic/docs_development_add-to-app_index_02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/docs_development_add-to-app_index_02.gif -------------------------------------------------------------------------------- /pic/fix_bug_provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/fix_bug_provider.png -------------------------------------------------------------------------------- /pic/get_it.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/get_it.png -------------------------------------------------------------------------------- /pic/graphics_pipline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/graphics_pipline.png -------------------------------------------------------------------------------- /pic/hybrid-ios-embed-xcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/hybrid-ios-embed-xcode.png -------------------------------------------------------------------------------- /pic/hybrid-ios-framework-search-paths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/hybrid-ios-framework-search-paths.png -------------------------------------------------------------------------------- /pic/jank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/jank.gif -------------------------------------------------------------------------------- /pic/load_balancer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/load_balancer.png -------------------------------------------------------------------------------- /pic/load_balancer_init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/load_balancer_init.png -------------------------------------------------------------------------------- /pic/provider_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/provider_error.png -------------------------------------------------------------------------------- /pic/simple_state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/simple_state.png -------------------------------------------------------------------------------- /pic/state_management_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/state_management_demo.png -------------------------------------------------------------------------------- /pic/switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/switcher.png -------------------------------------------------------------------------------- /pic/sync_func.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/sync_func.png -------------------------------------------------------------------------------- /pic/thrio/thrio-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/thrio/thrio-architecture.png -------------------------------------------------------------------------------- /pic/thrio/thrio-pop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/thrio/thrio-pop.png -------------------------------------------------------------------------------- /pic/thrio/thrio-popTo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/thrio/thrio-popTo.png -------------------------------------------------------------------------------- /pic/thrio/thrio-push.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/thrio/thrio-push.png -------------------------------------------------------------------------------- /pic/thrio/thrio-remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/thrio/thrio-remove.png -------------------------------------------------------------------------------- /pic/thrio/thrio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/thrio/thrio.png -------------------------------------------------------------------------------- /pic/tip_how_to_find_flutter_page_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/pic/tip_how_to_find_flutter_page_01.png -------------------------------------------------------------------------------- /tips/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tips/tip_how_to_ask_for_help.md: -------------------------------------------------------------------------------- 1 | # 开发 Flutter App 时遇到困难,如何寻求帮助 2 | 3 | Flutter 活跃的社区与快速迭代的特性,吸引了许多开发者的注意。由于许多特性处于较为早期的阶段,框架本身或文档不甚完善,导致开发者经常踩坑。 4 | 5 | 寻求帮助有很多途径,Flutter 团队官方以及社区比较推荐的方式是**到 GitHub flutter/flutter 仓库的 issue 区,利用搜索功能查询已提出的 issue**。 6 | 7 | 若能够找到你相同情况的 issue,请按照 issue 讨论的方案进行尝试,若能够解决问题,请在有效的讨论内容上给予👍,作为对解决方案的反馈与赞赏,给其他开发者提供参考。 8 | 9 | 若找到的 issue 不能解决问题,请在 issue 后描述出你的情况,以及你尝试过的方案,同其他开发者展开讨论。必要的时候可以附带截图或可复现问题的最简代码。 10 | 11 | 若无法找到适合的 issue,请新建一个 issue,并在正文中贴上你的 `$ flutter doctor` 输出信息,详细描述产生问题的步骤,最好配以可以复现问题的最简代码。 12 | 13 | 请注意,无论以何种方式参与 issue 区讨论,都请使用英文。不必过于担心语法和措辞,善用翻译工具,能够妥善表达你的状况就足够了。 -------------------------------------------------------------------------------- /tips/tip_how_to_control_image_cache.md: -------------------------------------------------------------------------------- 1 | # 如何控制图片缓存 2 | 3 | 为了提高图片加载效率,Flutter内部会将加载过的图片,依照最近最少使用原则缓存,这个缓存默认最大100M,默认最多存储1000条。 4 | 5 | 使用100M的内存来缓存图片,对很多APP来说似乎有些大,所以我们可以设置其大小。方法是: 6 | 7 | ```dart 8 | PaintingBinding.instance.imageCache.maximumSizeBytes = 10 << 20; // 10MiB,缓存最大10兆,你也可以设置其它值 9 | PaintingBinding.instance.imageCache.maximumSize = 100; // 最多缓存100条 10 | ``` 11 | 12 | 同时你也可以通过 `PaintingBinding.instance.imageCache.clear()` 来直接清除当前缓存,比如你当前页面需要很大的图片缓存,而退出当前页面又需要释放的时候。 13 | 14 | 15 | ## 原理 16 | 17 | 告诉完答案,下面说说为什么要这么干。 18 | 19 | 我们所有的Image类实例,无论是直接使用 `Image` 还是使用工厂方法 `Image.network` 或 `Image.asset` 等创建,其控制图片的都是其内部的image属性,image属性又是 `ImageProvider` 的子类,`ImageProvider` 通过 `solve` 方法找到图片,并返回 `ImageStream` ,而 `ImageStream` 就代表着图片。 20 | 21 | 在 `solve` 方法中,调用 `PaintingBinding.instance.imageCache.putIfAbsent()` 方法来获取并缓存图片。 22 | 23 | 所以我们可以看到,存储在 `PaintingBinding` 单例中的 `ImageCache` 实例,控制着图片缓存。`ImageCache` 里的宏定义 `_kDefaultSize` 和 `_kDefaultSizeBytes` 代表缓存的默认大小。想操作`ImageCache` 的那些实例方法,只有通过 `PaintingBinding.instance `单例才能拿到 `imageCache` 实例。 24 | 25 | 你可以直接翻看Flutter SDK里面的image_cache.dart源码,里面的方法都很好理解,并且有注释。 26 | 27 | -------------------------------------------------------------------------------- /tips/tip_how_to_find_flutter_page.md: -------------------------------------------------------------------------------- 1 | # 如何判断一个界面是 Flutter 构建的 2 | 3 | 总有开发者说:这种效果肯定不是 Flutter 做的。问其原因,他说 Flutter 做的肯定能看出来嘛(意思就是,Flutter 做的效果毕竟不能和 Native 的效果媲美)。结局当然是这位开发者被打脸,Native App 的效果,Flutter 基本都能做出来。 4 | 5 | 那么怎么样去判断一个界面是不是 Flutter 构建的呢?**最简单的一种办法**,用两指滑动屏幕上的滚动列表,如果此时滚动列表以 2 倍的速度滚动,那么这极大概率是用 Flutter 构建的。用三个四个五个手指滚动呢,那滚动速度就是 3 倍、4 倍、5 倍。这个小技巧对于判断应用的 Flutter 使用状况是非常有效的,你可以轻易地了解一款 App 的什么样的场景和业务使用了 Flutter 构建。 6 | 7 | 前文讲到的技巧,基于一个假设,假设这里有一个滚动列表可供你进行尝试。如果是一个不含可滚动列表的 Flutter 界面,我们如何判断呢。Flutter 的实现机制告诉我们,Flutter 只会有一层 Native 的“画布”,因此可以通过 FLEX、Lookin、Reveal 等工具确定页面上所有元素均绘制在一层 FlutterView 上。 8 | 9 | ![](../pic/tip_how_to_find_flutter_page_01.png) -------------------------------------------------------------------------------- /tips/tips-publication/tips_191123.md: -------------------------------------------------------------------------------- 1 | 本期 知识小集 x Flutter 为大家带来 Flutter 开发 Tips 合集。 2 | 3 | - 如何判断一个界面是 Flutter 构建的(作者:talisk) 4 | - 如何控制图片缓存(作者:Vadaski) 5 | - 开发 Flutter App 时遇到困难,如何寻求帮助(作者:talisk) 6 | 7 | # 如何判断一个界面是 Flutter 构建的 8 | 9 | 总有开发者说:这种效果肯定不是 Flutter 做的。问其原因,他说 Flutter 做的肯定能看出来嘛(意思就是,Flutter 做的效果毕竟不能和 Native 的效果媲美)。结局当然是这位开发者被打脸,Native App 的效果,Flutter 基本都能做出来。 10 | 11 | 那么怎么样去判断一个界面是不是 Flutter 构建的呢?**最简单的一种办法**,用两指滑动屏幕上的滚动列表,如果此时滚动列表以 2 倍的速度滚动,那么这极大概率是用 Flutter 构建的。用三个四个五个手指滚动呢,那滚动速度就是 3 倍、4 倍、5 倍。这个小技巧对于判断应用的 Flutter 使用状况是非常有效的,你可以轻易地了解一款 App 的什么样的场景和业务使用了 Flutter 构建。 12 | 13 | 前文讲到的技巧,基于一个假设,假设这里有一个滚动列表可供你进行尝试。如果是一个不含可滚动列表的 Flutter 界面,我们如何判断呢。Flutter 的实现机制告诉我们,Flutter 只会有一层 Native 的“画布”,因此可以通过 FLEX、Lookin、Reveal 等工具确定页面上所有元素均绘制在一层 FlutterView 上。 14 | 15 | ![](../pic/tip_how_to_find_flutter_page_01.png) 16 | 17 | # 如何控制图片缓存 18 | 19 | 为了提高图片加载效率,Flutter内部会将加载过的图片,依照最近最少使用原则缓存,这个缓存默认最大100M,默认最多存储1000条。 20 | 21 | 使用100M的内存来缓存图片,对很多APP来说似乎有些大,所以我们可以设置其大小。方法是: 22 | 23 | ```dart 24 | PaintingBinding.instance.imageCache.maximumSizeBytes = 10 << 20; // 10MiB,缓存最大10兆,你也可以设置其它值 25 | PaintingBinding.instance.imageCache.maximumSize = 100; // 最多缓存100条 26 | ``` 27 | 28 | 同时你也可以通过 `PaintingBinding.instance.imageCache.clear()` 来直接清除当前缓存,比如你当前页面需要很大的图片缓存,而退出当前页面又需要释放的时候。 29 | 30 | ## 原理 31 | 32 | 告诉完答案,下面说说为什么要这么干。 33 | 34 | 我们所有的Image类实例,无论是直接使用 `Image` 还是使用工厂方法 `Image.network` 或 `Image.asset` 等创建,其控制图片的都是其内部的image属性,image属性又是 `ImageProvider` 的子类,`ImageProvider` 通过 `solve` 方法找到图片,并返回 `ImageStream` ,而 `ImageStream` 就代表着图片。 35 | 36 | 在 `solve` 方法中,调用 `PaintingBinding.instance.imageCache.putIfAbsent()` 方法来获取并缓存图片。 37 | 38 | 所以我们可以看到,存储在 `PaintingBinding` 单例中的 `ImageCache` 实例,控制着图片缓存。`ImageCache` 里的宏定义 `_kDefaultSize` 和 `_kDefaultSizeBytes` 代表缓存的默认大小。想操作`ImageCache` 的那些实例方法,只有通过 `PaintingBinding.instance `单例才能拿到 `imageCache` 实例。 39 | 40 | 你可以直接翻看Flutter SDK里面的image_cache.dart源码,里面的方法都很好理解,并且有注释。 41 | 42 | # 开发 Flutter App 时遇到困难,如何寻求帮助 43 | 44 | Flutter 活跃的社区与快速迭代的特性,吸引了许多开发者的注意。由于许多特性处于较为早期的阶段,框架本身或文档不甚完善,导致开发者经常踩坑。 45 | 46 | 寻求帮助有很多途径,Flutter 团队官方以及社区比较推荐的方式是**到 GitHub flutter/flutter 仓库的 issue 区,利用搜索功能查询已提出的 issue**。 47 | 48 | 若能够找到你相同情况的 issue,请按照 issue 讨论的方案进行尝试,若能够解决问题,请在有效的讨论内容上给予👍,作为对解决方案的反馈与赞赏,给其他开发者提供参考。 49 | 50 | 若找到的 issue 不能解决问题,请在 issue 后描述出你的情况,以及你尝试过的方案,同其他开发者展开讨论。必要的时候可以附带截图或可复现问题的最简代码。 51 | 52 | 若无法找到适合的 issue,请新建一个 issue,并在正文中贴上你的 `$ flutter doctor` 输出信息,详细描述产生问题的步骤,最好配以可以复现问题的最简代码。 53 | 54 | 请注意,无论以何种方式参与 issue 区讨论,都请使用英文。不必过于担心语法和措辞,善用翻译工具,能够妥善表达你的状况就足够了。 -------------------------------------------------------------------------------- /translations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-tips/Flutter-Tips/79bc3585e4e913ce07aaad5f2faf307f13fda6b5/translations/.gitkeep -------------------------------------------------------------------------------- /translations/build-modes-in-flutter.md: -------------------------------------------------------------------------------- 1 | # Flutter 的构建模式 2 | 3 | [![JAY TILLU](https://miro.medium.com/fit/c/96/96/2*li4M13D8csNZa-aV6jWloQ.jpeg)](/@jaytillu?source=post_page-----b44f179ad718----------------------) 4 | 5 | [JAY TILLU](/@jaytillu?source=post_page-----b44f179ad718----------------------) 6 | 7 | ![](https://miro.medium.com/max/60/1*-c-1Cr0fgC6RV8AfUH6fRA.png?q=20) 8 | 9 | 10 | 11 | 要完全掌握一款框架,我们需要明白它内部的工作机制。我们必须弄清楚**它是如何运行的**、**为什么会这样**。就像人与人之间,_彼此了解越多,彼此越懂对方_。那么让我们试着了解一些 Flutter 内部细节以及它的构建模式。 12 | 13 | 通过这篇文章,我们将了解到: 14 | 15 | * 什么是构建模式? 16 | * Flutter 中有几种构建模式? 17 | * 我们为什么需要构建模式? 18 | 19 | # 什么是构建模式? 20 | 21 | > 在不同的开发阶段,框架用不同的方式,或者叫模式,来编译我们的代码,这里的模式我们就称为构建模式。 22 | 23 | 正如大家都知道的在某个时间点后,维护并调整代码真的很难很费时间。你去问任何一个移动应用开发者,他们都会告诉你这里的痛点。如果你在维护一个像 Facebook、Instagram、Uber 或是 WhatsApp 这样的巨型应用,那么哪怕仅仅是修改一个颜色,都需要花费**数小时**才能看到变化。如果你做 Android 开发,情况会很糟糕。 24 | 25 | 但 Flutter 的到来会让情况变好。让我们看看它是怎么做到的。 26 | 27 | Flutter 团队重新设计了整个开发过程,致力于提升编译速度,使其快如闪电。当你按下编译按钮,其他框架会重新编译全部代码,即使你只是做了很小的修改,也会耗费很长时间,但Flutter 不会那样。 28 | 29 | 比起一次又一次地重新编译整个代码,Flutter 团队把编译阶段分成三种模式。第一种模式适用开发 app 期间,第二种模式适用测试 app 性能,第三种模式适用 app 发布。通过将编译过程分成不同的模式,Flutter 能够以快如闪电的速度反应代码的变化。接下来让我们来了解下这些模式。 30 | 31 | # 一共有多少种模式?应该如何选用呢? 32 | 33 | * 大体上说,Flutter 有三种构建模式: 34 | 35 | 1. Debug 模式 36 | 2. Profile 模式 37 | 3. Release 模式 38 | 39 | # Debug 模式 40 | 41 | > Debug 模式被设计用于快速反应变化,但 app 的体积很大,性能很差。因此不该评判在此模式下 app 的体积和性能。 42 | 43 | * 作为开发流程中的首个阶段,app 开发阶段中开发者会修改很多代码,修 bug,随处修改。因此开发者需要随时看到变化。 44 | * 因此 debug 模式中,Flutter 优化了所见所得的速度。 45 | * Dart 语言帮助了 Flutter 更快地反馈变化。Dart 代码可以通过多种方式运行。在 debug 模式下,Flutter 在一个虚拟机中执行代码。在你作出修改然后触发 hot reload 时,Flutter 会把变化的代码注入 app 中,不需要重新编译。 46 | * 就像 web 开发一样,当你改了一些代码,然后按下刷新按钮,在浏览器里马上能看到变化,这其中的概念是一样的。你运行着的 app 代码发生变化,Flutter 会把变化反应到 app 上。 47 | * Flutter 不重新编译所有代码,就可以立即反馈给你变化,因此加快了开发速度,提升效率。 48 | * 由于 app 运行在虚拟机中,性能是最差的。所以你会感觉在 debug 模式下运行的 app 很慢。但此时 Flutter 专门为及时响应变化而做出优化,我们不该评判此时的 app 性能。 49 | * 同时 debug 模式下 app 看起来很大,所以也不要评判此时的 app 体积,因为它正运行在 debug 模式下…… 50 | 51 | **当你在 debug 模式下,同时请多关注这些重要的事。** 52 | 53 | * [断言机制](https://dart.dev/guides/language/language-tour#assert) 已被启用。 54 | * Service extensions 已被启用。 55 | * 调试工具已被启用。(你可以使用 [DevTools](https://flutter.dev/docs/development/tools/devtools/overview))。 56 | * 你可以在真机、模拟器上运行 app。 57 | 58 | # Profile 模式 59 | 60 | > Profile 模式用于在测试时分析性能。 61 | 62 | * 当 app 在 Profile 模式下编译时,Flutter 会假定你要评测你的 app 性能。因此 app 会尽可能地针对运行性能做优化。 63 | * 此时你的 app 已经开发完成,你可以分析 app 的实际性能。 64 | 65 | **当你在 profile 模式下,同时请多关注这些重要的事。** 66 | 67 | * 在 profile 模式下,你不能在模拟器中运行 app。因为并没有针对模拟器做出优化,在模拟器中 profile 模式是不可用的。 68 | * Tracing 功能已被启用。 69 | * 一些用于分析性能的 service extensions 已被启用。 70 | * 对 DevTools 的支持也被启用了。 71 | 72 | **_Flutter 推荐使用_** [**_DevTools_**](https://flutter.dev/docs/development/tools/devtools) **_来分析应用性能。_** 73 | 74 | # Release 模式 75 | 76 | > Released 模式被设计用于发布到 Play Store 或 App Store。这种模式旨在提供更快的启动速度、执行速度和最小应用体积。 77 | 78 | * 此时应用已经开发、测试结束了。是时候在 release 模式下编译了。Flutter 会通过 AOT 编译器把代码编译成 native 机器码,因此 app 会运行地很快。要了解编译器的更多内容,请[点击这里](/flutter-widgets/3-flutter-compilation-process-e602d939e147)。 79 | * 当 app 在 Release 模式下编译时,Flutter 会假定你要准备发布 app 了,所以优化到了极限。 80 | * 在 release 模式下,专门优化了启动速度、执行速度和尽可能小的 app 体积。 81 | 82 | **当你在 release 模式下,同时请多关注这些重要的事。** 83 | 84 | * 断言机制被禁用了。 85 | * 调试信息被剥离掉了。 86 | * 调试功能不可用。 87 | * Service extension 也被禁用了 88 | * Release 模式不支持模拟器。 89 | 90 | # 想了解更多信息请访问以下链接 91 | 92 | * [Fuchsia OS 官方网站](https://fuchsia.dev/) 93 | * [Dart 官方网站](https://dart.dev/) 94 | * [Flutter 官方网站](https://flutter.dev/) 95 | 96 | > 这就是关于构建模式的一些内容,希望能够帮助到你。如果我遗漏了什么请告诉我。保持 coding,保持心里有爱。 97 | > 98 | > 想和我交流?以下是我的联系方式。我很愿意与你成为朋友。😊 99 | > 100 | > [Twitter](https://twitter.com/jay_tillu) 101 | > 102 | > [Facebook](https://www.facebook.com/jaytillu.1314/) 103 | > 104 | > [Instagram](https://www.instagram.com/jay.tillu/) 105 | > 106 | > 或者给我发邮件:developerj13@gmail.com 107 | -------------------------------------------------------------------------------- /translations/dart-awesome-cheat-sheet-for-flutter-devs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Flutter 开发者的 Dart 速查表 3 | --- 4 | 5 | 原文作者:[![Temidayo Adefioye](https://miro.medium.com/fit/c/48/48/1*EHweqnww2uWcqa7d_90mZg.jpeg)](/@temidjoy?source=post_page-----d8cb52c978e1----------------------) 6 | 翻译:[talisk](https://github.com/talisk) 7 | 8 | ![](https://miro.medium.com/max/1766/1*oikrjJQi1b5JTpUM0LsQxw.png) 9 | 10 | 如果你的确对跨平台开发感兴趣,你会知道一个名为 Flutter 的新框架。这是一个 Google 开发的全新的移动应用 SDK,开发人员可以通过一份代码直接为移动端、Web 和桌面端编写漂亮的原生应用。越来越多的科技公司决定使用 Flutter 框架开发多个平台的应用程序。在过去两年中,技术行业对开发者的需求急剧增加。为了满足这一需求,我们有一个由富有热情的开发人员组成的庞大社区,他们愿意开发世界一流的应用程序,而不会遇到任何编程障碍,这一点至关重要。 11 | 12 | 在这篇文章中,我会与你分享**四个主要的 Dart 小窍门**,能够帮助你快速上手 Flutter 开发。 13 | 14 | # 字符串插值 15 | 16 | 每种语言都有自己的插入两个或多个单词或字符的方法。在 dart 中,可以将表达式的值放入字符串中,如下所示: 17 | 18 | ``` dart 19 | int x=6; 20 | int y=2; 21 | String sum = '${x+y}'; // 结果是 8 22 | String subtraction = '${x-y}'; // 结果是 4 23 | String upperCase = '${"hello".toUpperCase()}'; // 结果是 HELLO 24 | String concatXY = '$x$y'; // 结果是 '62' 25 | ``` 26 | 27 | # 方法 28 | 29 | Dart 语言是一种面向对象语言(OOL)。在这种语言中,函数属于对象,具有一个类型,Function。这意味着可以将函数分配给变量或作为参数传递给其他函数。有趣的是,你还可以像调用函数一样调用类的实例。太棒了是吧! 30 | 31 | ``` dart 32 | String fullName() { 33 | String firstName = "Temidayo"; 34 | String lastName = "Adefioye"; 35 | return '$firstName $lastName'; // 返回 'Temidayo Adefioye' 36 | } 37 | int length(String text) { 38 | return text.length; // 返回 text 的长度 39 | } 40 | ``` 41 | 42 | 上面的函数可以用更简洁的方式重写: 43 | 44 | ``` dart 45 | int length(String text) => return text.length; // 返回 text 的长度 46 | ``` 47 | 48 | 上述方法适用于函数只包含*一个*表达式的情况。这也被称为速记语法。 49 | 50 | # 避空运算符 51 | 52 | 处理好应用程序开发中的空指针异常非常重要,因为这能让您为用户创建无缝体验。Dart 为处理可能为空的值提供了一些便捷的运算符。一个是 `??=` 赋值运算符,仅当变量当前为空时才赋值: 53 | 54 | ``` dart 55 | int x; // 任何对象的初始值都为空 56 | x ??=6; 57 | print(x); // 结果是 6 58 | 59 | x ??=3; 60 | print(x); // 结果仍然是 6 61 | 62 | print(null ?? 10); // 结果是 10。如果不为空,则显示左侧的值,否则显示右侧的值 63 | ``` 64 | 65 | # List 数组 66 | 67 | 在几乎每种编程语言中,最常见的集合可能是对象的数组或有序集合。请注意,Dart 的数组是 List。 68 | 69 | ``` dart 70 | var numList = [1,2,3,5,6,7]; 71 | var countryList = ["Nigeria","United States","United Kingdom","Ghana","IreLand","Germany"]; 72 | String numLength = numList.length; // 结果是 6 73 | String countryLength = countryList.length; // 结果是 6 74 | String countryIndex = countryList[1]; // 结果是 'United //States' 75 | String numIndex = numList[0]; // 结果是 1 76 | 77 | countryList.add("Poland"); // 把一个新项目添加到数组中 78 | 79 | var emailList = new List(3); // 创建一个固定长度数组 80 | var emailList = new List(); // 数组中实例的类型是 //String 81 | ``` 82 | 83 | 一共就这些吗? 84 | 85 | ![](https://miro.medium.com/freeze/max/30/1*uGJysDvESMupwwDUQJJz0A.gif?q=20) 86 | ![](https://miro.medium.com/max/650/1*uGJysDvESMupwwDUQJJz0A.gif) 87 | 88 | 不! 89 | 90 | 我为那些对使用 Flutter 和 Dart 开发应用感兴趣的开发者精心策划了一个很棒的备忘清单。 91 | 92 | 你可以在[这里]](https://github.com/Temidtech/dart-cheat-sheet)查看清单。 93 | 94 | 在接下来的几周里,清单将更新更多的 Dart 小窍门。 95 | 96 | 祝你写 Flutter 写得开心~ -------------------------------------------------------------------------------- /translations/docs_development_add-to-app_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 将 Flutter 集成到现有应用 3 | description: 将 Flutter 作为 library 集成到现有的 Android 或 iOS 应用。 4 | --- 5 | 6 | ## 集成到现有应用 7 | 8 | 有时候,用 Flutter 一次性重写整个已有的应用是不切实际的。 9 | 对于这些情况,Flutter 可以作为一个库或模块, 10 | 集成进现有的应用当中。 11 | 模块引入到您的 Android 或 iOS 应用(当前支持的平台)中, 12 | 以使用 Flutter 来渲染一部分的 UI,或者仅运行多平台共享的 Dart 代码逻辑。 13 | 14 | 仅需几步,你就可以将高效而富有表现力的 Flutter 引入您的应用。 15 | 16 | 在 Flutter v1.12 中,添加到现有应用的基本场景已被支持, 17 | 每个应用在同一时间可以集成一个全屏幕的 Flutter 实例。 18 | 目前仍有以下限制: 19 | 20 | - 运行多个 Flutter 实例,或在屏幕局部上运行 Flutter 可能会导致不可预测的行为; 21 | 22 | - 在后台模式使用 Flutter 的能力还在开发中; 23 | 24 | - 将 Flutter 库打包进另一个可共享的库或将多个 Flutter 库打包到同一个应用中,都未被支持。 25 | 26 | ## 已支持的特性 27 | 28 | ### 集成到 Android 应用 29 | 30 | {% include app-figure.md image="docs_development_add-to-app_index_01.gif" alt="Add-to-app steps on Android" %} 31 | 32 | - 在 Gradle 脚本中添加一个自动构建并引入 Flutter 模块的 Flutter SDK 钩子。 33 | 34 | - 将 Flutter 模块构建为通用的 35 | [Android Archive (AAR)](https://developer.android.google.cn/studio/projects/android-library) 36 | 以便集成到您自己的构建系统中,并提高 Jetifier 与 AndroidX 的互操作性; 37 | 38 | - [FlutterEngine](https://api.flutter-io.cn/javadoc/io/flutter/embedding/engine/FlutterEngine.html) 39 | API 用于启动并持续地为挂载 40 | [FlutterActivity](https://api.flutter-io.cn/javadoc/io/flutter/embedding/android/FlutterActivity.html) 或 41 | [FlutterFragment](https://api.flutter-io.cn/javadoc/io/flutter/embedding/android/FlutterFragment.html) 42 | 提供独立的 Flutter 环境; 43 | 44 | - Android Studio 的 Android 工程与 Flutter 工程同时编辑,以及 Flutter module 创建与导入向导; 45 | 46 | - 支持了 Java 和 Kotlin 为宿主的应用程序; 47 | 48 | - Flutter 模块可以通过使用 [Flutter plugins](https://pub.flutter-io.cn/flutter) 与平台进行交互。 49 | Android 平台的 plugin 应该[迁移至 V2 plugin API](/docs/development/packages-and-plugins/plugin-api-migration) 50 | 以确保最佳的兼容性。在 Flutter v1.12,大多数 51 | [Flutter 团队维护](https://github.com/flutter/plugins/tree/master/packages) 的 plugin,以及 52 | [FlutterFire](https://github.com/FirebaseExtended/flutterfire/tree/master/packages) 都已完成迁移; 53 | 54 | - 支持通过从 IDE 或命令行中使用 `flutter attach` 来实现 Flutter 调试与有状态的热重载。 55 | 56 | ### 集成到 iOS 应用 57 | 58 | {% include app-figure.md image="docs_development_add-to-app_index_02.gif" alt="Add-to-app steps on iOS" %} 59 | 60 | - 在 Xcode 的 Build Phase 以及 CocoaPods 中,添加一个自动构建并引入 Flutter 模块的 Flutter SDK 钩子。 61 | 62 | - 将 Flutter 模块构建为通用的 [iOS Framework](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WhatAreFrameworks.html) 63 | 以便集成到您自己的构建系统中; 64 | 65 | - [FlutterEngine](https://api.flutter-io.cn/objcdoc/Classes/FlutterEngine.html) API 用于 66 | 启动并持续地为挂载 [FlutterViewController](https://api.flutter-io.cn/objcdoc/Classes/FlutterViewController.html) 67 | 提供独立的 Flutter 环境; 68 | 69 | - 支持了 Objective-C 和 Swift 为宿主的应用程序; 70 | 71 | - Flutter 模块可以通过使用 [Flutter plugins](https://pub.flutter-io.cn/flutter) 与平台进行交互; 72 | 73 | - 支持通过从 IDE 或命令行中使用 `flutter attach` 来实现 Flutter 调试与有状态的热重载。 74 | 75 | 查看 [add-to-app GitHub 示例仓库](https://github.com/flutter/samples/tree/master/experimental/add_to_app) 76 | 中在 iOS 和 Android 平台上引入 Flutter module 的示例项目。 77 | 78 | ## 开始 79 | 80 | 第一步,查看以下工程集成指南 81 | 82 | 98 | 99 | ## API 用法 100 | 101 | 将 Flutter 集成进您的工程后,可以查看以下 API 使用指南 102 | 103 | 119 | -------------------------------------------------------------------------------- /translations/dont-use-to-prefix-your-routes-in-flutter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 不要在 Flutter 路由中添加 “/” 前缀! 3 | --- 4 | 5 | 原文作者:[![Derek Lakin](https://miro.medium.com/fit/c/48/48/1*0wz5QRDbtj_-HDgo6fSlrQ.png)](/@dereklakin?source=post_page-----f3844ce1fdd5----------------------) 6 | 翻译:[talisk](https://github.com/talisk) 7 | 8 | 许多应用都有决定首屏展示内容的判断逻辑,比如展示登录页还是用户引导页面。如果你正在使用 **MaterialApp** widget,则通常会设置 **initialRoute** 属性来指定应用启动后期望的页面。(我也倾向于使用 **onGenerateRoute**,如下所示): 9 | 10 | ``` dart 11 | class MyApp extends StatelessWidget { 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | initialRoute: '/login', 16 | onGenerateRoute: generateRoute, 17 | title: 'Test App', 18 | ); 19 | } 20 | 21 | static Route generateRoute(RouteSettings settings) { 22 | switch (settings.name) { 23 | case '/': 24 | return MaterialPageRoute(builder: (_) => HomeView()); 25 | case '/login': 26 | return MaterialPageRoute(builder: (_) => LoginView()); 27 | default: 28 | return MaterialPageRoute( 29 | builder: (_) => Scaffold( 30 | body: Center( 31 | child: Text('No route defined for ${settings.name}'), 32 | ), 33 | ), 34 | ); 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | 你可能完全没有注意到,特别是如果你的页面性能出色或页面十分轻量的话(或许你已经像我一样踩到坑了!),在这种情况下发生的事情是,推入的第一个路由是 `/`,然后为 `/login` 推送了另一条路由,导致先创建了首页,然后创建了登录页面。 41 | 42 | 那么,这里发生了什么?好吧,如果您查看 [**initialRoute** property](https://api.flutter.dev/flutter/material/MaterialApp/initialRoute.html) 属性的文档: 43 | 44 | > 如果路由包含斜线,则将其视为 “deep link”,并且在推入此路由前,也会推入前级路由。例如,如果路由为 `/a/b/c`,则该应用将按顺序加载三个路由 `/a`,`/a/b` 和 `/a/b/c`。 45 | 46 | 该示例未能按照预期表现,如上所述,第一条推入的路由实际上是 `/`。 47 | 48 | 幸运的是,有一个非常简单的解决方案,那就是从路由中删除 `/` 前缀: 49 | 50 | ``` dart 51 | initialRoute: 'login', 52 | ``` 53 | 54 | 如果你 *需要* 在路径中留着斜杠以进行 “deep link” 或其他操作,则可以随时在 **onGenerateRoute** handler 方法中检查 **settings.isInitialRoute** 的值。对于推送到 **Navigator** 中的第一个路由,值将会是 **true**。 55 | 56 | 各位撸码快乐哦! -------------------------------------------------------------------------------- /translations/在既有iOS项目中添加Flutter module.md: -------------------------------------------------------------------------------- 1 | > Flutter 能以 framework 的形式添加到你的既有 iOS 应用中。本文将讲解如何做到这一点。 2 | 3 | # 集成 4 | 5 | ## 系统要求 6 | 7 | 你的开发环境必须满足 [Flutter 对 macOS 系统的版本要求](https://flutter.cn/docs/get-started/install/macos#system-requirements) 8 | 并 [已经安装 Xcode](https://flutter.cn/docs/get-started/install/macos#install-xcode),Flutter 支持 iOS 8.0 及以上。 9 | 10 | ## 创建 Flutter module 11 | 12 | 为了将 Flutter 集成到你的既有应用里,第一步要创建一个 Flutter module。 13 | 14 | 在命令行中执行: 15 | 16 | ```sh 17 | cd some/path/ 18 | flutter create --template module my_flutter 19 | ``` 20 | 21 | Flutter module 会创建在 `some/path/my_flutter/` 目录。 22 | 在这个目录中,你可以像在其它 Flutter 项目中一样,执行 `flutter` 命令。 23 | 比如 `flutter run --debug` 或者 `flutter build ios`。 24 | 你也同样可以在 [Android Studio/IntelliJ](https://flutter.cn/docs/development/tools/android-studio) 或者 [VS Code](https://flutter.cn/docs/development/tools/vs-code) 中运行这个模块, 25 | 并附带 Flutter 和 Dart 插件。在集成到既有应用前, 26 | 这个项目在 Flutter module 中包含了一个单视图的示例代码, 27 | 对 Flutter 侧代码的测试会有帮助。 28 | 29 | ### 模块组织 30 | 31 | 在 `my_flutter` 模块,目录结构和普通 Flutter 应用类似: 32 | 33 | ```text 34 | my_flutter/ 35 | ├─.ios/ 36 | │ ├─Runner.xcworkspace 37 | │ └─Flutter/podhelper.rb 38 | ├─lib/ 39 | │ └─main.dart 40 | ├─test/ 41 | └─pubspec.yaml 42 | ``` 43 | 44 | 添加你的 Dart 代码到 `lib/` 目录。 45 | 46 | 添加 Flutter 依赖到 `my_flutter/pubspec.yaml`, 47 | 包括 Flutter packages 和 plugins。 48 | 49 | `.ios/` 隐藏文件夹包含了一个 Xcode workspace,用于单独运行你的 Flutter module。 50 | 它是一个独立启动 Flutter 代码的壳工程,并且包含了一个帮助脚本, 51 | 用于编译 framewroks 或者使用 [CocoaPods](https://cocoapods.org) 将 Flutter module 集成到你的既有应用。 52 | 53 | > iOS 代码要添加到你的既有应用或者 Flutter plugin 中, 54 | > 而不是 Flutter module 的 `.ios/` 目录下。 55 | > `.ios/` 下的改变不会集成到你的既有应用。 56 | > 在 `my_flutter` 执行 `flutter clean` 57 | > 或者 `flutter pub get` 会重新生成这个目录。 58 | 59 | ## 在你的既有应用中集成 Flutter module 60 | 61 | 这里有两种方式可以将 Flutter 集成到你的既有应用中。 62 | 63 | 1. 使用 CocoaPods 依赖管理和已安装的 Flutter SDK 。(推荐) 64 | 65 | 2. 把 Flutter engine 、你的 dart 代码和所有 Flutter plugin 编译成 framework 。然后用 Xcode 手动集成到你的应用中,并更新编译设置。 66 | 67 | > 你的应用将不能在模拟器上运行 Release 模式, 68 | > 因为 Flutter 还不支持将 Dart 代码编译成 x86 ahead-of-time (AOT) 模式的二进制文件。 69 | > 你可以在模拟机和真机上运行 Debug 模式,在真机上运行 Release 模式。 70 | 71 | 使用 Flutter 会 [增加应用体积](https://flutter.cn/docs/resources/faq#how-big-is-the-flutter-engine) 。 72 | 73 | ### 选项 A - 使用 CocoaPods 和 Flutter SDK 集成 74 | 75 | 这个方法需要你的项目的所有开发者,都在本地安装 Flutter SDK。 76 | 只需要在 Xcode 中编译应用,就可以自动运行脚本来集成 dart 代码和 plugin。 77 | 这个方法允许你使用 Flutter module 中的最新代码快速迭代开发, 78 | 而无需在 Xcode 以外执行额外的命令。 79 | 80 | 下面的示例假设你的既有应用和 Flutter module 在相邻目录。如果你有不同的目录结构,需要适配到对应的路径。 81 | 82 | ```text 83 | some/path/ 84 | ├── my_flutter/ 85 | │ └── .ios/ 86 | │ └── Flutter/ 87 | │ └── podhelper.rb 88 | └── MyApp/ 89 | └── Podfile 90 | ``` 91 | 92 | 如果你的应用(`MyApp`)还没有 Podfile,根据 [CocoaPods getting started guide](https://guides.cocoapods.org/using/using-cocoapods.html) 来在项目中添加 `Podfile`。 93 | 94 | 1. 在 `Podfile` 中添加下面代码: 95 | 96 | ```ruby 97 | # MyApp/Podfile 98 | 99 | flutter_application_path = '../my_flutter' 100 | load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb') 101 | ``` 102 | 103 | 2. 每个需要集成 Flutter 的 Podfile target,执行`install_all_flutter_pods(flutter_application_path)`: 104 | 105 | ```ruby 106 | # MyApp/Podfile 107 | 108 | target 'MyApp' do 109 | install_all_flutter_pods(flutter_application_path) 110 | end 111 | ``` 112 | 113 | 3. 运行 `pod install`。 114 | 115 | > 当你在 `my_flutter/pubspec.yaml` 改变了 Flutter plugin 依赖, 116 | > 需要在 Flutter module 目录运行 `flutter pub get`, 117 | > 来更新会被`podhelper.rb` 脚本用到的 plugin 列表, 118 | > 然后再次在你的应用目录 `some/path/MyApp` 运行 `pod install`. 119 | 120 | `podhelper.rb` 脚本会把你的 plugins, 121 | `Flutter.framework`,和 `App.framework` 集成到你的项目中。 122 | 123 | 你应用的 Debug 和 Release 编译配置,将会集成相对应的 124 | Debug 或 Release 的 [编译产物](https://flutter.cn/docs/testing/build-modes)。 125 | 可以增加一个 Profile 编译配置用于在 profile 模式下测试应用。 126 | 127 | > `Flutter.framework` 是 Flutter engine 的框架, 128 | > `App.framework` 是你的 Dart 代码的编译产物。 129 | 130 | 在 Xcode 中打开 `MyApp.xcworkspace` ,你现在可以使用 `⌘B` 编译项目了。 131 | 132 | ### 选项 B - 在 Xcode 中集成 frameworks 133 | 134 | 除了上面的方法,你也可以创建必备的 frameworks,手动修改既有 Xcode 项目,将他们集成进去。 135 | 当你组内其它成员们不能在本地安装 Flutter SDK 和 CocoaPods, 136 | 或者你不想使用 CocoaPods 作为既有应用的依赖管理时,这种方法会比较合适。 137 | 但是每当你在 Flutter module 中改变了代码, 138 | 都必须运行 `flutter build ios-framework`。 139 | 140 | 如果你使用前面的 “使用 CocoaPods 和 Flutter SDK 集成” ,你可以跳过本步骤。 141 | 142 | 下面的示例假设你想在 `some/path/MyApp/Flutter/` 目录下创建 frameworks: 143 | 144 | ```sh 145 | flutter build ios-framework --output=some/path/MyApp/Flutter/ 146 | ``` 147 | 148 | ```text 149 | some/path/MyApp/ 150 | └── Flutter/ 151 | ├── Debug/ 152 | │ ├── Flutter.framework 153 | │   ├── App.framework 154 | │   ├── FlutterPluginRegistrant.framework 155 | │   └── example_plugin.framework (each plugin with iOS platform code is a separate framework) 156 | ├── Profile/ 157 | │ ├── Flutter.framework 158 | │ ├── App.framework 159 | │ ├── FlutterPluginRegistrant.framework 160 | │ └── example_plugin.framework 161 | └── Release/ 162 | ├── Flutter.framework 163 | ├── App.framework 164 | ├── FlutterPluginRegistrant.framework 165 | └── example_plugin.framework 166 | ``` 167 | 168 | > 在 Xcode 11 中, 你可以添加 `--xcframework --no-universal` 参数来生成 XCFrameworks,而不是通用 framework。 169 | 170 | 在 Xcode 中将生成的 frameworks 集成到你的既有应用中。 171 | 例如,你可以在 `some/path/MyApp/Flutter/Release/` 172 | 目录拖拽 frameworks 到 你的应用 target 编译设置的 173 | General > Frameworks, Libraries, and Embedded Content 下, 174 | 然后在 Embed 下拉列表中选择 "Embed & Sign"。 175 | 176 | ![](../pic/hybrid-ios-embed-xcode.png) 177 | 178 | 在 target 的编译设置中的 Framework Search Paths (`FRAMEWORK_SEARCH_PATHS`) 增加 `$(PROJECT_DIR)/Flutter/Release/`。 179 | 180 | ![](../pic/hybrid-ios-framework-search-paths.png) 181 | 182 | 在 Xcode 项目中即成 frameworks 有很多方法 —— 选择最适合你的项目的。 183 | 184 | 你现在可以在 Xcode中使用 `⌘B` 编译项目。 185 | 186 | > 如果你想在 Debug 编译配置下使用 Debug 版本的 Flutter frameworks, 187 | > 在 Release 编译配置下使用 Release 版本的 Flutter frameworks, 188 | > 在 `MyApp.xcodeproj/project.pbxproj` 文件中, 189 | > 尝试在所有 Flutter 相关 frameworks 上使用 190 | > `path = "Flutter/$(CONFIGURATION)/example.framework";` 191 | > 替换 `path = Flutter/Release/example.framework;` 192 | > (注意添加引号 `"`)。 193 | 194 | > 你也必须在 Framework Search Paths 编译设置中使用 `$(PROJECT_DIR)/Flutter/$(CONFIGURATION)`。 195 | 196 | # 开发 197 | 198 | 下面我们在既有 iOS 应用中添加单个 Flutter 页面。 199 | 200 | ## 启动 FlutterEngine 和 FlutterViewController 201 | 202 | 为了在既有 iOS 应用中展示 Flutter 页面,请启动 [`FlutterEngine`](https://api.flutter-io.cn/objcdoc/Classes/FlutterEngine.html) 和 [`FlutterViewController`](https://api.flutter-io.cn/objcdoc/Classes/FlutterViewController.html)。 203 | 204 | > `FlutterEngine` 充当 Dart VM 和 Flutter 运行时的主机; 205 | > `FlutterViewController` 依附于 `FlutterEngine`,给 Flutter 传递 UIKit 的输入事件,并展示被 `FlutterEngine` 渲染的每一帧画面。 206 | 207 | `FlutterEngine` 的寿命可能与 `FlutterViewController` 相同,也可能超过 `FlutterViewController`。 208 | 209 | 210 | > 通常建议为您的应用预热一个“长寿”的 `FlutterEngine` 是因为: 211 | > 212 | > - 当展示 `FlutterViewController` 时,第一帧画面将会更快展现; 213 | > 214 | > - 你的 Flutter 和 Dart 状态将比一个`FlutterViewController` 存活更久; 215 | > 216 | > - 在展示 UI 前,你的应用和 plugins 可以与 Flutter 和 Dart 逻辑交互。 217 | 218 | 219 | [加载顺序和性能](https://flutter.cn/docs/development/add-to-app/performance) 里有更多关于预热 engine 的延迟和内存取舍的分析。 220 | 221 | ### 创建一个 FlutterEngine 222 | 223 | 224 | 创建 `FlutterEngine` 的合适位置取决于您的应用。作为示例,我们将在应用启动的 app delegate 中创建一个 `FlutterEngine`, 并作为属性暴露给外界。 225 | 226 | 如果你使用 Objective-C 227 | **在 `AppDelegate.h`:** 228 | 229 | ```objectivec 230 | // AppDelegate.h 231 | 232 | @import UIKit; 233 | @import Flutter; 234 | 235 | @interface AppDelegate : FlutterAppDelegate // 以下有关于 FlutterAppDelegate 的更多信息 236 | @property (nonatomic,strong) FlutterEngine *flutterEngine; 237 | @end 238 | ``` 239 | 240 | **在 `AppDelegate.m`:** 241 | 242 | ```objectivec 243 | // AppDelegate.m 244 | 245 | #import // Used to connect plugins. 246 | 247 | #import "AppDelegate.h" 248 | 249 | @implementation AppDelegate 250 | 251 | - (BOOL)application:(UIApplication *)application 252 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 253 | self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"]; 254 | // 使用默认 Flutter 路由运行默认 Dart 入口 255 | [self.flutterEngine run]; 256 | [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine]; 257 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 258 | } 259 | 260 | @end 261 | ``` 262 | 263 | 如果你使用 Swift 264 | 265 | **在 `AppDelegate.swift`:** 266 | 267 | ```swift 268 | // AppDelegate.swift 269 | 270 | import UIKit 271 | import Flutter 272 | import FlutterPluginRegistrant // 用于连接 plugins 273 | 274 | @UIApplicationMain 275 | class AppDelegate: FlutterAppDelegate { // FlutterAppDelegate 有更多信息 276 | lazy var flutterEngine = FlutterEngine(name: "my flutter engine") 277 | 278 | override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 279 | // 使用默认 Flutter 路由运行默认 Dart 入口 280 | flutterEngine.run(); 281 | GeneratedPluginRegistrant.register(with: self.flutterEngine); 282 | return super.application(application, didFinishLaunchingWithOptions: launchOptions); 283 | } 284 | } 285 | ``` 286 | 287 | ### 使用 FlutterEngine 展示 FlutterViewController 288 | 289 | 下面的例子展示了一个普通的 ViewController,包含一个 present `FlutterViewController` 的按钮。 290 | 291 | 如果你使用 Objective-C 292 | 293 | ```objectivec 294 | // ViewController.m 295 | 296 | @import Flutter; 297 | #import "AppDelegate.h" 298 | #import "ViewController.h" 299 | 300 | @implementation ViewController 301 | - (void)viewDidLoad { 302 | [super viewDidLoad]; 303 | 304 | // 制作一个按钮,当点击的时候调用 showFlutter 方法 305 | UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; 306 | [button addTarget:self 307 | action:@selector(showFlutter) 308 | forControlEvents:UIControlEventTouchUpInside]; 309 | [button setTitle:@"Show Flutter!" forState:UIControlStateNormal]; 310 | button.backgroundColor = UIColor.blueColor; 311 | button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0); 312 | [self.view addSubview:button]; 313 | } 314 | 315 | - (void)showFlutter { 316 | FlutterEngine *flutterEngine = 317 | ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine; 318 | FlutterViewController *flutterViewController = 319 | [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil]; 320 | [self presentViewController:flutterViewController animated:YES completion:nil]; 321 | } 322 | @end 323 | ``` 324 | 325 | 如果你使用 Swift 326 | 327 | ```swift 328 | // ViewController.swift 329 | 330 | import UIKit 331 | import Flutter 332 | 333 | class ViewController: UIViewController { 334 | override func viewDidLoad() { 335 | super.viewDidLoad() 336 | 337 | // 制作一个按钮,当点击的时候调用 showFlutter 方法 338 | let button = UIButton(type:UIButton.ButtonType.custom) 339 | button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside) 340 | button.setTitle("Show Flutter!", for: UIControl.State.normal) 341 | button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0) 342 | button.backgroundColor = UIColor.blue 343 | self.view.addSubview(button) 344 | } 345 | 346 | @objc func showFlutter() { 347 | let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine 348 | let flutterViewController = 349 | FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil) 350 | present(flutterViewController, animated: true, completion: nil) 351 | } 352 | } 353 | ``` 354 | 355 | 现在,你的 iOS 应用中集成了一个 Flutter 页面。 356 | 357 | > 在上一个例子中,你的默认 Dart 库的默认入口函数 `main()`,将会在 `AppDelegate` 创建 `FlutterEngine` 并调用 `run` 方法时调用。 358 | 359 | ### *或者* —— 使用隐式 FlutterEngine 创建 FlutterViewController 360 | 361 | 上一个示例还有另一个选择,你可以让 `FlutterViewController` 362 | 隐式创建它自己的 `FlutterEngine`,而不用提前预热 engine。 363 | 364 | 不过不建议这样做,因为按需创建`FlutterEngine` 的话,在 `FlutterViewController` 被 present 出来之后,第一帧图像渲染完之前,将会引入明显的延迟。但是当 Flutter 页面很少被展示时,当对决定何时启动 Dart VM 没有好的启发时,当 Flutter 无需在页面(view controller)之间保持状态时,此方式可能会有用。 365 | 366 | 为了不使用已经存在的 `FlutterEngine` 来展现 `FlutterViewController`, 367 | 省略 `FlutterEngine` 的创建步骤, 368 | 并且在创建 `FlutterViewController` 时,去掉 engine 的引用。 369 | 370 | 如果你使用 Objective-C 371 | 372 | ```objectivec 373 | // "ViewController.m 374 | 375 | // 省略已经存在的代码 376 | - (void)showFlutter { 377 | FlutterViewController *flutterViewController = 378 | [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil]; 379 | [self presentViewController:flutterViewController animated:YES completion:nil]; 380 | } 381 | @end 382 | ``` 383 | 384 | 如果你使用 Swift 385 | 386 | ```swift 387 | // ViewController.swift 388 | 389 | // 省略已经存在的代码 390 | func showFlutter() { 391 | let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil) 392 | present(flutterViewController, animated: true, completion: nil) 393 | } 394 | ``` 395 | 396 | 查看 [加载顺序和性能](https://flutter.cn/docs/development/add-to-app/performance) 了解更多关于延迟和内存使用的探索。 397 | 398 | ## 使用 FlutterAppDelegate 399 | 400 | 推荐让你应用的 `UIApplicationDelegate` 继承 `FlutterAppDelegate`,但不是必须的。 401 | 402 | `FlutterAppDelegate` 有这些功能: 403 | 404 | - 传递应用的回调,例如 405 | [`openURL`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application), 406 | 到 Flutter plugins,例如 [local_auth](https://pub.dev/packages/local_auth); 407 | 408 | - 传递状态栏点击(这只能在 AppDelegate 中检测)到 Flutter 的点击置顶行为。 409 | 410 | 如果你的 app delegate 不能直接继承 `FlutterAppDelegate`, 411 | 让你的 app delegate 实现 `FlutterAppLifeCycleProvider` 协议, 412 | 来确保 Flutter plugins 接收到必要的回调。 413 | 否则,依赖这些事件的 plugins 将会有无法预估的行为。 414 | 415 | 416 | 例如: 417 | 418 | ```objectivec 419 | // AppDelegate.h 420 | 421 | @import Flutter; 422 | @import UIKit; 423 | @import FlutterPluginRegistrant; 424 | 425 | @interface AppDelegate : UIResponder 426 | @property (strong, nonatomic) UIWindow *window; 427 | @property (nonatomic,strong) FlutterEngine *flutterEngine; 428 | @end 429 | ``` 430 | 431 | App delegate 的实现中,应该最大化地委托给 `FlutterPluginAppLifeCycleDelegate`: 432 | 433 | ```objectivec 434 | // AppDelegate.m 435 | 436 | @interface AppDelegate () 437 | @property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate; 438 | @end 439 | 440 | @implementation AppDelegate 441 | 442 | - (instancetype)init { 443 | if (self = [super init]) { 444 | _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; 445 | } 446 | return self; 447 | } 448 | 449 | - (BOOL)application:(UIApplication*)application 450 | didFinishLaunchingWithOptions:(NSDictionary*))launchOptions { 451 | self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil]; 452 | [self.flutterEngine runWithEntrypoint:nil]; 453 | [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine]; 454 | return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions]; 455 | } 456 | 457 | // 返回 key window 的 rootViewController, 如果它是一个 FlutterViewController 458 | // Otherwise, returns nil. 459 | - (FlutterViewController*)rootFlutterViewController { 460 | UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController; 461 | if ([viewController isKindOfClass:[FlutterViewController class]]) { 462 | return (FlutterViewController*)viewController; 463 | } 464 | return nil; 465 | } 466 | 467 | - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { 468 | [super touchesBegan:touches withEvent:event]; 469 | 470 | // 传递状态栏的点击到 key window 上 Flutter 的 rootViewController 471 | if (self.rootFlutterViewController != nil) { 472 | [self.rootFlutterViewController handleStatusBarTouches:event]; 473 | } 474 | } 475 | 476 | - (void)application:(UIApplication*)application 477 | didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings { 478 | [_lifeCycleDelegate application:application 479 | didRegisterUserNotificationSettings:notificationSettings]; 480 | } 481 | 482 | - (void)application:(UIApplication*)application 483 | didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { 484 | [_lifeCycleDelegate application:application 485 | didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 486 | } 487 | 488 | - (void)application:(UIApplication*)application 489 | didReceiveRemoteNotification:(NSDictionary*)userInfo 490 | fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { 491 | [_lifeCycleDelegate application:application 492 | didReceiveRemoteNotification:userInfo 493 | fetchCompletionHandler:completionHandler]; 494 | } 495 | 496 | - (BOOL)application:(UIApplication*)application 497 | openURL:(NSURL*)url 498 | options:(NSDictionary*)options { 499 | return [_lifeCycleDelegate application:application openURL:url options:options]; 500 | } 501 | 502 | - (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url { 503 | return [_lifeCycleDelegate application:application handleOpenURL:url]; 504 | } 505 | 506 | - (BOOL)application:(UIApplication*)application 507 | openURL:(NSURL*)url 508 | sourceApplication:(NSString*)sourceApplication 509 | annotation:(id)annotation { 510 | return [_lifeCycleDelegate application:application 511 | openURL:url 512 | sourceApplication:sourceApplication 513 | annotation:annotation]; 514 | } 515 | 516 | - (void)application:(UIApplication*)application 517 | performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem 518 | completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) { 519 | [_lifeCycleDelegate application:application 520 | performActionForShortcutItem:shortcutItem 521 | completionHandler:completionHandler]; 522 | } 523 | 524 | - (void)application:(UIApplication*)application 525 | handleEventsForBackgroundURLSession:(nonnull NSString*)identifier 526 | completionHandler:(nonnull void (^)(void))completionHandler { 527 | [_lifeCycleDelegate application:application 528 | handleEventsForBackgroundURLSession:identifier 529 | completionHandler:completionHandler]; 530 | } 531 | 532 | - (void)application:(UIApplication*)application 533 | performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { 534 | [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler]; 535 | } 536 | 537 | - (void)addApplicationLifeCycleDelegate:(NSObject*)delegate { 538 | [_lifeCycleDelegate addDelegate:delegate]; 539 | } 540 | @end 541 | ``` 542 | 543 | ## 启动选项 544 | 545 | 例子中展示了使用默认启动选项运行 Flutter。 546 | 547 | 为了定制化你的 Flutter 运行时,你也可以置顶 Dart 入口、库和路由。 548 | 549 | ### Dart 入口 550 | 551 | 在 `FlutterEngine` 上调用 `run`,默认将会调用你的 `lib/main.dart` 文件里的 `main()` 函数。 552 | 553 | 你也可以使用另一个入口方法 554 | [`runWithEntrypoint`](https://api.flutter-io.cn/objcdoc/Classes/FlutterEngine.html#/c:objc(cs)FlutterEngine(im)runWithEntrypoint:), 555 | 并使用 `NSString` 字符串指定一个不同的 Dart 入口。 556 | 557 | > 使用 `main()` 以外的 Dart 入口函数,必须使用下面的注解, 558 | > 防止被 [tree-shaking](https://en.wikipedia.org/wiki/Tree_shaking) 优化掉,而没有编译。 559 | > 560 | > ```dart 561 | > // main.dart 562 | > 563 | > @pragma('vm:entry-point') 564 | > void myOtherEntrypoint() { ... }; 565 | > ``` 566 | 567 | ### Dart 库 568 | 569 | 另外,在指定 Dart 函数时,你可以指定特定文件的特定函数。 570 | 571 | 下面的例子使用 `lib/other_file.dart` 文件的 572 | `myOtherEntrypoint()` 函数取代 `lib/main.dart` 的 `main()` 函数: 573 | 574 | 如果你使用 Objective-C 575 | 576 | ```objectivec 577 | [flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"]; 578 | ``` 579 | 580 | 如果你使用 Swift 581 | 582 | ```swift 583 | flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart") 584 | ``` 585 | 586 | ### 路由 587 | 588 | 当构建 engine 时,可以为你的 Flutter [`WidgetsApp`](https://api.flutter-io.cn/flutter/widgets/WidgetsApp-class.html) 设置一个初始路由。 589 | 590 | 如果你使用 Objective-C 591 | 592 | ```objectivec 593 | FlutterEngine *flutterEngine = 594 | [[FlutterEngine alloc] initWithName:@"my flutter engine"]; 595 | [[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute" 596 | arguments:@"/onboarding"]; 597 | [flutterEngine run]; 598 | ``` 599 | 600 | 如果你使用 Swift 601 | 602 | ```swift 603 | let flutterEngine = FlutterEngine(name: "my flutter engine") 604 | flutterEngine.navigationChannel.invokeMethod("setInitialRoute", arguments:"/onboarding") 605 | flutterEngine.run() 606 | ``` 607 | 608 | 这段代码使用 `"/onboarding"` 取代 `"/"`,作为你的 `dart:ui` 的 [`window.defaultRouteName`](https://api.flutter-io.cn/flutter/dart-ui/Window/defaultRouteName.html) 609 | 610 | > 请注意: 611 | > 612 | > `navigationChannel` 上的 `"setInitialRoute"` 613 | > 必须在启动 `FlutterEngine` 前调用,才能在 Flutter 的第一帧中显示期望的路由。 614 | > 615 | > 特别是,它必须在运行 Dart 入口函数前被调用。入口函数可能会引起一系列的事件, 616 | > 因为 [`runApp`]({{site.api}}/flutter/widgets/runApp.html) 617 | > 搭建了一个 Material/Cupertino/WidgetsApp, 618 | > 进而隐式创建了一个 [Navigator]({{site.api}}/flutter/widgets/Navigator-class.html), 619 | > Navigator 又可能在第一次初始化 620 | > [`NavigatorState`]({{site.api}}/flutter/widgets/NavigatorState-class.html) 621 | > 时读取 `window.defaultRouteName`。 622 | > 623 | > 运行 engine 后设置初始化路由,将不会有作用. 624 | 625 | 626 | 另外 627 | 628 | > 如果在 `FlutterEngine` 启动后,迫切得需要在平台侧改变你当前的 Flutter 路由, 629 | > 可以使用 `FlutterViewController` 里的 630 | > [pushRoute](https://api.flutter-io.cn/objcdoc/Classes/FlutterViewController.html#/c:objc(cs)FlutterViewController(im)pushRoute:) 631 | > 或者 632 | > [popRoute](https://api.flutter-io.cn/objcdoc/Classes/FlutterViewController.html#/c:objc(cs)FlutterViewController(im)popRoute)。 633 | > 634 | > 在 Flutter 侧推出 iOS 路由,调用 635 | > [`SystemNavigator.pop()`](https://api.flutter-io.cn/flutter/services/SystemNavigator/pop.html)。 636 | 637 | 查看 [路由和导航](https://flutter.cn/docs/development/ui/navigation) 了解更多 Flutter 路由的内容。 638 | 639 | ### 其它 640 | 641 | 之前的例子仅仅展示了怎样定制 Flutter 实例初始化的几种方式, 642 | 通过 [撰写双端平台代码](https://flutter.cn/docs/development/platform-integration/platform-channels), 643 | 你可以在 `FlutterViewController` 展示 Flutter UI 之前, 644 | 自由地选择你喜欢的,推入数据和准备 Flutter 环境的方式。 645 | 646 | # 参考文献 647 | 648 | 本文由作者翻译的两篇 Flutter 官方文档合并而成 649 | 650 | - [将 Flutter module 集成到 iOS 项目](https://flutter.cn/docs/development/add-to-app/ios/project-setup) 651 | - [在 iOS 应用中添加 Flutter 页面](https://flutter.cn/docs/development/add-to-app/ios/add-flutter-screen) 652 | --------------------------------------------------------------------------------