3.学习Flutter -- 构建之 Widget

通过一系列文章记录一下学习 Flutter 的过程,总结一下相关知识点。

  1. 学习Flutter -- 框架总览
  2. 学习Flutter -- 启动过程做了什么
  3. 学习Flutter -- 构建之 Widget

Widget 是什么

Everything is a Widget in Flutter

在 Flutter 中,一切皆 Widget,为什么这么说呢?它不像我们 iOS 和 Android 开发中 View 的概念,不仅可以标识 UI 元素,也可以标识一些功能性的组件,总之一切与图形构建相关的东西都是 Widget,比如:

  • 一个 UI 结构元素(如:Button、Text、GestureDetector等)
  • 一个 UI 样式元素(如:color、font、Theme等)
  • 一个 UI 布局元素(如:Row、Padding等)

等等这些都称之为 Widget,是一种合理的命名抽象。

Widget 的功能是 "描述一个 UI 元素的配置信息",注意是"配置信息",Flutter 只是通过 Widget 来接收各种参数, 拿 Text 来说比如:文本内容、对齐方式、颜色样式等等,然后再通过一系列的转换和渲染操作,最终在屏幕上显示出来的元素并不简单的是我们书写的 Widget。

在 Flutter 中通过 Widget 构建 UI 的过程中,有一些显著的特点:

  • 声明式 UI

声明式编程:告诉"机器" 你想要什么(what)即可,机器自己计算如何去做(how)

命令式编程:告诉"机器" 如何去做(how),无论你想要什么(what),都需要你一步步的告诉机器。

相比于传统的命令式编程,声明式编程的优势很显著,不仅可以提高我们的开发效率和代码质量,减少代码出错的可能性,同时也能够促进程序的复用性和可维护性。

  • 不可变性

Flutter 中所有Widget都是不可变的(@immutable),并且其内部成员都是不可变的(final),对于需要变化的部分可通过 Widget-State ( 如:StatefuleWidget)的方式实现;

  • 组合大于继承

Widget 设计遵循组合大于继承这一优秀的设计理念,通过将多个功能相对单一的Widget组合起来得到功能相对复杂的Widget。

Widget 类型

我们先从全局的角度看看 Widget 的种类有哪些,然后在具体的看下每种 Wdiget 的作用以及我们该如何使用他们。

如图所示,各种 Widget 之间的继承关系一目了然,带背景色的 Widget 都是抽象类,无法直接使用,都是在各个子类中实现的对应的功能。按照功能划分 Widget 可分为 3 大类

  • 组合类(Component Widget)

    我们开发中用到最多的就是这一类型的 Widget,这类 Widget 都是直接或者间接的继承自 StatelessWidget 或 StatefulWidget,通过组合一些功能相对比较单一的 Widget 来得到满足我们需求的比较复杂的 Widget。

    StatelessWidget 无状态的 Widget,常见的子类有:Text、Container 等。

    StatefulWidget 有状态的 Widget,常见的子类有:Image、CheckBoxs 等。

  • 代理类(Proxy Widget)

    ProxyWidget 总共有两个子类 InheritedWidget 和 ParentDataWidget

    这类 Wdiget 可以快速追溯父节点,主要的作用就是为其 Child Widget 提供一些中间的附加功能。比如:InheritedWidget 常被用于做数据共享,Theme/ThemeData 就是通过它来实现的数据共享,以及 Flutter 中知名的状态管理框架 Provider 也是通过它实现的。

  • 渲染类(Renderer Widget)

    RenderObjectWidget 是Flutter 中最核心的 Widget 类型,只有它才会直接参与最终的 layout 与 paint 流程。无论是 Component Widget 还是 Proxy Widget 最终都会映射到 Renderer Widget 上。

Widget 源码

下面我们先来看一下 Widget 类的源码,主要介绍一下核心的属性和方法

