暫くQiitaを書いてませんでした。
最近のあるFlutterアプリ開発案件ですが、Androidがマルチスクリーンモードを入ると、画面の部品が崩れる事象が発生してました。
一応、Flutter側はマルチスクリーンを検知できることはない感じ...
ぐぐって見たが、
一応、Android端末のマルチスクリーンを検知できることは可能であるようですが、詳しい説明がなくてこちらで、詳細な解決案と、実装を紹介したいと思います。
Qiitaには、昔以下のような紹介がありました。
但し、記事の作成時間が古くて、あんまりの助けにならない感じ???
以下のAndrodi開発者公式サイトでは、詳しく説明したようが、実装例がないぽい
https://developer.android.com/guide/topics/ui/multi-window?hl=ja
一応、実機環境では
minSDKVersion 21
targetSDKVersion 33
ではサポートされなかったので、それぞれ24,34にあげておきました。
では、実際の実装に進めたいと思います。
1) マルチスクリーン事件の発生を検知したい
Flutter のAndroidディレクトリの MainActivity.ktファイルに以下のソースを追加します。
class MainActivity: FlutterActivity() {
override
fun onMultiWindowModeChanged(isInMutiWindowMode: Boolean) {
super.onMultiWindowModeChanged(isInMutiWindowMode)
Log.d(TAG, "onMultiWindowModeChanged $isInMutiWindowMode")
}
}
上記実装により、Android向けにビルドされたアプリがマルチスクリーンモードに入ってるか、抜けてるかの通知を検知できるようになりました。
2) Flutter側に通知を送りたい
次に検知された、マルチスクリーン事件を、Flutterアプリに通知する仕組みを作りたいと思います。
同じMainActivity.ktファイルが存在するフォルダー下に、以下のようにフラグインファイルを新規作成します。
/** MultiScreenCheckPlugin */
class MultiScreenCheckPlugin : FlutterPlugin, MethodCallHandler {
}
Flutterと情報交換するには、MethodChannelというものを利用してますが、Qiitaにも色々詳しく紹介している記事が多いので、こちらは割愛させていただきます。
以下のように、フラグインの基本構造を実装します。
/** MultiScreenCheckPlugin */
class MultiScreenCheckPlugin : FlutterPlugin, MethodCallHandler {
companion object {
private const val CHANNEL_NAME = "com.example.android_multi_screen_checker/methodchannel"
private const val METHOD_SPLIT_SCREEN_STATUS = "splitScreenStatus"
}
private var pendingResult: MethodChannel.Result? = null
private lateinit var messenger: BinaryMessenger
private lateinit var methodChannel: MethodChannel
override
fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
messenger = flutterPluginBinding.binaryMessenger
methodChannel = MethodChannel(messenger, CHANNEL_NAME)
methodChannel.setMethodCallHandler(this)
}
override
fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
Log.d(TAG, "onDetachedFromEngine!!!!!")
methodChannel.setMethodCallHandler(null)
}
override
fun onMethodCall(@NonNull call: MethodCall,
@NonNull result: MethodChannel.Result ) {
Log.d(TAG, "onMethodCall!!!!!")
}
}
もし、Flutter側から、呼ばれたメソットを受けて処理する場合は、onMethodCall()中に弄らなければならないですが、
今度のように、AndroidのPlatformから発生されたイベントをFlutterへ送るようになったので、このonMethodCall()は空ままにしておきます。
上記の MultiScreenCheckPlugin クラス内に、送信メソットを追加実装します。
public fun sendSplitScreenStatus(isSplitScreen: Boolean) {
if(methodChannel == null) {
Log.d(TAG, "methodChannel is null")
return
}
Log.d(TAG, "sendSplitScreenStatus $isSplitScreen")
methodChannel.invokeMethod(METHOD_SPLIT_SCREEN_STATUS, isSplitScreen)
}
このままだと、ビルドに大変怒られるので、必要なライブラリをそれぞれimportしなければなりませんね...
一応、MultiScreenCheckPlugin.ktファイルの全部内容は以下となります。
package jp.co.origon.android_multi_screen_checker
import android.util.Log
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/** MultiScreenCheckPlugin */
class MultiScreenCheckPlugin : FlutterPlugin, MethodCallHandler {
private val TAG = "MultiScreenCheckPlugin"
companion object {
private const val CHANNEL_NAME = "com.example.android_multi_screen_checker/methodchannel"
private const val METHOD_SPLIT_SCREEN_STATUS = "splitScreenStatus"
}
private var pendingResult: MethodChannel.Result? = null
private lateinit var messenger: BinaryMessenger
private lateinit var methodChannel: MethodChannel
override
fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
messenger = flutterPluginBinding.binaryMessenger
methodChannel = MethodChannel(messenger, CHANNEL_NAME)
methodChannel.setMethodCallHandler(this)
}
override
fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
Log.d(TAG, "onDetachedFromEngine!!!!!")
methodChannel.setMethodCallHandler(null)
}
override
fun onMethodCall(@NonNull call: MethodCall,
@NonNull result: MethodChannel.Result ) {
Log.d(TAG, "onMethodCall!!!!!")
}
public fun sendSplitScreenStatus(isSplitScreen: Boolean) {
if(methodChannel == null) {
Log.d(TAG, "methodChannel is null")
return
}
Log.d(TAG, "sendSplitScreenStatus $isSplitScreen")
methodChannel.invokeMethod(METHOD_SPLIT_SCREEN_STATUS, isSplitScreen)
}
}
今度は、MainActivity.ktファイルに戻って、せっかく作成されたフラグインを登録しておきます。
private var checkerPlugin = MultiScreenCheckPlugin()
override
fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
flutterEngine.plugins.add(checkerPlugin)
}
同じく、こちらもそれぞれの必要なライブラリをimportしなければなりません。
一応、MainActivity.ktの全体内容は以下となります。
package jp.co.origon.android_multi_screen_checker
import android.util.Log
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"
private var checkerPlugin = MultiScreenCheckPlugin()
override
fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
flutterEngine.plugins.add(checkerPlugin)
}
override
fun onMultiWindowModeChanged(isInMutiWindowMode: Boolean) {
super.onMultiWindowModeChanged(isInMutiWindowMode)
Log.d(TAG, "onMultiWindowModeChanged $isInMutiWindowMode")
checkerPlugin.sendSplitScreenStatus(isInMutiWindowMode)
}
}
上記の実装で、Androud Platformからのマルチスクリーン事件の送信は何となくできました。
以下から、Flutter側の受信処理を引き続き、紹介します。
3) Flutter側でマルチスクリーンの変化を感じたい
まずは、flutter_riverpodと言う状態管理ライブラリを導入して実装進めたいと思います。(他にも色々の状態管理ライブラリがありますが、好みに選んでも構いませんよ〜)
Pubspec.yaml に以下のようflutter_riverpodライブラリを登録して、
Pub getを実行します。
dependencies:
flutter:
sdk: flutter
...
flutter_riverpod: ^2.3.6
...
次に、Dartのlibフォルダー下にAndroid Platformから、送信されるイベントを受信する機能の
platform_handler.dartファイルを作成して、PlatformHandlerクラスを実装します。
...
import 'package:flutter_riverpod/flutter_riverpod.dart';
...
class PlatformHandler {
final Ref ref;
PlatformHandler(this.ref) {}
}
上記クラス外に、マルチスクリーン状態を保存する、状態プロバイダと
PlatformHandlerを管理するプロバイダをそれぞれ実装しておきます。
final isSplitScreenMode = StateProvider<bool>((ref) => false);
final platformHandler = Provider<PlatformHandler>((ref) {
return PlatformHandler(ref);
});
Flutter側が受信されるChannelのCallbackメソットを初期化メソットをそれぞれ実装します。
作成したファイルと実装内容の以下になります。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final isSplitScreenMode = StateProvider<bool>((ref) => false);
final platformHandler = Provider<PlatformHandler>((ref) {
return PlatformHandler(ref);
});
const CHANNEL_NAME = "com.example.android_multi_screen_checker/methodchannel";
const METHOD_SPLIT_SCREEN_STATUS = "splitScreenStatus";
class PlatformHandler {
final Ref ref;
PlatformHandler(this.ref) {}
void initialize() {
debugPrint('PlatformHandler() init');
const channel = MethodChannel(CHANNEL_NAME);
channel.setMethodCallHandler(_methodHandle);
}
Future<dynamic> _methodHandle(MethodCall call) async {
debugPrint('_methodHandle() MethodCall: $call');
if (call.method == METHOD_SPLIT_SCREEN_STATUS) {
final isSplitScreen = call.arguments as bool;
ref.read(isSplitScreenMode.notifier).state = isSplitScreen;
return Future.value(isSplitScreen);
}
return null;
}
}
上記実装により、受信された状態値がisSplitScreenModeに保存されるはずです。
これで、上記のisSplitScreenMode変化を監視して、必要な制御が可能になりました。
以下は、AppBarに状態変化により、表示文言を変える実装にしたいと思いまう。
4) Flutter画面にマルチスクリーンの変化を表示させよう
一先ず、flutter_riverpodを投入することで、main.dartファイルに
ProviderScope()定義させる必要があります。
void main() {
runApp(const ProviderScope(child: MyApp()));
}
アプリ起動の時にplatformHandlerの初期化を登録します。
...
class MyApp extends ConsumerWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.read(platformHandler).initialize();
return MaterialApp(
...
以下、アプリタイトルバーに表示される文言がマルチスクリーン状況により変化される実層を追加します。
...
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(ref.watch(isSplitScreenMode)
? 'is in Split Screen mode'
: 'is NOT in Split Screen mode')),
body: Container(),
...
以下、実装完了したmain.dartファイルの全部内容になります。
import 'package:android_multi_screen_checker/platform_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.read(platformHandler).initialize();
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Android Split Screen Checker',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(ref.watch(isSplitScreenMode)
? 'is in Split Screen mode'
: 'is NOT in Split Screen mode')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Android Split screen mode\n support SDK API 24 later',
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center),
const SizedBox(height: 50),
const Text(
'if you any question pls contact me info@origon.co.jp'),
],
),
),
);
}
}
以上、色々書かれて長文になりましたが、お助けになれれば大変幸いと思います。
実装ソースは以下のGithubにアップロードしましたので、ご確認頂ければと思います。