Flutter | 三棵树构建流程分析(上)

简介: Flutter | 三棵树构建流程分析(上)

引言


在 Flutter 中,很多人都知道三棵树,最熟悉就是其中的 Widget 树了,这也是平常开发的过程中最多用到的东西,那么其他两棵树你知道是什么吗,了解他们的构建流程吗?


Widget 树


在开发过程中,与我们息息相关的就是 widget 了,几乎所有页面上显示的都是 widget ,Widget 是 Flutter 的核心,是用户界面的不可变描述。


事实上,widget 的功能就是描述一个 UI 元素的配置数据 ,也就是说 widget 并不是最终绘制到屏幕上的元素,它只是描述显示元素的一个配置而已。


在代码的运行过程中并没有明确的 widget 树的概念,这棵树是我我们在开发的过程中对 widget 嵌套的描述,因为确实长得像是一棵树


abstract class Widget {
  const Widget({ this.key });
  final Key key;
  @protected
  Element createElement();//注释1
  static bool canUpdate(Widget oldWidget, Widget newWidget) {//注释2
   return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}


Widget 本身是一个抽象类,接收一个 key,至于key的原理和使用可查看这篇文章,


注释1


createElement 是抽象方法,子类必须实现,该方法创建了一个 Element,所以每个 Element 都会对应一个 widget 对象。


注释2


判断 oldWidget 和 newWidget 是不是同一个 widget,如何 runtimeType 和 key 相同则认为是同一个 widget。


需要注意的是 widget 不能被修改,如果要修改只能重新创建了,因为 wdiget 并不参与渲染,它只是一个配置文件而已,只需要告诉渲染层自己的样式即可。


Element 树


Flutter 中真正显示到屏幕上的元素是 Element 类,也就是说 widget 只是描述 Element 的配置数据,并且 widget 可以对应多个 Element。这是因为同一个 widget 可以被添加到 Element 树的不同部分。而真正渲染的时候,每一个 Element 都会对应着一个 widget 对象。


所谓的 UI 树就是由一个个 Element 节点构成。组件的最终 Layout,渲染都是通过 RenderObject 来完成的,从创建到渲染的大体流程就是:根据 widget 生成 Element,然后在创建相应的 RenderObject 并关联到 Element.renderObject 属性上,最后在通过 RenderObject 来完成布局排列和绘制。


Element 表示一个 widget 树中特定位置的实例,大多数的 Element 只有惟一的 RenderObject,但是还有一些 Element 会有多个子节点,如继承自 RenderObjectElement 的一些类,比如 MultiChildRenderObjectObject。最终所有的 Element 的 RenderObject 构成一棵树,我们称之为 渲染树。


总结一下,我们可以认为 Flutter 的 UI 系统中包含了三棵树:Widget 树,Element 树,渲染树,他们的对应关系式 Element 树是根据 Widget 树生成,而渲染树有依赖于 Element 树,如图所示:



Element 类源代码


abstract class Element extends DiagnosticableTree implements BuildContext {
  Element(Widget widget)
    : assert(widget != null),
      _widget = widget;
  Element _parent;
  @override
  Widget get widget => _widget;
  Widget _widget;
  RenderObject get renderObject { ... }
  @mustCallSuper
  void mount(Element parent, dynamic newSlot) { ... }
  @mustCallSuper
  void activate() { ... }
  @mustCallSuper
  void deactivate() { ... }
  @mustCallSuper
  void unmount() { ... }
}


Element 的生命周期


initial


初始状态


_ElementLifecycle _lifecycleState = _ElementLifecycle.initial;


active


//RenderObjectElement 的 mount 方法
@override
void mount(Element? parent, Object? newSlot) {
  super.mount(parent, newSlot);
  //.....
  _renderObject = widget.createRenderObject(this);
  assert(_slot == newSlot);
  attachRenderObject(newSlot);
  _dirty = false;
}


当 fragment 调用 element.mount 方法后,mount 方法中会首先调用 element 对应 widget 的 createRenderObject方法来创建与 element 对应的 RenderObject 对象。


然后调用 element.attachRenderObject 将 element.renderObject 添加到渲染树插槽的位置(这一步不是必须的,一般发生在 Element 树结构发生变化时才需要重新 attach。


插入到渲染后的 element 就处于 active 状态,处于 active状态后就可以显示在屏幕上了(可以隐藏)。


super.mount(parent,newslot)

_lifecycleState = _ElementLifecycle.active


当 widget 更新时,为了避免重新创建 element 会判断是否可以更新,会调用 updateChild 方法


@protected
@pragma('vm:prefer-inline')
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
  //当没有新 widget,并且有原来的widget,则移出原来的child,因为他不再有配置
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }
  final Element newChild;
  //原来有 child
  if (child != null) {
    bool hasSameSuperclass = true;
  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)) 
        //父控件类型相同,并且可以更新 widget,则更新child
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      assert(child.widget == newWidget);
      assert(() {
        child.owner!._debugElementWasRebuilt(child);
        return true;
      }());
      newChild = child;
    } else {
      //不能更新,需要先移出原有的 child,并创建新的 child 并添加
      deactivateChild(child);
      assert(child._parent == null);
      newChild = inflateWidget(newWidget, newSlot);
    }
  } else {
    //没有 child,直接创建新的 child 并添加
    newChild = inflateWidget(newWidget, newSlot);
  }
  return newChild;
}


weidget.canUpdate ,主要判断 type 和 key 是否相同。如果我们需要强制更新,只需要修改key即可,官方不推荐修改 runtimetype


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


从“非活动”到“活动”生命周期状态的转换


在上面的 updateChild 方法中,最后如果调用了 inflateWidget() 方法后,就需要将状态从 inactive 转到 active 状态


@mustCallSuper
void activate() {
  assert(_lifecycleState == _ElementLifecycle.inactive);
  assert(widget != null);
  assert(owner != null);
  assert(depth != null);
  final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
  _lifecycleState = _ElementLifecycle.active;
  // We unregistered our dependencies in deactivate, but never cleared the list.
  // Since we're going to be reused, let's clear our list now.
  _dependencies?.clear();
  _hadUnsatisfiedDependencies = false;
  _updateInheritance();
  if (_dirty)
    owner!.scheduleBuildFor(this);
  if (hadDependencies)
    didChangeDependencies();
}


inactive


从“活动”到“非活动”生命周期状态的转换


在上面的 updateChild 方法中我们可以看到新的 widget 为空并且存在旧的,就会调用deactiveChild 移除 child,然后调用 deactivate 方法将 _lifecycleState 设置为 inactive


@mustCallSuper
void deactivate() {
  assert(_lifecycleState == _ElementLifecycle.active);
  assert(_widget != null); // Use the private property to avoid a CastError during hot reload.
  assert(depth != null);
  if (_dependencies != null && _dependencies!.isNotEmpty) {
    for (final InheritedElement dependency in _dependencies!)
      dependency._dependents.remove(this);
  }
  _inheritedWidgets = null;
  _lifecycleState = _ElementLifecycle.inactive;
}


defunct


从“非活动”到“已失效”生命周期状态的转换


@mustCallSuper
void unmount() {
  assert(_lifecycleState == _ElementLifecycle.inactive);
  assert(_widget != null); // Use the private property to avoid a CastError during hot reload.
  assert(depth != null);
  assert(owner != null);
  // Use the private property to avoid a CastError during hot reload.
  final Key? key = _widget!.key;
  if (key is GlobalKey) {
    owner!._unregisterGlobalKey(key, this);
  }
  // Release resources to reduce the severity of memory leaks caused by
  // defunct, but accidentally retained Elements.
  _widget = null;
  _dependencies = null;
  _lifecycleState = _ElementLifecycle.defunct;
}


StatelessElement


Container 创建出来的是 StatelessElement,下面我们浅析一下他的调用过程,部分代码省略,只显示核心代码!