dart 复制代码
@immutable // 不可变的
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key? key;

  @protected
  @factory
  Element createElement();

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

    表示 Widget 是不可变的,同时也限制了 Widget 的属性(配置信息)是不可变的,因为如果属性发生变化会导致 Wdiget 树的重新构建,即会用新的 Widget 实例替换旧的 Widget 实例,所以一旦属性变了自身就会被替换,那么允许属性变化是没有意义的,属性必须是 final 的。

  • Key

    在同一父节点下,用作兄弟节点间的唯一标识,主要作用是决定在下一次 build 时是否复用旧的 widget ,还是重建新的 widget,用在了 canUpdate 方法中。

  • createElement()

    每个 widget 都会有一个对应的 Element,就是由该方法创建的。Flutter 构建 UI 树时,会先调用此方法生成对应节点的 Element 对象。Widget 的子类都会重写此方法,从而生成不同类型的 Element 对象。

Flutter 中的 Widget 是以树的结构进行渲染的,实际上 Element 才是树的节点,所有对 Widget 的添加、删除等操作,实际上是对 Element 进行操作的,Widget 就是 Element 的配置项而已。

  • canUpdate(...)

    是一个静态方法,主要用于当 widget 树重新 build 时,判断新旧 widget 的 runtimeType 和 key 是否相等,决定是否用 new widget 对象去更新旧 UI 树上的 Element 配置。如果两个值都相等,则会用new widget 去更新 Element 对象配置,若不相等,则会创建新的 Element 对象。

在我们日常开发中,打交道最多的应该是 StatelessWidget 和 StatefulWidget 这两个类,他们都是继承自 Widget 的抽象类。StatelessWidget 和 StatefulWidget 这两个类他们之间的相同点是都属于组合型widget,区别是无状态与有状态之分。下面我们通过源码来具体分析一下。

StatelessWidget

无状态的 - 组合型Widget。

所谓无状态,意思是不需要管理内部状态,通常在其 build 方法中描述组合 UI 的层级结构,在其生命周期内是不可保存和修改状态的。

源码分析

dart 复制代码
abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key key }) : super(key: key);

  /// Creates a [StatelessElement] to manage this widget's location in the tree.
  ///
  /// It is uncommon for subclasses to override this method.
  @override
  StatelessElement createElement() => StatelessElement(this);

  @protected
  Widget build(BuildContext context);
}
  • abstract

    该类是一个抽象类,继承自它的子类只需要实现 build() 方法即可。

  • StatelessElement createElement()

    子类一般无需重写该方法,已经给我们创建好了一个 StatelessElement 类型的 Element,会将当前 widget 对象(this)当参数传进去,子类对应的 Element 类型也为StatelessElement。

  • Widget build(BuildContext context)

    build 方法属于 Flutter 体系中的核心方法之一,子类需要重写该方法,通过声明式 UI

的形式来描述 widget 的层级结构以及样式信息。该方法会被Flutter 系统自动调用,我们开发者重写即可,该方法被调用的情况有:

  1. widget 首次被加入到 Widget Tree 中(准确的说是其对应的 Element 被加入到 Element Tree 中,即 Element 首次被挂载时);
  2. Parent Widget 修改了配置信息;
  3. 该 Widget 依赖的 Inherited Widget 发生变化时;
  • BuildContext

    build 方法的参数是一个 BuildContext 类型的实例,表示当前 widget 在 widget 树的上下文。可通过 context 从当前 widget 开始向上遍历 widget 树,以及可能够按照 widget 类型向上查找父级 widget。

dart 复制代码
// 在 widget 树中向上查找最近的父级`Scaffold`  widget 
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title 
var title = (scaffold.appBar as AppBar).title;

优化建议

