Flutter

2021-11-08 10:35:13 浏览数 (1)

一、Flutter 和 React Native 本质区别

  • React Native框架,只是通过Javascript虚拟机扩展调用系统组件,由Android 和 iOS系统进行组件的渲染
  • Flutter则是自己完成了组件渲染的闭环

二、 Flutter的渲染机制

1. Flutter渲染机制之三棵树

在Flutter中和Widgets一起协同工作的还有另外两个伙伴:Elements和RenderObjects;由于它们都是有着树形结构,所以经常会称它们为三棵树。

  • Widget:Widget是Flutter的核心部分,是用户界面的不可变描述。
  • Element:Element是实例化的 Widget 对象,通过 Widget 的 createElement() 方法,是在特定位置使用 Widget配置数据生成;
  • RenderObject:用于应用界面的布局和绘制,保存了元素的大小,布局等信息;

2. 初次运行时的三棵树

初步认识了三棵树之后,那Flutter是如何创建布局的?以及三棵树之间他们是如何协同的呢?接下来就让我们通过一个简单的例子来剖析下它们内在的协同关系:

代码语言:javascript复制
class ThreeTree extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
      child: Container(color: Colors.blue)
    );
  }
}

上面这个例子很简单,它由三个Widget组成:ThreeTree、Container、Text。那么当Flutter的runApp()方法被调用时会发生什么呢?

当runApp()被调用时,第一时间会在后台发生以下事件:

Flutter会构建包含这三个Widget的Widgets树; Flutter遍历Widget树,然后根据其中的Widget调用createElement()来创建相应的Element对象,最后将这些对象组建成Element树; 接下来会创建第三个树,这个树中包含了与Widget对应的Element通过createRenderObject()创建的RenderObject; Flutter创建了三个不同的树,一个对应着Widget,一个对应着Element,一个对应着RenderObject。每一个Element中都有着相对应的Widget和RenderObject的引用。可以说Element是存在于可变Widget树和不可变RenderObject树之间的桥梁。Element擅长比较两个Object,在Flutter里面就是Widget和RenderObject。它的作用是配置好Widget在树中的位置,并且保持对于相对应的RenderObject和Widget的引用。

3. 三棵树的作用

简而言之是为了性能,为了复用Element从而减少频繁创建和销毁RenderObject。因为实例化一个RenderObject的成本是很高的,频繁的实例化和销毁RenderObject对性能的影响比较大,所以当Widget树改变的时候,Flutter使用Element树来比较新的Widget树和原来的Widget树:

代码语言:javascript复制
//framework.dart
 @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    Element newChild;
    if (child != null) {
      assert(() {
        final int oldElementClass = Element._debugConcreteSubtype(child);
        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
        hasSameSuperclass = oldElementClass == newWidgetClass;
        return true;
      }());
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) 
      {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        newChild = child;
      } else {
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      newChild = inflateWidget(newWidget, newSlot);
    }

    assert(() {
      if (child != null)
        _debugRemoveGlobalKeyReservation(child);
      final Key key = newWidget?.key;
      if (key is GlobalKey) {
        key._debugReserveFor(this, newChild);
      }
      return true;
    }());

    return newChild;
  }
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

如果某一个位置的Widget和新Widget不一致,才需要重新创建Element; 如果某一个位置的Widget和新Widget一致时(两个widget相等或runtimeType与key相等),则只需要修改RenderObject的配置,不用进行耗费性能的RenderObject的实例化工作了; 因为Widget是非常轻量级的,实例化耗费的性能很少,所以它是描述APP的状态(也就是configuration)的最好工具; 重量级的RenderObject(创建十分耗费性能)则需要尽可能少的创建,并尽可能的复用; 看到这里你是否会觉得整个Flutter APP就像是一个RecycleView呢?

因为在框架中,Element是被抽离开来的,所以你不需要经常和它们打交道。每个Widget的build(BuildContext context)方法中传递的context就是实现了BuildContext接口的Element。

4. 更新时的三棵树

因为Widget是不可变的,当某个Widget的配置改变的时候,整个Widget树都需要被重建。例如当我们改变一个Container的颜色为橙色的时候,框架就会触发一个重建整个Widget树的动作。因为有了Element的存在,Flutter会比较新的Widget树中的第一个Widget和之前的Widget。接下来比较Widget树中第二个Widget和之前Widget,以此类推,直到Widget树比较完成。

