Flutter 学习笔记

作为一个多年经验的 Web 后台开发程序员,我还有少数一些前端的经验(有 Bootstrap,jQuery 之类框架的使用经验),最近因为公司的项目,想要着手一款客户端软件的开发,这些年其实一直很想开发一款自己的手机 App,不过一直都执行力不足,之前尝试的是 iOS,始终没有成效,这回大家都一口推荐 Flutter,所以,我再做一次尝试。

安装

Flutter 的安装,真是一个不错的体验,登录英文官网,照着指引,一步步做,就真的可以安装成功。首先是下载对应操作系统的版本,然后配置好 PATH 路径,使得 flutter 指令可以直接在命令行执行。然后,执行 flutter doctor 指令,就可以检查操作系统的依赖是否完备,这个设计太贴心了。按照指引,再去安装 Android Studio 之类的就可以。

这里想分享的一点就是,这里大家尽量完全按照官网的指引去做比较好,虽然 flutter doctor 给出了指引,但是还是不如官网说得详尽。我安装了两次,第一安装按照官网指引很顺利,第二次,我就照着记忆和 flutter doctor 的指引去做了,结果,竟然卡住了。主要是有个步骤需要从 Android Studio 的配置项里激活安装步骤的,就是 Android Studio 的命令行工具的安装,但是 flutter doctor 的提示一直是没有签署协议,如果看官网,就不会漏过这一步。所以,这里的主要感受就是,一定要认真看官网

Hello, World!

学习每种技术,第一个项目,无疑都是“你好,世界!”,感觉 Flutter 作为一个移动端的开发框架来说,真的做得无以伦比。只要在命令行执行:

1
flutter create my_app

然后,进入目录后,执行

1
flutter run

就可以轻松地跑起来了。如果没有事先启动模拟器,就会在 Chrome 浏览器里,启动一个 Web 版的实例,启动 iPhone 模拟器后,就会自动链接 iPhone 模拟器,启动一个 App 版本。简直太棒了。对于我这种首次开始客户端开发的小白来说,编译、模拟之类的工作正是最大的难点。但是这些都省略了,可以直接进入到框架学习和代码编写阶段,简直完美无缺。这个环节,我给打 100 分,完美。

Dart

学习 Dart 完全就是因为 Flutter,不过,除了 Flutter,还有 AngularDart 也是使用 Dart 语言。这门语言可能也有不少其他领域的应用,不过应该成气候的不多,至少我没怎么听说过。实际使用下来,语法不用多言,对于一个熟练的程序员来说,没有太大的难点,语法糖很多,各种现代化的特性都已经支持,泛型、异步、函数式等等,都不在话下,整体来说,编写 Dart 还是非常愉快的。

Dart 语言是 Google 创造的,有比较强大的支持资源,因为 Flutter 不可抵挡的趋势,背后社区也非常可观。据说,在 Google 内部还是比较受欢迎的。

constfinal 修饰符

在 Dart 里,constfinal 是两个很常见的修饰符。我以前用过的语言里,final 比较少见,const 比较常见。

const 很容易理解,代表的意思是一个常量。既可以修饰变量(也就是等号左边的),也可以修饰值(等号右边的)。

1
2
3
const a = const [];
const a = []; // 与第 1 行效果完全一样
var a = const []; // 与第 1,2 行意思不一样,a 可以重新赋值

const 在 Dart 里,还可以用来修饰类(Class) 的构造函数。而且,这个用法也是非常常见,在创建一个 Widget 的时候,尤其是 StatelessWidget 的时候,官方推荐的 Snippet 就使用 const 来修饰构造函数。其含义是,该类的成员不可更改(immutable)。

1
2
3
class SearchButton extends StatelessWidget {
const SearchButton({Key? key}) : super(key: key);
}

