Flutter学习笔记&学习资料推荐

对Flutter的学习已经有一段时间了,这里做一下总结记录,东西比较多,可能主要是一些学习资料的记录,还有一些杂七杂八的学习笔记。

文章目录

    • Flutter 初体验
    • Flutter 环境配置
    • Flutter 学习书籍
    • Dart 开发语言
    • Flutter 官网教程
    • Flutter pub 社区
    • Flutter 学习实例
    • Flutter 的一些实际开发问题
    • Flutter 代码/性能优化
    • Flutter 解决嵌套地狱的几种方案
    • Flutter 调试相关
    • Flutter 在PC端和Web端的应用

Flutter 初体验

先说说对Flutter的感受吧,总的来说上手还是比较容易的,因为他的思想理念跟React十分相似,都是通过状态管理控制UI界面的更新,甚至连setState的方法名都是一样的,如果你是学习过类似React的框架的话,将很容易理解,基本可以直接开撸。但是它有好的地方,也有不好的地方。

好的地方

  • UI性能流畅, flutter的UI性能体验的确很棒(要运行release版本的,debug版本的并不流畅,我是运行到android手机的体验),整体的体验甚至超过了原生,至少在Android机上是这样的感觉,这点的确无愧它高性能渲染的招牌。与RN底层调用原生 Native 的 View 组件不同,Flutter使用Skia作为其2D图形渲染引擎,直接向GPU提供渲染数据,也就是说它的所有界面都是一帧一帧的绘制到界面上的,因此它的渲染速度要比RN更快一步。关于Skia,其实Android底层最终就是用的它绘制的,Flutter只不过是又拿来利用了,Skia是用C语言编写,基本可以在任何操作系统平台编译,因此不依赖于目标平台,这也就是为什么Flutter能跑在iOS平台的根本原因。
  • Reload调试,flutter在调试模式下的Reload热加载机制, 使得写代码可以快速应用到手机上不用每次运行,只需保存一下即可;这跟AS的instant run还有本质的不同,在android当中我们每次都要运行一个完整的apk到手机上,这无疑增加了等待时间,而时间本身就是开发成本。
  • 无需单独的界面文件,在flutter当中已经没有android原生或者web当中的界面和代码分开之说了,就是不会再有一个单独的文件来写类似xml或者html标签的了,其实这在React当中已经有体现了,React的JSX语法已经有意打破UI元素和纯js代码的界限。Flutter基于dart,而dart是面向对象语言,不管是UI组件还是数据源控制类,所有的东西都是new出来的对象,所以在flutter当中你几乎可以一把梭子撸到底,别提有多爽了。
  • 支持平台众多,flutter目前支持的平台:android、iOS、fuchsia(Google自研,感觉目前要凉凉)、web、linux、windows、macOS,可以看出Google想利用flutter一统江湖的野心还是比较大的。如果未来Google真的能在这么多平台上优化好Flutter, 那么在未来Flutter无疑是真香定律。

不好的地方

  • 嵌套地狱,这也是为人诟病和业界吐槽最多的地方:
    Flutter学习笔记&学习资料推荐_第1张图片
    这个图我还没有截全,可以看出,只是一个简单的demo他的嵌套层级就如此之多,让你分不清的括号。。眼已瞎。。。真到实际项目中的话,简直要人命。前面提到它的好处,可以一撸到底,也许这可能就是它一撸到底的代价,当然嵌套问题在其他语言当中本身也会存在,但是一般情况下不会那么明显,比如android开发中的布局xml文件,如果你的布局复杂的话也会嵌套的层次比较深,还有web开发中的html标签也可能会嵌套的很深,但是在Flutter当中这个缺点被无限的放大了,原本在其他语言当中不需要嵌套一层的在flutter中就不行,比如padding(内边距), 在android当中基本上是所有UI控件的一个基本属性,而在web当中你可以通过css来控制,但是在flutter中你必须在需要加padding的控件外面包一层。有部分开发人员已经就这个问题并从技术上做了一些改变,但实际使用感觉有些也不是很理想,文章的后面我会分享。
  • 平台Native能力缺乏, flutter一个致命的点就是对原生多媒体的使用以及一些复杂控件的展示等能力十分脆弱,比如我要用Camera拍照、要获取设备的图片、要申请权限、使用WebView(Flutter中没有自带的WebView控件)等等,可能不止这些,基本上是涉及到平台硬件能力相关的,在flutter中这些只能通过去开发原生插件的方式去实现(Flutter SDK中为提供了响应的Flutter API与原生API进行交互的方法)。在flutter的官方github上为我们提供了一些官方的常用插件,但是实际当中还是会有需要自己去开发原生插件的情况,作为开发者这时就会很吃力了,因为你不但需要开发android版本的还需要同时开发ios版本的相应功能,这样你才能完整的开发完一个至少满足两个平台的Flutter原生插件。如果你不是两端都会的话,可能需要两端的人来同时参与插件的维护。而且如果在未来你希望你的插件除了android和ios外支持更多的平台,那么同一套插件可能需要更多平台的人来维护,比如有某个插件在手机端android和ios都实现了拍照功能,那么有一天希望它能在windows上面跑的话,就需要插件添加对win平台的拍照能力支持。

Flutter 环境配置

首先你要知道Flutter的中文官网:https://flutter.cn/
Flutter SDK的下载地址:https://flutter.cn/docs/get-started/install
你也可以直接在官方上点击“开始使用”跳转到这个页面,选择对应的操作系统安装就可以了。建议直接下载zip包的方式安装,因为clone的方式去下载github的代码很慢。

