[Flutter] Flutter 快速上手指南

前言:学习 Flutter 有一段时间了,本篇文章主要是记录下 Flutter 学习历程的一些心得和开发体验,罗列出一些重要的点,需要结合官方文档食用,希望能够帮助到大家,共同学习进步。

目录:
0.学习资料汇总
1.对 Flutter 的初步认识
2.环境搭建&项目结构
3.Dart语言
4.直接撸一个登录页面?or 先学一些基础的控件?
5.页面绘制 - 熟能生巧
6.项目中功能点实现(网络请求、数据共享、数据存储、路由跳转)
7.总结

0. 学习资料汇总

网站 介绍
Flutter 中文网 资料很全,对于不同平台的开发者了解入门 Flutter 很有帮助
Flutter 实战 系统化的 Flutter 开发教程,由浅入深
[Flutter SDK源码] 项目中 command + 单击就可以查看,注释很清楚,组件都带有使用示例
Dart 语言学习 Dart官方教程
咸鱼技术 咸鱼团队掘金地址,Flutter的进阶使用以及原理探索
[Gallery源码] Flutter 官方示例 APP
flutter-go Flutter开发者最强辅助 App(阿里开源),包含常用Widget 的介绍和使用示例(目前不再维护,但仍可作为参考)

1. 对 Flutter 的初步认识

提起Flutter,不得不说一下跨平台开发,先来了解一下跨平台开发的一些知识。

1.1 跨平台技术

根据其原理,大致分为三类:

  1. H5 + 原生混合开发(Cordova、 lonic、微信小程序)
  • 需要动态变更的内容通过H5 实现,WebView 加载,系统能力则需要原生支持;
  • web 技术栈,社区及资源丰富;
  • 复杂界面或动画有性能问题
  1. JavaScript 开发 + 原生渲染(React Native、Weex)
  • 使用JavaScriptCore将虚拟DOM布局信息传递给原生,原生根据布局信息通过对应的原生控件去渲染
  • web 开发技术栈,上手快,开发成本低,性能比 H5 提高很多,支持动态化;
  • JS 为脚本语言,执行时需要 JIT(Just In Time,运行时编译),执行效率和 AOT(Ahead Of Time,运行前编译)代码有差距,依赖原生控件,不同平台控件需要单独维护;
  1. 自绘 UI + 原生(Flutter、QT for mobile)
  • 在不同平台实现统一接口的渲染引擎绘制 UI,不依赖系统原生控件;
  • 性能高,灵活、容易维护,同一套代码,使用同一个渲染引擎;
  • 动态性不足,采用 AOT 模式编译发包
截屏2021-06-05 下午9.23.51.png

补充知识:AOT & JIT
程序主要有两种运行方式:静态编译动态解释

  • AOT(Ahead of time),即提前编译,在程序执行前就编译好了,比如原生开发打包都是先将代码提前编译好;
  • JIT(Just-in-time),即即时编译,代码一边翻译一边运行,代表语言 JavaScript、python 等脚本语言;

上面👆内容详细可见:Flutter实现:跨平台技术简介

1.2 Flutter 框架介绍

官方提供的Flutter框架的分层结构图如下:


image.png
1.2.1 Flutter Framework
  • Dart 实现的 SDK,实现了一套基础库
  • 最上层提供了 Material (安卓样式) 和 Cupertino (苹果样式) 两种风格的组件库;
  • WidgetsFlutter提供的一套基础组件库;
  • Rendering 抽象的布局层,Flutter UI框架的核心,依赖于最下面两层,会构建一个 UI 树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终绘制到屏幕,类似于React中的虚拟DOM
  • 最下两层(Foundation和Animation、Painting、Gestures)对应 dart:ui package,提供动画、手势、绘制能力;

一般来说,我们开发接触最多的也就是最上面两层了。

1.2.2 Flutter Engine
  • 纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑;
  • Skia作为渲染/GPU后端,在Android和Fuchsia上使用FreeType渲染,在iOS上使用CoreGraphics来渲染字体;

Flutter 的底层图像渲染引擎是 Skia,Skia 是 Android 官方的图像渲染引擎,支持跨平台,使用同一个渲染引擎保证了在不同平台上的渲染效果, 但是Flutter iOS SDK则需要嵌入 Skia,导致 Flutter iOS SDK 打包的包体积要比 Android 大一些。