Dart 的 IDE 环境(Android Studio、Visual Studio Code)里,都有很强的提示功能。当你使用 const 修饰一个构造函数的时候,你会注意到这个关键词的内涵。首先所有的成员必须都是 final 的,其次,所有成员必须是在构造时候就赋值。你很容易感觉到,这些保留字的作用并非是正交互斥(MECE)的,很多时候,其含义是重叠的,时不时就让人迷糊。如果你写一阵子就会明白我说的,写得更久一点,你又会适应,觉得自然了。

final 保留字,用来修饰一个变量,声明这个变量不可修改(immutable),似乎官方也推荐尽可能把变量声明为 final,似乎这样可以优化性能或者内存之类的。同样是 immutable,那么与 const 到底什么区别呢,有时候免不了要迷惑。区别就在于,const 是在编译阶段就确定了是常量的,而 final 可以在运行时才确定其值。

1
2
final today = DateTime.now();
// const today = DateTime.now(); // 这行无法编译通过

声明为 final 的变量,必须在行内赋值。如果不想在行内赋值也可以,必须要使用 late 保留字来修饰,就可以在需要的时候再赋值,当然,只能赋值一次。而且,要注意的是,使用 late 修饰的声明为 final 的变量,你是没有任何办法判定其是否已经进行了赋值的。如果你要使用这个 late final 的变量,就必须 100% 确定其已经赋值了,否则程序就会崩溃。如果你不能做到这一点,很简单,你就不该使用 late final

强类型

Dart 是一门强类型语言,所有的变量,一经指定类型就不能更改,只能存储这个类型的值。声明变量的时候,可以使用类型进行声明,也可以简单得只用 var 声明,因为编译器可以进行类型推导,不一定总是需要强制指定类型。不过有类型推导,不指定类型,不代表就是动态类型。

Dart 提供了一个特殊的类型叫 dynamic,可以适配各种不同的类型,估计也是为了提供一定的灵活性,不过呢,我还没有深刻体会到这个类型的用法。

有了强类型后,就要提到这个语言的泛型。这是强类型的语言必备的功能。Dart 里的泛型使用非常普遍,很多类都支持泛型,最常见的就是基本数据结构 List 和 Map,都是泛型的。

函数

Dart 里的函数是一等公民,所以,也可以支持函数式编程泛型,感觉已经是现代编程语言的标配了。我所不熟悉的就是 Dart 里的函数参数定义,有所不同,支持位置参数,也支持命名参数两种定义方式。并且,还支持位置可选参数和命名可选参数。不过在定义可选参数的时候,只可以使用其中一种。举个例子:

1
2
3
4
5
6
7
8
void helloWorld( String name, [int times] ) {
print("Hello" + name);
}

// 也可以
void helloWorld( { @required String name, int times } ) {
print("Hello" + name);
}

上面的代码例子里,展示了位置参数,可选位置参数,可选命名参数,这些参数的设定也支持缺省值。并且函数也可以省去类型声明,因为支持类型推断,这个和变量的类型推断是一致的。

箭头函数

可能这是 Dart 代码里,另外一个极其常用的语法了,虽然没什么特殊的,不少语言里也有类似的功能,比如 Python 的 lambda 表达式,PHP 里也有匿名函数之类的结构。不过,箭头函数的使用真的极其普遍。所以,如果不熟悉的话,可是要好好了解了。

异步

同样,这门语言里也提供了异步的支持,有原生元语 async/await,可以轻松定义异步调用,也支持生成器语法 yield 关键字。这些很多现代语言都支持了,就不赘述了。

Flutter

其实,Dart 的学习和 Flutter 的学习是穿插的,除了刚开始学编程的新手,工作过几年的人,都会掌握一门类 C 语言,所以在这个基础上,看 Dart 也只能是极其眼熟。如果对现代编程语言有所涉猎,就更容易了,很多高级特性都是共通的。

