项目集成

Android 集成 Flutter 项目

  • 通过 Android Studio 创建 Andriod 原生应用 AndroidApp
  • 通过 Android Studio 创建 New Flutter Project, 选择 Flutter Module,创建 Flutter Module FlutterApp
  • Android Studio 打开 AndroidApp 项目,然后 New -> Module-> Import Flutter Module 添加 Flutter Module FlutterApp 依赖

    开发调试

纯 Flutter 项目开发调试

  • 方式一: vscode 打开 Flutter 项目, 按 F5 进 DEBUG,CTRL+F5 进行 HOT RESTART 可以实现 Hot Reload (如果是 flutter run, 修改文件后,需要安 r)
  • 方式二:Android Studio 打开 Flutter 项目,image.png

Android 集成 Flutter 开发调试

Android 与 Flutter 混合模式: Android 项目集成 Flutter Module

  • 首先,进入 Flutter Module 目录, 命令行执行 flutter attach 修改代码后,press “r” 即可实现 Hot Reload

image.png

  • Andorid Studio 打开 Android 项目,点击绿色三角按钮 image.png 进入 原生 App Debug 模式

安装插件

安装第三方依赖需要在 pubspec.yaml 的 dependencies 节点添加插件配置,类似与 npm

  1. dependencies:
  2. flutter:
  3. sdk: flutter
  4. cupertino_icons: ^0.1.2
  5. # flutter webview 插件
  6. flutter_webview_plugin: 0.3.3
  7. # sqlite 数据库
  8. sqflite: ^1.1.0

添加好以后,点击 AS 右上方的 Packages get 安装依赖或者进入项目根目录执行 flutter packages get 安装依赖

入口函数

  • pubspec.yaml文件中,将flutter的值设置为:uses-material-design: true。这允许我们可以使用一组预定义Material icons
  • 为了继承主题数据,widget需要位于MaterialApp内才能正常显示, 因此我们使用MaterialApp来运行该应用。
  1. import 'package:flutter/material.dart';
  2. class App extends StatelessWidget {
  3. // This widget is the root of your application.
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. title: 'Flutter Demo'
  8. );
  9. }
  10. }
  11. void main() {
  12. runApp(App());
  13. }

Widget

一切 VIew 都为 Widget:Widget 分是无状态的StatelessWidget或者是有状态的StatefulWidget

实现自定义 widget

  • 实现继承自StatefulWidget的类来表示你要自定义的可变控件
  • 实现继承自 State 的类来处理可变控件的状态和样式(build方法)
  • 当用户交互发生(onPressed), 可以调用setState方法告诉组件需要重绘
  1. class MyStatefulWidget extends MyStatefulWidget {
  2. final String title;
  3. final String content;
  4. @override
  5. _MyStatefulState createState() => _MyStatefulState();
  6. }
  7. class _MyStatefulState extends State<MyStatefulWidget> {
  8. @override
  9. Widget build(BuildContext context) {
  10. return Scaffold(
  11. appBar: AppBar(
  12. title: Text(widget.title),
  13. centerTitle: true,
  14. ),
  15. body: Center(
  16. child: Text(widget.content),
  17. ),
  18. );
  19. }
  20. }

常用布局 https://flutterchina.club/widgets/material/

MaterialApp Widget

Material Design 风格应用,封装了 MD 应用常用的组件。MaterialApp 一般作为顶层的 Widget 使用,可以用于应用主题配置

  1. new MaterialApp(
  2. title: 'Flutter应用',
  3. theme: new ThemeData(
  4. //主题色
  5. primarySwatch: Colors.blue,
  6. ),
  7. routes: {
  8. '/home':(BuildContext context) => HomePage(),
  9. '/category':(BuildContext context) => CategoryPage(),
  10. //....
  11. },
  12. initialRoute: '/home',
  13. ......
  14. );

Scaffold Widget