代码语言:javascript复制
class ThreeTree extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.orange,
      child: Container(color: Colors.blue,),
    );
  }
}

Flutter遵循一个最基本的原则:判断新的Widget和老的Widget是否是同一个类型:

如果不是同一个类型,那就把Widget、Element、RenderObject分别从它们的树(包括它们的子树)上移除,然后创建新的对象; 如果是一个类型,那就仅仅修改RenderObject中的配置,然后继续向下遍历; 在我们的例子中,ThreeTree Widget是和原来一样的类型,它的配置也是和原来的ThreeTreeRender一样的,所以什么都不会发生。下一个节点在Widget树中是Container Widget,它的类型和原来是一样的,但是它的颜色变化了,所以RenderObject的配置也会发生对应的变化,然后它会重新渲染,其他的对象都保持不变。

注意这三个树,配置发生改变之后,Element和RenderObject实例没有发生变化。

上面这个过程是非常快的,因为Widget的不变性和轻量级使得他能快速的创建,这个过程中那些重量级的RenderObject则是保持不变的,直到与其相对应类型的Widget从Widget树中被移除。

5. 当Widget的类型发生改变时的三棵树

代码语言:javascript复制
class ThreeTree extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.orange,
      child: FlatButton(
        onPressed: () {},
        child: Text('三棵树'),
      ),
    );
  }
}

和刚才流程一样,Flutter会从新Widget树的顶端向下遍历,与原有树中的Widget类型进行对比。

因为FlatButton的类型与Element树中相对应位置的Element的类型不同,Flutter将会从各自的树上删除这个Element和相对应的ContainerRender,然后Flutter将会重建与FlatButton相对应的Element和RenderObject。

当新的RenderObject树被重建后将会计算布局,然后绘制在屏幕上面。Flutter内部使用了很多优化方法和缓存策略来处理,所以你不需要手动来处理这些。

三、 Flutter 实现原理

架构采用分层设计,从下到上分为三层,依次为:Embedder、Engine、Framework

  • Embedder操作系统适配层,实现了渲染Surface设置,现场设置,以及平台插件等平台相关特性的适配。
  • Engine层主要包含Skia、Dart和Text, 实现了Flutter的渲染引擎、文字排版、事件处理 和Dart运行时等功能。
  • Framework层则是一个用Dart实现的UI SDK,包含了动画、图形绘制和手势识别等功能。

页面中的各界面元素(Widget)以树的形式组织,即控件树。Flutter 通过控件树中的每个控件创建不同类型的渲染对象,组成渲染对象树。而渲染对象树在 Flutter 的展示过程分为四个阶段:布局、绘制、合成和渲染。

布局

Flutter 采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,完成布局过程。

为了防止因子节点发生变化而导致整个控件树重新布局,Flutter 加入了一个机制——布局边界(Relayout Boundary),可以在某些节点自动或手动地设置布局边界,当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然。

绘制

布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter 会把所有的渲染对象绘制到不同的图层上。与布局过程一样,绘制过程也是深度优先遍历,而且总是先绘制自身,再绘制子节点。

Flutter 提出了与布局边界对应的机制——重绘边界(Repaint Boundary)。在重绘边界内,Flutter 会强制切换新的图层,这样就可以避免边界内外的互相影响,避免无关内容置于同一图层引起不必要的重绘。

合成和渲染

终端设备的页面越来越复杂,因此 Flutter 的渲染树层级通常很多,直接交付给渲染引擎进行多图层渲染,可能会出现大量渲染内容的重复绘制,所以还需要先进行一次图层合成,即将所有的图层根据大小、层级、透明度等规则计算出最终的显示效果,将相同的图层归类合并,简化渲染树,提高渲染效率。合并完成后,Flutter 会将几何图层数据交由 Skia 引擎加工成二维图像数据,最终交由 GPU 进行渲染,完成界面的展示。

四、Dart 的基础

未初始化的变量的值都是 null,所有类型都是对象类型,都继承自顶层类型 Object

Dart 内置了一些基本类型,如 num、bool、String、List 和 Map

Dart 的数值类型 num

代码语言:javascript复制
int x = 1;
int hex = 0xEEADBEEF;
double y = 1.1;
double exponents = 1.13e5;
int roundY = y.round();

检查变量是否为 0,在 Dart 中需要显示地与 0 做比较:

