Flutter Tips 小技巧(更新中)

6,272 阅读8分钟

大佬们点波关注呀,从小家境贫寒,没见过四位数字,让我关注人数破千吧~

1.销毁和未创建调用

及时停止或者销毁监听,例如一个定时器:

Timer _countdownTimer;
  @override
  void dispose() {
    _countdownTimer?.cancel();
    _countdownTimer = null;
    super.dispose();
  }

为了保险我们还要在调用setState()前判断当前页面是否存在:

_countdownTimer = Timer.periodic(Duration(seconds: 2), (timer) {
    if (mounted){
      setState(() {
        
      });
    }
  });

2.先绘制再请求

addPostFrameCallback 回调方法在Widget渲染完成时触发,所以一般我们在获取页面中的 Widget 大小、位置时使用到。

解决方法就是使用 addPostFrameCallback 回调方法,等待页面 build 完成后在请求数据:

@override
void initState() {
  WidgetsBinding.instance.addPostFrameCallback((_){
    /// 接口请求
  });
}

3.保持页面状态

比如点击导航栏来回切换页面,默认情况下会丢失原页面状态,也就是每次切换都会重新初始化页面。这种情况解决方法就是 PageViewBottomNavigationBar结合使用,同时子页面 State 中继承 AutomaticKeepAliveClientMixin 并重写 wantKeepAlive 为true。代码大致如下:

class _TestState extends State<Test> with AutomaticKeepAliveClientMixin{
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Container();
  }
  @override
  bool get wantKeepAlive => true;
}

4.预先缓存图片

在 Flutter 中,加载本地图片会存在一个加载过程。比如点击图标做图标的切换时,那么首次会发生闪动的情况。尤其是做类似引导页这类需求是,通过左右滑动切换图片时会发生比较明显的白屏一闪而过。

https://cdn.nlark.com/yuque/0/2019/gif/302712/1576208285207-344db2c0-0fba-430b-a7c9-3a4b04c3eb3b.gif#align=left&display=inline&height=800&margin=[object Object]&originHeight=800&originWidth=400&size=0&status=done&style=none&width=400

解决方法很简单,就是使用 precacheImage,它将图像预存到图像缓存中。如果图像稍后被 Image、BoxDecation 或 FadeInImage 使用,它会被加载得更快。

precacheImage(AssetImage("assets/logo"), context);

本问题详细的代码见:点击查看

5.屏幕方向

新建的 Flutter 项目默认并没有限制屏幕的横竖屏,所以如果你的项目并没有适配横竖屏,需要限制某一方向。我以限制竖屏为例:

void main(){
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown
  ]).then((_){
    runApp(MyApp());
  });
}

6.FutureBuilder 懒加载

Flutter 中通过 FutureBuilder 或者 StreamBuilder 可以和简单的实现懒加载,通过 future 或者 stream “异步” 获取数据,之后通过 AsyncSnapshot 的 data 再去加载数据,至于流和异步的概念,以后再展开吧。

const FutureBuilder({
    Key key,
    this.future,//获取数据的方法
    this.initialData,
    @required this.builder//根据快照的状态,返回不同的widget
  }) : assert(builder != null),
       super(key: key);

future 就是一个定义的异步操作,注意要带上泛型,不然后面拿去 snapshot.data 的时候结果是 dynamic 的 snapshot 就是 future 这个异步操作的状态快照,根据它的 connectionState 去加载不同的 widget 有四种快照的状态:

enum ConnectionState {
   //future还未执行的快照状态
  none,
  //连接到一个异步操作,并且等待交互,一般在这种状态的时候,我们可以加载个菊花
  waiting,
  //连接到一个活跃的操作,比如stream流,会不断地返回值,并还没有结束,一般也是可以加载个菊花
  active,
  //异步操作执行结束,一般在这里可以去拿取异步操作执行的结果,并显示相应的布局
  done,
}

7.StreamBuilder 流控制管理

从一端发射一个事件,从另外一端去监听事件的变化,通过 Stream 我们可以在 Flutter 上设计出基于事件流驱动的响应式代码逻辑。常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。

class StreamDemo extends StatefulWidget {
  @override
  _StreamDemoState createState() => _StreamDemoState();
}

class _StreamDemoState extends State<StreamDemo> {
  var index = 0;
  var streamController;
  StreamSink<String> get streamSink => streamController.sink;
  Stream<String> get streamData => streamController.stream;

  @override
  void initState() {
    super.initState();
    streamController = StreamController<String>();
    streamSink.add("0");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('streamBuilder')),
        body: StreamBuilder<String>(
          stream: streamData,
          builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
            return Text('Result: ${snapshot.data}');
          },
        ),
        floatingActionButton: FloatingActionButton(
            onPressed: onFloatActionButtonPress, child: Icon(Icons.add)));
  }

  void onFloatActionButtonPress() {
    index++;
    streamSink.add(index.toString());
  }
}

