プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

Flutter プラグインの Android 側プラットフォームコードのデバッグ

Flutter って、 pub.dev で簡単に各種パッケージ(ライブラリ)を選んで入れるのができるのが便利ですよね。

先日、ある Flutter のプロジェクトのアップデート作業をしていたら、そこで使ってたパッケージ(プラグイン)でエラーを吐くものが出てきました。最初に作成した当時から Android のバージョンとかが上がっていたのがどうも関係してそうで、これをデバッグする必要が出てきました。

Flutter のプラグインは各プラットフォーム毎のAPIを利用しているものになります。これだけでちょっとややこしそうですよね。 Flutter のプラグイン開発なんてやったことないので、最初はデバッグなんて避けて、別の類似のパッケージに切り替えようと思ったんですが、それはそれでアプリケーション側で問題があることがわかってきたので、今回挑戦してみました。

ということで、ここではその際のデバッグ方法をメモとしてまとめておきます。あ、デバッグしたのは Android 側のプラットフォームコードです。

なお、使った環境は下記でした。

  • Android Studio Dolphin 2021.3.1 Patch 1
  • Flutter 3.7.12 / Dart 2.19.6

Flutter のパッケージについて

ちなみに、 Flutter のパッケージは大きく3つにわかれていて、

  • Dart package
  • Plugin package
  • FFI Plugin package

と呼ばれてるようです。

最初の Dart package は Dart を省略して package とだけ呼ばれることもあります。これは、 Dart のみで作成されたパッケージです。

Plugin package は省略して plugin と呼ばれることがあります。これは、プラットフォーム固有のAPIを呼び出す機能を持ったパッケージになります。一般に、

  • Android : Java / Kotlin
  • iOS : Objective-C / Swift
  • macos : Swift
  • WIndows : C++
  • Linux : C++
  • Web : Dart

のコードを呼び出すことになります。

最後の FFI Plugin package ですが、dart:ffi という dart の C API 呼び出し機能を用いたプラットフォーム固有の実装を持つプラグインとなるようです。

ついでに、プラグインはプラットフォーム固有部分をそれぞれの言語で書かずに、 Dart で書くのもありらしいです(そんなケースあるのか?)。おまけに、 Dart + プラットフォーム固有の言語、という構成もとれるそうです。柔軟といえば柔軟だけど、なかなかややこしいなー。

プラグインプロジェクトを作成

ここではテスト用に、まずデバッグする対象となるプラグインロジェクトを作ります。Android Studio を起動して、Welcome 画面で

『New Flutter Project』を選択します。 そうすると、 Flutter の SDK パスを確認されるので、複数の SDK がある場合は使いたいものを選択しておきます。

『Next』を押すと、プロジェクトの設定画面が表示されます。

Project Type で Application が選択されているので

これを Plugin に変更します。

Android langurage および iOS language は使いたい言語を選択します。Platform はサポートしたいプラットフォームを選択します。今回は Android と iOS としました。

『Finish』を押すとプロジェクトが作成されて、Android Studio が起動します。

プラグインプロジェクトについて

ちなみに作成直後のプロジェクトの状態はこんな感じになってました。

このプラグインには getPlatformVersion というメソッドが定義されていて、Android/iOSのバージョンを返すというものです。

class PluginTest {
  Future<String?> getPlatformVersion() {
    return PluginTestPlatform.instance.getPlatformVersion();
  }
}

example/lib/main.dart にこのプラグインを使ったサンプルプロジェクトがあります。

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';
  final _pluginTestPlugin = PluginTest();

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String platformVersion;
    // Platform messages may fail, so we use a try/catch PlatformException.
    // We also handle the message potentially returning null.
    try {
      platformVersion =
          await _pluginTestPlugin.getPlatformVersion() ?? 'Unknown platform version';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }
(後略)

