[Flutter] Study Flutter Componentization‌‌

Posted by xiuyuantech on 2023-12-12

Flutter 是 Google 开源的应用开发框架,仅通过一套代码库,就能构建精美的、原生平台编译的多平台应用。
Flutter 只要一套代码兼顾Android、iOS、Web、Windows、macOS和Linux六个平台。
Flutter 由Dart语言强力驱动,助力高效构建全平台应用。

前言

组件化一直是移动端比较流行的开发方式,有着编译运行快,业务逻辑分明,任务划分清晰等优点。大多数情况下,我们创建一个 Flutter 项目,默认是一个单包项目,该项目由一个pubspec.yaml、lib文件夹组成。所有的逻辑和功能都在这一个包中实现,但是也有一些复杂的 Flutter 项目会按功能,模块,组件等拆分为多个包。

相对应的 Flutter 项目的组件化也是一样的道理,我们也能把各个功能独立的组件比如用户模块、支付模块等,这些模块在项目以单独项目的方式存在,通过本地 path 依赖或者 melos 的方案去组合起来,由于 Flutter 项目的特殊性,我们还能在单独模块项目中可以创建各自的 sample 模块去独立运行这个模块从而实现组件化。

方案

  1. 组件化工具
    Melos是一个专为Dart和Flutter项目设计的工具,旨在简化包含多个包的monorepo(单一代码库)的管理工作‌。Melos的主要功能包括多包管理、依赖同步、脚本执行、版本管理和开发环境一致性等‌。

  2. 架构设计模式

  3. 常用库选择
    状态管理:riverpodflutter_hookshooks_riverpod
    路由管理:auto_route
    网络请求:dio
    进度条:percent_indicator
    设备信息:device_info_plus
    图片库:cached_network_image
    首次初始化:after_layout
    跨组件通信:event_bus
    图片预览、缩放:photo_view
    动画库:animations
    轮播图:carousel_slider
    图表:fl_chart
    屏幕适配工具:flutter_screenutil
    无线滚动:infinite_scroll_pagination
    权限申请:permission-handler
    包信息:package_info_plus
    文件选择:file_picker
    返回键拦截器:back_button_interceptor
    可见性监听:visibility_detector
    无障碍工具:accessibility_tools
    解压缩文件:archive
    路径处理:path
    deeplink:url_launcher
    分享:share_plus
    裁剪:croppy
    文字播报:flutter_tts
    图片选择:image_picker
    网路监听:connectivity_plus
    状态观察者库:rxdart
    HTML解析:html
    评级:flutter_rating_bar
    打开文件:open_file
    网络信息:network_info_plus
    上拉下拉:pull_to_refresheasy_refresh
    webview:webview_all
    查找文件:path_provider
    文件保存:file_saver
    保存到相册:image_gallery_saver
    唤醒:wakelock_plus
    sharedpreferences:shared_preferences
    数据库:sqflite_common
    骨架屏:skeletonizer
    基础工具类:basic_utils
    高级富文本:extended_text_library
    剪切板:clipboard
    键盘监听:flutter_keyboard_visibility
    自适应大小文本:auto_size_text
    自适应大小文本框:auto_size_text_field
    二维码:qrbarcodezxing_lib
    截图:screenshot
    扩展组件:expandable
    toast:bot_toast
    文件缓存:flutter_cache_manager
    打开文件:open_file
    日志:logger
    通知:awesome_notifications
    步骤线:stepper_list_view
    日历:table_calendarcalendar_date_picker2
    json解析:json_serializabledart_mappable
    地图:flutter_mapmapmaps_launchermaps_toolkitflutter_map_location_marker

  4. 项目结构

项目结构