1.2.3 Embedder
  • 嵌入层,把Flutter嵌入到各个平台上去,主要工作包括渲染Surface设置,线程设置,以及插件等;

从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。

1.3 为什么选择 Flutter?

因为 Boss 直聘上看到很多岗位要求中,Flutter 都作为了加分项,另外,作为一名称职码农,多一门手艺也能多混一碗饭吃。😺

但是实际上我是被 Flutter 的优点所吸引的,主要是以下两点:

  1. 跨平台
    保持两个端(Web 端的开发还没有体验过,暂且不提)的视觉效果一致,另一方面方便资本主义更好的剥削我们,之前需要 Android&iOS两个端的开发成本,现在减半了;

  2. 开发效率高
    Debug 模式基于 JIT,热重载(Hot Reload)多🐂🍺,不用重新编译就能看到改动后的效果;
    Release 模式基于AOT,发布时通过 AOT 生成高效的本地代码,保证应用性能;

  3. 响应式框架
    相比于 Vue 这个框架来说,略有不及,但比原生好的太多了。

这些概念在Flutter官网以及一些初识Flutter 博客上都有介绍(毕竟要吹嘘一番),对这些概念有了解之后,才能更好的向身边其他人推广Flutter。

2. 环境搭建&项目结构

2.1 环境搭建

Flutter 开发最好使用 Mac book,因为需要支持 iOS 开发。

环境搭建基本流程:

  • 下载 Flutter 官方 SDK,要使用稳定版本的哦;
  • 配置环境变量;
  • 下载 Xcode (主要是支持 iOS 开发),安装 CocoaPods,以及开发使用的 IDE:Android Studio 或者 VSCode;
  • IDE 中下载相应的 Flutter 插件;

具体操作可见Flutter实战:安装 Flutter

2.2 项目结构认识

以 VS Code 为例,如下图:


截屏2021-06-05 下午10.31.09.png
  • Image 文件夹是后来创建的,存放图片资源
  • iOS为 iOS 部分代码,使用 CocoaPods 管理依赖;
  • android 为 Android 部分代码,使用 Gradle 管理依赖;
  • lib 为 dart 代码,我们之后写的代码都存放在lib 下
  • build 文件夹存放的是开发中编译的产物
  • pubspec.yaml 存放项目中的依赖,包括第三方的依赖和图片、字体等资源依赖
2.2.1 项目依赖库管理

可以在 pub.dev 上面搜索到支持使用的依赖包,然后在 pubspec.yaml 文件中进行配置,如下所示:
pubspec.yaml 文件如下:

image.png

当有添加或者删除文件配置时,需要执行(flutter pub get),在 VS Code中 Command +S 保存时,默认去执行了,如果使用 Android Studio 的话,右上角会有pub get的按钮,点击即可。

2.2.2 图片资源配置

在根目录创建一个文件夹,将所需要的图片资源放到该文件夹下,然后将图片路径写到 pubspec.yaml 文件中,在 Image 控件中填写路径即可。具体可以查看:Flutter 中文网:在 Flutter 中添加资源和图片

这里有一个注意的点,可以不用把每个图片的详细路径写到pubspec.yaml 文件中,直接写文件夹就可以,如下图:

image.png

2.2.3 程序的启动入口:lib/main.dart

Flutter中 main()函数也是应用程序的起点,在"lib/main.dart"文件中,实现很简单:

void main() {
  runApp(MyApp());
}

MyApp()可以理解为一个控件或者说一个页面视图,runApp 方法就是将这个控件显示到屏幕上,这个控件就是视图树的根节点。

应用的入口介绍详细可以看:Flutter实战:计数器应用示例

3. Dart 语言

大概了解了项目接口之后,我们先别急着去写一些布局,在此之前我们需要先学习 Dart 语言,这非常重要。

Dart 语言和Java & JavaScript 在某些方面很相似,集百家之长吧,变量的声明&类型&使用,函数的使用,类的使用都很简单,需要重点学习的有:

  • 异步操作(await、async & Future),对应 js 中的(await、async & Promise)
  • mixin 混入,在类的声明处使用 with 关键字,和 js 中的一致
  • 函数的可选参数(在控件的构造方法中经常使用)
  • Dart中的空安全(Dart 2.12 之后引入)主要是标明变量可能为 null,相当于 Swift 的可选值,同样有? 声明, ! 强制解包操作
  • 类的命名构造函数

Dart 语言的学习,推荐Dart中文网中学习,很全,推荐先过一遍,太过复杂的可以先略过,用到的时候再去学习。

4. 直接撸一个登录页面?or 先学一些基础的控件?

相信你肯定忍不住去先学习了一些基础的控件,这是对的,希望你能够继续认真了解一些基础控件,这样能够帮助你顺利的撸一个登录页面出来。

在 Flutter 开发中,万物皆 Widget,比如加个视图(页面)、点击事件、加个背景色、改个位置等,都得需要相应的Widget,实例如下:

// 可点击的文本
Widget testWidget() {
    return GestureDetector( // 给非 button 组件增加点击事件
      onTap: () {
        print("点击了按钮");
      },
      child: Padding( // 设置 padding
        padding: EdgeInsets.all(10),
        child: Text("文本显示"),
      ),
    );
  }

如果没有前端开发经验的小伙伴们最好先在网上了解一下Margin(组件之间的距离)和Padding(组件内部内容的边距)的作用,网上搜了这篇文章:CSS 彻底理解margin与padding,理解Margin 和 Padding的意思即可。有意思的是 Flutter 提供了 Padding 组件,并没有提供 Margin 组件,我都是使用 Container(相当于多个组件的结合体)。

控件(Widget) 是一个抽象类,我们可以简单把它理解为 视图(View)来使用,在实际开发中我们自定义控件并不直接继承 Widget ,一般继承StatelessWidget(无状态组件)或者StatefulWidget(有状态组件),这里的状态可以理解为组件内的数据,它们也是抽象类,继承 Widget。

StatelessWidget(无状态组件) 和 StatefulWidget(有状态组件)最大的区别在于:

  • StatelessWidget只是根据数据提供一个 View,内部不维护动态可变的数据
  • StatefulWidget 是有状态(数据)的,多了一个相关的 state 类用于维护状态,内部的状态(数据)改变时,可以调用 setState 方法,重新 调用build方法

这里的build 方法是上面两个组件的共同点,我们自定义控件的时候,也是重写build 方法来构建控件,示例代码:

# StatelessWidget:
class Echo extends StatelessWidget {
  const Echo({
    Key key,  
    @required this.text,
    this.backgroundColor:Colors.grey,
  }):super(key:key);
    
  final String text;
  final Color backgroundColor;

  @override
// 重写 build 方法构建控件
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

# StatefulWidget
class CounterWidget extends StatefulWidget {
  const CounterWidget({
    Key key,
    this.initValue: 0
  });

  final int initValue;

  @override
  // createState 方法中,返回了对应的 State 类的实例
  _CounterWidgetState createState() => new _CounterWidgetState();
}

// 和 StatefulWidget 对应的 State 类
class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  @override
  void initState() {
    super.initState();
    //初始化状态  
    _counter=widget.initValue;
    print("initState");
  }

  @override
  // 重写 build 方法,StatefulWidget的 build 方法在 State 类中
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          //点击后计数器自增,相当于状态改变了,这里调用 setState方法,之后会调用 build 方法,根据当前的_counter去显示
          onPressed:()=>setState(()=> ++_counter,
          ),
        ),
      ),
    );
  }
}

也就是说,在开发中,如果我们想单纯显示数据的话就继承 StatelessWidget 去自定义控件,如果控件中的数据可能会变化,进而引起视图的变化,就需要继承StatefulWidget 了。
StatefulWidget 相比于 StatelessWidget 更复杂,因为它多了状态管理这个功能,我们需要着重了解 State 的生命周期了,如下图:

image.png

理解StatefulWidget 的生命周期很重要,开发中经常需要用到,上面的知识来自于Flutter实战:Widget简介

下面列出一些常用的基础组件布局组件,大概先了解一下就行,明白什么样的布局使用什么样的控件就行,开发时可以直接点进去查看文档以及附带的使用示例:

