Flutter应用程序性能优化建议

Flutter 应用程序性能优化建议

视频

www.bilibili.com/video/BV1ht...

前言

原文 ducafecat.com/blog/boosti...

Flutter应用程序默认已经具有良好的性能,因此您只需要避免常见的陷阱,就可以获得出色的性能。

您设计和实现应用程序的用户界面的方式可能会对其运行效率产生重大影响。

本文这些最佳实践建议将帮助您编写性能最佳的Flutter应用程序。

那么让我们开始吧!

正文

代码结构拆分合理

干净架构

细致的拆分

marketplace.visualstudio.com/items?itemN...

使用状态管理

需要一套规范来耦合所有的内容

常见的优秀状态管理有:

  • provider
  • bloc
  • getx
  • riverpod

可以看下各种状态管理文章 ducafecat.com/blog/flutte...

使用代码分析工具

代码分析工具,如Flutter分析器和Lint,对于提高代码质量和减少错误和漏洞的风险非常有帮助。这些工具可以帮助识别潜在问题,防止它们成为问题,并提供改进代码结构和可读性的建议。

shell 复制代码
flutter analyze lib/

使用 Flutter Inspector 进行调试

shell 复制代码
flutter run --debug

之前录过一个 dev tools 性能调优的视频

www.bilibili.com/video/BV1Tb...

ducafecat.tech/2022/03/17/...

懒加载和分页

一次获取和渲染大量数据可能会显著影响性能。实现延迟加载和分页,根据需要加载数据,特别是对于长列表或数据密集的视图。

ListView.builder 方式

dart 复制代码
List<Item> loadItems(int pageNumber) {
}

ListView.builder(
  itemCount: totalPages,
  itemBuilder: (context, index) {
    return FutureBuilder(
      future: loadItems(index),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          // Build your list item here.
        } else {
          return CircularProgressIndicator();
        }
      },
    );
  },
);

pull_to_refresh_flutter 方式

dart 复制代码
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SmartRefresher(
        enablePullDown: true,
        enablePullUp: true,
        header: WaterDropHeader(),
        footer: CustomFooter(
          builder: (BuildContext context,LoadStatus mode){
            Widget body ;
            if(mode==LoadStatus.idle){
              body =  Text("pull up load");
            }
            else if(mode==LoadStatus.loading){
              body =  CupertinoActivityIndicator();
            }
            else if(mode == LoadStatus.failed){
              body = Text("Load Failed!Click retry!");
            }
            else if(mode == LoadStatus.canLoading){
                body = Text("release to load more");
            }
            else{
              body = Text("No more Data");
            }
            return Container(
              height: 55.0,
              child: Center(child:body),
            );
          },
        ),
        controller: _refreshController,
        onRefresh: _onRefresh, // 下拉刷新
        onLoading: _onLoading, // 上拉载入
        child: ListView.builder(
          itemBuilder: (c, i) => Card(child: Center(child: Text(items[i]))),
          itemExtent: 100.0,
          itemCount: items.length,
        ),
      ),
    );
  }

pub-web.flutter-io.cn/packages/pu...

压缩图片

你获取到一张宽 40000 的图片,如果你直接打印,真的噩梦了。

你需要本地压缩后再显示 flutter_image_compress。

pub.dev/packages/fl...

dart 复制代码
  Future<Uint8List> testCompressFile(File file) async {
    var result = await FlutterImageCompress.compressWithFile(
      file.absolute.path,
      minWidth: 2300,
      minHeight: 1500,
      quality: 94,
      rotate: 90,
    );
    print(file.lengthSync());
    print(result.length);
    return result;
  }

优化动画

避免使用对应用程序性能产生影响的繁重或复杂的动画,尤其是在旧设备上。谨慎使用动画,并考虑使用Flutter内置的动画,如 AnimatedContainerAnimatedOpacity 等。

dart 复制代码
// 1 秒太长了
AnimatedContainer(
  duration: Duration(seconds: 1),
  height: _isExpanded ? 300 : 1000,
  color: Colors.blue,
);

// 缩短动画时长
AnimatedContainer(
  duration: Duration(milliseconds: 500),
  height: _isExpanded ? 300 : 100,
  color: Colors.blue,
);