避坑/面试

  • dynamic,var,object三者的区别
    dynamic 允许变量在运行时动态改变类型,编译器不进行类型检查。
    var 是类型推断声明,变量类型在声明时确定,之后不能改变。
    Object 是所有 Dart 对象的基类,允许任何类型的值,但需要进行类型转换才能使用具体类型的值。

  • flutter里的key有什么用
    GlobalKey 是一个全局唯一的键,用于标识整个应用中的特定 Widget。它可以跨越 Widget 树的多个层次来访问 Widget,并提供更强的控制和状态管理。
    LocalKey 是 Key 的基类,分为两种:ValueKey 和 ObjectKey,用于在局部 Widget 树中唯一标识某个 Widget。
    Key的作用:保持状态,优化性能,唯一标识。

  • flutter中Widget的分类
    组合类:StatelessWidget和StatefulWidget
    生命周期对比

    生命周期方法StatelessWidgetStatefulWidget
    build
    createState
    initState
    didChangeDependencies
    didUpdateWidget
    deactivate
    dispose

    代理类:inheritedwidget、ParentDataWidget
    inheritedwidget一般用于在Widget树中传递共享数据,状态共享,如Theme 、Localizations 、 MediaQuery 等,都是通过它实现共享状态,这样我们可以通过 context 去获取共享的状态,比如 ThemeData theme = Theme.of(context);

    绘制类:RenderObjectWidget
    RenderObject 的布局相关方法调用顺序是 : layout -> performResize -> performLayout -> markNeedsPaint

  • mixin extends implement 之间的关系
    继承(关键字 extends)、混入 mixins (关键字 with)、接口实现(关键字 implements)。
    在Flutter中,Mixins是一种在多个类层次结构中复用类代码的方法。mixins的对象是类,mixins绝不是继承,也不是接口,而是一种全新的特性,可以mixins多个类,mixins的使用需要满足一定条件。

    使用mixins的条件:
    mixins类只能继承自object
    mixins类不能有构造函数
    一个类可以mixins多个mixins类
    可以mixins多个类,不破坏Flutter的单继承

    这三者可以同时存在,前后顺序是extends -> mixins -> implements。

  • async、await、Future 和 Stream
    async 和 await:用于简化异步操作的处理,使代码看起来像同步代码。
    Future:表示一次性异步操作的结果,完成后返回单一结果。
    Stream:表示多个异步数据序列,支持多次返回数据更新。

  • flutter中的多线程
    Isolate 是 Dart 中的一个并发模型,类似于线程,但每个 Isolate 有自己独立的内存空间。Isolate 之间不能直接共享内存,需要通过消息传递来通信。
    Future 和 async/await 用于异步操作,常用于网络请求、文件读取等需要等待结果的任务。使用 await 等待 Future 的结果。
    await for 用于异步地遍历 Stream,适合处理一系列异步数据的场景。

  • Stream 有哪两种订阅模式
    Stream 类提供了两种订阅模式:单订阅模式(single-subscription)和广播模式(broadcast)。单订阅就是只能有一个订阅者,而广播是可以有多个订阅者。
    Stream 默认处于单订阅模式,所以同一个 stream 上的 listen 和其它大多数方法只能调用一次,调用第二次就会报错。但 Stream 可以通过 transform() 方法(返回另一个 Stream)进行连续调用。通过 Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream,isBroadcast 属性可以判断当前 Stream 所处的模式。

  • BuildContext 具体是什么东西
    在 Flutter 中,BuildContext 表示了当前 Widget 在树中的位置和上下文信息。它提供了与当前 Widget 相关的信息,如父级 Widget、Theme、MediaQuery、Navigator 等,以便在构建 UI 时使用。BuildContext 实际上是一个指向当前 Widget 在 Widget 树中位置的引用。
    BuildContext 的作用:
    定位位置:BuildContext 可以告诉你当前 Widget 在 Widget 树中的位置,如父级 Widget 是什么、祖先 Widget 是什么等。
    获取相关信息:通过 BuildContext 可以获取当前 Widget 上下文相关的信息,如主题、设备信息、国际化信息等。
    构建 UI:在构建 Widget 时,通常需要使用 BuildContext 来访问上下文信息,例如使用 Theme.of(context) 获取当前主题。

  • Dart异步编程中的Future关键字作用
    Dart中,执行一个异步任务使用Future来处理。在 Dart 的每一个 Isolate 当中,执行的优先级为 :Main > MicroTask > EventQueue。

  • 描述Flutter的核心渲染模块三棵树
    WidgetTree:存放渲染内容、它只是一个配置数据结构,创建是非常轻量的,在页面刷新的过程中随时会重建

    Element 是分离 WidgetTree 和真正的渲染对象的中间层, WidgetTree 用来描述对应的Element 属性,同时持有Widget和RenderObject,存放上下文信息,通过它来遍历视图树,支撑UI结构。

    RenderObject (渲染树)用于应用界面的布局和绘制,负责真正的渲染,保存了元素的大小,布局等信息,实例化一个 RenderObject 是非常耗能的

    当应用启动时 Flutter 会遍历并创建所有的 Widget 形成 Widget Tree,通过调用 Widget 上的 createElement() 方法创建每个 Element 对象,形成 Element Tree。最后调用 Element 的 createRenderObject() 方法创建每个渲染对象,形成一个 Render Tree。

  • Flutter三棵树设计的核心原因与优势‌
    分层解耦:职责分离与性能优化。
    高效更新‌:Element树的Diff机制与RenderObject复用‌。
    状态持久化‌:Element维护动态数据‌。
    ‌跨平台一致性‌:RenderObject统一渲染管线‌。

  • Flutter线程管理模型
    Flutter Engine层会创建一个Isolate,并且Dart代码默认就运行在这个主Isolate上。必要时可以使用spawnUri和spawn两种方式来创建新的Isolate,在Flutter中,新创建的Isolate由Flutter进行统一的管理。
    事实上,Flutter Engine自己不创建和管理线程,Flutter Engine线程的创建和管理是Embeder负责的,Embeder指的是将引擎移植到平台的中间层代码。

    Flutter 中存在的四大线程:分别为 UI Runner、GPU Runner、IO Runner, Platform Runner (原生主线程) ,

    在 Flutter 中可以通过 isolate 或者 compute 执行真正的跨线程异步操作。

  • Future还是isolate使用场景分析
    如果代码段可以独立运行而不会影响应用程序的流畅性,建议使用 Future (需要花费几毫秒时间)。
    如果繁重的处理可能要花一些时间才能完成,而且会影响应用程序的流畅性,建议使用 isolate (需要几百毫秒)。

  • Flutter是如何与原生Android、iOS进行通信的
    Flutter 通过 PlatformChannel 与原生进行交互,其中 PlatformChannel 分为三种:
    BasicMessageChannel :用于传递字符串和半结构化的信息。
    MethodChannel :用于传递方法调用(method invocation)。
    EventChannel : 用于数据流(event streams)的通信。

  • 性能优化

    • 避免Widget无效的重建
      使用const构造函数,对于不会改变的Widget,使用const构造函数创建常量Widget,可以避免不必要的重建。
      合理使用Key,在ListView或GridView等可滚动列表中,为列表项指定Key可以帮助Flutter识别哪些项发生了变化,从而只重建发生变化的项。
      避免在build方法中进行复杂操作,build方法应该只负责构建UI,避免在其中进行复杂的计算或数据处理。

    • 长列表滚动性能问题
      使用 ListView.builder:对于长列表,使用 ListView.builder,它只在滚动时创建可见的项目。
      禁用addRepaintBoundaries(默认开启):对简单列表项关闭重绘边界,减少渲染管线开销‌。
      禁用addAutomaticKeepAlives(默认开启):减少内存占用,必要时手动实现AutomaticKeepAliveClientMixin保持状态‌。
      使用KeepAlive组件配合SliverPersistentHeader,优化头部/尾部固定内容的性能‌。
      使用 CacheExtent:设置 cacheExtent 提前加载部分项目,避免滚动时卡顿。
      图片加载优化:使用 FadeInImage 或 cached_network_image 库优化图片加载。
      设置ScrollAwareImageProvider:判断快速滑动, 下载和解码会停止。
      使用懒加载与分页,避免一次性请求过多资源。
      通过 evictCallback 和 dispose() 确保图片/纹理等敏感资源释放‌。

      1
      2
      3
      4
      CachedNetworkImage(  
      imageUrl: url,
      evictCallback: (url) => true, // 列表项销毁时触发缓存回收
      )
    • 使用异步操作
      使用 compute 函数:将耗时操作移到后台 Isolate 执行,避免阻塞主线程。
      异步编程:使用 Future、Stream 和 async/await 进行异步编程,避免阻塞UI。

    • 布局和绘制性能问题
      避免不必要的层级:尽量减少 Widget 层级,避免过度嵌套。
      使用 RepaintBoundary:将需要频繁重绘的部分包裹在 RepaintBoundary 中,减少重绘范围。
      布局缓存:使用 AutomaticKeepAliveClientMixin 保持列表项的状态,避免重建。