基础组件名字 介绍
Image 图片显示,支持本地图片资源显示和网络图片显示
Text 文字显示,相当于 Label,其中 style 可以设置各种样式
Text.rich + TestSpan 富文本显示,比如登录页面一般都有是否同意<xxx 用户协议>
RaisedButton 、FlatButton、OutlineButton等等 按钮,提供点击事件
Icon 图标显示,内置了一些图标,估计实际开发中也不太能用得上
TextField 文本输入,设置多行输入就成了 TextView 了哦
Wrap 流水式布局
ListView 滚动视图
布局组件名字 介绍
Flex(Row、Column) 相当于前端的 Flex 布局,Row&Column分别是在横向和纵向对于 Flex 布局的封装
Expanded 在 Flex 的 children 中使用,内部flex属性,决定对于剩余空间的占有比例
Stack + Positioned 可以实现绝对布局
Align (Center) 控制自身在父控件中的位置
Padding 控制自身内边距
Container 使用的比较多,可以设置边框、背景色等
Scaffold 页面布局脚手架,AppBar(导航栏) + BottomNavigationBar(底部选项卡) + Drawer(抽屉) + FloatButton(右下角浮动按钮)
功能类组件 介绍
Inkwell 给控件增加点击事件,有涟漪效果
GestureDetector 给控件增加点击事件,没有涟漪效果,手势很多
SafeArea 页面安全区域适配

上面👆的组件使用在Flutter 实战
中都可以找到详细的介绍,当然也可以在 B站上可以搜索一下 Flutter教学视频,上面有很多哦。

5. 页面绘制 - 熟能生巧

5.1 登录页面的绘制

终于到了这一步,开始上手实操了,先截个登录页面的效果图吧:


image.png

UI 布局大概如下:

  • 页面整体是一个 Column 组件
  • 顶部使用了 Stack + Positioned 控制 x 号和 暂不登录的位置(也可以用 Row 来做)
  • 接着是按顺序平铺 Image、Text、TextField和一个登录按钮(GestureDetector + Container实现)
  • 最下方使用了 Expanded + Align 控制TextSpan 实现的富文本显示在最下层

布局方式并不是唯一的,只要能够实现就可以。

另外,实现一个完整的登录页面需要注意以下几点:

  • 注意安全区域(顶部不规则状态栏和底部圆下巴)的设置,需要整体包裹在SafeArea 控件中
  • 键盘弹起会将底部用户协议那块顶起,需要设置ScaffoldresizeToAvoidBottomInset,或者页面使用ListView 实现
  • 点击空白处需要隐藏键盘,可以给Scaffoldbody 包裹GestureDetector控件,增加点击手势
  • 监听两个 TextField 的文本输入,都不为空时登录按钮显示为可点击状态
  • 获取验证码,需要使用 Future 进行倒计时,可以尝试一下哦
5.2 首页底部选项卡功能

先看一下整体的页面效果,先忽略首页的页面布局:


image.png

上面提到过的 Scaffold 就可以实现这种布局,实现方式如下:

  • 自定义一个 Widget 继承 StatefulWidget
  • body 使用 PageView 来管理需要切换的页面
  • 底部使用 BottomNavigationBar 创建底部选项卡
  • State 类中增加 selectIndex,通过点击底部选项卡更改 selectIndex,控制PageView 显示指定的子页面

实现代码如下:

class TabbarPage extends StatefulWidget {
  @override
  _TabbarPageState createState() => _TabbarPageState();
}