由于 Parent Widget 或依赖的 Inherited Widget 频繁变化时,导致 build 方法也会被频繁调用,因此对 build 方法的性能提升就显的尤为重要。Flutter 官方给出了几点建议:

  • 减少不必要的中间节点,即减少 UI 层级结构。比如:为了实现某种复杂的UI 效果,不一定通过组合多个 Container ,在配置 Decoration 来实现,通过 CustomPaint 自定义或许是更好的选择;
  • 尽量使用 const Widget,为 Widget 提供 const 构造方法,可参考: Dart Constant Constructors
  • 尽量减少 rebuild 的范围,比如:某个 Widget 的 build 需要频繁执行,可以分析出哪些 Widget 是真正需要变化的部分,从而封装成更小的独立的 Widget,并尽量将该 Widget 推向树的叶子节点,从而减小 rebuild 的范围。

StatefulWidget

有状态的- 组合型Widget。

什么叫做有"状态"呢?通过 Widget 源码我们知道,Widge 在 Flutter 中是不可变的,通常情况下每次刷新都会重新构建一个新的 Widget 对象,而无法知道保留之前的状态。 StatefulWidget 通过关联一个 State 对象,实现了状态的保存。

比如:一个 button 具有 normal、highlighted 两种状态,当和用于交互时会展现不同的样式,那么样式更新的过程其实就是状态更新的过程。

下面通过源码来看看 StatefulWidget 有哪些特别之处。

源码分析

dart 复制代码
abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);
    
  @override
  StatefulElement createElement() => StatefulElement(this);
    
  @protected
  State createState();
}

通过观察可发现与 StatelessWidget 不同之处有:

StatelessWidget

  1. 默认创建了 StatelessElement 类型的 Element
  2. 需要子类实现 build 方法,返回 Widget 对象

StatefulWidget

  1. 默认创建了 StatefulElement 类型的 Element

  2. 需要子类实现 createState 方法,返回 State 对象。一个 StatefulWidget 类会对应一个 State 类,State 就是 Widget 要维护的状态的实现。

State

状态就是保存在 State 这个抽象类中,通过一张类图来帮助我们梳理一下 State 内部结构:

State 中的属性:

  • widget

内部持有与该 State 关联的 widget 实例,Flutter 框架动态关联设置的。

注意:这种关联关系不是永久的,在应用生命周期中,UI 树上的某个节点的 widget 实例会在重新构建时会发生变化,但是 State 对象只会在第一次插入到树中被创建,当重新构建时,如果 widget 被修改了,系统会自动重新关联 State 的 widget 实例。

  • context

作用同 StatelessWidget 的BuildContext。这个 context 对象 '==' _element。

State 生命周期

Flutter 中说到的生命周期,是指有状态的 Widget 的生命周期,对于无状态的 Widget 的生命周期只有一个 build 过程,也就只会渲染一次。

有状态的 Widget 的生命周期如下:

状态说明:

  1. createElement()

首先,当一个 StatefulWidget 需要被显示到页面上时,会创建一个 StatefulElement , 在 StatefulElement 的构造函数中会调用 createState()

  1. createState()

创建 State 对象,同时将创建的 element 对象赋值给 state 的_element 对象,此时 element 就挂在到了 Element Tree 中,state 就处于 mounted 状态。

  1. initState()

在element 挂在的过程中紧接着会调用该方法,并且只会被调用一次。我们可以重写该方法,执行一些初始化操作。(注意:此时已经可以引用 context 、widget 属性了)

  1. didChangeDependencies()

首先,当 initState 调用结束后也会调用一次。并且,当 state 依赖的对象状态发生变化时,该方法也会被调用。如:父级 widget 中包含了InheritedWidget,它的状态发生了变化,那么InheritedWidget 的子 Widget 的 didChangeDependencies 方法也会被调用。

子类一般很少重写该方法,除非有非常耗时的不适合在 build 方法中进行的操作。

  1. build()

主要用于构建 Widget 子树的,会在以下场景被调用:

  • 调用 initState() 之后
  • 调用 didChangeDependencies 之后
  • 调用 didUpdateWidget() 之后
  • 调用 setState() 之后
  • 当 State 对象从树中一个位置移除后,又被插入到其他位置之后
  1. didUpdateWidget()