代码语言:javascript复制
// 检查是否为0.
var number = 0;
assert(number == 0);
// assert(number); 错误

Dart 的String类型

代码语言:javascript复制
var s = 'cat';
var s1 = 'this is a uppercased string: ${s.toUpperCase()}';

// 对于多行字符串的构建,你可以通过三个单引号或三个双引号的方式声明
var s3 = """This is a
multi-line string.""";

List 与 Map

代码语言:javascript复制
var arr1 = ["Tom", "Andy", "Jack"];
var arr2 = List.of([1,2,3]);
arr2.add(499);
arr2.forEach((v) => print('${v}'));
  
var map1 = {"name": "Tom", 'sex': 'male'}; 
var map2 = new Map();
map2['name'] = 'Tom';
map2['sex'] = 'male';
map2.forEach((k,v) => print('${k}: ${v}')); 

常量定义

  • const 表示变量在编译期间即能确定的值
  • final 则不太一样,用它定义的变量可以在运行时确定值,而一旦确定后就不可再变
代码语言:javascript复制
final name = 'Andy';
const count = 3;

var x = 70;  
var y = 30;
final z = x / y;

函数

代码语言:javascript复制
//要达到可选命名参数的用法,那就在定义函数的时候给参数加上 {}
void enable1Flags({bool bold, bool hidden}) => print("$bold , $hidden");

//定义可选命名参数时增加默认值
void enable2Flags({bool bold = true, bool hidden = false}) => print("$bold ,$hidden");

//可忽略的参数在函数定义时用[]符号指定
void enable3Flags(bool bold, [bool hidden]) => print("$bold ,$hidden");

//定义可忽略参数时增加默认值
void enable4Flags(bool bold, [bool hidden = false]) => print("$bold ,$hidden");

//可选命名参数函数调用
enable1Flags(bold: true, hidden: false); //true, false
enable1Flags(bold: true); //true, null
enable2Flags(bold: false); //false, false

//可忽略参数函数调用
enable3Flags(true, false); //true, false
enable3Flags(true,); //true, null
enable4Flags(true); //true, false
enable4Flags(true,true); // true, true

代码语言:javascript复制
class Point {
  num x, y;
  static num factor = 0;
  //语法糖,等同于在函数体内:this.x = x;this.y = y;
  Point(this.x,this.y);
  void printInfo() => print('($x, $y)');
  static void printZValue() => print('$factor');
}

var p = new Point(100,200); // new 关键字可以省略
p.printInfo();  // 输出(100, 200);
Point.factor = 10;
Point.printZValue(); // 输出10

要使用混入,只需要 with 关键字即可

代码语言:javascript复制
class Coordinate with Point {
}

var yyy = Coordinate();
print (yyy is Point); //true
print(yyy is Coordinate); //true

Dart 多了几个额外的运算符,用于简化处理变量实例缺失(即 null)的情况

  • ?. 运算符:假设 Point 类有 printInfo() 方法,p 是 Point 的一个可能为 null 的实例。那么,p 调用成员方法的安全代码,可以简化为 p?.printInfo() ,表示 p 为 null 的时候跳过,避免抛出异常。
  • ??= 运算符:如果 a 为 null,则给 a 赋值 value,否则跳过。这种用默认值兜底的赋值语句在 Dart 中我们可以用 a ??= value 表示。
  • ?? 运算符:如果 a 不为 null,返回 a 的值,否则返回 b。在 Java 或者 C 中,我们需要通过三元表达式 (a != null)? a : b 来实现这种情况。而在 Dart 中,这类代码可以简化为 a ?? b。

五、Flutter的基础

StatelessWidget

Widget 采用由父到子、自顶向下的方式进行构建,父 Widget 控制着子 Widget 的显示样式,其样式配置由父 Widget 在构建时提供。用这种方式构建出的 Widget,在创建时,除了这些配置参数之外不依赖于任何其他信息,换句话说,它们一旦创建成功就不再关心、也不响应任何数据变化进行重绘。这样的 Widget 被称为 StatelessWidget(无状态组件)。

StatefulWidget

一些 Widget(比如 Image、Checkbox)的展示,除了父 Widget 初始化时传入的静态配置之外,还需要处理用户的交互(比如,用户点击按钮)或其内部数据的变化(比如,网络数据回包),并体现在 UI 上。换句话说,这些 Widget 创建完成后,还需要关心和响应数据变化来进行重绘。这一类 Widget 被称为 StatefulWidget(有状态组件)。