class StatelessElement extends ComponentElement {
  //通过 createElement 创建时传入的 widget
  StatelessElement(StatelessWidget widget) : super(widget);
  @override
  StatelessWidget get widget => super.widget as StatelessWidget;
  //这里调用的 build 就是我们自己实现的 build 方法
  @override
  Widget build() => widget.build(this);
}
abstract class ComponentElement extends Element {
  /// Creates an element that uses the given widget as its configuration.
  ComponentElement(Widget widget) : super(widget);
  Element? _child;
  bool _debugDoingBuild = false;
  @override
  bool get debugDoingBuild => _debugDoingBuild;
  @override
  void mount(Element? parent, Object? newSlot) {
    _firstBuild();
  }
  void _firstBuild() {
    rebuild();
  }
  @override
  @pragma('vm:notify-debugger-on-exception')
  void performRebuild() {
   //.....
  }
  @protected
  Widget build();
}
abstract class Element extends DiagnosticableTree implements BuildContext {
  // 构造方法, 接收一个widget参数
  Element(Widget widget)
    : assert(widget != null),
      _widget = widget;
  @override
  Widget get widget => _widget;
  Widget _widget;
  void rebuild() {
    if (!_active || !_dirty)
      return;
    Element debugPreviousBuildTarget;
    // 这里调用的performRebuild方法, 在当前类并没有实现, 只能去自己的类里面查找实现
    performRebuild();
  }
  /// Called by rebuild() after the appropriate checks have been made.
  @protected
  void performRebuild();
}


梳理一下流程,如下:


这里创建了 StatelessElement ,创建成功后,framework 就会调用 mount方法,因为 StatelessElement 没有实现 mount,所以这里调用的是 ComponentElement 的 mount。


在 mount 中调用了 _firstBuild 方法进行第一次构建。(这里调用的是实现类(StatelessElement) 的 _firstBuild 方法)


_firstBuild 方法最后调用的是 super._firstBuild() ,也就是ComponentElement 的 _firstBuild 方法,在其中调用了 rebuild() 。由于 ComponentElement 没有重写,所以最终调用的是 Element 的 rebuild 方法。


rebuild 最终又会调用到 ComponentElement 的 performRebuild 方法中。 如下:


@override
  @pragma('vm:notify-debugger-on-exception')
  void performRebuild() {
    if (!kReleaseMode && debugProfileBuildsEnabled)
      Timeline.startSync('${widget.runtimeType}',  arguments: timelineArgumentsIndicatingLandmarkEvent);
    assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
    Widget? built;
    try {
      assert(() {
        _debugDoingBuild = true;
        return true;
      }());
      // 调用 build ,这里调用的是实现类 StatelessElement 的,最终是调用到我们自己实现的 build 中
      built = build();
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      // catch
    } finally {
      _dirty = false;
      assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
    }
    try {
      //最终调用 updateChild 方法
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      //....
      _child = updateChild(null, built, slot);
    }
    if (!kReleaseMode && debugProfileBuildsEnabled)
      Timeline.finishSync();
  }


@pragma('vm:prefer-inline')
Element inflateWidget(Widget newWidget, Object? newSlot) {
  assert(newWidget != null);
  final Key? key = newWidget.key;
  if (key is GlobalKey) {
    final Element? newChild = _retakeInactiveElement(key, newWidget);
    if (newChild != null) {
      assert(newChild._parent == null);
      assert(() {
        _debugCheckForCycles(newChild);
        return true;
      }());
      newChild._activateWithParent(this, newSlot);
      final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
      assert(newChild == updatedChild);
      return updatedChild!;
    }
  }
  //创建对应的 element  
  final Element newChild = newWidget.createElement();
  assert(() {
    _debugCheckForCycles(newChild);
    return true;
  }());
  //  调用 mount 方法
  newChild.mount(this, newSlot);
  assert(newChild._lifecycleState == _ElementLifecycle.active);
  return newChild;
}


上面代码中最终调用到了 updateChild 方法,这个方法在上面 Element的生命周期中有提到。


在 updateChild 方法中就会判断 built 是否需要更新或者替换,如果需要替换,就会清除原来的,并且对新的 built 创建 对应的 Element ,并且在最后调用 built 对应 Element 的 mount 方法。这里的 Element 就不一定是 StatelessElement 了,而是看在 build 方法中的 widget 对应的 Element 是什么。


总结一下


从上面的流程分析中我们可以看出来整个流程就像是一个环,最开始的framework 调用 mount 。在 mount 中最终调用到了 performRebuild ,在 performRebuild 中通过调用我们实现的 build 方法,拿到对应的 widget 后,如果需要替换,就会重新创建 widget 的 element,并且调用这个 element 的 mount 方法。


0a2653c851af460fa595bd959398a8f1.png


整个流程大致如上图所示


RenderObjectElement


我们以 Flex 为例来看一下是如何创建 Element 的。


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


Fiex 是通过父类 SingleChildRenderObjectWidget 来创建的 Element,、MultiChildRenderObjectElement 是继承自 RenderObjectElement 的。


接下来我们浅析一下 RenderObjectElement的调用过程


class MultiChildRenderObjectElement extends RenderObjectElement {
      @override
  void mount(Element? parent, Object? newSlot) {
    //调用super.mount 将传入的 parent 插入到树中  
    super.mount(parent, newSlot);
     // 
    final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
    Element? previousChild;
     //遍历所有的child
    for (int i = 0; i < children.length; i += 1) {
      //加载child  
      final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
      children[i] = newChild;
      previousChild = newChild;
    }
    _children = children;
  }
}  
abstract class RenderObjectElement extends Element {
  @override
  void mount(Element? parent, Object? newSlot) {
    //将传入的 parent 插入到树中  
    super.mount(parent, newSlot);
    //创建与element相关联的renderObject 对象
    _renderObject = widget.createRenderObject(this);
    //将element.renderObjectZ 插入到渲染树中的指定位置  
    attachRenderObject(newSlot);
    _dirty = false;
  }
}
相关文章
|
3月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
353 4
|
11天前
|
前端开发 安全 开发工具
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
141 90
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
27天前
|
Dart 前端开发
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
116 75
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
3月前
|
缓存 监控 前端开发
优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面
本文探讨了优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面,并通过案例分析展示了具体措施和效果,强调了持续优化的重要性及未来优化方向。
118 10
|
16天前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
121 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
24天前
|
Dart 前端开发 容器
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
75 18
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
14天前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
37 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
1月前
|
前端开发 Java 开发工具
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
81 18
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
|
29天前
|
缓存 前端开发 Android开发
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
79 12
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
|
10天前
|
开发工具 Android开发 iOS开发
零基础构建即时通讯开源项目OpenIM移动端-Flutter篇
OpenIM 为开发者提供开源即时通讯 SDK,作为 Twilio、Sendbird 等云服务的替代方案。借助 OpenIM,开发者可以构建安全可靠的即时通讯应用,如 WeChat、Zoom、Slack 等。 本仓库基于开源版 OpenIM SDK 开发,提供了一款基于 Flutter 的即时通讯应用。您可以使用此应用程序作为 OpenIM SDK 的参考实现。 开发环境 在开始开发之前,请确保您的系统已安装以下软件: 操作系统:macOS 14.6 或更高版本 Flutter:版本 3.24.5(根据官网步骤进行安装) Git:用于代码版本控制 同时,您需要确保已经部署了最
58 9

热门文章

最新文章

  • 1
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 2
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 3
    【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 4
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 5
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 6
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 7
    零基础构建即时通讯开源项目OpenIM移动端-Flutter篇
  • 8
    flutter3-dart3-dymall原创仿抖音(直播+短视频+聊天)商城app系统模板
  • 9
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 10
    【06】flutter完成注册页面-密码登录-手机短信验证-找回密码相关页面-并且实现静态跳转打包demo做演示-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 1
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
    14
  • 2
    零基础构建即时通讯开源项目OpenIM移动端-Flutter篇
    58
  • 3
    flutter3-dart3-dymall原创仿抖音(直播+短视频+聊天)商城app系统模板
    38
  • 4
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    141
  • 5
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    37
  • 6
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
    73
  • 7
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    121
  • 8
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
    75
  • 9
    flutter开发中Use ‘const’ with the constructor to improve performance. Try adding the ‘const’ keyword to the constructor invocation.报错如何解决-优雅草卓伊凡
    18
  • 10
    【06】flutter完成注册页面-密码登录-手机短信验证-找回密码相关页面-并且实现静态跳转打包demo做演示-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
    27