8.Future 发起多个异步操作的方式

复杂耗时操作或者多个网络请求并对完整性要求较高的操作,可以使用 Future 异步方法:

Future.wait([
  // 2秒后返回结果  
  Future.delayed(new Duration(seconds: 2), () {
    return "hello";
  }),
  // 4秒后返回结果  
  Future.delayed(new Duration(seconds: 4), () {
    return " world";
  })
]).then((results){
  print(results[0]+results[1]);
}).catchError((e){
  print(e);
});

9.Text 上下边距调整

https://cdn.nlark.com/yuque/0/2019/png/302712/1577269455072-eb175824-798b-4da2-a786-e60a062aa92b.png#align=left&display=inline&height=741&margin=[object Object]&name=IeWYG.png&originHeight=741&originWidth=2078&size=59953&status=done&style=none&width=2078

左侧的彩色矩形是支柱(尽管实际上支柱没有宽度)。这个矩形的高度是最小的线条高度。这条线不能再短了。但它可以更高。

  • 上升是从基线到文本顶部的距离(由字体定义,而不是任何特定的字形)
  • 下降是从基线到文本底部的距离(由字体定义,而不是任何特定的字形)

前导(读作“ledding”,如旧排字机用来排字的铅字金属)是一行底端和下一行顶端之间的距离。在支柱中,一半的引线放在顶部,一半放在底部。是图中的灰色区域。 > 可以使用乘数更改支柱的垂直尺寸。

class TextLineHeight extends StatelessWidget {
  final String textContent;

  final double leading;
  final double textLineHeight;
  final double fontSize;

  TextLineHeight({
    this.textContent,
    this.leading: 0,
    this.textLineHeight: 0.93,
    this.fontSize: 26,
  });

  @override
  Widget build(BuildContext context) {
    return Transform.translate(
      offset: Offset(0, fontSize/11.52),
      child: Text(
        textContent,
        strutStyle: StrutStyle(
          forceStrutHeight: true,
          height: textLineHeight,
          leading: leading,
          fontSize: fontSize,
        ),
        style: TextStyle(
          fontSize: fontSize,
          color: Colors.black,
        ),
      ),
    );
  }
}

效果对比:

https://cdn.nlark.com/yuque/0/2019/png/302712/1577269400049-1e0b56c7-a0a2-4ccd-a278-3417ab2ddb3e.png#align=left&display=inline&height=154&margin=[object Object]&name=截屏2019-12-25下午6.22.51.png&originHeight=504&originWidth=456&size=25753&status=done&style=none&width=139

10.空判断

使用 null-aware operators 判断 null,减少代码量。

// User below
title ??= "Title";

// instead of
if (title == null) {
  title = "Title";
}

上面的方法可以在只有一层判断中做保护,如果你有一连串的’.’取值,那就需要用这种方法了。

xxCount = xxModel?.xxInfo?.xxCount ?? 0

11.VSCode 配置项

{
    "version": "0.2.0",
    "configurations": [
        
        {
            "name": "Main",
            "type": "dart",
            "request": "launch",
            "program": "lib/main.dart”,
            "flutterMode": "profile" # 测试完后记得把它改回去!
        },
        {
            "name": "Dev",
            "type": "dart",
            "request": "launch",
            "program": "lib/main_dev.dart",
            "args": [
                "--flavor",
                "universal",
            ],
            "flutterMode": "profile" # 测试完后记得把它改回去!
        },
        {
            "name": "Prod",
            "type": "dart",
            "request": "launch",
            "program": "lib/main_prod.dart",
            "args": [
                "--flavor",
                "universal",
            ],
        },
    ]
}

12.使用私人库 pubspec.yaml

可以指定 Git 私人库,引用链接、分支、tag,使用示例:

dependencies:
  # 混编路由库
  flutter_boost:
    git:
      url: git@github.com:ReverseScale/flutter_boost.git
      ref: v1.1

官网:dart.dev/tools/pub/d…

13.检查是否为发布环境

判断当前环境是否为发布模式:

const bool kReleaseMode = bool.fromEnvironment('dart.vm.product')

使用系统级顶级状态 kReleaseMode 获取当前是否为发布环境:

import 'package:flutter/foundation.dart';
print('Is Release Mode: $kReleaseMode');

使用这个可以用于控制日志输出,比如 release 模式关闭日志:

if (isProduction) {
  debugPrint = (String message, {int wrapWidth}) => {};
}

14.状态栏的高度

final double statusBarHeight = MediaQuery.of(context).padding.top;

15.底部栏的高度

final double bottomHeight = MediaQuery.of(context).padding.bottom;

16.科学计数法表达