Flutter错误收集

布局溢出
1、使用DebugPaintSizeEnabled将DebugPaintSizeEnabled widget包裹在你的应用根widget上,来在屏幕上绘制出每个widget的边界和大小。这有助于你直观地看到哪些widget可能超出了其父widget的边界。
2、检测工具 Flutter Inspector (debug模式下),使用DevTools
3、在你的widget树中,你可以编写自定义的溢出检测逻辑。这通常涉及到在widget的build方法或自定义RenderObject的performLayout方法中检查子widget的布局尺寸和父widget的约束。

异常收集
1、Dart层异常捕获;可以通过try-catch机制直接捕获
2、Flutter框架错误捕获

  • 异步异常 Future.catchError适用于捕获异步操作中的异常
  • 全局异常捕获 FlutterError.onError全局异常捕获
  • 全局异步回调异常 PlatformDispatcher.instance.onError
  • 使用Zone.runZoned集中捕获:(过时)

3、原生崩溃捕获:集成原生崩溃收集工具、接入firebase、bugly等

总结

总之,Flutter最大的优势在于其跨平台能力,社区成熟。使用Dart语言,可以同时为iOS、Android、PC端开发应用,减少了代码重复,提高了开发效率。但在跨平台上Jetpack Compose版本仅支持5.0以上,社区不如Flutter那样成熟,同时fultter最低版本支持4.1,可以根据业务选择使用。