Scaffold是 Material Design 布局结构的基本实现, 定义好了基本的页面结构(appBar, body, bottomNavigationBard等),只需要配置相关信息即可快速实现一个框架页面。

  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. appBar: AppBar(
  5. title: Text(widget.title),
  6. centerTitle: true,
  7. ),
  8. body: Center(
  9. child: _widgetOptions.elementAt(_selectedIndex),
  10. ),
  11. bottomNavigationBar: BottomNavigationBar( // 典型的底部 Tab 模式
  12. items: <BottomNavigationBarItem>[
  13. BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
  14. BottomNavigationBarItem(icon: Icon(Icons.category), title: Text('Category')),
  15. BottomNavigationBarItem(icon: Icon(Icons.person), title: Text('Profile')),
  16. ],
  17. currentIndex: _selectedIndex,
  18. fixedColor: Colors.deepPurple,
  19. onTap: _onItemTapped,
  20. ),
  21. );
  22. }

Material 主题常用 Widget

Scaffold 标准的页面骨架

提供 appBar, body, bottomNavigationBar 等配置入口

  1. class About extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return new Scaffold(
  5. appBar: new AppBar(
  6. title: new Text("关于"),
  7. centerTitle: true,
  8. leading: new IconButton(
  9. icon: new Icon(Icons.arrow_back),
  10. onPressed: () {
  11. SystemNavigator.pop(); // remove this activity from the stack
  12. }
  13. )
  14. ),
  15. body: new Center(
  16. child: new Text("Flutter About Page"),
  17. ),
  18. );
  19. }
  20. }

刷新与分页

  • 结合 RefreshIndicator,通过 onRefresh 实现下拉刷新
  • 结合 RefreshIndicator,通过 controller (ScrollController)实现上拉分页加载
  • 通过 ListView.builder 和 ListTile 构建列表
  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. appBar: AppBar(
  5. title: Text("列表加载测试"),
  6. centerTitle: true,
  7. leading: new IconButton(
  8. icon: new Icon(Icons.arrow_back),
  9. onPressed: () {
  10. SystemNavigator.pop(); // remove this activity from the stack
  11. }
  12. )
  13. ),
  14. body: new RefreshIndicator(
  15. child: ListView.builder(
  16. itemCount: listItems.length,
  17. itemBuilder: (context, index) {
  18. return ListTile(
  19. leading: new Icon(Icons.list),
  20. title: Text("列表加载测试-$index"),
  21. onTap:(){}
  22. );
  23. },
  24. controller: _scrollController, // 使用 ScrollController 组件实现 上拉分页
  25. ),
  26. onRefresh: _pullRefresh, // 下拉刷新回掉,默认是转圈loading
  27. ),
  28. );
  29. }

页面跳转

静态路由

在 main.dart 文件 app 启动入口通过 routes 配置静态路由或者 onGenerateRoute 动态处理路由。 注意:默认路由是 /,不支持参数参数, 支持 home/list 模式,但不能使用 /home/list 模式,这样会导致匹配到 / 路由

  • 静态路由定义
  1. void main() => runApp(App());
  2. class App extends StatelessWidget {
  3. @override
  4. Widget build(BuildContext context) {
  5. return MaterialApp(
  6. theme: ThemeData(
  7. ),
  8. home: MyHomePage(title: 'HappyFlutter'),
  9. routes: <String, WidgetBuilder> {
  10. // 定义静态路由,不能传递参数
  11. 'home': (BuildContext context) => new Home(false),
  12. 'profile': (BuildContext context) => new Profile(),
  13. },
  14. onGenerateRoute: (settings) {
  15. print('--name' + settings.name);
  16. print(settings.arguments);
  17. }
  18. );
  19. }
  20. }
  • 静态路由跳转

通过 Navigator.of(context).pushNamed 方式进行静态路由跳转

  1. Navigator.of(context).pushNamed('home');
  2. Navigator.of(context).pushNamed('home/dialog').then((value) {
  3. // 获取 view 返回值
  4. })

动态路由

  1. Navigator.of(context).push(new PageRouteBuilder(pageBuilder:(BuildContext context,
  2. Animation<double> animation,Animation<double> secondaryAnimation) {
  3. // 可以通过构造函数传递参数
  4. return new Home();
  5. }));