在windows上安装完毕后,需要将flutter\bin的完整路径添加到系统环境变量的path即可,最后运行命令flutter doctor进行检测,这里可能会遇到一些问题,不过基本在网上能找到解决方法,可自行查找。另外你需要在android studio中安装Dart插件和Flutter插件:

Flutter学习笔记&学习资料推荐_第2张图片

另外为了摆脱天朝网络封锁,使用flutter的时候有时下载依赖库会很慢,还需要配置如下环境变量:

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
Flutter学习笔记&学习资料推荐_第3张图片

其实就是配置了两个镜像网站,当然还有很多其他的镜像网站可用,
你可以在这里找到更多: 在中国网络环境下使用 Flutter


最后在安装完SDK后,打开如下目录:

D:\flutter_windows_1.20.3-stable\flutter\packages\flutter_tools\gradle

打开该目录下的flutter.gradle文件修改如下内容:

buildscript {
    repositories {
        //google()
        //jcenter()
		maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.0.1'
    }
}

class FlutterPlugin implements Plugin {
    //private static final String DEFAULT_MAVEN_HOST = "https://storage.googleapis.com";
	private static final String DEFAULT_MAVEN_HOST = "https://storage.flutter-io.cn";
	...
}

至于为啥这样改就不用多说了,做android开发的应该比较清楚,其实主要是针对android端的,另外gradle插件不一定是4.0.1的可以根据你的AS版本来。网上有人说还要修改resolve_dependencies.gradle文件中的地址,但是我修改了会出现问题,其实在最新版的文件里,它已经替换成你前面配置的环境变量的值啦,你可以看到:

Flutter学习笔记&学习资料推荐_第4张图片

Flutter 学习书籍

  • 第一个首推wendux的《Flutter实战》 (机械工业出版社/2020-03-20出版)

    作者是国产巨佬,书的内容质量自然是没话说,讲的非常详细,几乎各个方面都涉及到了,而且有示例代码,可以边看边实践,应该有很多小伙伴跟我一样看的是这本。 作者写的网络请求开源库Dio已经成了Flutter社区最流行的网络请求库, 看了确实不错,好像很多人也都在用这个。

    需要指出的是,该书有部分内容可能你只需要了解大概实现原理,实际中pub社区有现成的库可以直接依赖使用,如第七章中的跨组件状态共享Provider我们可以直接使用provider库;json转Model序列化中提到的json_model库可以使用json_to_model这个库来代替, 因为他原来的库好像没有升级了依赖有点冲突问题;关于原生插件开发一节内容建议可以直接去看官网的介绍,因为最新的plugin实现方式好像与文中提到的略有不同了,已经不需要在一个MainActivity去做了。其他基本没有啥大问题。

  • 另外一个推荐的是《Flutter In Action》(美国Manning(曼宁)出版社/2020-1月出版)

    不过这本书是外国的,目前国内市面上还买不到,京东预售价居然要486?还是看电子版的好啊,我放在了百度网盘上:
    链接:https://pan.baidu.com/s/1ZFtzlo5xX0YjxIC6AlHCcg
    提取码:vsux

    只可惜该书目前没有中文翻译版的,全书都是英文版的,看得可能会有些吃力(正在研读中。。)不过美国人写书的视角跟中国人完全不同,书里介绍的也非常详细,看完应该能学到很多,毕竟flutter也是外国人开发的。

Flutter更新的速度也是够快,还没有一本书能追上它更新的速度。

Dart 开发语言

Flutter是一个高性能的渲染引擎框架,本身不是一门语言,它是使用Dart语言开发的,就像Android是用java、H5是用js开发的一样,首先要学习了解的是Dart语言。

关于Dart,它其实是一个集成了众多优秀开发语言如 Java、JS、C++、C#等语言的优秀特性的一个面向对象的开发语言,如果你是接触过web开发或者是客户端原生开发的,学习dart几乎不费力,简直可以无缝过渡,因为你能看到一些你非常熟悉的东西,所以花一天或半天的时间了解一下就可以了。

主要记住Dart是一个纯面向对象的,强类型的语言,这点很重要。

学习Dart的主要网站还是先看官网的:Dart编程语言中文官网,
我觉得官网的这个是必看的:Dart语言概览
官网除了Dart语言概览还有其他内容,可以简单浏览一下,用的时候再详细查。

除了官方的网站,下面的博文你可以选择性的学习:

  • Dart 中 List 数组的常用方法

  • Dart快速学习之路

  • flutter开发之Dart[必读篇]

  • Dart语言之从入门到放弃

其中第一个可以看一下,毕竟数组是开发中高频使用的东西,其他三个看一个就可以了,内容基本类似,跟官网的教程查不多,但是有些细节会提到不一样的地方。如果是纯练习Dart语法的话,你可以使用Visual Studio运行,参考上面的第二个博文链接里有介绍。另外,如果已安装了Flutter SDK的话是不需要再另外单独安装Dart SDK的,因为Flutter SDK中已经包含了Dart。

Flutter 官网教程

