背景
由于项目需要,团队使用flutter进行开发,实现一款门店点餐的app(android双屏设备),工作人员使用主屏操作点餐,副屏显示餐单和价格等信息给顾客。
技术方案:
- 整体项目为flutter-app形式,我们将副屏能力封装成plugin提供给主程序使用;
- 副屏显示方案为presentation;
- 副屏一个维护flutterEngine,主屏维护一个flutterEngine,两个engine使用channel进行关联通信;
记录实现步骤:
1. 使用 androidStudio 创建一个 flutterPlugin 项目: flutter_subscreen_plugin(目录结构如下)
2. 第二步,封装原生能力,提供唤起第二屏幕的能力
创建一个类 FlutterSubScreenPresentation 继承自 Presentation,作为副屏的UI载体:
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
class FlutterSubScreenPresentation(outerContext: Context?, display: Display?) : Presentation(outerContext, display) {
lateinit var flutterEngine: FlutterEngine
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val engine = FlutterEngine(context)
flutterEngine = engine
//指定初始化路由
flutterEngine.navigationChannel.setInitialRoute("subMain");
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"main"))
setContentView(R.layout.flutter_presentation_view)
val flutterView: FlutterView = findViewById(R.id.flutter_presentation_view)
flutterView.attachToFlutterEngine(flutterEngine)
// 一定要调用 不然页面会卡死不更新
flutterEngine.lifecycleChannel.appIsResumed()
}
override fun dismiss() {
flutterEngine.lifecycleChannel.appIsDetached()
super.dismiss()
}
}
flutter_presentation_view 文件如下:
技术点:
- 继承 presentation, 重写onCreate,新建一个flutterEngine,用于关联flutterView,将flutterView作为 setContentView 的入参,实现使用flutter层来绘制副屏页面;
- setInitialRoute("subMain") 用于指定main.dart中的初始化路由
- dartExecutor.executeDartEntrypoin 用于指定engine对应的渲染页面的路径:lib/main.dart
- flutterView.attachToFlutterEngine(flutterEngine) 此时进行UI渲染
- flutterEngine.lifecycleChannel.appIsResumed() 生命事件传递
3. 主副屏间的交互通信:
当我们创建了plugin项目时,自动生成了一个类 FlutterSubscreenPlugin ,主副屏通过这个中间件来进行交互。
我们将结构分为三种颜色来进行标记:(如下)
- 定义两个channel,一个用于主屏与原生交互,一个用于副屏与原生交互(蓝色)
- 本插件(FlutterSubscreenPlugin)与主工程(主屏)进行绑定时,onAttachedToEngine被触发,此时,使用mainChannel来进行绑定监听,在onMethodCall中处理事件监听,将mainChannel接收到的事件传递给subChannel 进行分发(红色)【主 --> 副】
- 提供方法给外部初始化,提供能力将subChannel接收到的事件传递给mainChannel 实现副屏与主屏的数据传递(绿色)【副 --> 主】
4. 新建一个工具类 FlutterSubScreenProvider ,提供副屏初始化方法
class FlutterSubScreenProvider {
companion object {
//初始化副屏
fun configSecondDisplay(plugin: FlutterSubscreenPlugin, context: Context) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val manager =
context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val displays = manager.displays
if (displays.size > 1) {
val display = displays[1]
val handler = FlutterSubScreenPresentation(context, display)
handler.show()
plugin.onCreateViceChannel(handler.flutterEngine.dartExecutor)
}
}
} catch (e: Throwable) {
println(e.message)
e.printStackTrace()
}
}
}
5. 定义副屏初始化时机,让 FlutterSubscreenPlugin 实现 ActivityAware 接口,重写 onAttachedToActivity 方法,调用初始化副屏:
class FlutterSubscreenPlugin: FlutterPlugin, ActivityAware, MethodCallHandler{
...
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
//your plugin is now attached to an Activity
//初始化副屏
FlutterSubScreenProvider.configSecondDisplay(this, binding.activity)
}
...
}
接下来粘贴一下dart文件中UI层需要做的处理:main.dart
void main() {
var defaultRouteName = window.defaultRouteName;
if ("subMain" == defaultRouteName) {
viceScreenMain();
} else {
defaultMain();
}
}
//主屏ui
void defaultMain() {
runApp(MainApp());
}
//副屏ui
void viceScreenMain() {
runApp(SubApp());
}
在main方法中获取initRoute做区分,绑定对应的widget
新建一个工具类用于channel 主副屏交互:
///封装方法用于主副屏交互
class SubScreenPlugin {
static const _mainChannelName = 'screen_plugin_main_channel';
static const _subChannelName = 'screen_plugin_sub_channel';
// ignore: close_sinks
static StreamController _subStreamController;
// ignore: close_sinks
static StreamController _mainStreamController;
static MethodChannel _mainChannel = MethodChannel(_mainChannelName)
..setMethodCallHandler(_onMainChannelMethodHandler);
static MethodChannel _subChannel;
static Stream get viceStream {
if (_subChannel == null) {
_subChannel = MethodChannel(_subChannelName)
..setMethodCallHandler(_onSubChannelMethodHandler);
}
if (_subStreamController == null) {
_subStreamController = StreamController.broadcast();
}
return _subStreamController.stream;
}
static Stream get mainStream {
if (_mainStreamController == null) {
_mainStreamController = StreamController.broadcast();
}
return _mainStreamController.stream;
}
static Future _onSubChannelMethodHandler(MethodCall call) async {
//副屏channel 每接收到一个事件都放进去流里, 由外部监听
_subStreamController?.sink?.add(call);
return "success";
}
static Future _onMainChannelMethodHandler(MethodCall call) async {
//主屏channel 每接收到一个事件都放进去流里, 由外部监听
_mainStreamController?.sink?.add(call);
return "success";
}
//给主屏幕调用,发送事件体给副屏
static Future sendMsgToViceScreen(
String method, {
Map params,
}) async {
await _mainChannel.invokeMethod(method, params);
}
//给副屏幕调用,发送事件体给主屏
static Future sendMsgToMainScreen(
String method, {
Map params,
}) async {
await _subChannel.invokeMethod(method, params);
}
}
通过如下方法,可以拿到主屏传递给副屏的所有事件数据:
//发送数据
SubScreenPlugin.sendMsgToViceScreen("test",params: {"content": "test"});
//获取数据
SubScreenPlugin.viceStream.listen((event) {
val name = event.method;//test
val params = event.arguments;// {"content": "test"}
});
注意:使用android的双屏,需要在清单配置文件,添加如下两个权限: