本文用于记录我在项目中集成 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 { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo' , theme: ThemeData( primarySwatch: Colors.blue, primaryColor: Color(0xFFf7f7f7 ), brightness : Brightness.dark, ), 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()); flutterEngine.getDartExecutor().executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ); 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' );
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 解析:
使用在线工具 JSON_to_Dart ,生成实体类;
使用 json.decode(_json) 解析JSON 字符串;
调用实体类的 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} "}' );
首先声明函数为 async
异步函数,使用 await
修饰push
方法打开子页面;(await会将当前线程阻塞?)
子页面在 pop()
函数中传递参数;
父页面处理返回值;
上面的示例代码中我使用的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( 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 { } }); 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 切圆的几种方法
ClipOval1 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 , ),
使用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 混开的集成方式有两种:
产物集成 (aar)
源码集成 (flutter module)
这里我更推荐源码集成,而不是侵入性更低的产物集成,原因是 AAR 产物存在大量的天坑。
网上所谓的各种解决方法也因人而异、因版本而异,并不存在通用的完美解决方法。
源码集成相对比较简单,出错几率更低,无非就是所有开发者都需要安装 Flutter SDK 而已,并不是什么大事。
源码集成的坑就一个,一定不要再打包的时候指定 flavor !
我们经常会单独打渠道包,这种操作在 flutter module 集成方式下,会出现丢文件的问题,最终导致 APK 文件无法安装,或者是安装后打开闪退。
解决方法只有一个:./gradlew assembleRelease
直接打包全部渠道包!如果渠道很多,只需要单独打某个的话,就在build.gradle
文件中注释掉其他 flavor,这样可以加快打包速度!