Flutter 入门简介

Flutter 入门简介

1 简介

Flutter 是由谷歌开发推出的一款开源的移动应用开发框架,其主打的就是跨平台、高保真、高性能。其开发呢主要使用的dart语言。一套代码可以同时在Android和iOS 系统上运行。并且Flutter 还可以和原生进行混合开发。

2 主要优势

2.1跨平台自绘渲染引擎

Flutter 采用自绘渲染引擎Skia,那么和其他的跨平台框架不同的是,其他框架要不采用webView进行渲染,要不采用操作原生控件进行渲染, 并且避免了像RN,weex那样不断和native进行布局同步,导致的布局卡顿,手势卡顿等问题。那么Flutter利用自己的跨平台2d绘制渲染引擎,不仅可以保证UI的一致性,而且能降低维护的成本。(Skia是安卓系统一直使用的一个2d绘制渲染引擎)。

2.2 高性能

Flutter 支持了JIT(Just-In-Time 即时编译(动态编译)) 和 AOT (Ahead-of-time 静态编译)。

JIT 也就是程序在运行过程中实时将源码编翻译成机器码,也叫做动态编译,这种方式被称为JIT,典型的代表则为JavaScript。值得一提的是其实绝大多数的脚本语言都支持JIT。

AOT 需要提前将程序编译成机器码或者机器字节码,也叫做静态编译。比如我们OC、Swift、安卓的java、C、C++,都需要编译、链接之后才能运行程序。

那么Flutter既支持了JIT有支持了AOT,也就是说我们在开发的时候可以使用JIT进行快速开发,不需要保证应用的性能,可以做到随时改动、随时看到。在发布包的时候采用AOT,保证程序运行的新能。可以说鱼与熊掌兼得。

2.3 快速内存分配

Flutter 使用函数式流,而dart的开发成员很多是来自于Chrome的开发,鉴于Chrome V8的javaScript的内存分配,dart底层也拥有一个能够有效处理琐碎任务的内存分配器。

课外小知识–函数式流

函数式流通常指的是使用函数式编程范式来处理数据流和状态变化,函数式编程是一种编程范式,它强调使用纯函数(无副作用的函数)和不可变数据结构来编写程序。这种范式有助于提高代码的可读性、可维护性和可测试性。

函数式编程范式中的不可变数据结构和纯函数有助于减少内存分配。由于数据结构是不可变的,我们不需要为修改数据结构而分配新的内存。相反,我们可以通过共享不可变数据结构的部分来创建新的数据结构,从而减少内存分配。

2.4 类型安全和空安全

dart 语言是类型安全语言, 2.12版本支持了空安全。支持静态类型检测,所以可以再编译前发现一些类型的错误,排除一些错误。

课外小知识–空安全

空安全是一种编程语言的特性,指的是在帮助开发者避免空引用的错误,通常指试图访问一个空(null)对象的属性或者方法时,可能导致程序出现崩溃或者不可预期的错误。

2.5 dart 团队就在身边

因为dart团队和flutter团队同处于一个公司,那么对于Flutter想要dart支持的一些特性、功能dart团队也能快速沟通、快速实现。举例来说就是:最初dart团队并没有提供生成原生二进制的工具链,但是后边它实现了,因为dart团队专门为flutter开发了这一工具链。

3 Flutter框架结构

3.1 framework 层

framework层是完全有dart语言实现的sdk,它实现了一套基础库。从底向上分别是为

3.1.1 Foundation,Animation、Painting、Gestures

这一层又被称之为UI层。是Engine暴漏的底层UI层,主要提供了动画、绘制、手势。

3.3.2 Rendering

这一层是渲染层,渲染层会构建一颗由RenderObject组成的渲染树。当动态更新这些对象的时候,渲染树找出变化的部分,然后更新渲染。它是Flutter比较重要的部分,不仅要确定渲染对象的位置、大小还要进行坐标变换和绘制,当然绘制调用dart:UI层进行。