当 widget 重新构建时,Flutter 框架会调用 widget.canUpdate 方法来检测 Widget 树中同一位置的新旧节点,判断是否需要更新,如果 widget.canUpdate 返回 true表示需要更新,那么则会触发此回调。

  1. deactivate()

在Widget 树更新过程中,任何节点都有被移除的可能,State 也会随之移除,当 State 对象从树中被移除时候,会调用此方法。

有些场景下,会被重新插入到树中新的位置,此时会重新走 build 方法。如果没有被插入到新的位置,则会调用 dispose 方法进行销毁。

  1. dispose()

销毁方法,当前帧动画结束时扔未被插入到新的节点,则表示 State 对象从树中被永久移除,该方法会被调用,State 生命周期随之结束。

子类通常重写改方法,在这里执行一些资源释放的操作。

setState 方法介绍

上面在介绍 State 生命周期的过程中,我们开发中一般通过调用 setState() 方法来刷新 UI,此时 Flutter 框架会触发 build 方法重新构建 Widget 树,setState 工作机制流程基本如下:

那么 setState() 方法具体做了什么呢,下面我们来看一下源码的具体的实现"

dart 复制代码
  @protected
  void setState(VoidCallback fn) {
    //fn 不能为空
    assert(fn != null);
    assert(() {
       //销毁后调用会报错
      if (_debugLifecycleState == _StateLifecycle.defunct) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() called after dispose(): $this'),
        ]);
      }
      //初始化但未挂载状态调用会报错
      if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() called in constructor: $this'),
        ]);
      }
      return true;
    }());
    //执行回调 fn
    final Object? result = fn() as dynamic;
    assert(() {
      //异步方法会报错
      if (result is Future) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() callback argument returned a Future.'),
        ]);
      }
      return true;
    }());
    //标记需要刷新
    _element!.markNeedsBuild();
  }

根据源码以及相关注释,使用 setState() 方法时候有几点需要注意:

  • 在 State 的 dispose() 方法后不能调用 setState(注意:程序中有些异步或延时的方法中,是否调用了);

  • 在 State 的构造方法中不能调用 setState;

  • setState 方法的回调函数 fn 不能是异步的 (返回值为 Future 类型),因为系统需要回调产生的新状态去刷新 UI,如果回调里有很长的耗时任务,那么后续刷新的代码就会延迟执行,从而导致 build 也就延迟了;

  • 最终 UI 的是通过 _element!.markNeedsBuild() 标记需要刷新,会等到下一个 Vsync 信号到来时才进行重新 build 和渲染。

ParentDataWidget

ParentDataWidget 继承自 ProxyWidget ,是 Proxy 型 Widget,主要用于配置子 Widget 的 RenderObject 提供 ParentData 数据,比如:布局、绘制等。例如,Stack 使用 Positoned(继承 ParentDataWidget)来定位每个子 widget。

首先看一下源码定义:

scala 复制代码
abstract class ParentDataWidget<T extends ParentData> extends ProxyWidget {
  
  const ParentDataWidget({ super.key, required super.child });

  @override
  ParentDataElement<T> createElement() => ParentDataElement<T>(this);

	void applyParentData(RenderObject renderObject);
  
}
  • ParentDataWidget 本身也是抽象类,继承自 ProxyWidget

  • 在定义上使用了泛型

  • 需要子类去实现 applyParentData 方法,子类在这个方法中设置 renderObject.parentData 数据

ParentData 是什么?

ParentData 是 parent RenderObject 在布局 child RenderObject 时所用到的布局定位信息,其存储在子 widget 上。比如:子类对父类布局算法的输入参数,或子类相对其他子类的位置信息。

例子

配合 Stack 使用的 Positioned(继承 ParentDataWidget), 源码如下