Native 跳转 Flutter 传递参数

目前在 Native 中直接打开 FlutterActivity 是不支持直接传递参数的(唯一的参数就是 route) , 不过可以把 route 以 url 的形式传递参数是可行的, 然后在 Flutter 的 onGenerateRoute 函数对 url 进行解析,就能变相实现参数传递。

  • Android 代码
  1. Intent intent = new Intent(this, NativeFlutterActivity.class);
  2. intent.setAction(Intent.ACTION_RUN);
  3. intent.putExtra("route", "test?msg=Native跳转Flutter参数测试&id=111111");
  4. startActivity(intent);
  • Flutter 代码
  1. // This widget is the root of your application.
  2. class App extends StatelessWidget {
  3. @override
  4. Widget build(BuildContext context) {
  5. return MaterialApp(
  6. theme: ThemeData(
  7. primaryColor: Color(0xFF008577),
  8. ),
  9. home: MyHomePage(title: 'HappyFlutter'),
  10. routes: <String, WidgetBuilder> {
  11. 'home': (BuildContext context) => new Home()
  12. },
  13. onGenerateRoute: (settings) {
  14. Uri uri = Uri.parse(settings.name);
  15. String route = uri.path;
  16. Map<String, String> params = uri.queryParameters;
  17. switch(route) {
  18. case 'test':
  19. return MaterialPageRoute(builder: (context)=> Detail(params));
  20. break;
  21. }
  22. }
  23. );
  24. }
  25. }

导航栏

Navigator.pop(context) 关闭当前 FlutterView SystemNavigator.pop() 关闭当前Flutter 所在的 Activity

  1. import 'package:flutter/services.dart';
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. appBar: AppBar(
  6. title: Text("导航栏标题"),
  7. centerTitle: true, // 标题居中
  8. leading: new IconButton( // 顶部导航栏右边返回箭头图片
  9. icon: new Icon(Icons.arrow_back),
  10. onPressed: () { //
  11. SystemNavigator.pop(); // remove this activity from the stack
  12. }
  13. ),
  14. actions:[ // 右边菜单
  15. new IconButton(
  16. icon: new Icon(Icons.settings),
  17. onPressed: () {
  18. Scaffold.of(context).showSnackBar(new SnackBar(content: new Text("设置")));
  19. }
  20. )
  21. ]
  22. )
  23. );
  24. }

异步请求

Dart 语言中使用 Future (类比Promise) 实现异步操作,一般配合 async和 await 使用。

  1. // 多个 Future 执行,注意不是顺序执行
  2. Future.wait([f1(), f2()])
  3. .then((List responses) => {})
  4. .catchError((e) => {});
  5. // 延迟 2s
  6. Future.delayed(Duration(seconds: 2), () {});
  • 从数据库查询用户信息 ```dart // 定义返回值 Future 函数 Future>> getFavoriteList() async { Database dbClient = await db; return await dbClient.rawQuery(‘SELECT * FROM user_info’); }

// 调用 //直接调用

getFavoriteList().then(((rows){

});

// async 函数内部调用 void getFavoriteListTest() async { List> list = await getFavoriteList(); …… }

  1. <a name="lUSSW"></a>
  2. ### 数据请求
  3. > 可以通过 **FutureBuilder** 异步数据请求与UI数据绑定
  4. ```dart
  5. Future<List<Map<String, dynamic>>> getFavoriteList() async {
  6. return [];
  7. }
  8. @override
  9. Widget build(BuildContext context) {
  10. return Scaffold(
  11. body: FutureBuilder<List<Map<String, dynamic>>>(
  12. future: favoriteDB.getFavoriteList(),
  13. builder: (context, snapshot) {
  14. if (snapshot.hasError) print(snapshot.error);
  15. List<Map<String, dynamic>> list = snapshot.data;
  16. return ListView.builder(
  17. itemCount: snapshot.data.length,
  18. itemBuilder: (_, int position) {
  19. Map item = list[position];
  20. return Card(
  21. child: ListTile(
  22. title: Text(item['title']),
  23. onTap: () {
  24. NavigationChannel.pushWebRoute({"title": item['title'].toString(), "url": item['url'].toString()});
  25. },
  26. ),
  27. );
  28. },
  29. );
  30. },
  31. ),
  32. ),
  33. };

