8.1-Flutter混合开发

混合开发Flutter集成步骤

  • 创建Flutter module
  • 添加Flutter module依赖
  • 在Java/Object-c中调用Flutter module
  • 编写Dart代码
  • 运行项目
  • 热重启/重新加载
  • 调试Dart代码
  • 发布应用

创建Flutter module

项目目录xxx/flutter/Navive项目:

cd xxx/flutter/
flutter create -t module flutter_module

上面的代码会切换到Android/iOS项目的上一级目录,并创建flutter模块

flutter中文件用途

  • .android - flutter_module的Android宿主工程;
  • .ios - flutter_module的iOS宿主工程
  • lib - flutter_module的Dart部分代码
  • pubspec.yaml - flutter_module的项目依赖配置文件
image

Flutter_module是上面创建的flutter module,Android Hybrid是Android工程

Flutter Android混合开发

添加flutter依赖

在Android工程中的settings.gradle文件中进行如下配置

include ':app'
setBinding(new Binding([gradle:this]))
evaluate(new File(
        settingsDir.parentFile,
        'flutter_module/.android/include_flutter.groovy'
))

同步后会在工程目录中显示我们的flutter module

image

在app的build.gradle中添加对flutter的依赖

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
        ...
    implementation project(':flutter')
}

使用flutter要求minSDK最小应该>=16

另外使用flutter也需要设置支持Java8特性

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

在Java中调用Flutter module

Java中调用Flutter模块有俩种方式

  • 使用Flutter.createView API方式
  • 使用FlutterFragment的方式
使用Flutter.createView API的方式
View flutterView = Flutter.createView(
    MainActivity.this,
    getLifecycle(),
    "route1"
);
使用FlutterFragment的方式
FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
tx.replace(R.id.someContainer,Flutter.createFragment('route1'));
tx.commit();

字符串"route1"用来告诉Dart代码在Flutter视图中显示哪个小部件,Flutter模块项目的lib/main.dart文件需要通过window.defaultRouteName来获取Native指定要显示的路由名,以确定要创建哪个窗口小部件并传递给runAPP:

调用Flutter module时传递数据

无论通过Flutter.createView的方式,还是通过Flutter.createFragment的方式,都允许我们加载Flutter module时传递一个String类型的initialRoute参数,也可以穿Json字符串.

Native代码

        mBtnCreate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
                //为dart模块初始化所设置的参数
                ft.replace(R.id.container, Flutter.createFragment("{name:'yangdxg',dataList:['aa','bb','cc']}"));
                ft.commit();
            }
        });
    <Button
        android:id="@+id/btn_create"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Test" />

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    </FrameLayout>

Dart代码

import 'package:flutter/material.dart';
import 'dart:ui';

//使用window.defaultRouteName获取Native传递过来的参数
void main() => runApp(MyApp(initParams: window.defaultRouteName,));

class MyApp extends StatelessWidget {

  final String initParams;

