前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

Flutter

原创
作者头像
愤怒的小鸟
修改2021-11-08 10:35:13
1.8K0
修改2021-11-08 10:35:13
举报
文章被收录于专栏:web shareweb share

一、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 配套使用。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Flutter 和 React Native 本质区别
  • 二、 Flutter的渲染机制
    • 1. Flutter渲染机制之三棵树
      • 2. 初次运行时的三棵树
        • 3. 三棵树的作用
          • 4. 更新时的三棵树
            • 5. 当Widget的类型发生改变时的三棵树
            • 三、 Flutter 实现原理
              • 布局
                • 绘制
                  • 合成和渲染
                    • Dart 的数值类型 num
                    • Dart 的String类型
                    • List 与 Map
                    • 常量定义
                    • 函数
                    • 要使用混入,只需要 with 关键字即可
                    • Dart 多了几个额外的运算符,用于简化处理变量实例缺失(即 null)的情况
                • 四、Dart 的基础
                • 五、Flutter的基础
                  • StatelessWidget
                    • StatefulWidget
                      • State 生命周期
                        • 创建
                        • 更新
                        • 销毁
                      • 生命周期回调
                        • 帧绘制回调
                        • 六、Flutter的经典控件
                          • Image控件
                            • ListView控件
                              • CustomScrollView控件
                                • ScrollController 与 ScrollNotification
                                  • 单子 Widget 布局:Container、Padding 与 Center
                                    • 多子 Widget 布局:Row、Column 与 Expanded
                                      • 层叠 Widget 布局:Stack 与 Positioned
                                        • 分平台主题定制
                                        • 七、Flutter的依赖管理
                                          • 资源管理
                                            • 原生平台的资源设置
                                            • 八、第三方库
                                            • 九、跨组件传递数据
                                              • InheritedWidget
                                              相关产品与服务
                                              容器服务
                                              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                              领券
                                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档