优化应用程序启动时间

通过优化初始化过程来减少应用程序的启动时间。使用 flutter_native_splash 包在应用程序加载时显示启动画面,并延迟非必要组件的初始化直到应用程序启动后。

pub.dev/packages/fl...

多些组件抽取

不要去写层次很深的代码, 多些代码抽取。

dart 复制代码
  // 主视图
  Widget _buildView() {
    List<Widget> ws = [];
    
    // 标题
    if (title != null) {
      ws.add(_buildTitle(title!));
    }

    // 统计栏
    ws.add(_buildTotalBar(win, draw, lose, winAvg, loseAvg));

    // 视图
    for (var item in fixtures) {
      // 栏
      if (item.league?.id != lastLeagueId) {
        lastLeagueId = item.league?.id ?? 0;
        ws.add(_buildLeagueBar(lastLeagueId, item.league?.name ?? ""));
      }

      // 行
      ws.add(_buildRow(item));

      // 分隔符
      ws.add(Container(
        height: 0.5,
        color: AppColors.surfaceVariant,
      ));
    }

    return ws.toColumn();
  }

使用级联(..)

如果你刚开始使用Flutter,你可能还没有使用过这个运算符,但当你想在同一个对象上执行某些任务时,它非常有用。

dart 复制代码
//Bad
var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

//Good
var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

使用扩展运算符(...)

其它语言也有,可以用来合并集合,只是下面的用法太神奇。

dart 复制代码
// 很啰嗦
@override
Widget build(BuildContext context) {
  bool isTrue = true;
  return Scaffold(
    body: Column(
      children: [
        isTrue ? const Text('One') : Container(),
        isTrue ? const Text('Two') : Container(),
        isTrue ? const Text('Three') : Container(),
      ],
    ),
  );
}

// 才知道可以这样用 ... 符号
@override
Widget build(BuildContext context) {
  bool isTrue = true;
  return Scaffold(
    body: Column(
      children: [
        if(isTrue)...[
          const Text('One'),
          const Text('Two'),
          const Text('Three')
        ]
      ],
    ),
  );
}

抽取你的样式定义

dart 复制代码
// 繁琐
Column(
  children: const [
    Text(
      'One',
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.normal,
      ),
    ),
    Text(
      'Two',
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.normal,
      ),
    ),
  ],
)

// 复用 重构
Column(
  children: [
    Text(
      'One',
      style: Theme.of(context).textTheme.subtitle1,
    ),
    Text(
      'Two',
      style: Theme.of(context).textTheme.subtitle1,
    ),
  ],
),

局部刷新

StatefulBuilder 方式

dart 复制代码
 int a = 0;
 int b = 0;

 // 1、定义一个叫做"aState"的StateSetter类型方法;
 StateSetter? aState;

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           // 2、将第一个"ElevatedButton"组件嵌套在"StatefulBuilder"组件内;
           StatefulBuilder(
             builder: (BuildContext context, StateSetter setState) {
               aState = setState;
               return ElevatedButton(
                 onPressed: () {
                   a++;
                   // 3、调用"aState"方法对"StatefulBuilder"内部进行刷新;
                   aState(() {});
                 },
                 child: Text('a : $a'),
               );
             },
           ),
           ElevatedButton(
             onPressed: () {
               b++;
               setState(() {});
             },
             child: Text('b : $b'),
           ),
         ],
       ),
     ),
   );
 }

也可以用 getx GetBuilder 这种状态组件实现局部刷新。

定义 GetBuilder,设置 id 名称

dart 复制代码
  @override
  Widget build(BuildContext context) {
    return GetBuilder<HomeIndexController>(
      init: HomeIndexController(),
      id: "home_index",
      builder: (_) {
        return Scaffold(
          appBar: appBarWidget(...),
          body: _buildView(),
        );
      },
    );
  }

控制器触发, 制定 id 名称,可以是一个列表

dart 复制代码
update(["home_index"]);

多使用 Widget 抽取组件,而不是函数

您可以节省CPU周期,并使用const构造函数,在仅在需要时进行重建,并获得更多的好处(例如重用等)。