  const MyApp({Key key, this.initParams}) :super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter 混合开发',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter 混合开发', initParams: initParams),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title, this.initParams}) : super(key: key);

  final String title;
  final String initParams;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'initParams:${widget.initParams}',
            ),
            Text(
              '$_counter',
              style: Theme
                  .of(context)
                  .textTheme
                  .display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Android调用Flutter

热重启/重新加载

混合开发后热重启/重新加载会失效,我们可以通过把flutter_moudle和Android工程进行关联恢复热重启/重新加载

  • 打开模拟器或将设备连接到电脑上

  • 关闭App,运行flutter attach

    cd flutter_hybrid/flutter_module
    flutter attach
    

    注:如果同时有多个模拟器或连接的设备,运行flutter attach时会提示选择设备

    flutter attach -d 'emulator-5554'
    

    出现等待连接后运训Android程序Waiting for a connection from Flutter on Android SDK built for x86...

    flutter部分运行后会出现如下提示

    Done.
    Syncing files to device Android SDK built for x86...                    
     2,243ms (!)                                       
    
    🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
    An Observatory debugger and profiler on Android SDK built for x86 is available at: http://127.0.0.1:55352/QMwyyeCLcSc=/
    For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".
    
    • 热加载按r
    • 热重启按R
    • 请求帮助按h
    • 断开链接按d
    • 推出按q

    更改代码后按下r设备就会更新显示

调试Dart代码

  • 关闭APP
  • 点击AndroidStudio的Flutter Attach按钮(必须安装好Flutter与Dart插件)
  • 启动APP

发布应用

打包
生成Android签名证书

为Android项目生成签名证书(就是原生操作)

配置gradle
  • 将签名证书copy到android/app目录下

  • 编辑~/.gradle/gradle.properties或../android/gradle.properties(一个全局的,一个项目中的),加入如下代码

    MYAPP_RELEASE_STORE_FILE = your keystore filename
    MYAPP_RELEASE_KEY_ALIAS = your keystore alias
    MYAPP_RELEASE_STORE_PASSWORD = ******
    MYAPP_RELEASE_KEY_PASSWORD = ******
    
  • 编辑 android/app/build.gradle文件

    android{
      defaultConfig{...}
      signingConfigs{
          release{
              storeFile file(MYAPP_RELEASE_STORE_FILE)
              storePassword MYAPP_RELEASE_STORE_PASSWORD
              keyAlias MYAPP_RELEASE_KEY_ALIAS
              keyPassword MYAPP_RELEASE_KEY_PASSWORD
          }
      }
      buildTypes{
          release{
              ...
              signingConfig signingConfigs.release
          }
      }
    }
    
签名打包APK

terminal进入项目下的Android目录,运行如下代码

./gradlew assembleRelease

打包完成apk在app/build/outputs/apk

Flutter iOS混合开发

稍后补充

Flutter与Native通信

Flutter与Native的通信是通过Channel来完成的

消息使用Channel(平台通道)再客户端(UI)和主机(平台之间传递)

Flutter中的消息传递完全是异步的

Channel所支持的数据类型

Dart Android iOS
null null nil (NSNull when nested)
bool java.lang.Boolean NSNumber numberWithBool:
int java.lang.Integer NSNumber numberWithInt:
int, if 32 bits not enough java.lang.Long NSNumber numberWithLong:
double java.lang.Double NSNumber numberWithDouble:
String java.lang.String NSString
Uint8List byte[] FlutterStandardTypedData typedDataWithBytes:
Int32List int[] FlutterStandardTypedData typedDataWithInt32:
Int64List long[] FlutterStandardTypedData typedDataWithInt64:
Float64List double[] FlutterStandardTypedData typedDataWithFloat64:
List java.util.ArrayList NSArray
Map java.util.HashMap NSDictionary

Flutter定义了三种不同类型的Channel:

  • BasicMessageChannel:用于传递字符串和半结构化的信息,持续通信,接收到消息后可以回复此消息,
  • MethodChannel:用于传递方法调用,一次性通信,如Flutter调用Native拍照
  • EventChannel:用于数据流的通信,持续通信,收到消息后无法回复此消息,通常用于Native向Dart的通信,如网络变化,传感仪检测

BasicMessageChannel用法

Dart端
const BasicMessageChannel(this.name,this.codec);
  • String name — Channel 的名字,要和Native端保持一致;
  • MessageCodec<T> codec — 消息的编解码器,要和Native端保持一致,有四种实现(见Native端讲解)

sendMessageHandler方法

void    sendMessageHandle(Future<T> handler(T message))
  • Future<T> handler(T message) — 消息处理器,配置BinaryMessage完成消息的处理

在创建好BasicMessageChannel后,如果要让其接收Native发来的消息,则需要调用它的setMessageHandler方法为其设置一个消息处理器

Future<T> send(T message)
  • T message — 要传递给Native的具体消息
  • Future<T> — 消息发出去后,收到Native回复的回调函数

MessageChannel方法

Dart端
const MethodChannel(this.name[this.codec=const StandardMethodCodec])
  • name — Channel的名字,和Native端保持一致
  • MethodCodec codec — 消息的编解码器,默认时StandardMethodCodec,和Native端保持一致

invokeMethod方法

Future<T> invokeMethod<T>(String method,[dynamic arguments])
  • method:要调用Native的方法名

EvnetChannel

Dart端
const EventChannel(this.name,[this.codec = const StandardMethodCodec()])
  • name — Channel的名字,要和Native端保持一致
  • MethodCodec codec — 消息的编解码器,默认是StandardMethodCodec,要和Native端保持一致

receiveBroadcastStream

Stream<dynamic> receivedBroadcastStream([dynamic arguments])
  • dynamic arguments — 监听事件时向Native传递的数据;

Flutter与Android通信

BasicMessageChannel用法

Native端
BasicMessageChannel(BinaryMessenger messenger, String name, MessageCodec<T> codec) {
  • BinaryMessenger — 消息信使,是消息的发送与接收的工具
  • String name — Channel的名字,也是其唯一标识符
  • MessageCodec<T> — 消息的编解码器,它有几种不同类型的实现
    • BinaryCodec — 最为简单的一种Codec,其返回值类型和入参的类型相同,均为二进制格式(Android 中为ByteBuffer,iOS中为NSData),实际上,BinarCodec在编解码过程中什么都没做,只是原封不动将二进制数据消息返回而已,BinaryCodec并非没有意义,在某些情况下非常有用,比如使用BinaryCodec可以传递内存数据块时在编解码阶段等于内存拷贝
    • StringCodec — 用于字符串与二进制数据之间的编解码,其编码格式为UTF-8
    • JSONMessageCodec — 用于基础树与二进制数据之间的编解码,其支持基础数据类型以及列表,字典,其在iOS端使用NSJSONSerialization作为序列化的工具,而在Android端则使用了其自定义的JSONUTIL与StringCodec作为序列化工具;
    • StandardMessageCode — 是BasicMessageChannel的默认编解码器,支持基础数据类型,二进制数据,列表,字典,

setMessageHandle接收消息

void setMessageHandler(BasicMessageChannel.MessageHandler<T> handler)
  • Handler — 消息处理器,配合BinaryMessenger完成消息的处理

创建好BasicMessageChannel后,如果要让其接收Dart发来的消息,则需要调用它的setMessageHandler方法为其设置一个消息处理器

public interface MessageHandler<H>{
    void onMessage(T var1,BasicMessageChannel.Repl<T> var2);
}
  • onMessage(T var,BasicMessageChannel Reply<T> var2) — 用于接收消息,var1是消息内容,var2是回复此消息的回调函数

send方法,向Dart端发送消息

void send(T message);
void send(T message,BasicMessageChannel.Reply<T> callback)
  • T message — 要传递给Dart的具体信息
  • BasicMessageChannel.Reply<T> callback — 消息发出后,收到Dart 回复的回调函数

MethodChannel用法

Native端
//会构造一个StandardMethodCodec.INSTANCE类型的MethodCodec
MethodChannel(BinaryMessage messager,String name)
//或
MethodChannel(BinaryMessage message,String name,MethodCodec codec)
  • messager — 消息信使
  • name — Channel名字
  • codec — 编解码器
setMethodCallhandler(@Nullable MethodChannel.MethodCallHandler handler)
  • Handler — 消息处理器,配合BinaryMessenger完成消息的处理;
public interface MethodCallHandler{
    void onMethodCall(MethodCall var1,MethodChannel.Result var2);
}
  • onMessageCall — 用于接收消息,call是消息内容,call.method表示调用的方法名,Object类型的call.arguments表示调用方法所传递的入餐;result是回复此消息的回调函数提供了success,error,noImplemented方法调用

EventChannel用法

Native端
EventChannel(BinaryMessenger messenger, String name) 
EventChannel(BinaryMessenger messenger, String name, MethodCodec codec)
  • 参数意义同上

接收消息

setStreamHandler(EventChannel.StreamHandler handler)
public interface StreamHandler{
    void onListen(Object args,EventChannel.EventSink eventSink);
    void onCancle(Object o);
}
  • eventSink回调函数
  • onCancle取消监听检测

Flutter与Android通信实战

Flutter与iOS通信

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 本文先介绍一下现有工程如何集成 Flutter 实现混合开发,以及混合项目如何打包,再探索下如何降低原生和 Flu...
    koin447阅读 5,620评论 1 37
  • 文|拾忆年 中午下班没有回家,直接和办公室的同事一起去了外面餐馆吃饭。 屁股刚刚落在椅子上,餐还没有点完,同事就开...
    句号与逗号阅读 1,177评论 6 3
  • 人潮拥挤 我已失散 人潮已散 我心拥挤 独守原地 空自悲
    sugar和图图阅读 137评论 1 4
  • 本想着,今日不写的。 凌晨十一点半左右,下班。跑过步,回住处冲冷水澡,尝试是欢喜的,过程是颤抖着。 工作十四时又两...
    昉之阅读 186评论 2 3