class _TabbarPageState extends State<TabbarPage> {
  // 四个子页面
  var _pageList = [HomePage(), FoundPage(), StudyPage(), MyPage()];
  // 记录当前选择的Index
  var _selectInex = 0;
  // 控制pageView滑动
  PageController _pageController = PageController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 这里不需要 AppBar,子页面中带有就行
      // PageView 控制页面切换
      body: PageView(
        children: _pageList,
        controller: _pageController,
        physics: NeverScrollableScrollPhysics(), // 禁止滑动
      ),
      // 使用 BottomNavigationBar 类即可,这里我是自定义的,为了去除点击涟漪效果
      bottomNavigationBar: BottomAppBar(
          child: Container(
        height: 50,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            tabbarItem(0, "首页", "Image/tabbar/icon_home_normal.png",
                "Image/tabbar/icon_home_selected.png"),
            tabbarItem(1, "发现", "Image/tabbar/icon_found_normal.png",
                "Image/tabbar/icon_found_selected.png"),
            tabbarItem(2, "学习", "Image/tabbar/icon_study_normal.png",
                "Image/tabbar/icon_study_selected.png"),
            tabbarItem(3, "我的", "Image/tabbar/icon_my_normal.png",
                "Image/tabbar/icon_my_selected.png")
          ],
        ),
      )),
    );
  }

  // 底部item
  Widget tabbarItem(
      int index, String title, String normalImgName, String selectImgName) {
    var imgName = index == _selectInex ? selectImgName : normalImgName;
    var titleColor = index == _selectInex
        ? ColorUtil.hexColor(0x007AFF)
        : ColorUtil.hexColor(0x666666);
    return GestureDetector(
      onTap: () {
        // 底部 item 点击时的事件
        if (_selectInex == index) {
          return;
        }
        // 调用 setState 方法,重新 build
        setState(() {
          // 控制 PageView 显示第一个子页面
          _pageController.jumpToPage(index);
          _selectInex = index;
        });
      },
      child: Container(
        padding: EdgeInsets.symmetric(vertical: 5, horizontal: 20),
        child: Column(
          children: [
            Image.asset(
              imgName,
              width: 20,
              height: 20,
            ),
            Text(
              title,
              style: TextStyle(color: titleColor),
            )
          ],
        ),
      ),
    );
  }
}

这种布局需要注意的是子页面状态的保存,可以在每个子页面的 initState 方法中输出一下,每一次切换都会重新调用 initState 方法,说明页面重新创建了,这时需要子页面的 State 类混入(mixin)AutomaticKeepAliveClientMixin,有三步:

  • State 使用 with 关键字混入AutomaticKeepAliveClientMixin
  • 重写get wantKeepAlive方法
  • build方法中调用 super.build(context)

代码如下:


image.png
5.3 首页布局挑战 & 用户信息页面实现

可以找一个稍微复杂点的App 首页进行模仿,先不要急着去封装控件,直接写下来就行,这一步主要练习的是基础控件的使用和基础的布局方式。


image.png

基本上写完这一个首页之后,对于基础控件&布局就可以熟练使用了。

然后,实现一个我的页面,显示登录用户的信息,示例如下:


image.png

要求:

  • 顶部红线框,根据 bool 值(之后就是用户的登录状态)可以判断显示用户登录&非登录样式
  • 底部红线框根据 bool 值判断“退出登录”按钮是否显示

登录页面 + 主页选项卡 + 我的信息页面为下面的功能实现提供了基础,下面会讲到网络请求、数据共享、数据存储、路由跳转等功能实现。

6. 功能实现

6.1 网络请求管理

Dart IO库中提供了用于发起Http请求的一些类,可以直接使用HttpClient来发起请求。使用HttpClient发起请求的示例:

try {
    //创建一个HttpClient
    HttpClient httpClient = new HttpClient();
    //打开Http连接
    HttpClientRequest request = await httpClient.getUrl(
    Uri.parse("https://www.baidu.com"));
    //使用iPhone的UA
    request.headers.add("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1");
    //等待连接服务器(会将请求信息发送给服务器)
    HttpClientResponse response = await request.close();
    //读取响应内容
    _text = await response.transform(utf8.decoder).join();
    //输出响应头
    print(response.headers);
    //关闭client后,通过该client发起的所有请求都会中止。
    httpClient.close();

} catch (e) {
    _text = "请求失败:$e";
 }

使用HttpClient发起网络请求比较麻烦,目前使用最多第三方网络请求库就是dio, 支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等,使用很简单,文档非常详细,简单的get 请求示例:

Response response;
var dio = Dio();
response = await dio.get('/test?id=12&name=wendu');
print(response.data.toString());
// Optionally the request above could also be done as
response = await dio.get('/test', queryParameters: {'id': 12, 'name': 'wendu'});
print(response.data.toString());

使用时只需要按需对其进行简单封装即可,可以参考这篇文章:强大的 dio 封装,可以满足你的一切需要

6.2 数据转Model