除了wendux的《Flutter实战》, 官网的 Flutter 开发文档 有些内容也是初学者必看的,我这里把我看的觉得重要的列一下:

  • Flutter 中的布局

  • 布局构建教程

  • 深入理解布局约束

  • 处理边界约束 (Box constraints) 的问题

  • 在后台处理 JSON 数据解析 (可以学到如何在不阻塞界面的情况下处理耗时任务)

  • Flutter Packages 的开发和提交

  • 升级Android插件(如果是新开发一个插件的话直接看它最后的介绍)

  • 使用 Flutter inspector 工具

其中关于布局约束一节官方总结的很精辟:
Flutter学习笔记&学习资料推荐_第5张图片
可能英文的这张图片更好理解,说的更加简洁:

Flutter学习笔记&学习资料推荐_第6张图片

理解Flutter的布局约束原理对于流畅的书写布局十分重要,布局时你可能不会感到像android中那样舒服,因为经常会遇到一些约束边界报错的问题或者溢出问题,官网教程给出了大概的原理可以帮助你理解并解决这些问题。

关于最新版的插件开发方式官网介绍的还不是很详细,补充两篇:Flutter Plugin插件开发填坑指南、Flutter 插件的创建及使用

其实最新的主要是通过实现一些接口来做的:


public class MyFlutterPlugin implements FlutterPlugin, ActivityAware, MethodCallHandler {
     
	....
}

Flutter还支持将原生应用的部分页面替换成flutter, Android中集成教程可以看这里 在 Android 中集成 Flutter

Flutter pub 社区

Flutter 提供了一个发布网站,可以搜索别人开发好的插件:https://pub.dev

一些官方推荐的插件还有一些优秀个人开发的插件都会在这上面能搜索到,如果你想使用某个功能,如获取照片?拍照?像gilde一样的具有缓存功能的网络图片加载库?首先到pub上搜索,将大大减少你的开发时间,当然如果你有好的插件也可以开源发布到pub上给别人使用。

注意:pub上只支持英文package插件名的搜索,搜中文是搜不到的

下图是Flutter提供的官方维护的插件列表:

Flutter学习笔记&学习资料推荐_第7张图片

需要的可以直接按名字到pub上去搜索,一般搜索结果的第一个就是。只看名字你就能猜出来,很多插件基本上是完全模仿的android开发当中的库。其中有一些开发中会高频使用到的,如device_info获取设备信息、camera拍照、image_picker图片选择、shared_preferences类似android的sp存取数据、path_provider文件操作、url_launcher打开外部链接、video_player视频播放、webview_flutter浏览器等。当然官方维护的插件在Github上也提供了源码,需要的话可以直接下载下来修改代码然后本地使用或者作为新版本发布到pub, 官方的插件源码也提供了插件开发方式最好的学习例子。

另外补充一些比较好用的插件:

插件名字 功能
dio 超级好用的网络请求库
sqflite 类似android中的sqflite数据库
fluttertoast 类似android中的toast功能
wechat_assets_picker 类似原生的微信图片选择器
flutter_staggered_grid_view 支持瀑布流的gridview, 比flutter自带的强大太多
provider 跨组件状态共享的官方推荐库
event_bus 类似android中的EventBus消息通信开源库
web_socket_channel websocket库在官方自带的基础上封装的
flutter_webview_plugin 比官方的功能要好一点的,官方的插件目前还是开发者预览版
json_annotation json序列化注解,详细使用可以参考《Flutter实战》中的介绍
json_serializable json序列化工具dev_dependencies库,详细使用可以参考《Flutter实战》中的介绍
json_to_model Json文件转Dart类工具dev_dependencies库,《Flutter实战》中json_model 优化版本
intl 国际化翻译,详细使用可以参考《Flutter实战》中的介绍
intl_translation 国际化翻译工具dev_dependencies库,详细使用可以参考《Flutter实战》中的介绍
cached_network_image 一个支持占位图和图片缓存的网络图片加载库 还可以支持图片下载进度
progress_dialog 轻量级的进度等待弹窗控件 类似android系统默认进度弹窗
camera_camera 相机拍照录视频插件,比官方的好用一些

Flutter 学习实例

以下列举一些都是我在G站上发现的,虽然有些功能不是很完善,但是当做入门实例来学习的都是相当不错的:

  • Flutter版的微信客户端框架:wechat_flutter

  • Flutter版的玩android客户端:flutter_wanandroid

  • Flutter版的玩android客户端2:WanAndroid_Flutter

(PS:这里吐槽一下 感觉鸿洋的玩android网站要被玩坏的节奏啊,各种版本的玩android: java版的、 kotlin版的、小程序版的。。现在又出了flutter版的,光flutter版的github上就一搜一大堆。。不过他的网站上有开放的API请求接口可以使用,拿来练手的确是最好不过的了,也难怪这么多人拿它来开刀。。。)

  • Flutter版的Github客户端实例:gsy_github_app_flutter (这个作者也写了一本Flutter开发的书,可以去看看)

  • Flutter 练习项目:flutter_deer (更切近实际开发问题)

更多实例参考:flutter-do 、 awesome-flutter(这个是国外大神维护的)

另外特别推荐两个可以用来当做API控件功能查询使用的:

  • Flutter UI集录指南: FlutterUnit(特点是可以一边看效果一边展开当前效果对应的代码,demo支持手机/桌面/web端,支持搜索,UI也比较漂亮)
  • Flutter 控件大全_老孟(这个就更厉害了,330多个控件每个控件的使用例子他都整理了,支持搜索,感觉有了这个都不需要去官网查API了。。老孟是个狠人。。。)

Flutter 的一些实际开发问题