平台通信

https://flutterchina.club/platform-channels/

Flutter平台特定的API支持不依赖于代码生成,而是依赖于灵活的消息传递的方式:

  • 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)。
  • 宿主监听的平台通道,并接收该消息。然后它会调用特定于该平台的API(使用原生编程语言) - 并将响应发送回客户端,即应用程序的Flutter部分。

在客户端,MethodChannel (API)可以发送与方法调用相对应的消息。 在宿主平台上,MethodChannel 在Android((API) 和 FlutterMethodChannel iOS (API) 可以接收方法调用并返回结果。这些类允许您用很少的“脚手架”代码开发平台插件。

Flutter 调用 Native

  • Dart 发起信道调用
  1. import 'package:flutter/services.dart';
  2. static const MethodChannel methodChannel = MethodChannel('samples.flutter.io/battery');
  3. Future<void> _getBatteryLevel() async {
  4. String batteryLevel;
  5. try {
  6. final int result = await methodChannel.invokeMethod('getBatteryLevel');
  7. batteryLevel = 'Battery level: $result%.';
  8. } on PlatformException {
  9. batteryLevel = 'Failed to get battery level.';
  10. }
  11. }
  • Java 注册信道
  1. 直接继承 FlutterActivity 可以快速建立 Native 与 Flutter 的通信桥梁
  2. 非 FlutterActivity 时,需要自己处理 FlutterView 的应用,可以在创建 FlutterView 时,创建在 Android Application 里面
  1. public class NativeFlutterActivity extends FlutterActivity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. GeneratedPluginRegistrant.registerWith(this);
  6. // Native 与 Flutter 通信依赖 FlutterView
  7. new MethodChannel(getFlutterView(), 'samples.flutter.io/battery').setMethodCallHandler(
  8. new MethodCallHandler() {
  9. @Override
  10. public void onMethodCall(MethodCall call, Result result) {
  11. if (call.method.equals("getBatteryLevel")) {
  12. int batteryLevel = getBatteryLevel();
  13. if (batteryLevel != -1) {
  14. result.success(batteryLevel);
  15. } else {
  16. result.error("UNAVAILABLE", "Battery level not available.", null);
  17. }
  18. } else {
  19. result.notImplemented();
  20. }
  21. }
  22. }
  23. );
  24. }
  25. }

see code: https://github.com/flutter/flutter/examples/platform_channel

Native 调用 Flutter

通过 MethodChannel.invokeMethod 可以主动调用 Flutter 通信

  • java 代码

  • ```java package com.easy.team.module;

import android.app.AlertDialog; import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.Nullable; import android.util.Log; import java.util.HashMap; import java.util.Map; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel;

public class NativeActivity extends Activity {

  1. public static String CHANNEL = "com.happy.message/notify";
  2. private MethodChannel channel;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_native);
  7. channel = new MethodChannel(MessageChannel.getFlutterView(), CHANNEL);
  8. // Native 主动调用 Flutter
  9. Map<String,String> map = new HashMap<String, String>();
  10. map.put("from", "native");
  11. notifyFlutter("getFlutterVersion", map);
  12. }
  13. protected void notifyFlutter(String method, Object args) {
  14. channel.invokeMethod(method, args, new MethodChannel.Result() {
  15. @Override
  16. public void success(@Nullable Object o) {
  17. Toast.makeText(NativeActivity.this, "message:" + o.toString(), Toast.LENGTH_SHORT).show();
  18. }
  19. @Override
  20. public void error(String s, @Nullable String s1, @Nullable Object o) {
  21. Log.d("--NativeActivity:error--", s1);
  22. }
  23. @Override
  24. public void notImplemented() {
  25. Log.d("--NativeActivity:notImplemented--","");
  26. }
  27. });
  28. }

}

  1. - Flutter 代码
  2. - <br />
  3. ```dart
  4. static const MethodChannel methodChannel = MethodChannel('com.happy.message/notify');
  5. Future<dynamic> settingChannelHandler(MethodCall methodCall) async {
  6. switch (methodCall.method) {
  7. case 'getFlutterVersion':
  8. return '1.0.0';
  9. default:
  10. }
  11. }
  12. @override
  13. void initState() {
  14. super.initState();
  15. methodChannel.setMethodCallHandler(this.settingChannelHandler);
  16. }