dart 复制代码
class Positioned extends ParentDataWidget<StackParentData> {
...
  @override
  void applyParentData(RenderObject renderObject) {
    assert(renderObject.parentData is StackParentData);
    final StackParentData parentData = renderObject.parentData! as StackParentData;
    bool needsLayout = false;

    if (parentData.left != left) {
      parentData.left = left;
      needsLayout = true;
    }

    if (parentData.top != top) {
      parentData.top = top;
      needsLayout = true;
    }
   
    ......
    
    if (needsLayout) {
      final AbstractNode? targetParent = renderObject.parent;
      if (targetParent is RenderObject) {
        targetParent.markNeedsLayout();
      }
    }
  }
  
 ... 
}

从源码中可以看出,Positioned 会将自己的属性赋值给 renderObject.parentData,也就是 StackParentData,并且最后对 parent RenderObject 调用 markNeedsLayout,从而重新 layout,毕竟修改了布局信息。

总之,ParentDataWidget 就是用来配置 RenderObject 的 ParentData 数据的, ParentDataWidget 并不是任何 Widget 都能继承使用的,并配置指定的 ParentData 才行。

InheritedWidget

InheritedWidget 同样继承自 ProxyWidget ,是 Proxy 型 Widget,它的主要功能是用于在 Widget 树上向下传递数据(注意是从上到下)。比如我们在根 Widget 中通过 InheritiedWidget 共享一个数据,那么在所有子 Widget 中都可以获取这个共享数据,如 Flutter 框架正式通过 InheritedWidget 实现的共享主题(Theme)、当前语言环境(Local)、MediaQuery 等。

源码:

scala 复制代码
abstract class InheritedWidget extends ProxyWidget {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const InheritedWidget({ super.key, required super.child });

  @override
  InheritedElement createElement() => InheritedElement(this);

  /// Whether the framework should notify widgets that inherit from this widget.
  ///
  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
  • 默认创建了 InheritedElement 类型的 Element,一般子类无需重写

  • 子类重写 updateShouldNotify 方法,当 InheritedWidget 数据发生变化,判断那些依赖它的子 Widget 是否需要进行 rebuild。

例子

看下我们经常上使用 MediaQuery 的源码,如下:

dart 复制代码
class MediaQuery extends InheritedWidget {
  MediaQuery({
    Key? key,
    required this.data,
 		required Widget child,
  }) : super(key: key, child: child);

  final int data; //需要在子树中共享的数据 

  //定义一个便捷方法,方便子树中的widget获取共享数据
  static MediaQueryData? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
  }

  //该回调决定当data发生变化时,是否通知子树中依赖data的Widget重新build
  @override
  bool updateShouldNotify(ShareDataWidget old) {
    return old.data != data;
  }
}
  • of 方法

通常为了使用方便,提供了 of 方法,内部调用了 BuildContext 的dependOnInheritedWidgetOfExactType 方法可以获取最近的 InheritedWidget,

of 方法可以直接返回 Widget, 也可以返回 data 数据。

  • updateShouldNotify

判断新旧 Widget 的 data 不相等时,才去更新依赖它的子 Widget。

didChangeDependencies

在介绍 StatefulWidgt 时, 我们提到了 State 的生命周期中有一个 didChangeDependencies 方法回调,该方法会在'依赖'发生变化时被 Flutter 框架调用,而这个'依赖'就是指子 Widget 中使用了父 Widget 中InheritedWidget 中的数据。如果使用了,则代表有子 Widget 依赖,没有使用,则代表没有依赖。比如:当主题(Theme)、本地语言(Locale)等发生变化时,依赖他们的子 Widget 的 didChangeDependencies 会自动被调用。

为什么会被调用?

首先,对依赖数据是通过 of 方法获取的, of 方法内部又调用 BuildContext 的dependOnInheritedWidgetOfExactType 方法,我们追踪源码看下

dart 复制代码
  static MediaQueryData of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
  }

dependOnInheritedWidgetOfExactType,方法内部有调用了dependOnInheritedElement