dart 复制代码
// 定义成 widget
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      HeaderWidget(), 
      SubHeaderWidget(), 
      ContentWidget()
    ]
  );
}

使用 final

使用 final 关键字可以极大地提高您的应用程序的性能。当一个值被声明为 final 时,它只能被设置一次,之后不会再改变。这意味着框架不需要不断地检查变化,从而提高了性能。

dart 复制代码
  final String tag;
  final Color? color;
  final Size? size;
  final double? radius;
  final Color? fontColor;

使用 const

如果已经定义了,您可以使用相同的 Widget 来节省RAM。 const widgets 在编译时创建,因此在运行时更快。

dart 复制代码
x = const Container(); 
y = const Container(); 

使用 const 类构造函数

这有助于 Flutter 仅重新构建应更新的 Widget。

dart 复制代码
class FbTagWidget extends StatelessWidget {
  const FbTagWidget(this.tag,
      {super.key, this.color, this.size, this.radius, this.fontColor});

尽可能使用 private 关键词

这更像是 Dart 的最佳实践,而不是性能。

但是,最佳实践可以在某种程度上提高性能,比如理解代码,减少复杂性等等。

dart 复制代码
class Student{
  String _name;
  String _address;
  Student({
    required String name,
    required String address,
  }): 
  _name = name,
  _address = address;
}

使用nil代替const Container()

零消耗

dart 复制代码
// 原来
text != null ? Text(text) : const Container()

// 后来
text != null ? Text(text) : const SizedBox()

// 现在
text != null ? Text(text) : nil 

在ListView中使用itemExtent来处理长列表

这有助于Flutter计算滚动位置,而不是计算每个 Widget 的高度,并使滚动动画更加高效。

默认情况下,每个子项都必须确定其范围,这在性能方面是非常昂贵的。显式设置值可以节省大量的CPU周期。列表越长,使用此属性可以获得更多的速度提升。

dart 复制代码
final List<int> _listItems = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9];

@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemExtent: 150,
    itemCount: _listItems.length, 
    itemBuilder: (context, index) {
      var item = _listItems[index];
      return Center(
        child: Text(item.toString())
      );
    }
}

避免在 setState 中使用 AnimationController

错误的方式,将 addListener 去掉。

dart 复制代码
void initState() {
  _controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 1), 
  )..addListener(() => setState(() {})); 
}

Column( 
  children: [
    Placeholder(), // rebuilds 
    Placeholder(), // rebuilds 
    Placeholder(), // rebuilds 
    Transform.translate( // rebuilds
      offset: Offset(100 * _controller.value, 0),
      child: Placeholder(), 
    ),
  ], 
),

使用Keys来加速Flutter性能

在Flutter中,使用Keys可以帮助加速性能并优化应用程序的重建过程。Keys在Flutter中有多种用途,其中一项重要的功能是帮助Flutter识别小部件树中的特定小部件,从而在进行重建时更有效地更新小部件。以下是一些示例,说明如何使用Keys来加速Flutter性能:

保留状态 :使用GlobalKey作为Key的一种常见用法是在需要保留小部件状态的情况下。通过在重建时将相同的GlobalKey分配给相同类型的小部件,可以确保小部件在重建后保留其先前的状态,而不会丢失用户的输入或滚动位置。

dart 复制代码
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      // Widget content
    );
  }
}

列表中的重用 :在ListViewGridView等可滚动列表中,使用Key可以帮助Flutter跟踪列表项并在数据源更改时有效地更新列表项,而无需重新创建整个列表。

dart 复制代码
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      key: Key(items[index].id.toString()),
      title: Text(items[index].title),
    );
  },
)

动态添加或移除小部件 :在动态添加或移除小部件时,使用Key可以帮助Flutter正确识别要添加或移除的小部件,而不会影响其他部分的布局。

dart 复制代码
List<Widget> widgets = [
  Container(key: Key('1'), child: Text('Widget 1')),
  Container(key: Key('2'), child: Text('Widget 2')),
];

// Add a new widget
widgets.add(Container(key: Key('3'), child: Text('Widget 3')));

// Remove a widget
widgets.removeWhere((widget) => widget.key == Key('2'));

