美文网首页Flutter飞起Flutter面试Flutter
[Flutter] Flutter 快速上手指南

[Flutter] Flutter 快速上手指南

作者: 沉江小鱼 | 来源:发表于2021-06-05 23:08 被阅读0次

    前言:学习 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大一统还是不太可能。

    相关文章

      网友评论

        本文标题:[Flutter] Flutter 快速上手指南

        本文链接:https://www.haomeiwen.com/subject/nkxceltx.html