FlutterPlugin 实现双屏

背景

由于项目需要,团队使用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 文件如下:




    


技术点:
  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)
  }
...
}
接下来粘贴一下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的双屏,需要在清单配置文件,添加如下两个权限:


整个调用关系的结构如下:
image.png
git已上传,项目路径:https://github.com/liyufengrex/flutter_subscreen_plugin
plugin已上传,使用方式:Flutter 使用插件实现双屏交互

你可能感兴趣的:(FlutterPlugin 实现双屏)