下面几篇是超级干干干货的Flutter文章,特别实用,介绍了很多实际开发场景中遇到的问题及解决方法:

  • Flutter开发中的一些Tips

  • Flutter开发中的一些Tips(二)

  • Flutter开发中的一些Tips(三)

  • 正确操作Dart中的字符串【译】

  • Flutter三种方式实现页面切换后保持原页面状态

下面是我在学习过程中遇到的一些问题:

国际化过程中执行命令arb文件生成dart时报错

在Flutter实战教程中,最后一步根据翻译好的arb文件生成dart文件,执行如下命令报错Cannot open file

flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/localization_intl.dart l10n-arb/intl_*.arb

解决方法:文件管理器中进入项目根目录下,在git bash终端执行命令即可,原因是windows下执行不识别*号通配符会报错。

这句命令在首次运行时会在"lib/l10n"目录下生成多个文件,对应多种Locale,这些代码便是最终要使用的dart代码。

Android Studio运行Flutter时无法连接设备

如下图,有时会发现这里找不到设备无法运行:

Flutter学习笔记&学习资料推荐_第8张图片
解决方法:先关闭AS,然后到flutter sdk的安装目录下, 将\flutter\bin\cache下的lock文件删除,再重启AS。如果还是不行,那么多试几次就可以了。或者有其他的IDE工具连接了你的设备,关闭其他的IDE就可以了。

Flutter学习笔记&学习资料推荐_第9张图片

Fliutter运行时一直卡在Running Gradle task 'assembleDebug’

这个有时会卡很久,有时卡一会儿就好了,原因是AS正在后台执行下载,此时可以打开任务管理器,查看AS的磁盘和网络使用状况:

Flutter学习笔记&学习资料推荐_第10张图片
如果你发现这里的速度很高,说明它正在疯狂的下载,一般多等待一会儿就可以了。可是如果你发现这里也没有什么流量,而它一直卡在那里,说明是依赖库无法下载,如果是android端,可以打开项目的android文件夹,然后修改build.gradle文件如下:

Flutter学习笔记&学习资料推荐_第11张图片
使用阿里云的maven地址就可以,速度会很快,然后在AS的Tools工具菜单中执行Flutter>Flutter Clean,再重新运行即可。如果还是不行,首先到前面的Flutter 环境配置参考url的配置。如果还不行的话,那可能需要在Terminal终端中cd到android目录下,执行下面命令:

gradlew clean
gradlew build

或者可以选中android文件夹右键Flutter->Open Android module in Android Studio可以把android目录当成一个标准的android项目打开重新build即可。最终再重新回到Flutter项目运行。

showSnackBar报错

Unhandled Exception: Scaffold.of() called with a context that does not contain a Scaffold.

解决方法:使用GlobalKey

  var _scaffoldkey = GlobalKey<ScaffoldState>();

   return Scaffold(
      key: _scaffoldkey,
      	........
      ),
      .......
      onPressed: () {
     
     	_scaffoldkey.currentState.showSnackBar(SnackBar(content: Text('显示snackBar')));
     }

使用Utf8Decoder时提示参数错误

在Flutter实战教程中遇到代码片段socket.transform(utf8.decoder).join()报参数不匹配的错误,于是找到了stack overflow的这个utf8.decoder not working after latest Flutter Upgrade,原来这只是一个api的兼容性问题,解决方法:

使用StreamTransformer.bind(Stream) 代替 Stream.transform(StreamTransformer).
例:

  • Before: foo.transform(utf8.decoder)...
  • After: utf8.decoder.bind(foo)...

viewport was given unbounded height.width.

一般情况下是需要给一个固定的高度或宽度,可以参考这个, 但是有时可能都不起作用,还是嵌套的布局使用有问题,去参考官网的布局约束原理 | 处理边界问题。

从其他应用切回Flutter应用页面无响应

这是我偶尔发现的一个神奇的问题:Flutter应用在切出应用以后再切回到应用,页面没有响应(不响应任何点击事件),测试手机版本:Android 7.1.1 魅族Pro6

本来我愉快的写着demo, 但是突然有一天,当我按下Home返回桌面再返回来的时候,发现什么都不能点击了。。whf。。?

Flutter学习笔记&学习资料推荐_第12张图片

后来我想了下Flutter这么牛逼的团队,应该不会把有如此明显的严重问题的版本发出来使用吧。。但是它确实是发生了,就发生在我的眼前,而且是百分百重现。

这个问题困扰了好久,因为几乎所有的我创建的应用都是这样的,从github上下载的其他人的demo也有类似情况,但是大多数都是正常的。

后来调查发现是跟flutter sdk版本有关系:

版本 测试结果
flutter sdk 1.17.4 切回应用正常
flutter sdk 1.20.1 切回应用无响应
flutter sdk 1.20.2 切回应用无响应
flutter sdk 1.20.3 切回应用正常

现在至少可以确定在 >1.17.4 <1.20.3之间的版本是可能会有兼容性问题的。也可能是Flutter团队在最新版本修复了这个不为人知的bug。

获取屏幕的宽高尺寸

 double width = MediaQuery.of(context).size.width;
 double height = MediaQuery.of(context).size.height;

有时遇到一些边界问题,不得不指定一个固定的宽度高度,这时可以参考屏幕的宽高,尽量按屏幕尺寸的百分比去设置,而不是写死魔法值。

判断当前是哪个平台