State 生命周期

创建

创建State 初始化时会依次执行 :

构造方法 -> initState -> didChangeDependencies -> build,随后完成页面渲染。

  • 构造方法是 State 生命周期的起点,通过构造方法,来接收父 Widget 传递的初始化 UI 配置数据
  • initState,会在 State 对象被插入视图树的时候调用。这个函数在 State 的生命周期中只会被调用一次,所以我们可以在这里做一些初始化工作,比如为状态变量设定默认值
  • didChangeDependencies 则用来专门处理 State 对象依赖关系变化,会在 initState() 调用结束后,被 Flutter 调用。
  • build,作用是构建视图。经过以上步骤,Framework 认为 State 已经准备好了,于是调用 build。我们需要在这个函数中,根据父 Widget 传递过来的初始化配置数据,以及 State 的当前状态,创建一个 Widget 然后返回。

更新

Widget 的状态更新,主要由 3 个方法触发:

setState、didchangeDependencies 与 didUpdateWidget。

  • setState:我们最熟悉的方法之一。当状态数据发生变化时,我们总是通过调用这个方法告诉 Flutter:“我这儿的数据变啦,请使用更新后的数据重建 UI!”
  • didUpdateWidget:当 Widget 的配置发生变化时,比如,父 Widget 触发重建(即父 Widget 的状态发生变化时),热重载时,系统会调用这个函数。

销毁

系统会调用 deactivate 和 dispose 这两个方法,来移除或销毁组件。

  • 当组件的可见状态发生变化时,deactivate 函数会被调用,这时 State 会被暂时从视图树中移除。值得注意的是,页面切换时,由于 State 对象在视图树中的位置发生了变化,需要先暂时移除后再重新添加,重新触发组件构建,因此这个函数也会被调用。
  • 当 State 被永久地从视图树中移除时,Flutter 会调用 dispose 函数。而一旦到这个阶段,组件就要被销毁了,所以我们可以在这里进行最终的资源释放、移除监听、清理环境,等等。

生命周期回调

didChangeAppLifecycleState 回调函数中,有一个参数类型为 AppLifecycleState 的枚举类,这个枚举类是 Flutter 对 App 生命周期状态的封装。它的常用状态包括 resumed、inactive、paused 这三个。

  • resumed:可见的,并能响应用户的输入。
  • inactive:处在不活动状态,无法处理用户响应。
  • paused:不可见并不能响应用户的输入,但是在后台继续活动中。
代码语言:javascript复制
class _MyHomePageState extends State<MyHomePage>  with WidgetsBindingObserver{//这里你可以再回顾下,第7篇文章“函数、类与运算符:Dart是如何处理信息的?”中关于Mixin的内容
...
  @override
  @mustCallSuper
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);//注册监听器
  }
  
  @override
  @mustCallSuper
  void dispose(){
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);//移除监听器
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    print("$state");
    if (state == AppLifecycleState.resumed) {
      //do sth
    }
  }
}

帧绘制回调

WidgetsBinding 提供了单次 Frame 绘制回调,以及实时 Frame 绘制回调两种机制,来分别满足不同的需求:

  • 单次 Frame 绘制回调,通过 addPostFrameCallback 实现。它会在当前 Frame 绘制完成后进行进行回调,并且只会回调一次,如果要再次监听则需要再设置一次。
代码语言:javascript复制

WidgetsBinding.instance.addPostFrameCallback((_){
   print("单次Frame绘制回调");//只回调一次
});
  • 实时 Frame 绘制回调,则通过 addPersistentFrameCallback 实现。这个函数会在每次绘制 Frame 结束后进行回调,可以用做 FPS 监测。
代码语言:javascript复制

WidgetsBinding.instance.addPersistentFrameCallback((_){
  print("实时Frame绘制回调");//每帧都回调
});

六、Flutter的经典控件

Image控件

Image 控件需要根据图片资源异步加载的情况,决定自身的显示效果,因此是一个 StatefulWidget。图片加载过程由 ImageProvider 触发,而 ImageProvider 表示异步获取图片数据的操作,可以从资源、文件和网络等不同的渠道获取图片。