通过使用Keys,开发人员可以更精确地控制Flutter小部件树的重建过程,避免不必要的重建,提高应用程序的性能和响应性。

使用 ListView 列表视图时优化内存

dart 复制代码
ListView.builder( 
  ...
  addAutomaticKeepAlives: false (true by default)
  addRepaintBoundaries: false (true by default) 
);
  • addAutomaticKeepAlives 当这个属性设置为true时,Flutter会尝试在滚动列表时保留列表项的状态。这意味着即使列表项在屏幕外被移除,它们的状态仍然会被保留,以便在滚回到它们时可以保持其状态。
  • addRepaintBoundaries 当这个属性设置为true时,Flutter会尝试在列表项之间创建重绘边界。这意味着在滚动列表时,只有在需要时才会重绘列表项,而不是每次滚动都重绘所有内容。

使用 for/while 代替 foreach/map

如果你要处理大量的数据,使用正确的循环可能会对你的性能产生影响。

预缓存您的图片和图标

图片

dart 复制代码
precacheImage(
  AssetImage(imagePath), 
  context
);

svg

dart 复制代码
precachePicture( 
  ExactAssetPicture(SvgPicture.svgStringDecoderBuilder, iconPath), 
  context
);

使用SKSL预热

如果一个应用在第一次运行时的动画不流畅,但后来相同的动画变得流畅,那很可能是由于着色器编译引起的不流畅。

dart 复制代码
flutter run --profile --cache-sksl --purge-persistent-cache
flutter build apk --cache-sksl --purge-persistent-cache

使用 RepaintBoundary

RepaintBoundary是一个 Widget ,用于将其子部件的绘制内容分离为单独的绘制层。这样做的主要目的是减少不必要的重绘操作,提高应用程序的性能。当RepaintBoundary包裹一个子部件时,该子部件及其所有子部件将被视为一个整体,即使其中的其他部分发生重绘,RepaintBoundary内的内容也不会重绘。

RepaintBoundary的主要作用包括:

  1. 减少重绘范围 :通过将子部件包裹在RepaintBoundary中,可以将其视为一个整体,仅在该部件内部发生重绘时才重新绘制,而不会影响到其他部分。

  2. 性能优化:避免不必要的重绘操作,可以提高应用程序的性能,特别是在具有复杂界面或动态内容的情况下。

  3. 避免全局重绘 :在某些情况下,只需要更新特定部分的UI,而不是整个界面。通过使用RepaintBoundary,可以限制重绘的范围,避免全局重绘。

  4. 边界控制 :可以通过RepaintBoundary来控制重绘的边界,确保只在需要时才进行重绘操作,而不会影响到其他部分。

RepaintBoundary是一个有用的工具,可以帮助优化Flutter应用程序的性能,特别是在需要控制重绘范围和避免不必要重绘操作的情况下。在开发复杂界面或需要动态更新的应用程序时,合理使用RepaintBoundary可以提高应用程序的性能和用户体验。