3.3.3 Widgets

Widgets是Flutter提供给我们一套基础组件库,提供了Material、Cupertino 两套分别符合安卓和iOS设计规范的组件。

值得一说的是,其实我们打交道最多的就是framework层,其中widgets是开发应用最多的,但是其中如果你有自定义(或者定制)的widgets,得需要Rendering层或者Painting。

3.2 Flutter Engine

这一层是Flutter的引擎层,其中包含了Skia渲染引擎、Dart运行时、以及文字排版引擎等。其使用C++实现。上一层的dart:UI最终会调用到这一层。

3.3 Embedder

嵌入层。嵌入层呢主要将我们的Flutter引擎安装到对应平台上。其实现是采用当前平台的原生语言实现。通过嵌入层,可以将flutter代码以模块方式嵌入到当前应用中,或者作为主体。

4 集成Flutter开发环境

4.1 开发IDE

推荐使用Android studio或者VS Code开发,官方提供了他们开发Flutter的插件,支持更多功能。

4.2 安装Flutter

具体查看Flutter中文网 或者Flutter 官网。照着做即可,不在赘述。

4.3 链接模拟器/真机

具体查看Flutter中文网 或者Flutter 官网。照着做即可,不在赘述。

5 Widgets

Flutter Widget 不同于我们原生开发所说的控件,Flutter 几乎所有的对象都叫做Widget,它不仅可以表示UI控件,也可以表示功能性组件,比如手势检测器GestureDetector。Widget只是UI信息配置信息的对象,并不是真正绘制到屏幕上的元素。

5.1 Flutter 中的四棵树

基本流程为:

  1. 根据Widget树生成Element树,每个Element节点类都继承于Element类;
  2. 根据Element树生成Render树(渲染树),渲染树中每个节点类都继承于RenderObject;
  3. 根据Render树生成Layer树,然后显示到屏幕上,Layer树中的每个节点类都继承于Layer类。

真正的渲染和布局是在Render树中,Element树是Widget和Render的桥梁。

举个栗子:

Container( // 一个容器 widget
  color: Colors.blue, // 设置容器背景色
  child: Row( // 可以将子widget沿水平方向排列
    children: [
      Image.network('https://www.example.com/1.png'), // 显示图片的 widget
      const Text('A'),
    ],
  ),
);

其中Widget和Element是一一对应的,但是并不合Render一一对应。其实我们通常讲的Flutter只有上面三棵树,但是其实在上屏之前呢,还会根据Render树生成一颗Layer树。

5.2 StatelessWidget 和StatefulWidget 以及State

StatelessWidget 和 StatefulWidget都继承于Widget抽象类。

其中StatelessWidget 是用于不需要维护状态的场景,通常我们会在其override 方法 build(BuildContext context)里组合嵌套其他的组件构建UI。

StatefulWidget 是用于需要维护状态的场景,与StatelessWidget不同的是,我们不再拥有 build(BuildContext context)方法。取而代之的是createState()方法,我们需要生成一个State类,用来存储状态。

import 'package:flutter/material.dart';

class TestStatelessWidget extends StatelessWidget {
  const TestStatelessWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(color: Colors.blue, child: const Center(child: Text("StatelessWidget")));
  }

}

class TestStatefulWidget extends StatefulWidget {
  const TestStatefulWidget({super.key, this.count = 0});

  final int count;

  @override
  TestWidgetState createState() => TestWidgetState();
}

class TestWidgetState extends State<TestStatefulWidget> {

  int _count = 0;

  // Widget 第一次插入Widget树调用,对于每一个State对象,只会调用一次
  @override
  void initState() {
    super.initState();
    _count = widget.count;
    debugPrint("initState");
  }

  // 用于构建Widget
  @override
  Widget build(BuildContext context) {
    debugPrint("build");
    return Scaffold(
      body: Center(
        child: TextButton(
          child: Text('$_count'),
          onPressed: () => {
            _count += 1,
          }
        )));
  }

