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 上怎么表现?…… 还有那么多那么多的控件上怎么表现呢?难道一个一个看代码?

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