こんな感じに、アプリが起動するとプラットフォームのバージョン情報を各プラットフォーム側に問い合わせて取得します。で、上記では省略しましたが、取得した結果を表示するというアプリになってます。実際に実機をつないで Android Studio で実行させると、

こんな感じで動作していることが確認できます。

プラットフォーム(Android)側のコードも見てみます。 android/src/main/java/com.example.plugin_test/PluginTestPlugin.java というファイルがあり、その中で

public class PluginTestPlugin implements FlutterPlugin, MethodCallHandler {
  /// The MethodChannel that will the communication between Flutter and native Android
  ///
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  private MethodChannel channel;

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "plugin_test");
    channel.setMethodCallHandler(this);
  }

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    if (call.method.equals("getPlatformVersion")) {
      result.success("Android " + android.os.Build.VERSION.RELEASE);
    } else {
      result.notImplemented();
    }
  }
(後略)

というように、 onMethodCall というメソッド内で呼ばれた際の処理が記述されています(Kotlin を選択したら、 .kt のファイルが作られます)。

デバッグ

プラグインプロジェクトができたので、デバッグしてみます。

うまくいかないやり方

まずは、 example/lib/main.dart の

と、プラグインのコード

と、 Android 側のコードにブレークポイントを設定してみます。

デバッグ実行すると、最初のサンプルコードのところで止まります。そのまま続行すると、次にプラグインのコードで止まります。そのまま続行すると、 Android のコードで止まるかと思いきや、何も起こらずにそのまま処理が実行されます。

これだと、プラットフォーム側のデバッグができないことがわかりました。

正しいやり方

じゃあ、どうすればいいんだろうか?と思って調べてみると、ありました。正しいデバッグ手順の元ネタは下記の記事です。

Flutter:Android側のコードをAndroid Studioでデバッグしたい | by Keisuke Kawajiri | Learn about Flutter | Medium

まあ、この通りにやれば特に問題ないのですが、一応自分なりに手順をまとめておきます。

プラグイン側にブレークポイントを設定して、デバッグ実行するところまでは先ほどと同じです。デバッグ実行後、ブレークポイントで停止したら、プラットフォームコードのファイルを選択して、右クリックし

『Flutter』->『Open for Editing Android Studio』を選択します。次に、プロジェクトを開く際の確認ダイアログが出てくるので、

『New Window』を選択します。すると、 example/android にあるサンプルプロジェクトと一緒に、プラットフォーム側のプロジェクトファイルが、別ウィンドウで起動した Android Studio に表示されます(この時点で Android Studio が2つ立ち上がっているはずです)。

ここで、あとから起動した Android Studio 上でデバッグしたいプラットフォーム側のコードを開いて、

のようにブレークポイントを設定します。

次に、この Android Stduio 側で画面上部のツールバーの『Attach Debugger to Android Process』を選択します。

もちろん、メニューから

『Run』->『Attach Debugger to Android Process』と選択しても OK です。

これを選択すると、

のように実行中のプロセスを選択する画面が表示されるので、これを選択して、OKを押します。

ここで、ブレークポイントで停止中の最初の Android Studio に戻り、実行を継続すると

おぉ!プラットフォーム側でデバッガが止まり、変数とかも中身が確認できました。2つのデバッガで1つのプロセスを見る感じになるんですね。

これで、プラグインのデバッグもできますね!

ちなみに、上記のデバッグのやり方の公式の説明は下記になるようです。

Using an OEM debugger | Flutter

エラーが起きた場合

2つ目の Android Studio でデバッグで停止中のプロセスにアタッチする際に

Warning: debug info can be unavailable. Please close other application using ADB: Monitor, DDMS, Eclipse

というエラーが表示されることがあります。

これの解決方法は、一番簡単にはデバッグプロセスを実行中の端末を再起動することだそうです。

android - How to resolve "Warning: debug info can be unavailable. Please close other application using ADB: Restart ADB integration and try again" - Stack Overflow

動画もありました。

www.youtube.com