import 'package:flutter/foundation.dart';
...
//TargetPlatform目前支持6种平台
if (defaultTargetPlatform == TargetPlatform.android) {
     
   // 是安卓系统,do something
}

// 我们可以通过显式指定debugDefaultTargetPlatformOverride全局变量的值来指定应用平台。比如:
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
print(defaultTargetPlatform); // 会输出TargetPlatform.iOS
//上面代码即在Android中运行后,Flutter APP就会认为是当前系统是iOS,Material组件库中所有组件交互方式都会和iOS平台对齐,defaultTargetPlatform的值也会变为TargetPlatform.iOS。

RefreshIndicator有时无法下拉刷新

RefreshIndicator组件的子组件listview或gridview的item数量较少时,无法触发下拉刷新操作,解决方法:滚动组件的physice属性设置为AlwaysScrollableScrollPhysics(),总是保持可滚动状态。

release模式下运行app无法连接网络

首先release模式运行的命令:

flutter run --release

运行之后发现请求接口的列表都出不来了,原来是AndroidManifest文件中默认没有添加INTERNET权限,手动添加上即可。奇怪的是,在debug模式下运行,就算不添加默认也是能请求网络的,害我找了好久原因。。

AS中运行除了main.dart以外的dart文件到手机上

Android Studio中运行除了main.dart以外的dart文件(必须包含main函数),Terminal执行命令:

flutter run lib/animated_list.dart

热加载直接在Terminal键入:r 即可

pubspec.yaml中批量添加图片资源

一开始我都是按照规矩这样添加的:

flutter:
  uses-material-design: true
  # To add assets to your application, add an assets section, like this:
  assets:
     - images/ic_test.png
     - images/ic_timg.jpg
     - images/timg4.jpg
     - images/avatar.png

后来看了官网的demo,原来这样也可以:

flutter:
  uses-material-design: true
  assets: [images/]

这样如果我有很多图片就不用一个一个的去添加依赖了,省了很多力气,哈哈

Flutter 代码/性能优化

目前主要学到的几点:

  • 尽量使用无状态的类组件来代替函数式的组件
  • 尽量保持build方法的纯净,减少跟UI无关的逻辑处理
  • 尽量使用const final等修饰符来避免重复创建新的组件

主要理解两个问题

第一个问题:为啥要使用类组件来代替函数组件?

Class Widget VS Function Widget 的口水之战:

  • Why should I ever use stateless widgets instead of functional widgets?
    (为什么我应该使用无状态组件而不是函数组件?) ——来自Flutter的Github源码的一个issue,国外各路神仙们就此展开了激烈辩论。。
  • What is the difference between functions and classes to create reusable widgets?(使用类和函数构建可复用组件的不同点是什么?) ——来自stackoverflow的issue

两个链接都是出自同一大神Rousselet的回答,他首先指出的是这两者之间的一个显著的不同点,就是:Flutter的framework对函数是无感知的,但是对类是有感知的

也就是说通过类创建的组件最终会作为一个独立的element被挂载在element树上面去,而函数构建方式仅仅是纯粹的插入到了调用的地方,函数内的组件只是作为调用者的组件的一部分而已。使用函数构建组件会比较容易产生bug,这并不是说你使用函数构建组件就一定会有bug, 只不过如果你使用类构建组件的话,则一定能够保证不会面临用函数构建组件时可能会产生的bug。

既然使用函数实际也是可以的,也不一定就会产生bug, 而且使用函数会比使用类少写很多代码,那么为啥还一定要推荐类组件呢,有啥更特别的地方吗?

——因为使用类组件你会发现有如下显著优势:

  • 允许性能优化(使用const构造函数,做到更精细的重建)
  • 确保在两种不同布局之间的切换时能正确地释放资源(函数可能会重用一些先前的状态)
  • 能确保热重载正常工作(函数则可能会中断热重载)
  • 类组件会被显示到devTool的widget inspector检查器当中,便于调试
  • 类组件可以拥有key, 而函数则不行
  • 类组件可以使用context API, 而函数则不行
  • 类组件可以通过重写运算符==, 减少重建次数, 而函数则不行

Github上的那个issue评论了两年多终于被关闭了。。不得不佩服外国人的钻研精神。。但是直到这个issue被关闭的时候,仍然有很多人还是不是很理解,为啥要优先用类,其实官方的说法也只是强烈推荐,但并不是必须的强制规则。也有很多人提出了如下代码:

class Toggler extends StatelessWidget {
     
  final VoidCallback onToggle;
  const Toggler({
     Key key, this.onToggle}) : super(key: key);

  @override
  Widget build(BuildContext context) {
     
    return Row(
      children: <Widget>[
        FlatButton(onPressed: onToggle, child: Text('On')),
        FlatButton(onPressed: onToggle, child: Text('Off')),
      ],
    );
  }
}

上面方法中有两个FlatButton是一样的使用方式,那么你可能会考虑这样去重构:

  //...
  Widget _toggleButton(String text) =>
        FlatButton(onPressed: onToggle,  child: Text(text));

  @override
  Widget build(BuildContext context) {
     
    return Row(
      children: <Widget>[
        _toggleButton('On'),
        _toggleButton('Off'),
      ],
    );
  }

按照我的理解,其实这样做是完全可以的,因为这两种方式最终是等价的,包括我们经常会写的构建列表组件会使用map方法:

  final List<Fruit> fruits;