Native 集成 Flutter

Native Activity 模式

Native 继承 FlutterActivity 就可以快速与 Flutter 建立通道。Flutter 中通过消息通知 Native 用 Activity 的方式打开 Flutter View,这种方式可以解决Native 和 Flutter 返回键问题,也就是统一交给 Native 处理。

  1. // 自定义 FlutterActivity
  2. public class NativeFlutterActivity extends FlutterActivity {
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. GeneratedPluginRegistrant.registerWith(this);
  7. }
  8. }
  9. // Native 使用 FlutterActivity 打开指定 Flutter View
  10. Intent intent = new Intent(getActivity(), NativeFlutterActivity.class);
  11. // 需要指定 ACTION_RUN,FlutterActiviy 底层处理
  12. intent.setAction(Intent.ACTION_RUN);
  13. // FlutterActiviy 会 从Intent 里面取 route 参数,目前不支持直接传递参数
  14. intent.putExtra("route", route);
  15. startActivity(intent);

Native Fragment 模式

在 Native 中 Fragment 根据 Flutter 路由动态创建 Fragment

  1. public class NativeFlutterFragment extends Fragment {
  2. private FlutterView flutterView;
  3. public static Fragment newInstance(String route) {
  4. NativeFlutterFragment fragment = new NativeFlutterFragment();
  5. Bundle bundle = new Bundle();
  6. bundle.putString("route", route);
  7. fragment.setArguments(bundle);
  8. return fragment;
  9. }
  10. public static Fragment newInstance(String route, Bundle bundle) {
  11. NativeFlutterFragment fragment = new NativeFlutterFragment();
  12. if (bundle == null) {
  13. bundle = new Bundle();
  14. }
  15. bundle.putString("route", route);
  16. fragment.setArguments(bundle);
  17. return fragment;
  18. }
  19. @Override
  20. public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
  21. return this.getFlutterView();
  22. }
  23. @Override
  24. public void onActivityCreated(@Nullable Bundle savedInstanceState) {
  25. super.onActivityCreated(savedInstanceState);
  26. this.registerMethodChannel(this.getFlutterView());
  27. // 保存当前活动窗口 FlutterView 实例引用,Activity 可以通过 MessageChannel.getFlutterView() 与 Flutter 通信
  28. MessageChannel.setFlutterView(this.flutterView);
  29. }
  30. @Override
  31. public void onHiddenChanged(boolean hidden) {
  32. Log.d("--onHiddenChanged--", String.valueOf(hidden));
  33. super.onHiddenChanged(hidden);
  34. if (!hidden) {
  35. MessageChannel.setFlutterView(this.flutterView);
  36. }
  37. }
  38. protected void registerMethodChannel(FlutterView flutterView) {
  39. new MethodChannel(flutterView, "com.happy/navigation").setMethodCallHandler(
  40. new MethodChannel.MethodCallHandler() {
  41. @Override
  42. public void onMethodCall(MethodCall call, MethodChannel.Result result) {
  43. Log.i("--MethodChannel--", "method:" + call.method);
  44. if (call.method.equals("pushRoute")) {
  45. String route = call.argument("route");
  46. HashMap args = (HashMap)call.arguments;
  47. Log.i("--MethodChannel--", "route:" + route);
  48. if ("native".equals(route)) {
  49. Intent intent = new Intent(getActivity(), NativeActivity.class);
  50. intent.putExtra("args", args);
  51. startActivity(intent);
  52. } else if ("web".equals(route)) {
  53. Intent intent = new Intent(getActivity(), NativeWebViewActivity.class);
  54. intent.putExtra("title", args.get("title").toString());
  55. intent.putExtra("url", args.get("url").toString());
  56. startActivity(intent);
  57. } else { // 每个 Flutter View 使用一个 Native Activity 方式打开页面
  58. Intent intent = new Intent(getActivity(), NativeFlutterActivity.class);
  59. intent.setAction(Intent.ACTION_RUN);
  60. intent.putExtra("route", route);
  61. startActivity(intent);
  62. }
  63. result.success("success");
  64. } else if (call.method.equals("pop")) {
  65. result.success("success");
  66. } else {
  67. result.notImplemented();
  68. }
  69. }
  70. }
  71. );
  72. }
  73. public FlutterView getFlutterView(){
  74. if (this.flutterView == null) {
  75. String route = getArguments().getString("route");
  76. this.flutterView = Flutter.createView(getActivity(), getLifecycle(), route);
  77. // 解决 Flutter 加载黑屏问题
  78. this.flutterView.setZOrderOnTop(true);
  79. // this.flutterView.setZOrderMediaOverlay(true);
  80. this.flutterView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
  81. }
  82. return this.flutterView;
  83. }
  84. }

