Help us understand the problem. What is going on with this article?

Flutter の PlatformView を使って Kotlin で実装したコンポーネントを Widget として読み込む

本投稿は、 Flutter AdventCalendar 2019 10日目の参加記事です。

PlatformView とは

PlatformView とは、Android / iOS 向けに実装されたコードを Widget として利用できる Flutter の機能のことを指します。Android 向けには AndroidView、iOS UIKitView という Class で、それぞれ提供されます。

今回の記事では Android かつ Kotlin を対象として取り扱います。iOS は解説の対象外としますが、後日改めて公開する予定です。

完成図

今回解説する内容で実現できるものを以下に示します。

Artboard 1@2x.png

2つの Card スタイルの Widget がレンダリングされています。各々 Kotlin で実装された View を Flutter から表示しています。

コード全体を以下の GitHub リポジトリに公開しています。

ここからは実装の要点を解説していきます。

おもな登場人物と用語の整理

  • Flutter : v1.9.1+hotfix.6
  • Kotlin : 1.2.71
  • Gradle : 3.2.1
  • PlatformView : 各プラットフォーム向けにネイティブコードで実装された View を Widget として呼び出す Flutter の仕組み
  • AndroidView : PlatformView の機能を利用して Android 向けにネイティブコードで実装された View を呼び出す Widget
  • Layout XML : Android アプリのレイアウトを定義したファイル

大まかな流れ

PlatfovmView の機能を利用するために必要なおもな工程を以下に示します。

  1. Flutter プロジェクトを Kotlin 向けに初期化する(されていない場合)
  2. ここからネイティブコード (Kotlin) の話: Layout XML (first_widget.xml) を実装する
  3. Kotlin で各 Class(FirstWidget, FirstWidgetFactory, FirstWidgetPlugin) を実装する
  4. MainActivity.kt に FirstWidgetPlugin を利用する設定を追記
  5. ここから Flutter (Dart) の話: Flutter で AndroidView を利用した Widget を実装する

SecondWidget~ を実装する場合は [3.] からくり返します。

各工程の解説に進みます。

1. プロジェクトを Kotlin 向けに初期化する

flutter create コマンドで各プラットフォーム向け言語を明示しなかった場合、Android 向けには Java、iOS 向けには Objective-C でプロジェクトが初期化されます。今回は Kotlin を対象とした話なので、Kotlin 向けにプロジェクトを初期化します。
新規プロジェクトの場合、flutter create コマンドに -a kotlin オプションを付与して実行します。

$ flutter create --androidx -a kotlin -i swift my_project

既存プロジェクトの場合でも、プロジェクト配下の ./android ディレクトリを待避することで再初期化が可能です。./android ディレクトリ配下以外には影響がないので、AndroidManifest.xml を始めとするディレクトリ配下のファイルを変更していない場合には以下に示すコマンドで再初期化できます。そうでない場合には別途マイグレーションが必要になります(今回は解説しません)。

# 既存プロジェクトを Kotlin 向けに初期化しなおす場合。実行注意
$ mv ./android ./backup_android
$ flutter create --androidx -a kotlin .

2. Layout XML の用意

プロジェクトの初期化に成功したら、Layout を定義した XML first_widget.xml を用意します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/first_widget">
<TextView
        android:id="@+id/first_widget_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="FirstWidget from Android!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

この XML ファイルは、のちほど実装する FirstWidget Class から読み込みます。

3. Kotlin で各 Class の実装を行う

Layout XML の準備が出来たら、FirstWidget.kt を実装します。
first_widget.xml の読み込みと(今回は特に使用しないのですが)Flutter 側からメソッドを呼び出せるよう MethodChannel の実装をしておきます。

class FirstWidget internal constructor(context: Context, id: Int, messenger: BinaryMessenger) : PlatformView, MethodCallHandler {
    private val view: View
    private val methodChannel: MethodChannel

    override fun getView(): View {
        return view
    }

    init {
        view = LayoutInflater.from(context).inflate(R.layout.first_widget, null)
        methodChannel = MethodChannel(messenger, "plugins/first_widget_$id")
        methodChannel.setMethodCallHandler(this)
    }

    override fun onMethodCall(methodCall: MethodCall, result: Result) {
        when (methodCall.method) {
            "ping" -> ping(methodCall, result)
            else -> result.notImplemented()
        }
    }

    private fun ping(methodCall: MethodCall, result: Result) {
        result.success(null)
    }

    override fun dispose() {
    }
}

次に FirstWidget を生成する FirstWidgetFactory.kt を実装します。

class FirstWidgetFactory(private val messenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {

    override fun create(context: Context, id: Int, o: Any?): PlatformView {
        return FirstWidget(context = context, id = id, messenger = messenger)
    }
}

Flutter から引数を渡している場合、引数は create メソッド引数 o を利用して取り出すことができます。

次に、FirstWidgetFactory を PlatformView に登録する FirstWidgetPlugin.kt を実装します。

object FirstWidgetPlugin {
    fun registerWith(registrar: Registrar) {
        registrar
                .platformViewRegistry()
                .registerViewFactory(
                        "plugins/first_widget", FirstWidgetFactory(registrar.messenger()))
    }
}

FirstWidgetPlugin では、お作法に則ったコードを書くのみです。

4. MainActivity.kt の修正

Kotlin 側の実装の最後に、MainActivity.kt に実装されている onCreate() メソッド内に、FirstWidgetPlugin を登録する処理を追記します。

package com.example.my_project
import android.os.Bundle
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)
FirstWidgetPlugin.registerWith(this.registrarFor("com.example.flutter_platformview_example.FirstWidgetPlugin"))
  }
}

5. Flutter で AndroidView を利用した Widget を実装する

ここからは Flutter 側の実装です。Flutter の Widget first.dart に FirstWidget Class と _FirstWidgetState Class を実装します。AndroidView を使用する部分以外は、一般的な StatefullWidget と同じです。

class _FirstWidgetState extends State<FirstWidget> {
  @override
  Widget build(BuildContext context) {
    if (defaultTargetPlatform == TargetPlatform.android) {
      return AndroidView(
        viewType: 'plugins/first_widget',
        onPlatformViewCreated: _onPlatformViewCreated,
        creationParamsCodec: const StandardMessageCodec(),
      );
    }
    return const Text('iOS platform version is not implemented yet.');
  }
void _onPlatformViewCreated(int id) {
    if (widget.onFirstWidgetWidgetCreated == null) {
      return;
    }
    widget.onFirstWidgetWidgetCreated(FirstWidgetController._(id));
  }
}
class FirstWidgetController {
  FirstWidgetController._(int id)
      : _channel = MethodChannel('plugins/first_widget_$id');
final MethodChannel _channel;
Future<void> ping() async {
    return _channel.invokeMethod('ping');
  }
}

AndroidView の引数 viewType は、FirstWidgetPlugin の registerViewFactory() の第一引数で指定した命名と一致させます。これで AndroidView と Kotlin 側で実装した View とが関連付きます。Flutter側からパラメータを渡したい場合は、creationParams という引数を追加して値を渡します。Dart から Kotlin にデータを受け渡すことになるので、型の対応に注意する必要があります。

FirstWidgetController については今回は使用していないのですが、MethodChannel をつうじて Flutter 側から ネイティブコードで実装したメソッドを呼び出すために必要な記述です。
これで、ネイティブコードで実装した View が、Flutter の Widget であるかのように取り扱うことができるようになります。

コードの全体を公開している GitHub リポジトリを再掲します。

上記リポジトリのサンプルコードでは、FirstWidget のほかにもうひとつ SecondWidget を表示し、同一のスクリーンにふたつの Widget をレンダリングしています。

まとめ

今回の内容で学ぶことができた内容をまとめます。
PlatformView (AndroidView, UIKitView)の機能を使うことで、ネイティブコードで実装した View を Flutter Widget として扱える
Android 側のネイティブコードは、Kotlin で問題なく実装できる
ここまで理解できていれば、iOS に対応するネイティブコードを Swift で実装することもそう難しいことではありません。

以上です。

参考にしたドキュメント

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away