dart 复制代码
  @override
  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

dependOnInheritedElement

dart 复制代码
  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies!.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget as InheritedWidget;
  }

看到这里,我们就明白了,最终在 dependOnInheritedElement 方法内部,所有对 InheritedWidget 中数据有依赖的子 Widget 注册了依赖关系,之后当 InheritedWidget 中数据发生变化时,便会更新依赖它的子Widget,也就是调用这些子 Widget 的 didChangeDependencies() 方法和build() 方法。

RenderObjectWidget

RenderObject 是什么?

RenderObject 对象作为 Render Tree 中的节点, Flutter 最终的布局、渲染的算法都是通过对应的 RenderObject 对象来实现的,比如:Stack(层叠布局)对应的 RenderObject 对象类型就是 RenderStack,层叠布局的实现就在 RenderStack 中。

RenderObjectWidget

RenderObjectWidget 属于布局类组件,此类组件都会包含一个或多个子组件,不同类型的布局类组件对子组件排列(layout)方式不同。根据子节点个数不同,共有三个子类:

  • LeafRenderObjectWidget

作为叶子节点,只提供 paint 能力,如:RawImage。我们常用的 Image 组件内部的绘制工作全由 RawImage 实现。

  • SingleChildRenderObjectWidget

仅接收单个 child 的 widget,如:Opacity。用来修改 child 透明度。

  • MultiChildRenderObjectWidget

可接受多个 child 的 widget,提供 layout 能力,如:Stack 做层叠布局,Flex 做线性布局(Row、Column)。

我们来看下 RenderObjectWidget 的源码定义:

dart 复制代码
abstract class RenderObjectWidget extends Widget {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const RenderObjectWidget({ super.key });

  /// RenderObjectWidgets always inflate to a [RenderObjectElement] subclass.
  @override
  @factory
  RenderObjectElement createElement();
 
  @protected
  @factory
  RenderObject createRenderObject(BuildContext context);
 
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
 
  @protected
  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
  • createElement()

RenderObjectWidget 对应的 Element 类型是 RenderObjectElement,但是 RenderObjectElement 也是抽象类,该方法需要被子类重写

  • createRenderObject()

创建 Render Widget 对应的 RenderObject 对象,创建时机是在 RenderObjectElement.mount 方法中,即 Element 被挂载到树上时调用,即 Element 被挂载过程中同时构建了 Render Tree。

  • updateRenderObject()

更新 RenderObject,当 Widget 更新后,会修改对应的 RenderObject。

  • didUnmountRenderObject()

RenderObject 从 Render Tree 上被移除时调用该方法。

以上,简要介绍了 Flutter 中 Widget 的几个大类,以及对应的源码定义,我们开发过程使用到的各种 Widget 基本都是继承自这些基类,感兴趣可以直接去看下他们的实现源码。

总结:

  • Widget 本质上是 UI 的配置信息
  • Widget 从功能上可以分为 3 大类:Component Widget、Proxy Widget 、Render Widget
  • Widget 与 Element 是一一对应的,Widget 中会提供 Element 的创建方法 createElement()
  • 只有 Render Widget 才会参与最终UI 的布局、渲染,提供 RenderObject 的创建方法 createRenderObject()
相关推荐
Summer不秃2 小时前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰2 小时前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
sunly_2 小时前
Flutter:AnimatedSwitcher当子元素改变时,触发动画
flutter
AiFlutter2 小时前
Flutter封装Coap
flutter
旭日猎鹰8 小时前
Flutter踩坑记录(三)-- 更改入口执行文件
flutter
旭日猎鹰8 小时前
Flutter踩坑记录(一)debug运行生成的项目,不能手动点击运行
flutter
️ 邪神8 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
比格丽巴格丽抱20 小时前
flutter项目苹果编译运行打包上线
flutter·ios
SoaringHeart20 小时前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter
AiFlutter1 天前
Flutter通过 Coap发送组播
flutter