Flutter 学习日记1

本文用于记录我在项目中集成 Flutter 遇到的种种问题,作者纯 Flutter 小白,所以可能会有大量错误,请大佬指出。

1 在原生项目中显示 Flutter 项目

我的原生项目是 Android ,使用 AS 开发,使用官网介绍的源码集成方式集成Flutter 模块,在原项目中的 settings.gradle 添加如下代码:

1
2
3
4
5
6
7
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'flutter_module/.android/include_flutter.groovy'
))
include ':flutter_module'
project(':flutter_module').projectDir = new File('../flutter_module')

这样既可在同一个工作目录下同时编辑 Android 项目与 Flutter 项目。

2 flutter 中状态栏字体颜色修改

修改 Flutter 页面中状态栏颜色:

1
2
3
4
5
6
7
8
9
10
void main() {
runApp(MyApp());
if (Platform.isAndroid) {
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: Colors.black, //状态栏设置为黑色
)
);
}
}

这会修改状态栏为黑色,此时状态栏文字颜色也是黑色,我们还需要在 MaterialApp中设置主题 Theme:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
primaryColor: Color(0xFFf7f7f7),
brightness : Brightness.dark, //dark状态栏会被修改为白色字体 light 修改为黑色
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

这样设置之后Flutter页面的状态栏就是正常的黑底白字。

3 从flutter页面返回原生页面(与原生进行交互)

原生页面实现一个MethodChannel 用于交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
FlutterEngine flutterEngine = new FlutterEngine(mContext);
// 设置初始路由
flutterEngine.getNavigationChannel().setInitialRoute("/home/feedback?"+mFlutterParams.toJson());
// 开始执行dart代码来pre-warm FlutterEngine
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
// 缓存FlutterEngine
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);
MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor(), "com.example.flutter/native");
nativeChannel.setMethodCallHandler((methodCall, result) -> {
switch (methodCall.method) {
case "goBack":
// 返回上一页
Logger.d("flutter调用了goBack");
Intent i = getActivity().getIntent();
mContext.startActivity(i);
break;
case "goBackWithResult":
Logger.d("flutter调用了goBackWithResult");
break;
case "jumpToNative":
// 跳转原生页面
Logger.d("flutter调用了jumpToNative");
break;
default:
result.notImplemented();
break;
}
});
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(mContext)
);

在flutter 页面:

1
2
3
4
5
static const nativeChannel = const MethodChannel('com.example.flutter/native');  //创建原生频道

//在需要调用原生方法的位置:

nativeChannel.invokeMethod('goBack'); //goBack是上面在原生中定义的方法

4 网络请求、json解析、列表展示

网络请求:

使用 Dio

1
2
3
4
dependencies:
flutter:
sdk: flutter
dio: ^3.0.10

使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Future<List<FeedBackData>> _getData() async {
String url = params['baseUrl'] + '/something';
Response response = await dio.get(url, queryParameters: params);
if (response.statusCode == HttpStatus.OK) {
var _json = response.data;
FeedBackBean bean = FeedBackBean.fromJson(json.decode(_json));
minid = bean.data.last.id;
if (bean.data.length < 10) {
enableLoadmore = false;
} else {
enableLoadmore = true;
}
return bean.data;
} else {
return null;
}
}

JSON 解析:

  1. 使用在线工具 JSON_to_Dart,生成实体类;
  2. 使用 json.decode(_json) 解析JSON 字符串;
  3. 调用实体类的 fromJson() 函数;

图片加载:
使用 cache_network_image

1
2
dependencies:
cached_network_image: ^2.5.0

使用:

1
2
3
4
5
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),

5 缓存engine导致设置的状态栏效果无效

使用缓存可以显著的提高 flutter 页面的打开速度,使其体验接近于原生,但是存在以下问题。

预热引擎会提前执行一次main函数,预热时执行的修改状态栏的代码是不起作用的。被预热的页面打开下一级 Flutter 页面也是无法修改其状态栏颜色的。

解决方法: 不适用缓存,使用 withNewEngine()

6 appbar消失

在使用 webview 时 出现了一个奇怪的现象,大致现象如下:

原生 => Flutter1(webview) => Flutter2(webview) 正常

当逐层返回到原生页面时,再次执行上面的操作, Flutter2 页面出现仅显示 webview 而不显示其他内容的问题,经过大量测试,问题应该出在 Webview组件 与 Scaffold组件上。

解决方法:使用 Material 组件替代 Scaffold

7 buildTypes

如果你自定义了 buildType,且修改了flutter.gradle,需要注意自定义的构建模式中 debuggable 的值,当你调试完毕,将 flutter.gradle 修改为 initWith release 后,需要将自定义构建模式中的 debuggable 修改为 false,否者无法编译。

8 分割线

水平分割线

1
2
3
4
Divider(
color: Color(0xFFCCCCCC),
height: 1,
),

分割线默认高度就是1 ,为什么还要设置1呢?设置1 是为了取消掉分割线自带的margin。

垂直分割线:

1
2
3
4
5
6
VerticalDivider(
color: Color(0xFFCCCCCC),
indent: 5,
endIndent: 5,
width: 1,
),