//...
  @override
  Widget build(BuildContext context) {
     
    return Column(
      children: _buildChildren(),
    );
  }

  List<Widget> _buildChildren() {
     
    return fruits.map((f) => _FruitInfo(fruit: f)).toList();
  }

这样应该也是完全OK的,也就是说使用箭头函数和inline组件的方式完全没有任何区别。更多的可能还是要考虑实际业务场景,如下面的代码,flutter就很难理解,某些情况下可能会造成不可预期的结果,这时应该去函数化来得到更加安全的保障。

bool condition;

Widget _foo();
Widget _bar();

Widget build(BuildContext context) {
     
  return condition
    ? _foo()
    : _bar();
}

为了更好理解再看两组代码

以下两组代码等价:

Widget functionA() => Container()

@override
Widget build() {
     
  return functionA()  
}
@override
Widget build() {
     
  return Container();  
}

以下两组代码等价:

class ClassA extends StatelessWidget {
     
  @override
  Widget build(BuildContext context) {
     
    return Container();  
  }
}

@override
Widget build() {
     
  return ClassA()  
}
@override
Widget build() {
     
  return KeyedSubtree(
    key: ObjectKey(ClassA),
    child: Container(),
  );
}

可以看出两种方式本质上的不同,类的优势明显优于函数。当然如果你还是比较习惯写函数式的组件,Rousselet大神写了一个库functional_widget可以让你以写函数组件的方式来写类组件:

@swidget
Widget foo(BuildContext context, int value) {
     
  return Text('$value');
}

比如,当你写上面的代码时,使用他这个库会生成下面的代码:

class Foo extends StatelessWidget {
     
  const Foo(this.value, {
     Key key}) : super(key: key);

  final int value;

  @override
  Widget build(BuildContext context) {
     
    return foo(context, value);
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
     
    super.debugFillProperties(properties);
    properties.add(IntProperty('value', value));
  }
}

第二个问题:如何避免没必要的重复构建?

How to deal with unwanted widget build? (如何处理多余的组件构建?)

这个链接同样也是Rousselet的回答,他的意思,简而言之就是build方法是Flutter中刷新界面时会高频调用的方法,所以尽量保持它的纯净。这对提高刷新性能尤为重要,我们需要把跟UI构建不直接相关的处理逻辑全部移出build方法。例如他提到的这部分优化代码就是将Future对象的构造从build中移了出来:

class Example extends StatefulWidget {
     
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
     
  Future<int> future;

  @override
  void initState() {
     
    future = Future.value(42);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
     
    return FutureBuilder(
      future: future,
      builder: (context, snapshot) {
     
        // create some layout here
      },
    );
  }
}

Flutter中实例相同的组件构建时不会被重新构建,利用这一点我们可以想办法缓存部分组件,如:

@override
Widget build(BuildContext context) {
     
  return const DecoratedBox(
    decoration: BoxDecoration(),
    child: Text("Hello World"),
  );
}

其中const修饰的组件不会每次都被重新创建,使用final关键字也可以做到:

@override
Widget build(BuildContext context) {
     
  final subtree = MyWidget(
    child: Text("Hello World")
  );

  return StreamBuilder<String>(
    stream: stream,
    initialData: "Foo",
    builder: (context, snapshot) {
     
      return Column(
        children: <Widget>[
          Text(snapshot.data),
          subtree,
        ],
      );
    },
  );
}

这里final修饰的组件也不会每次都被重新创建,这种模式在动画中被大量使用如AnimatedBuilder。另外,也提到其他解决方法如尽可能拆分到更小的独立Statefull组件中,或者使用Provider库来解决。

build方法在下面的场景下会被调用:

  • 调用 initState 方法之后
  • 调用 didUpdateWidget 方法之后
  • 调用 setState()方法时
  • 键盘被打开时
  • 屏幕方向发生旋转时
  • 父组件被构建子组件也会重新构建

自定义组件的一个重要点就是在didUpdateWidget中根据新旧组件的状态值比较决定是否要重新构建UI:

   @override
  void didUpdateWidget(MyRichText oldWidget) {
     
   if (widget.text != oldWidget.text) {
     
     _textSpan = parseText(widget.text);
   }
   super.didUpdateWidget(oldWidget);
  }

又如:

  @override
  void didUpdateWidget(CounterWidget oldWidget) {
     
    //通过新旧widget的一些属性来判断是否变化
    // 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
    if (Widget.canUpdate(widget.child, oldWidget.child)) {
     
      // child没变化,...
    } else {
     
      super.didUpdateWidget(oldWidget);
    }
  }

Flutter 解决嵌套地狱的几种方案

开头介绍过Flutter的不好的地方,第一个就是嵌套层级过多的问题,

第一种方法,将复杂的widget抽取变量,或者抽取类组件

详细的戳这篇文章:Flutter避免代码嵌套,写好build方法

这篇文章的主要思想是不断的复用抽取的控件变量来减少嵌套层级,包括利用控件变量结合if条件渲染, 以及将组件抽取到类当中(Stateful或Stateless)。注意这里提到的是抽取变量,而不是抽取函数,详细前面优化部分已经介绍过了。

其实简单来说,这种方法做的事情就是两个字:重构!不借助任何外力或者依赖多余的库,重新设计和组织你的代码,以得到更加清晰的代码架构和逻辑,并且能够减少bug。我觉得这个已经不算是什么新鲜的姿势了,而是作为一只优秀程序猿的基本素养。