首先,ImageProvider 根据 _ImageState 中传递的图片配置生成对应的图片缓存 key;然后,去 ImageCache 中查找是否有对应的图片缓存,如果有,则通知 _ImageState 刷新 UI;如果没有,则启动 ImageStream 开始异步加载,加载完毕后,更新缓存;最后,通知 _ImageState 刷新 UI。

注释:ImageCache 使用 LRU(Least Recently Used,最近最少使用)算法进行缓存更新策略,并且默认最多存储 1000 张图片,最大缓存限制为 100MB,当限定的空间已存满数据时,把最久没有被访问到的图片清除。图片缓存只会在运行期间生效,也就是只缓存在内存中。如果想要支持缓存到文件系统,可以使用第三方的CachedNetworkImage控件。

ListView控件

ListView 的构造函数 ListView.builder,则适用于子 Widget 比较多的场景。其中,itemExtent 并不是一个必填参数。但,对于定高的列表项元素,我强烈建议你提前设置好这个参数的值。

因为如果这个参数为 null,ListView 会动态地根据子 Widget 创建完成的结果,决定自身的视图高度,以及子 Widget 在 ListView 中的相对位置。在滚动发生变化而列表项又很多时,这样的计算就会非常频繁。

但如果提前设置好 itemExtent,ListView 则可以提前计算好每一个列表项元素的相对位置,以及自身的视图高度,省去了无谓的计算。

CustomScrollView控件

在 Flutter 中有一个专门的控件 CustomScrollView,用来处理多个需要自定义滚动效果的 Widget。

视差滚动是指让多层背景以不同的速度移动,在形成立体滚动效果的同时,还能保证良好的视觉体验。

以一个有着封面头图的列表为例,我们希望封面头图和列表这两层视图的滚动联动起来,当用户滚动列表时,头图会根据用户的滚动手势,进行缩小和展开。

经分析得出,要实现这样的需求,我们需要两个 Sliver:作为头图的 SliverAppBar,与作为列表的 SliverList。具体的实现思路是:

  • 在创建 SliverAppBar 时,把 flexibleSpace 参数设置为悬浮头图背景。flexibleSpace 可以让背景图显示在 AppBar 下方,高度和 SliverAppBar 一样;
  • 而在创建 SliverList 时,通过 SliverChildBuilderDelegate 参数实现列表项元素的创建;
  • 最后,将它们一并交由 CustomScrollView 的 slivers 参数统一管理。
代码语言:javascript复制
CustomScrollView(
  slivers: <Widget>[
    SliverAppBar(//SliverAppBar作为头图控件
      title: Text('CustomScrollView Demo'),//标题
      floating: true,//设置悬浮样式
      flexibleSpace: Image.network("https://xx.jpg",fit:BoxFit.cover),//设置悬浮头图背景
      expandedHeight: 300,//头图控件高度
    ),
    SliverList(//SliverList作为列表控件
      delegate: SliverChildBuilderDelegate(
            (context, index) => ListTile(title: Text('Item #$index')),//列表项创建方法
        childCount: 100,//列表元素个数
      ),
    ),
  ]);

ScrollController 与 ScrollNotification

ScrollController需要与具体的 ListView 绑定,才可以进行滚动信息的监听,进行相应的滚动控制。

通过 NotificationListener 则:

  • 可以监听其子 Widget 中的任意 ListView;
  • 不仅可以得到这些 ListView 的当前滚动位置信息,还可以获取当前的滚动事件信息 。

单子 Widget 布局:Container、Padding 与 Center

Container 容器与 Center 容器底层都依赖了同一个容器 Align,通过它实现子 Widget 的对齐方式。

多子 Widget 布局:Row、Column 与 Expanded

层叠 Widget 布局:Stack 与 Positioned

Stack 控件允许其子 Widget 按照创建的先后顺序进行层叠摆放,而 Positioned 控件则用来控制这些子 Widget 的摆放位置。需要注意的是,Positioned 控件只能在 Stack 中使用,在其他容器中使用会报错。

分平台主题定制

代码语言:javascript复制
// iOS浅色主题
final ThemeData kIOSTheme = ThemeData(
    brightness: Brightness.light,//亮色主题
    accentColor: Colors.white,//(按钮)Widget前景色为白色
    primaryColor: Colors.blue,//主题色为蓝色
    iconTheme:IconThemeData(color: Colors.grey),//icon主题为灰色
    textTheme: TextTheme(body1: TextStyle(color: Colors.black))//文本主题为黑色
);