// 输出:1e-8
var number = 1 / pow(10, 8);
// 输出:0.00000001
print(aaaa.toStringAsFixed(8));

17.InheritedWidget 存值方法

1.声明 ShareDataWidget:

class ShareDataWidget extends InheritedWidget {
  int count;

  ShareDataWidget({
    @required this.count,
    Widget child
  }): super(child: child);

  static ShareDataWidget of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(ShareDataWidget);
  }
  
  @override
  bool updateShouldNotify(InheritedWidget oldWidget) {
    return true;
  }
}

2.存入传递值,例如:body 下嵌套传值:

body: ShareDataWidget(
          count: this.widget.count,
          child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: <Widget>[
                  Text("Widget数量: ${this.widget.count}"),
                  _Counter(),
                  RaisedButton(
                    child: Text("增加"),
                    onPressed: () => setState(() => widget.count ++),
                  )
              ]
            ),
          ),
        )

3.取出传递值:

ShareDataWidget sdw = context.inheritFromWidgetOfExactType(ShareDataWidget);
print(sdw.count);

18.Cocoapod 引入 Flutter Framework

flutter_application_path = '../Seller/'load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')install_all_flutter_pods(flutter_application_path)

19.mixins 语法(高级)

class Musician extends Performer with Musical {
  // ···
}
class Maestro extends Person
    with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}
mixin Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;
  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}

20.泛型示例

var names = <String>['Seth', 'Kathy', 'Lars'];
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};
To specify one or more types when using a constructor, put the types in angle brackets (<...>) just after the class name. For example:
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
var nameSet = Set<String>.from(names);
var views = Map<int, View>();

打印泛型类型

class Foo<T extends SomeBaseClass> {
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}
class Extender extends SomeBaseClass {...}

21.导入库

库冲突

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

导入库的一部分

// Import only foo.
import 'package:lib1/lib1.dart' show foo;
// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

懒加载库

import 'package:greetings/hello.dart' deferred as hello;
Future greet() async {
  await hello.loadLibrary();
  hello.printGreeting();
}

22.Generators(迭代生成器)

同步的Generators

Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

异步的Generators

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

23.定义元数据

示例

library todo;
class Todo {
  final String who;
  final String what;
  const Todo(this.who, this.what);
}
使用
import 'todo.dart';
@Todo('seth', 'make this do something')
void doSomething() {
  print('do something');
}

24.私有 Pub

地址:pub.dev/packages/pu…

~ $ git clone <https://github.com/dart-lang/pub_server.git>
~ $ cd pub_server
~/pub_server $ pub get
...
~/pub_server $ dart example/example.dart -d /tmp/package-db

参数释义:

-s 是否fetch官方仓库
-h ${ip / domain}
-p 端口
-d 上传上来的插件包在服务器上的存储地址

使用它可以将新软件包上载到本地运行的服务器或下载本地可用的软件包或通过回退到以下软件包 pub.dartlang.org 很容易:

~/foobar $ export PUB_HOSTED_URL=http://localhost:8080
~/foobar $ pub get
...
~/foobar $ pub publish
Publishing x 0.1.0 to <http://localhost:8080>:
|-- ...
'-- pubspec.yaml

Looks great! Are you ready to upload your package (y/n)? y
Uploading...
Successfully uploaded package.

完善 pubspec.yaml 文件

插件名称
description: 插件描述
version: 0.0.1 版本号
author: xxxx<xx@xxx.com>
homepage: 项目主页地址
publish_to: 填写私有服务器的地址(如果是发布到flutter pub则不用填写,插件默认是上传到flutter pub)

发布至私有服务器

flutter packages pub publish --server $服务器地址

25.原生和 Flutter 之间数据交互类型有限制

在进行插件的开发时,就必定会涉及到原生和 Flutter 之间的数据交互.这里需要注意的是,就像我们在进行 ReactNative 和 JNI 的开发时,并不是什么类型的数据都是支持交互的。下面我给出原生和 Flutter 之间可交互的数据类型:

数据类型对比

这里我们用得最多的就是 boolintStringMap 这几个类型了。

26.检测模式

原来 flutter 已经帮我们封装好了检测模式的方法了:

import 'package:flutter/foundation.dart';
if(kReleaseMode){
    // release 模式该做的事
}
if(kProfileMode){
    // profile 模式该做的事
}
if(kDebugMode){
    // debug 模式该做的事情
}

看下源码:

// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// A constant that is true if the application was compiled in release mode.
///
/// More specifically, this is a constant that is true if the application was
/// compiled in Dart with the '-Ddart.vm.product=true' flag.
///
/// Since this is a const value, it can be used to indicate to the compiler that
/// a particular block of code will not be executed in release mode, and hence
/// can be removed.
const bool kReleaseMode = bool.fromEnvironment('dart.vm.product', defaultValue: false);

