FlutterPlugin 实现双屏

1,710 阅读3分钟

背景

由于项目需要,团队使用flutter进行开发,实现一款门店点餐的app(android双屏设备),工作人员使用主屏操作点餐,副屏显示餐单和价格等信息给顾客。

技术方案:

  1. 整体项目为 flutter-app 形式,我们将主副屏交互能力封装成plugin提供给主程序使用;
  2. 副屏显示方案为 presentation ;
  3. 副屏一个维护 flutterEngine,主屏维护一个 flutterEngine,两个 engine 使用 channel 进行关联通信;

实现步骤:

1. 使用 androidStudio 创建一个 flutterPlugin 项目: flutter_subscreen_plugin(目录结构如下)

image.png

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 文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <io.flutter.embedding.android.FlutterView
        android:id="@+id/flutter_presentation_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

技术点:

  1. 继承 presentation, 重写 onCreate,新建一个 flutterEngine,用于关联 flutterView,将 flutterView 作为 setContentView 的入参,实现使用 flutter 层来绘制副屏页面;
  2. setInitialRoute("subMain") 用于指定main.dart中的初始化路由
  3. dartExecutor.executeDartEntrypoin 用于指定 engine 对应的渲染页面的路径:lib/main.dart
  4. flutterView.attachToFlutterEngine(flutterEngine) 此时进行UI渲染
  5. flutterEngine.lifecycleChannel.appIsResumed() 生命事件传递
3. 主副屏间的交互通信

当我们创建了 plugin 项目时,自动生成了一个类 FlutterSubscreenPlugin ,主副屏通过这个中间件来进行交互。 我们将结构分为三种颜色来进行标记:(如下)

  1. 定义两个channel,一个用于主屏与原生交互,一个用于副屏与原生交互(蓝色)
  2. 本插件(FlutterSubscreenPlugin)与主工程(主屏)进行绑定时,onAttachedToEngine被触发,此时,使用 mainChannel 来进行绑定监听,在onMethodCall中处理事件监听,将 mainChannel 接收到的事件传递给 subChannel 进行分发(红色)【主 --> 副】
  3. 提供方法给外部初始化,提供能力将 subChannel 接收到的事件传递给 mainChannel 实现副屏与主屏的数据传递(绿色)【副 --> 主】

image.png

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)
  }
...
}
6. 接下来粘贴一下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

7. 新建一个工具类用于channel 主副屏交互:
///封装方法用于主副屏交互
class SubScreenPlugin {
  static const _mainChannelName = 'screen_plugin_main_channel';
  static const _subChannelName = 'screen_plugin_sub_channel';

  // ignore: close_sinks
  static StreamController<MethodCall> _subStreamController;

  // ignore: close_sinks
  static StreamController<MethodCall> _mainStreamController;

  static MethodChannel _mainChannel = MethodChannel(_mainChannelName)
    ..setMethodCallHandler(_onMainChannelMethodHandler);
  static MethodChannel _subChannel;

  static Stream<MethodCall> get viceStream {
    if (_subChannel == null) {
      _subChannel = MethodChannel(_subChannelName)
        ..setMethodCallHandler(_onSubChannelMethodHandler);
    }
    if (_subStreamController == null) {
      _subStreamController = StreamController<MethodCall>.broadcast();
    }
    return _subStreamController.stream;
  }

  static Stream<MethodCall> get mainStream {
    if (_mainStreamController == null) {
      _mainStreamController = StreamController<MethodCall>.broadcast();
    }
    return _mainStreamController.stream;
  }

  static Future<dynamic> _onSubChannelMethodHandler(MethodCall call) async {
    //副屏channel 每接收到一个事件都放进去流里, 由外部监听
    _subStreamController?.sink?.add(call);
    return "success";
  }

  static Future<dynamic> _onMainChannelMethodHandler(MethodCall call) async {
    //主屏channel 每接收到一个事件都放进去流里, 由外部监听
    _mainStreamController?.sink?.add(call);
    return "success";
  }

  //给主屏幕调用,发送事件体给副屏
  static Future<void> sendMsgToViceScreen(
    String method, {
    Map<String, dynamic> params,
  }) async {
    await _mainChannel.invokeMethod(method, params);
  }

  //给副屏幕调用,发送事件体给主屏
  static Future<void> sendMsgToMainScreen(
    String method, {
    Map<String, dynamic> 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的双屏,需要在清单配置文件,添加如下两个权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />

整个调用关系的结构如下: image.png

git已上传,项目路径:github.com/liyufengrex…
plugin已上传,使用方式:使用flutter插件开发双屏设备