// Android深色主题
final ThemeData kAndroidTheme = ThemeData(
    brightness: Brightness.dark,//深色主题
    accentColor: Colors.black,//(按钮)Widget前景色为黑色
    primaryColor: Colors.cyan,//主题色Wie青色
    iconTheme:IconThemeData(color: Colors.blue),//icon主题色为蓝色
    textTheme: TextTheme(body1: TextStyle(color: Colors.red))//文本主题色为红色
);

// 应用初始化
MaterialApp(
  title: 'Flutter Demo',
  theme: defaultTargetPlatform == TargetPlatform.iOS ? kIOSTheme : kAndroidTheme,//根据平台选择不同主题
  home: MyHomePage(title: 'Flutter Demo Home Page'),
);

七、Flutter的依赖管理

资源管理

在 Android、iOS 平台中,为了区分不同分辨率的手机设备,图片和其他原始资源是区别对待的:

  • iOS 使用 Images.xcassets 来管理图片,其他的资源直接拖进工程项目即可;
  • Android 的资源管理粒度则更为细致,使用以 drawable 分辨率命名的文件夹来分别存放不同分辨率的图片,其他类型的资源也都有各自的存放方式,比如布局文件放在 res/layout 目录下,资源描述文件放在 res/values 目录下,原始文件放在 assets 目录下等。

而在 Flutter 中,资源管理则简单得多:资源(assets)可以是任意类型的文件,比如 JSON 配置文件或是字体文件等,而不仅仅是图片。

代码语言:javascript复制
assets
├── background.jpg
├── icons
│   └── food_icon.jpg
├── loading.gif
└── result.json

对于上述资源文件存放的目录结构,以下代码分别演示了挨个指定和子目录批量指定这两种方式:

代码语言:javascript复制
flutter:
  assets:
    - assets/background.jpg   #挨个指定资源路径
    - assets/loading.gif  #挨个指定资源路径
    - assets/result.json  #挨个指定资源路径
    - assets/icons/    #子目录批量指定
    - assets/ #根目录也是可以批量指定的

原生平台的资源设置

更换 App 启动图标:

对于 Android 平台,启动图标位于根目录 android/app/src/main/res/mipmap 下。

对于 iOS 平台,启动图位于根目录 ios/Runner/Assets.xcassets/AppIcon.appiconset 下。

更换启动图:

对于 Android 平台,启动图位于根目录 android/app/src/main/res/drawable 下,是一个名为 launch_background 的 XML 界面描述文件。

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 白色背景 -->
    <item android:drawable="@android:color/white" />
    <item>
         <!-- 内嵌一张居中展示的图片 -->
        <bitmap
            android:gravity="center"
            android:src="@mipmap/bitmap_launcher" />
    </item>
</layer-list>

而对于 iOS 平台,启动图位于根目录 ios/Runner/Assets.xcassets/LaunchImage.imageset 下。我们保留原始启动图名称,将图片依次按照对应像素密度标准,更换为目标启动图即可。

八、第三方库

date_format

代码语言:javascript复制
print(formatDate(DateTime.now(), [mm, '月', dd, '日', hh, ':', n]));
//输出2019年06月30日01:56
print(formatDate(DateTime.now(), [m, '月第', w, '周']));
//输出6月第5周

九、跨组件传递数据

对于数据的跨层传递,Flutter 还提供了三种方案:InheritedWidgetNotificationEventBus

InheritedWidget

InheritedWidget 是 Flutter 中的一个功能型 Widget,适用于在 Widget 树中共享数据的场景。通过它,我们可以高效地将数据在 Widget 树中进行跨层传递。

Theme 类是通过 InheritedWidget 实现的典型案例

InheritedWidget 的使用方法。

  • 首先,为了使用 InheritedWidget,我们定义了一个继承自它的新类 CountContainer。
  • 然后,我们将计数器状态 count 属性放到 CountContainer 中,并提供了一个 of 方法方便其子 Widget 在 Widget 树中找到它。
  • 最后,我们重写了 updateShouldNotify 方法,这个方法会在 Flutter 判断 InheritedWidget 是否需要重建,从而通知下层观察者组件更新数据时被调用到。

InheritedWidget 仅提供了数据读的能力,如果我们想要修改它的数据,则需要把它和 StatefulWidget 中的 State 配套使用。

0 人点赞