这个真的是比较坑的一个地方,Flutter中并没有像Java开发中的Gson/Jackson一样的Json序列化类库。因为这样的库需要使用运行时反射,这在Flutter中是禁用的。因为运行时反射会干扰Dart的_tree shaking_(核心思想:一个程序所有可能的执行流程都可以用函数调用的树来表示,这样就可以消除那些从未被调用的函数。),使用_tree shaking_,可以在release版中“去除”未使用的代码,优化应用程序的大小。由于反射会默认应用到所有代码,因此_tree shaking_会很难工作,在启用反射时很难知道哪些代码未被使用,因此冗余代码很难剥离,所以Flutter中禁用了Dart的反射功能,而正因如此也就无法实现动态转化Model的功能。

所以一般定义 model 的时候,最终都是使用命名构造函数,参数为 Map<String, dynamic>类型:

class Student {
  late String name;
  late int age;
  late String sex;

  Student(this.name, this.age, this.sex);

  // 使用命名构造函数
  Student.fromJson(Map<String, dynamic> map) {
    this.name = map["name"];
    this.age = map["age"];
    this.sex = map["sex"];
  }
}

针对于有大量成员变量的类,下面几种方式可以稍微提高下效率:

  • 使用官方推荐的json_serializable;
  • json_to_dart 该网站可以复制 json 数据,直接生成指定类名的代码,稍作修改即可;
  • Android Studio 可以使用FlutterJsonBeanFactory插件,VS Code 可以使用Json to DartModel 插件(还没用过,可以自己试试)
6.3 数据存储

数据存储方式一般由:写入本地文件、数据库、使用三方库shared_preferences

使用最多的也就是shared_preferences了,比如保存用户登录信息、用户配置信息等。它保存数据的形式为 Key-Value,支持 Android 和 iOS。在 Android 中使用 SharedPreferences,在 iOS中使用 NSUserDefaults

使用方式很简单,如下:

// 保存数据
_saveData() async {
  var prefs = await SharedPreferences.getInstance();
  prefs.setInt('Key_Int', 12);
}

// 读取数据
Future<int> _readData() async {
  var prefs = await SharedPreferences.getInstance();
  var result = prefs.getInt('Key_Int');
  return result ?? 0;
}

但是需要注意的是获取 SharedPreferences单例对象 是一个异步操作,一般在 app 启动时会先获取到 SharedPreferences 的单例对象,保存起来,再继续往下操作。

6.4 路由管理
6.4.1 普通路由使用

系统提供了 Navigator 类来进行路由管理,简单的使用如下:

// 跳转一个新页面
Navigator.push( context,
           MaterialPageRoute(builder: (context) {
              return NewRoute();
           }));
          
6.4.2 路由传值操作

Navigator 的push 操作会返回一个Future 对象,用于接收二级页面的返回值:

# 一级页面中
// 打开`TipRoute`,并等待返回结果
var result = await Navigator.push(
   context,
   MaterialPageRoute(
    builder: (context) {
     return TipRoute(
      // 路由参数
      text: "我是提示xxxx",
     );
    },
  ),
);
//输出`TipRoute`路由返回结果
print("路由返回值: $result");

# 二级页面 pop 时回传值
Navigator.pop(context, "我是返回值"),
6.4.3 命名路由

开发中一般使用命名路由,就是建立一个路由表,路由跳转时根据对应的名字进行跳转:

MaterialApp(
  title: 'Flutter Demo',
  initialRoute:"/", //名为"/"的路由作为应用的home(首页)
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  //注册路由表
  routes:{
   "new_page":(context) => NewRoute(),
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
  },
  # home: MyHomePage(title: 'Flutter Demo Home Page'), // initialRoute 和 home 不能同时存在,因为都是指定首页
);
6.4.4 路由钩子

MaterialApp有一个onGenerateRoute属性,当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由,可以不使用routes属性,直接使用onGenerateRoute属性,这样实现控制页面权限的功能就非常容易:

  // 自定义路由
  MaterialPageRoute routeWithSetting(RouteSettings setting) {
    print("********************************");
    print("routeName:${setting.name}");
    print("routeArguments:${setting.arguments}");
    print("********************************");

    Object? arguments = setting.arguments;
    // 判断如果需要登录,返回登录页面路由
    if (arguments != null &&
        (arguments as Map)["needLogin"] == true &&
        !UserUtil().isLogin()) {
      return loginRoute();
    }

    // _routeMap 同样为维护的一个路由表
    WidgetBuilder? builder = _routeMap[setting.name];
    if (builder != null) {
      return MaterialPageRoute(builder: builder, settings: setting);
    }
    // 如果未找到,则返回指定的 404 页面
    return unknowRouteWithSetting(setting);
  }

# main.dart 中
MaterialApp(
...
  debugShowCheckedModeBanner: false,
  initialRoute: "/",
  onGenerateRoute: (settings) {
    // 自定义去处理
    return RouteManager().routeWithSetting(settings);
  },
...
}));
6.5 全局状态管理

Flutter 也是一个响应式的框架,就避免不了状态管理这个概念了,同一个页面多个子组件的状态可以由这个页面(父组件)去管理,但是跨页面的状态管理,就需要使用一个全局状态管理器了。

Provider 是官方推出的一个状态管理框架,也有一些其他开源的状态管理框架:Redux、BloC 等等。

Provider 是基于InheritedWidget的特性实现的,所以需要先了解一下 InheritedWidget:

  • InheritedWidget提供了一种数据在widget树中从上到下传递、共享的方式,比如我们在应用的根widget中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget中来获取该共享的数据。

  • 如果子 Widget使用了这个共享数据,则代表子widget依赖有依赖InheritedWidget,当共享数据发生变化时,子 widget 的didChangeDependencies 将被调用。这种机制可以使子组件在所依赖的InheritedWidget变化时来更新自身!比如主题更改功能的实现。

这里推荐:inherited_widget介绍provider实现两篇文章,能深入理解 provider 的实现。

简单的使用示例,以用户信息为例:

  • 先创建一个类混入ChangeNotifier
class UserInfoState with ChangeNotifier {
  Userinfo? userinfo;
  bool isLogin = false;

  UserInfoState() {
    _configUserInfo();
  }

  // 更新用户信息
  updateUserInfo() {
    _configUserInfo();
  }

  // 获取用户数据,并通知观察者
  _configUserInfo() {
    userinfo = UserUtil().userinfo;
    isLogin = userinfo != null;
    notifyListeners();
  }
}
  • 在合适的位置(这里放到了最顶层)创建
void main() {
  // 设置默认的请求环境,如果有多个,使用MultiProvider
  runApp(MultiProvider(providers: [
    ChangeNotifierProvider(
      create: (context) => UserInfoState(),
    ),
    // ChangeNotifierProvider(create: (context) => CommonState())
  ], child: MyApp()));
}
  • 在需要监听的位置使用Consumer(推荐),这样可以控制build的范围,提高性能
Consumer<UserInfoState>(builder: (context, data, child) {
    return Scaffold(
        appBar: AppBar(
        title: Text(
           "我的学习",
           style: TextStyle(fontSize: 18),
        ),
        elevation: 0,
        bottom: data.isLogin ? _topTabbar() : null,
    ),
    body: data.isLogin ? _bodyWidget() : _placeHolderWidget());
});

在需要改变状态的位置使用 Provider.of<UserInfoState>(context,listen: false).updateUserInfo() 这种形式更改状态,listen代表不会依赖(监听)这个状态,只是获取。

7. 总结

到了这一步之后,可以自己尝试撸一个完整的项目,同时去看一些Flutter SDK 的源码和进阶知识点了。

去年学习了Uni 开发框架( Vue.js 开发跨平台应用的前端框架),今年又重新学了一下 Flutter 框架,两者间布局方式异步处理方式(Future&Promise、await、async)都非常相似。
另外最重要的一点是:学习路线很像。了解框架 -> 学习框架使用的语言 -> 简单布局学习 -> 项目开发最常用的功能使用(网络请求、路由、状态)-> 进阶学习。

同时不得不感叹,前端 or 移动端的框架层出不穷,学好基础知识很有必要,这样可以让你在学习一个新的框架的时候能够快速上手。

目前来看,Flutter还是比较火的,因为其相对于原生开发而言:节省效率、开发成本低,相对于H5、React Native来说,性能更高。
但是,Flutter也是有局限性的:

    1. 目前不支持动态更新
    1. 只是UI框架,一些需求依赖原生去实现
      针对于不同的业务需求,肯定还是要采取合适的技术方案去实现。

感觉不支持动态更新是个致命的问题,开发成本、开发效率可以日夜加班来补足,性能最高 <= 原生,所以之后可能还是几种跨平台方案共存,Flutter大一统还是不太可能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容