dart 复制代码
class RepaintBoundaryExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('This is inside RepaintBoundary'),
          SizedBox(height: 20),
          CustomPaint(
            size: Size(200, 200),
            painter: MyPainter(),
          ),
        ],
      ),
    );
  }
}

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 5
      ..style = PaintingStyle.stroke;

    canvas.drawRect(Rect.fromLTWH(50, 50, 100, 100), paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

使用 Listview.builder

dart 复制代码
Listview.builder()

用了之后不出现在屏幕上的元素,不渲染。

不要使用 ShrinkWrap 来包裹可滚动 Widget

ShrinkWrap 动态确定子组件大小。

包裹滚动组件后可能有布局错误、性能问题。

处理高消耗操作时用 isolates

比如处理非常大的 json 文件、视频压缩。

这样不会卡主线程。

可以用一些 Dart 包简化代码。

pub-web.flutter-io.cn/packages/fl...

不要过度使用 isolates

如果你在每个最小的操作中都使用 isolates,你的应用程序可能会非常卡顿。

这是因为生成一个 isolates 并不是一项廉价的操作。它需要时间和资源。

释放你不用的内存数据

比如你载入一个图片数据进行加工,如加文字、加二维码,不用的时候请释放。

压缩数据处理

为了节省内存,请压缩您的数据。

比如你载入了百兆的 json 文件,你可以压缩起来放在内存中。

dart 复制代码
final response = await rootBundle.loadString('assets/en_us.json');

final original = utf8.encode(response); 

final compressed = gzip.encode(original); 
final decompress = gzip.decode(compressed);

final enUS = utf8.decode(decompress);

保持 Flutter 新稳定版本

在每个版本中,Flutter都变得越来越快。

所以不要忘记及时更新你的Flutter版本,并继续创作出令人惊艳的作品!

注意用稳定版。

docs.flutter.dev/release/arc...

请多准备几台真机调试

始终在真实设备上测试您的应用程序性能,包括较旧的型号,以便发现在模拟器或较新设备上可能不明显的性能问题。

使用StatelessWidget而不是StatefulWidget

一个 StatelessWidget 比一个 StatefulWidget 更快,因为它不需要像其名称所暗示的那样管理状态。

所以如果可能的话,你应该优先选择它。

不要使用OpacityWidget

Opacity Widget在与动画一起使用时可能会导致性能问题,因为 Opacity Widget的所有子Widget都会在每个新帧中重新构建。在这种情况下,最好使用 AnimatedOpacity 。如果您想要淡入一张图片,请使用FadeInImageWidget。如果您想要具有不透明度的颜色,请绘制具有不透明度的颜色。

dart 复制代码
//不推荐
Opacity(opacity: 0.5, child: Container(color: Colors.red))

//推荐
Container(color: Color.fromRGBO(255, 0, 0, 0.5))

使用SizedBox而不是Container

一个 Container Widget非常灵活。例如,您可以自定义填充或边框,而无需将其嵌套在另一个Widget中。但是,如果您只需要一个具有特定高度和宽度的框,最好使用 SizedBox Widget。它可以被设置为const,而 Container 则不行。

Row/Column, 中添加空格时,更倾向于使用 SizedBox 而不是 Container

dart 复制代码
@override
Widget build(BuildContext context) {
return Column(
  children: [ 
      Text(header),
      const SizedBox(height: 10), 
      Text(subheader), 
      Text(content)
    ]
  ); 
}

不要用 Clip

Clip是一项非常昂贵的操作,当你的应用程序变慢时应该避免使用。如果Clip行为设置为 Clip.antiAliasWithSaveLayer ,它的代价会更高。尝试找到其他不需要Clip的方法来实现你的目标。例如,可以使用 borderRadius 属性来创建带有圆角边框的矩形,而不是使用Clip。

dart 复制代码
Container(
  width: 100,
  height: 100,
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(50),
    image: DecorationImage(
      image: NetworkImage('https://example.com/image.jpg'),
      fit: BoxFit.cover,
    ),
  ),
)

使用Offstage

OffstageWidget允许您隐藏一个Widget,而不需要从Widget树中移除它。这对于提高性能很有用,因为框架不需要重新构建隐藏的Widget。

dart 复制代码
Offstage(
  offstage: !showWidget,
  child: MyWidget(),
)

在Flutter中, Offstage Widget用于在布局中隐藏子Widget,同时仍然是树的一部分。它可以用于有条件地显示或隐藏子Widget,而无需重新构建整个树。

Opacity Widget用于控制子Widget的透明度。它接受一个介于0.0和1.0之间的值,其中0.0表示完全透明,1.0表示完全不透明。然而,重要的是要注意它可能会影响性能,所以只在必要时使用。

Visibility Widget用于控制子Widget的可见性。它可以用于有条件地显示或隐藏子Widget,而无需重新构建整个树。

所有三个Widget都用于控制子Widget的显示,但它们的方式不同。

Offstage控制布局,Opacity控制透明度,Visibility控制可见性。

使用 addPostFrameCallback

在某些情况下,我们需要在帧渲染后执行某些操作。不要尝试使用任何延迟函数,也不要创建自定义回调!我们可以使用 WidgetsBinding.instance.addPostFrameCallback 方法来实现。这个回调将在帧渲染后被调用,并通过避免不必要的重建来提高性能。