  // Widget 重新构建时调用
  @override
  void didUpdateWidget(covariant TestStatefulWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    debugPrint("didUpdateWidget");
  }

  // State 对象从树中移除
  @override
  void deactivate() {
    // TODO: implement deactivate
    super.deactivate();
    debugPrint("deactivate");
  }

  // 热重载时
  @override
  void reassemble() {
    // TODO: implement reassemble
    super.reassemble();
    debugPrint("reassemble");
  }

  // 当state对象依赖发生变化时
  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    debugPrint("didChangeDependencies");
  }

  // 当state对象从树中永久移除时。
  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    debugPrint("didChangeDependencies");
  }
}
什么叫做状态?

在Flutter中,状态(State)是指应用程序中的数据,这些数据会随着时间的推移而发生变化。状态可以包括用户界面的外观、用户交互产生的数据、从服务器获取的数据等。状态的变化通常会导致应用程序的界面发生变化,例如按钮的颜色、文本的内容等。

在Flutter中,状态管理是一个重要的概念。由于Flutter使用声明式编程范式,组件(Widget)是不可变的,这意味着一旦创建,它们的属性就不能被修改。因此,当应用程序的状态发生变化时,我们需要创建一个新的组件实例来反映这些变化。

所以结合着以上所说的StatelessWidget和StatefullWidget、State想必大家就会有一个明确的认知State。

5.3 State状态管理(简述)

Flutter的状态管理分为以下几种:

  1. Widget 自身管理, 如上边的例子
  2. 父Widget管理子Widget状态;
  3. 混合管理,即组件自身管理一些内部状态,而父组件管理一些其他外部状态。
  4. 全局状态管理,用于跨组件/跨路由的状态管理

通常我们会采用专门用于状态的管理的三方包,进行简化管理,比如Bloc、Provider。(看情况出一个例子)

6 路由管理

所谓的路由管理,其实就是管理页面如何跳转,也称之为导航管理。那么对于Android和iOS客户端来说,都是维护一个路由栈,Push 入栈打开一个新页面, Pop出栈关闭页面。

6.1 通过Navigator 和 MaterialPageRoute 实现

// HomePageState 
@override
  Widget build(BuildContext context) {
    
    return Scaffold(
      ... // 省略代码
      body: Center(
        child: Column(
          ... // 省略代码
            TextButton(
                onPressed: _routePageA,
                child: const Text(
                  '打开pageA'
                )
            )
          ],
        ),
      ),
       ... // 省略代码
    );
  }

	void _routePageA() async {
    var result = await Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => const PageA()
        )
    );
    // 输出返回值
    print(result.toString());
  }

// PageA
@override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Center(
        child: Column(children: [
          ...
          CupertinoButton(
              onPressed: () => _back(context),
              child: const Text("返回"),
          )
        ])
      )
    );
  }

  void _back(BuildContext context) {
    Navigator.pop(context, "我是返回值");
  }

6.2 通过路由表名

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      routes: {
        "pageA": (context) {
          var arguments = ModalRoute.of(context)?.settings.arguments;
          return PageA(title: arguments is String ? arguments : "");
        },
        "PageB": (context) => const PageB(),
        "/": (context) => const MyHomePage(title: "首页")
      },
      initialRoute: "/", // 初始路由
    );
  }
}

// 使用
class _MyHomePageState extends State<MyHomePage> {
  void _routePageB() async {
    var result = await Navigator.pushNamed(context, "PageB");
    debugPrint("我是PageA反传值:$result");
  }
}

当然我们推荐使用路由表管理,因为1. 代码更好维护, 集中管理路由 2. 语义更加明确 3. 可以通过onGenerateRoute做路由跳转的一些前置处理。

7 包管理 & 资源管理

7.1 包管理