/// A constant that is true if the application was compiled in profile mode.
///
/// More specifically, this is a constant that is true if the application was
/// compiled in Dart with the '-Ddart.vm.profile=true' flag.
///
/// Since this is a const value, it can be used to indicate to the compiler that
/// a particular block of code will not be executed in profle mode, an hence
/// can be removed.
const bool kProfileMode = bool.fromEnvironment('dart.vm.profile', defaultValue: false);

/// A constant that is true if the application was compiled in debug mode.
///
/// More specifically, this is a constant that is true if the application was
/// not compiled with '-Ddart.vm.product=true' and '-Ddart.vm.profile=true'.
///
/// Since this is a const value, it can be used to indicate to the compiler that
/// a particular block of code will not be executed in debug mode, and hence
/// can be removed.
const bool kDebugMode = !kReleaseMode && !kProfileMode;

源码简简单单的定义了三个常量:kReleaseModekProfileModekDebugMode 分别对应着 release 模式、 profile 模式、debug 模式

原理也就是对 bool.fromEnvironment() 的封装,但是少些的东西,而且还给了默认值,用它没错了。

27.断言 assert 用处

assert 只会在 debug 模式才会运行,不仅可以用来判断运行模式,还可以用来检查程序是否按要求运行。

比如:一个函数传入的参数不能为 null:

void test(String name){
  assert((){
        print('用户名称为空');
        name!=null
    }());
  // 其他事情
}

上面的例子是 name 是从后台获取的用户名称,但是如果没获取到 或者后台报错,那么就可以很快定位错误了。

28.数据传递的三种方案

  1. InheritedWidget: 适用于父组件传递给子组件的场景, 可跨层级。
  2. Notification:适用于子组件通知父组件数据改变的场景。
  3. Event Bus:事件广播,可适于于各种类型的数据通知同步。

29.直播绘制瓶颈问题

Flutter定义的 Channel 机制从本质上说是提供了ー个消息传送机制,用于图像等数据的传输,这必然会引起内存和 CPU 的巨大消耗。

CleanShot_2021-04-06_at_09.20.212x.png

解决:外接纹理 + PixelBuffer 通道传输 OpenGLES Texture 给 Skia 直接绘制。

  • **节省 CPU 时间。**从我们的测试结果来看,Android 机型上ー帧 720P 的 RGBA 格式的视频,从 GPU 读取到 CPU 需要 5ms 左右,从 CPU 再传送到 GPU 又需要 5ms 左右,即使引入了 PBO,也还有 5ms 左右的耗时,这对于高帧率场景显然是不能接受的。
  • **节省 CPU 内存。**数据都在 GPU 中传递,对于图片场景尤其适用,因为可能同一时间会有很多图片需要展示。

30.Sentry 异常收集

Flutter 引擎已经默认帮助我们收集了未处理异常并提供了统一处理这些异常的接口,通过给 FlutterError.onError 注册回调,这些异常就可以被手动处理,再配合 Sentry 即可实现自动地收集异常及其详细的堆栈信息。

添加 Sentry 的 Dart 版客户端 并使用 DSN 进行初始化,DSN 可以在 Sentry 面板中找到。

import 'package:sentry/sentry.dart';

final sentry = SentryClient(dsn: 'DSN');

引入 flutter/fondation 并注册回调,这些异常信息将通过 Sentry 客户端进行上传。

import 'package:flutter/foundation.dart';
...
FlutterError.onError = (e) =>
        sentry.captureException(exception: e.exception, stackTrace: e.stack);
...

但是在开发环境下,我们通常需要直接在控制台内输出异常信息以帮助开发者快速修复问题,而不希望将它们反馈至 Sentry 。通过 DartVM 的环境变量 dart.vm.product, 我们可以判断当前是否处于生产模式,并仅在生产模式下注册异常收集回调。

...
if (bool.fromEnvironment('dart.vm.product'))
    FlutterError.onError = (e) =>
        sentry.captureException(exception: e.exception, stackTrace: e.stack);
...

31.Dispose 方法注意

重写 dispose 方法并配置 AnimationController 实例。

@override
dispose() {
  animationController.dispose(); 
  super.dispose();
}

32.获取 Render 方法

创建 GlobalKey

GlobalKey globalKey = GlobalKey();

对应的Widget 引用 ,如这里的Text

Text('张三',key:globalKey);

通过 globalKey 来获取 对应的Element(BuildContext)

BuildContext stackContext = globalKey.currentContext;

获取对应的 RenderObject

RenderBox renderBox = stackContext.findRenderObject();

/// 相对于全局的位置
Offset offset = renderBox.localToGlobal(Offset.zero);
/// 获取指定的Widget的大小 信息
Size size = renderBox.paintBounds.size;