indent 前缩进量,endIndent 后缩进量。需要注意的是,如果没有显示分割线,说明在当前其父容器的高度(VerticalDivider) or 宽度(Divider)是不确定的。

这种情况可以选择给其父容器加上高度 或者 宽度,或者选择套一个Container

9 传递参数

传参给下一级页面非常简单直接用构造器即可,而传回参数也一样简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//打开子页面
() async {
//跳转请假页面,在请假成功后返回成功,刷新页面
dynamic result = await Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
return AddLeave(widget.params);
}));
logger.d(result);
if (result != null) {
setState(() {});
}
},

//子页面返回
Navigator.of(context).pop(
'{"id":"${dataLists[index].id}","text":"${dataLists[index].daily_name}"}');
  1. 首先声明函数为 async 异步函数,使用 await 修饰push方法打开子页面;(await会将当前线程阻塞?)
  2. 子页面在 pop() 函数中传递参数;
  3. 父页面处理返回值;

上面的示例代码中我使用的JSON作为返回值,你也可以自定义对象来进行传递。

10 容器 Container 修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Container(
child: TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "Email",
hintText: "电子邮件地址",
prefixIcon: Icon(Icons.email),
border: InputBorder.none //隐藏下划线
)
),
decoration: BoxDecoration(
// 下滑线浅灰色,宽度1像素
border: Border(bottom: BorderSide(color: Colors.grey[200], width: 1.0)),
//剪裁圆形
borderRadius: BorderRadius.circular(1000),
),
)

11 dart 中的 =>

一个函数只有一条语句时,可以省略函数体的花括号 {} 直接使用 => 来指向这一条语句;

最常见的应该是:

1
2
void main() => runApp(myapp)

12 Dio 取消请求

你可以通过 cancel token 来取消发起的请求:

1
2
3
4
5
6
7
8
9
10
11
CancelToken token = CancelToken();
dio.get(url, cancelToken: token)
.catchError((DioError err){
if (CancelToken.isCancel(err)) {
print('Request canceled! '+ err.message)
}else{
// handle error.
}
});
// cancel the requests with "cancelled" message.
token.cancel("cancelled");

注意: 同一个cancel token 可以用于多个请求,当一个cancel token取消时,所有使用该cancel token的请求都会被取消。

完整的示例请参考取消示例.

13 setState的错误调用

This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.

出现这种报错的原因往往是因为在异步操作,比如发起网络请求,在widget被移除后,调用了setState() 函数。

解决方法:

1
2
3
4
5
if (mounted) {
setState(() {

});
}

14 Ink

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Ink(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFDE2F21), Color(0xFFEC592F)]),
borderRadius: BorderRadius.all(Radius.circular(20))),
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(20)),
child: Container(
padding:
EdgeInsets.symmetric(vertical: 8, horizontal: 20),
child: Text(
'这是InkWell的点击效果',
style: TextStyle(color: Colors.black),
),
),
onTap: () {},
),
),

不如 FlatButton 好用,在某些情况下很容易出现无法显示水波纹的问题;
但是 FlatButton 存在一些默认的设置,比如存在默认的最小宽度、无法设置高度,可以用 Container 包裹来解决。存在内部默认的padding,可以通过设置 padding: EdgeInsets.all(0) 来解决。

15 切圆的几种方法

  1. ClipOval
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Container(
    //头像
    margin: EdgeInsets.only(left: 12, right: 0, top: 12),
    child: ClipOval(
    child: CachedNetworkImage(
    imageUrl: params[KEY.BASE_URL] + data.avatar,
    placeholder: (context, url) =>
    Image.asset('assets/images/default_avatar.png'),
    errorWidget: (context, url, error) =>
    Image.asset('assets/images/default_avatar.png'),
    width: 45.0,
    height: 45.0, //限制图片大小
    ),
    ),
    width: 45.0,
    height: 45.0, //限制切圆大小
    ),
  2. 使用Container修饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
Container(
height: 40,
width: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: HexColor(color),
borderRadius: BorderRadius.circular(1000)),
child: Text(
type,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 13),
),
),

16 踩坑集成打包APK

项目运行起来、能通过AS部署到手机,不代表打包APK文件也是一帆风顺。

Flutter 与 Android 混开的集成方式有两种:

  1. 产物集成 (aar)
  2. 源码集成 (flutter module)

这里我更推荐源码集成,而不是侵入性更低的产物集成,原因是 AAR 产物存在大量的天坑。

在这里插入图片描述
网上所谓的各种解决方法也因人而异、因版本而异,并不存在通用的完美解决方法。

源码集成相对比较简单,出错几率更低,无非就是所有开发者都需要安装 Flutter SDK 而已,并不是什么大事。

源码集成的坑就一个,一定不要再打包的时候指定 flavor !

我们经常会单独打渠道包,这种操作在 flutter module 集成方式下,会出现丢文件的问题,最终导致 APK 文件无法安装,或者是安装后打开闪退。

解决方法只有一个:./gradlew assembleRelease

直接打包全部渠道包!如果渠道很多,只需要单独打某个的话,就在build.gradle 文件中注释掉其他 flavor,这样可以加快打包速度!