App 在实际开发中会依赖很多包,这些包通常可能会存在交叉依赖、版本依赖等。所以,这种开发生态都会出现一些包管理工具,比如Android使用Gradle来管理依赖,iOS 使用Cocoapods或者Carthage管理依赖。Flutter 也拥有自己包管理工具,它使用pubspec.yaml 文件管理。如:

name: flutter_in_action
description: First Flutter Application.

version: 1.0.0+1

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter
    
flutter:
  uses-material-design: true
  • name:应用或包名称。

  • description: 应用或包的描述、简介。

  • version:应用或包的版本号。

  • dependencies:应用或包依赖的其他包或插件。

  • dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。

  • flutter:flutter相关的配置选项。

    7.1.1 依赖三方pub

    官方Dart Package位于Pub(https://pub.dev/ ),我们可以在上面查找需要依赖的package以及版本号和package介绍。

    依赖方式如下:

    dependencies:
      flutter:
        sdk: flutter
      cupertino_icons: ^0.1.2
      dio: ^5.3.2
    

    然后执行: dart pub get 或者直接在Android studio里上方点击Pub get即可

    7.1.2 依赖本地包/私有包

    本地包:

    dependencies:
    	pkg1:
            path: ../../code/pkg1
    

    git 包(package 位于根目录):

    dependencies:
      pkg1:
        git:
          url: git://github.com/xxx/pkg1.git
    

    git 包(package 位于非目录):

    dependencies:
      pkg1:
        git:
          url: git://github.com/xxx/pkg1.git
          path: packages/package1
    

    7.1.3 Flutter 包的分类

    1. dart 包: 纯dart编写的包,指依赖flutter框架
    2. 插件包:包含Dart编写的API, 以及特定平台的特定实现,也就是说插件包需要实现原生功能交互。比如image_picker 实现了图片选取 + 照相功能。

7.2 资源管理

Flutter 资源管理也是在pubspec.yaml 文件里配置的。如下:

flutter:
  # To add assets to your application, add an assets section, like this:
   assets:
     - images/2x/a_dot_burr.png
     - images/3x/a_dot_burr.png
# 2x,3x代表分辨率

使用:

@override
  Widget build(BuildContext context) {
    // return Center(
    //   child: Image.asset("a_dot_burr.png")); 
    // 或者
    return const Center(
      child: Image(
        image: AssetImage("a_dot_burr.png"))
    );
  }

8 组件简介

Flutter 中的组件大概分为下面几类:

  • 基础组件
    • Text、TextStyle等
    • TextButton、ElevatedButton等
  • 布局类组件
    • Row、Colum 线性布局
    • Flex 弹性布局
    • Wrap、Flow 流式布局
    • Stack、Positioned 层叠布局
    • Align 对齐与相对定位
  • 容器类组件
    • Container 容器组件
    • DecoratedBox 装饰容器
    • Scaffold 页面脚手架
    • ….
  • 可滚动组件
    • SingleChildScrollView
    • ListView
    • GridView
    • PageView
    • ….
  • 功能性组件
    • WillPopScope 导航返回拦截
    • InheritedWidget 数据共享
  • 手势、事件处理和通知;动画;自定义组件,自行了解。

9 混合开发

一般情况下,我们不可能将现有的项目一下子都转化Flutter,只能循序渐进、逐渐的加入Flutter或者改写为Flutter,这个时候我们就需要考虑,以下几件事情:

  • 怎么继承Flutter开发的模块

    flutter 官网提供了 iOS 三种集成Flutter的方式:

    1. 使用 CocoaPods 依赖管理器安装 Flutter SDK 使用这种方法,每次构建应用的时候都会从源代码中编译 flutter_module。(推荐)
    2. 创建一个框架,把 Flutter 引擎、已编译的 Dart 代码和所有 Flutter 插件都放进去 这种方式你可以手动嵌入这个框架,并在 Xcode 中更改现有的应用的构建设置。如果不想要求开发团队的每一位成员都在本地安装 Flutter SDK 和 Cocoapods,这种方式比较适用
    3. 为已编译的 Dart 代码和所有 Flutter 插件创建一个框架,对 Flutter 引擎使用 CocoaPods 来管理 这种方式是将应用内容和插件作为内嵌的框架,但将 Flutter 引擎作为 CocoaPods podspec 分发。这有点类似第二种方式,但是它为分发大型的 Flutter.xcframework 文件提供了替代方案。

    首先通过以下命令创建一个flutter_module

    cd some/path/
    flutter create --template module my_flutter
    

    使用1.方式集成:

    1. flutter 模块执行flutter build ios –debug / –release

    2. Podfile 里加入一下代码

    flutter_application_path = '../my_flutter'
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
    target 'MyApp' do
      install_all_flutter_pods(flutter_application_path)
    end
    post_install do |installer|
      flutter_post_install(installer) if defined?(flutter_post_install)
    end
    
    1. pod install 执行完毕后,打开.xcworkspace即可

    使用2. 方法集成

    在flutter module 下执行 flutter build ios-framework --output=some/path/MyApp/Flutter/ 生成静态库

    嵌入到原生app中当做framework处理

    使用3. 方法集成

    使用以下命令生成Flutter.podspec

    flutter build ios-framework --output=some/path/MyApp/Flutter/
    

    采用Cocoapods集成:

    pod 'Flutter', :podspec => 'some/path/MyApp/Flutter/[build mode]/Flutter.podspec'
    

    安卓集成大概是两种:1. 生成aar 2. 依赖源码的模块,详细请移步到这里

    iOS 详细内容请移步到这里

  • 如何进行Flutter和原生的互通

    • 初始化需要初始化FlutterEngine以及注册Engine(安卓同学代码配置不一样,原理其实一致,详细移步到这里)

      class AppDelegate: FlutterAppDelegate {
          
          lazy var flutterEngine = FlutterEngine(name: "My Flutter Engine")
              
          private var channel: FlutterMethodChannel!
              
          override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
                  
              flutterEngine.run()
                  
              GeneratedPluginRegistrant.register(with: self.flutterEngine)
              return super.application(application, didFinishLaunchingWithOptions: launchOptions)
        }
      
    • Flutter 在原生的承载页iOS 为FlutterViewController, 安卓为FlutterActivity(安卓同学代码配置不一样,原理其实一致,详细移步到这里)

      比如我们原生直接采用以下方式打开Flutter

      @objc func testButtonClicked() {
              let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
              let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
              present(flutterViewController, animated: true)
          }
      
    • 双方互通采用Channel通道进行

      • Channel 分为三种 FlutterBasicMessageChannel、FlutterMethodChannel、FlutterEventChannel
      • BasicMessageChannel用于双向通信,支持发送和接收任意类型的消息,通信模式是无状态的。
      • MethodChannel用于远程过程调用,允许Dart代码调用原生代码中的方法,通信模式是有状态的。
      • EventChannel用于事件流通信,允许原生代码向Dart代码发送事件,通信模式是基于流的。
      class AppDelegate: FlutterAppDelegate {
          
          lazy var flutterEngine = FlutterEngine(name: "My Flutter Engine")
              
          private var channel: FlutterMethodChannel!
              
          override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
                  
              flutterEngine.run()
                  
              GeneratedPluginRegistrant.register(with: self.flutterEngine)
                  
              channel = FlutterMethodChannel(name: "flutter_channel", binaryMessenger: self.flutterEngine.binaryMessenger)
              channel.setMethodCallHandler { call, result in
                  if call.method == "getBattery" {
                      result(self.getBattery())
                  } else {
                      result(nil)
                  }
              }
                  
              channel.invokeMethod("invokeFlutterMethod", arguments: "我是原生") { result in
                  print(result ?? "没调用到")
              }
                  
              return super.application(application, didFinishLaunchingWithOptions: launchOptions)
          }
              
          func getBattery() -> String {
              UIDevice.current.isBatteryMonitoringEnabled = true
              return "您的电量为: \(-UIDevice.current.batteryLevel * 100) %"
          }
      }
      
  • 多Flutter实例

    比如NativePage->flutterpage ->nativePage->flutterPage的情况。

    Flutter 2 以及以上的版本针对多 Flutter 实例进行了优化,额外增加的 Flutter 实例只会增加约 180K 的内存占用,这种「固定成本」的降低,可以帮助你更轻松的将 Flutter 加入到现有应用 (add-to-app)

    官方代码

    主要采用新的API FlutterEngineGroup 去创建和管理多个Engine,从 FlutterEngineGroup 生成的 FlutterEngine 具有常用共享资源(例如 GPU 上下文、字体度量和隔离线程的快照)的性能优势,从而加快首次渲染的速度、降低延迟并降低内存占用。

    据说新的方式可以降低内存,那么在2.0之前,我们一直采用的是三方混合路由管理方案如:flutter_boost(咸鱼), 采用单例Engine共用的方式,减小内存。

    例如:

    class AppDelegate: FlutterAppDelegate {
        let engines = FlutterEngineGroup(name: "multiple-flutters", project: nil)
        ...
    }
      
    // 一个页面一个Flutter实例
    class SingleFlutterViewController: FlutterViewController {
          
        private var channel: FlutterMethodChannel!
          
        init(withEntrypoint entryPoint: String?) {
            let appDelegate = UIApplication.shared.delegate as! AppDelegate
            let newEngine = appDelegate.engines.makeEngine(withEntrypoint: entryPoint, libraryURI: nil)
            GeneratedPluginRegistrant.register(with: newEngine)
            super.init(engine: newEngine, nibName: nil, bundle: nil)
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            channel = FlutterMethodChannel(name: "multiple-flutters", binaryMessenger: self.engine!.binaryMessenger)
            channel.invokeMethod("setCount", arguments: nil)
            channel.setMethodCallHandler { call, result in
                if call.method == "next" {
                    let vc = HostViewController()
                    self.navigationController!.pushViewController(vc, animated: true)
                    result(nil)
                } else {
                    result(nil)
                }
            } 
        }
        
      // 一个页面多个Flutter实例
      class DoubleFlutterViewController: UIViewController {
      
        private let topFlutter: SingleFlutterViewController = SingleFlutterViewController(
            withEntrypoint: "topMain")
          private let bottomFlutter: SingleFlutterViewController = SingleFlutterViewController(
            withEntrypoint: "bottomMain")
      
          override func viewDidLoad() {
            addChild(topFlutter)
            addChild(bottomFlutter)
            let safeFrame = self.view.safeAreaLayoutGuide.layoutFrame
            let halfHeight = safeFrame.height / 2.0
            topFlutter.view.frame = CGRect(
              x: safeFrame.minX, y: safeFrame.minY, width: safeFrame.width, height: halfHeight)
            bottomFlutter.view.frame = CGRect(
              x: safeFrame.minX, y: topFlutter.view.frame.maxY, width: safeFrame.width, height: halfHeight)
            self.view.addSubview(topFlutter.view)
            self.view.addSubview(bottomFlutter.view)
            topFlutter.didMove(toParent: self)
            bottomFlutter.didMove(toParent: self)
          }
    }
        
    // 路由栈中多个Flutter实例
      extension HostViewController {  
        @objc func nextButtonClicked() {
            let navController = self.navigationController!
            if navController.viewControllers.count % 4 == 0  {
                let vc = SingleFlutterViewController(withEntrypoint: nil)
                navController.pushViewController(vc, animated: true)
            } else {
                let vc = DoubleFlutterViewController()
                navController.pushViewController(vc, animated: true)
            }
        }
    }
    

    学习 & 参考文档

    flutter 中文网

    flutter 官网

    flutter github

    Flutter 官方package github

    flutter pub package