从零开始
首先,创建一个新的项目文件夹用来存放Android项目和flutter的项目.
原生的Android项目我给它命名为AndroidWithFlutterProject.
然后创建一个flutter 的 module项目,我命名为flutter_with_android1 :
看提示也知道,这个Module就是为了混合进原生的.
创建好之后,我们的项目文件夹就变成了这个样子:
二者在同一目录,此时将flutter的项目引入到Android中:
在Android 的app上面右键,依次点击new , Module , :
然后选择最后的import flutter module
选择我们创建好的flutter项目就可以.
然后用Android Studio打开flutter项目编译一下, 就可以在Android项目看到它了:
在Android工程中创建Flutter的View
Flutter提供了两种方式让Android工程来引用组件,一种是View,一种是Fragment,这里选用View来进行讲解,Fragment同理。
这里我们用两种方式来引入FLutter,本质上是作为一个view引入布局还是将FlutterView作为Activity的根View。
以单个view引入布局
View flutterView = Flutter.createView(this, getLifecycle(), "route1");
通过上面很简单的一个方法,我们就能通过Flutter创建出一个view,这个方法提供三个参数,第一个是Activity,第二个参数是一个Lifecycle对象,我们取Activity的lifecycle即可,第三个参数为route,这个参数Flutter端可以通过window.defaultRouteName
获取,利用它flutter可知道要创建哪个widget.。
创建出这个FlutterView之后就可以按常规的操作来将它加入到任何你想要的布局中去了。
同理 Flutter.createFragment(String route)可生成FlutterFragment
以根view作为Activity
创建一个空的Activity,用Flutter创建一个View作为页面的根View:
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
View flutterView = Flutter.createView(this, getLifecycle(), "route1");
ViewGroup.LayoutParams layout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
addContentView(flutterView, layout);
}
}
这里我们并没有使用setContentView而是是用了addContentView这个方法,原因是这样的:
虽然FLutter的加载速度非常快,但是这个过程依然存在,在创建FLutterView之前我们先给ContentView设置了一个R.layout.activity_flutter布局,这个布局可以作为FlutterView加载完成之前展示给用户的界面,当然大部分情况下用户根本感知不到这个界面Flutter已经加载完成了,但我们仍需要它,因为debug模式下造成Flutter的加载速度并不是非常快,这个界面可以给开发人员看,还有就是如果没有这个界面的话在Activity的加载过程会出现一个黑色的闪屏,而这个情况对用户来说并不友好。
在Flutter工程中根据不同的route创建不同的组件
flutter 只有一个入口, 就是main.dart中的main方法 ,
void main() => runApp(new MyApp());
我们这要根据原生传来的route显示不同的组件,所以不执行runApp,而是通过window的全局变量中获取到当前的routeName来返回不同的组件,
window.defaultRouteName
正是Android中createView
方法中的第三个参数route.
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String defaultRouteName) {
switch (defaultRouteName) {
case 'route1':
return Container(
color: Colors.green,
child: Column(
children: <Widget>[
Icon(
Icons.accessible_forward,
size: 100,
textDirection: TextDirection.ltr,
),
Text('defaultRouteName:$defaultRouteName',
textDirection: TextDirection.ltr)
],
),
);
break;
default:
return Column(
children: <Widget>[
Icon(
Icons.accessible_forward,
size: 100,
textDirection: TextDirection.ltr,
),
Center(
child: Text('default,defaultRouteName:$defaultRouteName',
textDirection: TextDirection.ltr)),
],
);
break;
}
}
然后我们在mainActivity中加入一个跳转到第二个activity的方法:
记得在xml中加入这个按钮
public class MainActivity extends AppCompatActivity {
Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, SecondActivity.class));
}
});
}
}
让Flutter模块支持热加载
首先在Flutter目录下启动监听服务,在你的项目根目录/flutter下执行
flutter attach
执行后,监听服务会等待并监听debug应用中flutter的状态
然后在打开FlutterInAndroid项目的AS中以正常方式调试运行,在真机或模拟器中运行app后并不会立即出发flutter的监听服务,当flutter的view或Fragment激活时才会触发。
当flutter的监听服务和app建立连接后,终端会出现如下输出:
Waiting for a connection from Flutter on ONEPLUS A5000...
Done.
Syncing files to device ONEPLUS A5000... 1,782ms
� To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on ONEPLUS A5000 is available at: http://127.0.0.1:13379/
For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".
这时我们修改flutter工程中的dart代码文件,保存后在终端中点击r键即可进行热加载,R键进行热重启。
运行
此时启动Android项目 , 跳转到SecondActivity时 会看到有黑屏 但是release包就没有这种情况了.
更改flutter代码时, 在 flutter窗口的下面输入R即可实时刷新页面
在同一个页面显示android组件和flutter组件
如果想要实现这个需求.我们需要稍微修改一下代码.
还记得我们SecondActivity里的setContentView(R.layout.activity_second)
吗?
可以在R.layout.activity_second
布局文件中直接写一个组件:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondActivity">
<RelativeLayout
android:id="@+id/relative"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints">
<TextView
android:id="@+id/hello_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
如上所示, 我加入了一个RelativeLayout和一个Textview,
然后改变一下我们加入flutter view的方式:
public class SecondActivity extends AppCompatActivity {
TextView textView;
RelativeLayout relativeLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
textView = findViewById(R.id.hello_text);
relativeLayout = findViewById(R.id.relative);
View flutterView = Flutter.createView(this, getLifecycle(), "route1");
RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
layout.addRule(RelativeLayout.BELOW, R.id.hello_text);
relativeLayout.addView(flutterView, layout);
}
}
我们先获取到了textView
和 relativeLayout
,
之后创建flutter view
. 在布局参数那里, 我们设置成flutter view显示在TextView的下面, 然后再添加到布局中就好了.
解决黑屏问题
如果这样写,在跳转到含有flutter View的Activity中时, debug模式下会有明显的黑屏问题 .
release模式下几乎不可见,但低端手机上首次加载可能会有感知.
这是为什么呢?
主要是因为flutter view加载比较慢, 在它加载出来之前,它所处的区域会显示为黑色 . 那么我们怎么处理呢?
如上代码, 我将flutter添加到了relativeLayout中,我们可以在flutter view加载完成前隐藏这个layout, 在加载成功之后才显示出来, 看一下代码:
public class SecondActivity extends AppCompatActivity {
TextView textView;
RelativeLayout relativeLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
textView = findViewById(R.id.hello_text);
relativeLayout = findViewById(R.id.relative);
Intent i = getIntent();
String r = i.getStringExtra("name");
FlutterView flutterView = Flutter.createView(this, getLifecycle(), r);
relativeLayout.addView(flutterView);
final FlutterView.FirstFrameListener[] listeners = new FlutterView.FirstFrameListener[1];
listeners[0] = () -> relativeLayout.setVisibility(View.VISIBLE);
flutterView.addFirstFrameListener(listeners[0]);
}
}
activity_second.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondActivity">
<TextView
android:id="@+id/hello_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
android:id="@+id/relative"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible">
</RelativeLayout>
</LinearLayout>
在RelativeLayout中, 添加了android:visibility="invisible"
默认隐藏 ,
listeners[0] = () -> relativeLayout.setVisibility(View.VISIBLE);
可以写做:
listeners[0] = new FlutterView.FirstFrameListener() {
@Override
public void onFirstFrame() {
relativeLayout.setVisibility(View.VISIBLE);
}
};
意思是在渲染完第一帧之后才显示relativeLayout
这样就将黑屏的时间过渡掉了.
数据传递
涉及到EventChannel和MethodChannel.
- EventChannel:是native发送给flutter的数据用的
- MethodChannel: 是native接收flutter数据用的
在Android中添加:
写在调用flutter组件的activity的oncreate中:
//定义好通道的名字
private static final String BatteryLevelChannel = "samples.flutter.io/battery";
private static final String sendToFlutter = "samples.flutter.io/sendToFlutter";
...
//native接收flutter数据
new MethodChannel(flutterView, BatteryLevelChannel).setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
// methodCall.method 对应 Flutter端invokeMethod方法的第一个参数
if (methodCall.method.equals("123")) {
// 获取Flutter传递的参数
String msg = methodCall.argument("msg");
// 回传给Flutter
result.success(msg);
}
}
});
//native发送给flutter的数据
new EventChannel(flutterView, sendToFlutter).setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
eventSink.success("原生来的数据");
}
@Override
public void onCancel(Object o) {
// 做一些注销操作
}
});
Activity代码:
package com.example.androidwithflutterproject;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
import io.flutter.facade.Flutter;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.view.FlutterView;
public class SecondActivity extends AppCompatActivity {
TextView textView;
RelativeLayout relativeLayout;
private static final String BatteryLevelChannel = "samples.flutter.io/battery";
private static final String sendToFlutter = "samples.flutter.io/sendToFlutter";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
textView = findViewById(R.id.hello_text);
relativeLayout = findViewById(R.id.relative);
Intent i = getIntent();
String r = i.getStringExtra("name");
FlutterView flutterView = Flutter.createView(this, getLifecycle(), r);
relativeLayout.addView(flutterView);
final FlutterView.FirstFrameListener[] listeners = new FlutterView.FirstFrameListener[1];
listeners[0] = new FlutterView.FirstFrameListener() {
@Override
public void onFirstFrame() {
relativeLayout.setVisibility(View.VISIBLE);
}
};
flutterView.addFirstFrameListener(listeners[0]);
//native接收flutter数据
new MethodChannel(flutterView, BatteryLevelChannel).setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
// methodCall.method 对应 Flutter端invokeMethod方法的第一个参数
if (methodCall.method.equals("123")) {
// 获取Flutter传递的参数
String msg = methodCall.argument("msg");
// 回传给Flutter
result.success(msg);
}
}
});
//native发送给flutter的数据
new EventChannel(flutterView, sendToFlutter).setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
eventSink.success("原生来的数据");
}
@Override
public void onCancel(Object o) {
// 做一些注销操作
}
});
}
}
flutter中的代码就简单了
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String defaultRouteName) {
switch (defaultRouteName) {
case 'route1':
return MyApp();
break;
default:
return Column(
children: <Widget>[
Icon(
Icons.accessible_forward,
size: 100,
color: Colors.green,
textDirection: TextDirection.ltr,
),
Center(
child: Text('default,defaultRouteName:$defaultRouteName',
style: TextStyle(color: Colors.blue),
textDirection: TextDirection.ltr)),
],
);
break;
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
static const platform = const MethodChannel('samples.flutter.io/battery');
static const sendToFlutter =
const EventChannel("samples.flutter.io/sendToFlutter");
String _batteryLevel = 'Unknown battery level.';
StreamSubscription _timerSubscription = null;
Future sendToNative() async {
String batteryLevel =
await platform.invokeMethod('123', {'msg': '来自flutter的数据'});
setState(() {
_batteryLevel = batteryLevel;
});
}
void getDataFromNative() {
_timerSubscription = sendToFlutter.receiveBroadcastStream().listen((msg) {
setState(() {
_batteryLevel = msg;
});
}); // 添加监听
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: sendToNative,
child: Text('发送给Native数据并接收回调参数'),
),
Text(_batteryLevel),
RaisedButton(
onPressed: getDataFromNative,
child: Text('接收Native的数据'),
),
],
),
),
);
}
}