总的来说这种方法是比较原生态的,我个人也比较推荐这种。

第二种方法,通过Dart 2.7的新属性扩展函数来解决

主要看这篇文章:https://blog.csdn.net/c6E5UlI1N/article/details/104057737

虽然这种方式也能解决嵌套问题,但是最终使用起来你会发现它的思想都是反着去添加的,跟日常使用习惯部分,有点反人类。。所以我个人不推荐这种方式。

第三种方法,利用建造者模式来解决

这个可以看这里:flutter解决布局嵌套问题

设计模式是用来优化代码和重构的终极法宝,很多时候你想不到的答案可能先人已经总结了方法。按照build模式我们可以将常用的布局组件都封装一下使用,不限于文中介绍的。只是提供了一种思路。

第四种方法,将组件属性封装为方法来解决

这种其实是由上面第三种建造者模式得到的启发,但是这里我不用建造者模式,因为建造者模式到最后一步build才会生成对象实例,而我想先生成对象实例,然后再调用实例的方法来为其添加属性。最终想要的效果如下:

import 'package:flutter/material.dart';
import 'column_row_wrap.dart';

class UnNestLayoutTestPage extends StatelessWidget {
     
  @override
  Widget build(BuildContext context) {
     
    return Scaffold(
      appBar: AppBar(title: Text("去除嵌套示例"),),
      body: ColumnWidget()
            .addChild(Text("呵呵"))
            .addChild(Text("怎样"))
            .addChildByPadding(Text("可以"), EdgeInsets.all(10.0))
            .addChild(CenterWidget(Text("测试")))
            .addChild(RowWidget()
                      .addChild(Text("你好"))
                      .addChildByMargin(Text("很好"), EdgeInsets.all(20.0))
                      .addChild(Text("大家好")))
            .addChild(ContainerWidget(Text("中国"))
                      .addPadding(EdgeInsets.all(10.0))
                      .addDecoration(BoxDecoration(color: Colors.red)))
            .addChild(RaisedButtonWrap("提交")
                      .textColor(Colors.blue)
                      .onClick(() => {
      print("点击了提交")}))
    );
  }
}

可以看到,在build方法中我用了20行不到的方法添加了很多控件,并且可以轻松的设置颜色、padding、margin、点击事件等,如果要用Flutter原生的写法实现上面的代码远远不止20行,因为每一行要扩展出来好几行,代码量要暴增好几倍。

很显然,这样的代码,肯定是更加清晰且易于维护的。那怎么实现的呢,没有很神秘的东西,直接上代码:

import 'package:flutter/material.dart';

class ColumnWidget extends StatelessWidget {
     

  List<Widget> _children = <Widget>[];

  ColumnWidget addChild(Widget widget) {
     
    _children.add(widget);
    return this;
  }

  ColumnWidget addChildByPadding(Widget widget, EdgeInsetsGeometry padding) {
     
    _children.add(Padding(
      child: widget,
      padding: padding,
    ));
    return this;
  }

  @override
  Widget build(BuildContext context) {
     
    return Column(
      children: _children,
    );
  }

}

class RowWidget extends StatelessWidget {
     

  List<Widget> _children = <Widget>[];

  RowWidget addChild(Widget widget) {
     
    _children.add(widget);
    return this;
  }

  RowWidget addChildByMargin(Widget widget, EdgeInsetsGeometry margin) {
     
    _children.add(Container(
      child: widget,
      margin: margin,
    ));
    return this;
  }

  @override
  Widget build(BuildContext context) {
     
    return Row(
      children: _children,
    );
  }

}

class ContainerWidget extends StatelessWidget {
     

  final Widget children;

  ContainerWidget(this.children);

  EdgeInsetsGeometry _padding;
  EdgeInsetsGeometry _margin;
  Decoration _decoration;

  ContainerWidget addPadding(EdgeInsetsGeometry padding) {
     
    _padding = padding;
    return this;
  }

  ContainerWidget addMargin(EdgeInsetsGeometry margin) {
     
    _margin = margin;
    return this;
  }

  ContainerWidget addDecoration(Decoration decoration) {
     
    _decoration = decoration;
    return this;
  }

  @override
  Widget build(BuildContext context) {
     
    return Container(
      child: children,
      padding: _padding,
      margin: _margin,
      decoration: _decoration,
    );
  }

}

class RaisedButtonWrap extends StatelessWidget {
     
  String title;

  RaisedButtonWrap(this.title);

  Color _textColor;
  Color _bgColor;
  VoidCallback _clickListener;

  RaisedButtonWrap onClick(VoidCallback clickListener) {
     
    _clickListener = clickListener;
    return this;
  }

  RaisedButtonWrap textColor(Color textColor) {
     
    _textColor = textColor;
    return this;
  }

  RaisedButtonWrap bgColor(Color bgColor) {
     
    _bgColor = bgColor;
    return this;
  }

  @override
  Widget build(BuildContext context) {
     
    return RaisedButton(
      child: Text(title),
      textColor: _textColor,
      color: _bgColor,
      onPressed: _clickListener,
    );
  }
}

class CenterWidget extends StatelessWidget {
     

  final Widget child;

  CenterWidget(this.child);

  @override
  Widget build(BuildContext context) {
     
    return Center(
      child: child,
    );
  }
}

这里我只是最简单的封装实现,实际你可以做的更精细将方法封装的更全面一些。当然Flutter当中有300多个组件,我们并不需要将所有的组件都封装一遍,你可以只将常用或者高频使用的组件进行封装,不常用的可以暂时不用去管直接按原来的方式使用即可。

当然并不一定是全部只用这种写法写的,也可以混合在嵌套的布局中使用:

class UnNestLayoutTestPage2 extends StatelessWidget {
     
  @override
  Widget build(BuildContext context) {
     
    return Scaffold(
      appBar: AppBar(title: Text("去除嵌套示例"),),
      body: ColumnWidget()
            .addChild(Text("测试"))
            .addChild(Container(
                child: RowWidget()
                       .addChild(Text("你好"))
                       .addChildByMargin(Text("很好"), EdgeInsets.all(20.0))
                       .addChild(Text("大家好")),
            ))
            .addChild(Container(
                child:Center(
                  child: RaisedButtonWrap("提交")
                          .textColor(Colors.blue)
                          .onClick(() => {
      print("点击了提交")}),
                ),
            ))
    );
  }
}

但是很明显,一旦我们开始了嵌套,代码行数就开始多了起来,看起来就没有那么清爽了。

之所以会有这种感觉,本质上是因为Flutter当中所有的组件属性都是通过构造函数的命名参数传递的,并且没有提供任何的属性操作方法,这就使得你不得不去层层嵌套的使用,这跟android中的java类组件完全不一样,android中的所有类都是提供方法的。很难想象如果我定义了一个类和N多个属性,但是不提供操作方法来使用,而是只提供构造函数来传递这些属性参数。

Flutter 调试相关

普通的断点调试跟纯android的没有太大区别,比如当我点击按钮执行某个方法时可以断点分步执行等待。

Dart DevTools

这里只要记录一下Dart DevTools这个工具:
https://flutter.cn/docs/development/tools/devtools/inspector

在AS中点击debug运行或者运行Flutter Attach:

在这里插入图片描述
运行起来之后在控制台的会有一个dart的蓝色图标按钮,点击这个按钮就会在浏览器中打开Dart调试面板:

Flutter学习笔记&学习资料推荐_第13张图片
Flutter学习笔记&学习资料推荐_第14张图片
当我们选中Flex类型的布局(如Column和Row)时, 我们可以点击右侧的Layout Explorer查看布局的详细信息,不过这个目前支持flex类型的容器才行:
Flutter学习笔记&学习资料推荐_第15张图片
这里可以改变主轴和交叉轴的对齐方向,或者可以点击设置子组件的flex属性,可以在设备上实时预览改变的结果。这对于理解布局和解决一些UI溢出问题比较有帮助。
Flutter学习笔记&学习资料推荐_第16张图片
默认打开左侧是列出的首页界面的代码,如果想查看指定的组件,需要点击Select Widget Mode模式,然后在手机上点击想要查看的控件,浏览器就会刷新到对应的界面。

Flutter学习笔记&学习资料推荐_第17张图片
但是这个工具感觉目前还不成熟,点几下经常会报错或者失去反应,有时没有指定 textDirection: TextDirection.ltr 也可能导致Layout Explorer无法查看。

另外这个工具可以用来分析性能时间、内存、网络等,但是需要以--profile模式运行,这里可以先了解一下Flutter的四种运行模式:Debug、Release、Profile和test

我们现在terminal中运行 flutter run --profile,运行成功最终会看到如下界面 :
Flutter学习笔记&学习资料推荐_第18张图片
点击上面红框中的地址打开浏览器:

Flutter学习笔记&学习资料推荐_第19张图片
复制上面红框中的地址然后打开Tools-->Flutter-->Open Dart DevTools,并粘贴到打开页面的输入框当中(以http开头):
Flutter学习笔记&学习资料推荐_第20张图片
点击Connect就可以进入到性能相关的页面:
Flutter学习笔记&学习资料推荐_第21张图片
Flutter学习笔记&学习资料推荐_第22张图片
Flutter学习笔记&学习资料推荐_第23张图片
偶尔会失去反应,需要刷新一下页面。。

Dart Pad在线编辑器

这里有一个在线的Dart编辑器网站:https://dartpad.cn

可以在上面进行dart语言练习,甚至还可以在线运行flutter界面:

Flutter学习笔记&学习资料推荐_第24张图片
Flutter学习笔记&学习资料推荐_第25张图片
好像还可以在线分享你写的代码,可以自行点击它的右上角了解。

JsonToDart工具

推荐一个JsonToDart工具:https://juejin.im/post/6844904138032037895

在《Flutter实战》中有介绍json序列化相关的工具,但是这个工具跟教程中介绍的不一样,它是一个桌面版的独立工具应用,不是IDE中的一个插件库,使用方式类似Android Studio中的GsonFormat插件类似,不过他是生成的dart语言的实体类代码而已。相比flutter的json_to_model库有一定的方便之处,但并不是所有情况,比如不同的json嵌套子对象是一个已有的相同json对象时,这个工具并不能做到复用已有的对象实体类,这时还是用json_to_model的方式比较方便一些。

Flutter 在PC端和Web端的应用

Flutter目前主打手机端,对PC和Web端的支持比较弱,相关的资料也比较少,这里推荐两篇博文,需要的可自行了解:

Flutter 开发桌面应用——迁移已有应用到桌面版

Flutter Web网站搭建教程

其他内容:
flutter跨平台开发之App升级方案
Android项目引入Flutter组件

你可能感兴趣的:(Flutter,flutter,dart,android)