数据存储

sqfite 数据库

通过 sqflite 插件可以完成 sqlite 数据库操作。 经过测试,如果 Native 已经创建的数据库,通过如下方式是可以直接链接 Native 的数据库,也就是 Native 和 Flutter 数据库是可以互通操作。

  1. import 'package:sqflite/sqflite.dart';
  2. init() async {
  3. String databasesPath = await getDatabasesPath();
  4. String path = join(databasesPath, "app_data");
  5. return await openDatabase(path, version: 1, onCreate: _onCreate);
  6. }
  7. Future<Database> get db async {
  8. if (_db != null) return _db;
  9. _db = await init();
  10. return _db;
  11. }
  12. // 使用
  13. Future<List<Map<String, dynamic>>> getList() async {
  14. Database client = await db;
  15. return await client.rawQuery('SELECT * FROM user_info');
  16. }
  17. Future<int> delete(int id) async {
  18. Database dbClient = await db;
  19. return await client.rawDelete('DELETE FROM user_info WHERE id = ?', [id]);
  20. }

常见问题

  • 静态路由路径匹配问题

静态路由定义时,不能以 /home/test 多级 / 方式定义,否则出现如下错误,最终默认会指向 / 路由。

One or more of those objects was null, and therefore the initial route specified will be ignored and “/“ will be used instead. see https://stackoverflow.com/questions/54556381/flutter-error-could-not-navigate-to-initial-route
  • Android项目中嵌入Flutter工程时,切换 FlutterFragment页面时会出现黑屏问题

解决方案见:https://github.com/alibaba/flutter_boost/issues/105

  • Android Native 页面如何给 Flutter 页面发送消息?

Native 与 Flutter 通信,可以通过 MethodChannel 实现,而 MethodChannel 是依赖 FlutterView,但 Native Activity 并没有 FlutterView 的实例。目前是采用如下思路处理:在 Native FlutterView 创建时,保存当前最顶部的 FlutterView 全局引用,然后 Native Activity 拿到 FlutterView 全局引用后就可以进行通信了。

  • Native 与 混合栈以及返回键问题


    我们知道,默认 FlutterView 所有页面切换都是通过 View 实现的,而且都在一个 Activity 上,这样导致点击返回键时,导致所有页面都退出了,当然这里可以通过判断是拦截返回键解决。当遇到 Native ->Flutter->Native 这种交替时,整个堆栈的管理还是比较复杂的。目前是在创建 FlutterView 时,都通过一个 Activity 方式去承载,从而解决堆栈问题。当然这样处理也有不好的地方,会占用过多的内存,这一块需要继续研究一些,比如闲鱼提到的 FlutterView 重用机制。