dart 复制代码
WidgetsBinding.instance.addPostFrameCallback((_) {
 // ...
});

使用 AutomaticKeepAliveClientMixin

当使用 ListViewGridView 时,子部件可以被多次构建。为了避免这种情况,我们可以使用 AutomaticKeepAliveClientMixin 来处理子部件。这将保持子部件的状态并提高性能。

dart 复制代码
class MyChildWidget extends StatefulWidget {
  @override
  _MyChildWidgetState createState() => _MyChildWidgetState();
}

class _MyChildWidgetState extends State<MyChildWidget> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  
  @override
  Widget build(BuildContext context) {
    return Text("I am a child widget");
  }
}

在这个例子中, MyChildWidget 类使用 AutomaticKeepAliveClientMixin 混入,并且 wantKeepAlive 属性被设置为 true 。这将保持 MyChildWidget 的状态,并防止它被多次重建,从而提高性能。

避免使用 MediaQuery.of(context).size

当你在Flutter中使用MediaQuery.of(context).size时,Flutter会将你的小部件与MediaQuery的大小相关联。这意味着每次调用MediaQuery.of(context).size时,Flutter会检测MediaQuery的大小是否发生变化,从而可能导致不必要的重建(rebuilds)。

使用MediaQuery.sizeOf(context)来避免这些不必要的重建,从而提高应用程序的响应性。通过使用MediaQuery.sizeOf(context),你可以绕过与MediaQuery大小相关的重建过程,从而减少不必要的性能开销。

类似的优化方法也适用于其他MediaQuery方法。举例来说,建议使用MediaQuery.platformBrightnessOf(context)而不是MediaQuery.of(context).platformBrightness,以避免不必要的重建,从而提高应用的响应性。

不要在调试模式下测量性能

一个用于性能和内存测量的特殊模式,即Profile模式。您可以通过Android Studio或Visual Studio Code等IDE运行它,也可以通过执行以下CLI命令来运行:

shell 复制代码
flutter run -profile

不要在模拟器中测量性能

多用真机性能调试

小结

优化Flutter应用的性能对于提供无缝的用户体验至关重要。通过实施这些提示,您可以进一步优化Flutter应用的性能。请记住,性能优化是一个持续的过程,定期进行分析和测试是确保应用程序保持高性能标准的关键。

感谢阅读本文

如果有什么建议,请在评论中让我知道。我很乐意改进。


© 猫哥 ducafecat.com

end

相关推荐
jiejiejiejie_6 小时前
Flutter for OpenHarmony 底部选项卡与多语言适配小记:让 App 更贴心的两次小升级✨
flutter·华为·harmonyos
jiejiejiejie_7 小时前
Flutter for OpenHarmony 应用更新检测与萌系搜索功能实战小记✨
flutter·华为·harmonyos
IntMainJhy7 小时前
Flutter 三方库 Firebase Messaging 鸿蒙化适配与实战指南(权限检查+设备Token获取全覆盖)
flutter·华为·harmonyos
liulian09168 小时前
Flutter 依赖注入与设备信息库:get_it 与 device_info_plus 的 OpenHarmony 适配指南总结
flutter·华为·学习方法·harmonyos
里欧跑得慢9 小时前
微交互设计模式:提升用户体验的细节之美
前端·css·flutter·web
stringwu9 小时前
Flutter GetX 核心坑及架构选型与可替换性方案
前端·flutter
IntMainJhy9 小时前
【flutter for open harmony】第三方库Flutter 国际化多语言的鸿蒙化适配与实战指南
数据库·flutter·华为·sqlite·harmonyos
liulian09169 小时前
【Flutter for OpenHarmony 】地图功能适配与位置显示实现指南
flutter·华为·学习方法·harmonyos
IntMainJhy10 小时前
【flutter for open harmony】Flutter SQLite 本地数据库的鸿蒙化适配与实战指南
数据库·flutter·sqlite
IntMainJhy10 小时前
【flutter for open harmony】第三方库「Flutter 聊天组件鸿蒙化适配与实战:从零搭建鸿蒙跨平台聊天页面」
flutter·华为·harmonyos