所以,真正的学习步骤,其实一上来就接触 Flutter 了。虽然各种文字教程,视频教程,都很执拗地上来就要教 Dart,就好像我的学习笔记,我觉得只能是这样归类资料比较有条理而已,真正的学习绝对不是线性的。我甚至建议直接就开始学习 Flutter 好了,等遇到了看不懂的语法特性再去看 Dart 的参考。这样不会过早地消弭耐心,更好保持学习的节奏。

绘制页面

上手 Flutter,第一个学习的,肯定是绘制一个基础的页面,比如在一个 App 的第一屏上,放一个按钮,或者放一个 Label,展示个 “Hello,world!” 什么的。

官方提供了一个例子,还挺用心的,功能是通过一个加号按钮,点击就改变页面上的一个 Label 的值,使得里面的数字 +1。这个例子展示了 Flutter 框架里很重要的几个概念,Widget,StatelessWidget 和 StatefulWidget 等。

在 Flutter 的世界里,所有的界面都是 Widget,有些甚至不是界面,比如 Center,这种位置控制,也是用 Widget 实现的。按照我现在的理解,就是把关于布局和表现层的一些描述性的指令,全部封装到了 Widget 这个体系里面。然后,程序员可以用这些指令来结构化地描述自己的 App 的界面。

配合 Flutter 提供的一些特定的语法糖,让整个 Flutter 的代码,看起来更像是一种 Markup Language(其实不是)。

布局规则

其实 Flutter 的布局并没有看上去这么随意的,还是有一些约束的,而且和我原来有的感觉不太一样的。网上很容易搜索到 Flutter 的一些教程,不过更多是关于如何排版好一个 App 的界面,你看了一些,然后照着操作,会发现,很容易上手,但是当我真的独立去实现一个 App 的界面的时候,就会发现,轻松就能掉坑里。

官方文档也说布局的一些核心理念,我这时候才能有了一点点了解。“**Constraints go down. Sizes go up. Parent sets position.**” 当你的界面上出现了黄黑相间的斑马线的时候,你才能体会到这个原则对你带来的影响。

我遇到过些很难解决的问题,比如,我在 Column 里,嵌套了一个 Column,这个时候,内层的 Column 我想让它撑满高度也好,撑满宽度也好,都很难做到。原因其实我现在也没太看懂,不过看上面的三句话原则,就是 Sizes go up,大概是内层的元素没有指定一个有限的宽度和高度,所以,外层就会要求内层元素尽可能展示得小,网上会搜到很多的解决方法,比如设置一个 MainAxisSize.min,我试过,并不那么好用的,而且,网上搜到很多描写怎么解决让内层 Column 撑满高度的,没搜到什么撑满宽度的指导。最后,我给内层 Column 的元素,外面套了个 Container ,然后指定其宽度为 double.infinity ,反倒把我自己的问题解决了。

页面导航

页面导航跳转,可以用原生的 Navigator,也可以用各种现成的框架,不过我始终没明白,框架的必要性是什么,无法理解引入框架后,带来的真正好处,网上搜到很多介绍路由的框架比如 fluro,都是提及简单的用法,没有提及设计的原因和原理是什么。感觉比较优质的技术文章还是很稀缺的,指望能很深入地讲解问题的资料,还是很少见。你只能搜到如何做什么什么,至于为什么,就很难搞清楚。这点,即使对一个多年经验的程序员来说,也不是很友好。

我之前做了个例子,结果一直报一个错误 No Material Widget Found. 这个错误。完全让人一头雾水,错误消息说,Widget 没有被包裹在 Material Widget 里面。网上搜到的都说,外面套个 Scaffold 就好了,可是我看来看去,我的 Widget 明明就已经在 Scaffold 里面了啊。然后反复折腾很久,我才意识到,是我要跳转的目标页面,那个页面跟节点,没有套入一个 Material Widget,这才解决了问题。

状态管理

使用 Flutter 的命令,创建第一个模板 App 的时候,就会接触到状态管理这个问题,只是作为新手,那个时候,体验真的不深。当我尝试做一个简单的 App,画完界面的时候,就开始发现,这是一个不得不搞清楚的话题了。

我所熟悉的 Web 后台开发,是面向 HTTP 协议的,所以早就习惯了“无状态”,用户的每个请求都会携带所有的参数,系统处理用户的每个请求,都是无状态的。比如 Web 后台开发的过程中,很多 API 的实现,有一个基本的要求就是“幂等”,足见这种业务模型的特点了。

而客户端的开发就有极大的不同,用户直接就置身在状态的海洋中,极目所至,看到的全是状态,每个细节都充分可视化到用户的面前,如果有什么变化了,就要立刻可视化到用户的面前,相反,没变的东西,在界面上就不该变,不能吸引用户的注意。简直从不关心状态到状态变化极度敏感的另一个极端。

Flutter 的状态管理有多种方法。框架自带的命令行工具,创建的模板 App 里,展示了一个计数器的例子,就展示了最基础的一种状态管理方案,就是 State,也是最基础的状态管理方案。不过,这显然不是完美的方案,可能仅仅是最简单的一种方案。因为比较容易看明白吧。

此外,很快就会接触到的有 Provider,至少我看了几个例子,都是大量使用了 Provider 的。Bloc,Redux,等等也是常常能看到的关键词。框架底层似乎有一些关键词是 InheritedWidget,Stream 等等。反正,我来到这个领域,马上被各种概念的海洋给包围了。罕有适合我这样的程度阅读的资料。只能硬着头皮,一个碎片一个碎片在脑海里拼接了。

网络请求

根据我的了解,网络请求,最常见的就是使用一个叫 Dio 的类库,不过虽然是个封装好的包,仍然是不能直接使用的,(这点蛮奇怪的),很多的包引入后,都要习惯性地再封装一次 Util 类才能正常使用。就我的观察来看,是这样。

之前发现了一个包叫 flustars,里面封装了各类的 Utils 类,也包含了 DioUtil。不过我还是从一个我看了很多次的范例 App 的代码里直接拷贝了一个 net 目录过来,里面有很多封装好的代码,我简单做了个适配。主要解决了一些问题,比如统一的报错处理,还有一个类似拦截器的东西,Interceptor,可以在请求前灌入一些自定义的 Header,用来处理身份鉴权之类的 header 比较有效,还有就是强类型的语言,在返回值的处理上,也要费不少功夫。

怎么处理返回的 json 值,不像在 PHP 或者 Python 里面那么简单,还是要一层层的处理,先解析成 Map,然后再继续构造对象,这个过程也需要不少衔接的代码。

本地存储

这也是必然会遇到的一块内容,一个客户端 App,一旦登录后,第一个要解决的就是保存用户的登录态,当然,如果你有很多设置的选项,也免不了要保存。在 Android 里面有个叫 Shared Preference 的东西,就是专门用来存储这些的,在 iOS 里面应该叫做 UserDefaults,名字不一样,但是性质应该是一样的,都是 key-value 类型的本地存储。所以,在 Flutter 里面有了一个统一的封装。

不过就像很多的功能一样,直接拿来使用的话,也会显得很原始,上面也有各类的包封装,在 flustars 里面也有个类,叫 sp_util,已经抽离成了单独的包,可以引入进来使用,当然,引进来后,你可能还要继续封装(苦笑脸)。

使用主题

主题也是一个我百思不得其解的东西,感觉当我按照设计图进行设计的时候,各种尺寸,各种颜色都会出现,而主题控制则极为局限。而且主题的应用的时候,也比较难用。另外可能也是没看文档,或者说我英语不好,看到主题里提到的那些名字,根本无法了然,设定的每个颜色会应用在哪些地方。比如 PrimaryColor 和 AccentColor 到底会在 button 上怎么表现?在 TextField 上怎么表现?…… 还有那么多那么多的控件上怎么表现呢?难道一个一个看代码?

实在是困扰啊,提供了一个好的机制,但是没有好的指导来判断怎么使用,真是苦恼。