プログラマーのメモ書き

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

Flutter の Android 用設定の targetSdkVersion とかの設定場所について

Flutter でアプリを作成すると android/app/build.gradle ファイルとかに、 targetSdkVersion とかが書かれると思います。

いままでは、これって数字がハードコードされていたと思ってたけど、ふと最近作ったプロジェクトを見直したら、

        minSdkVersion flutter.minSdkVersion
        targetSdkVersion flutter.targetSdkVersion

のように設定されていました。これ、元の値はどこにあるんだろうか?

設定元

これは、 Flutter SDK の < SDK install folder > /flutter/packages/flutter_tools/gradle にある flutter.gradle ファイルに設定されているそうです。

(前略)
/** For apps only. Provides the flutter extension used in app/build.gradle. */
class FlutterExtension {
    /** Sets the compileSdkVersion used by default in Flutter app projects. */
    static int compileSdkVersion = 33

    /** Sets the minSdkVersion used by default in Flutter app projects. */
    static int minSdkVersion = 16

    /** Sets the targetSdkVersion used by default in Flutter app projects. */
    static int targetSdkVersion = 33
(後略)

上記は Flutter SDK 3.7.12 のもの。

いつからなんだろうか?と思って調べてみると、下記の記事によると Flutter 2.8 からだそうです。

Flutter2.8以降におけるAndroidのビルドファイル(build.gradle)の各種SDKversion数値の確認方法 | みんプロ式 - 初心者専門Flutterでスマホアプリプログラミング講座

でもこの記事によると公式の説明がないそうです。そういうところが Google さんもなんだかなー・・・

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

TimeMachine を設定

修復した Mac mini ですが、簡単に復旧できたとは言え、バックアップは複数あった方が安心です。 ちょうど、 QNAP の NAS にまだ余裕もあるので、これをバックアップ先にして TimeMachine を設定することにしました。

設定時の環境は下記の通りでした。

  • QNAP TS-251+, QTS 5.0.1.2376
  • Mac mini (2018), macOS 13.3.1 (a) Ventura

QNAP 側の準備

早速、QNAP側の設定、と行きたいところだったんですが、バックアップデータの保存先の空き容量がちょっと心もとないので、まずは空き容量を増やします。

QNAPの場合、ボリュームの空き容量も簡単に増やせるのがうれしいですね。

シックボリュームまたはシンボリュームのサイズ変更 | QTS 5.0.x

QNAP にログインして、『ストレージ&スナップショット』を開きます。『ストレージ』->『ストレージ/スナップショット』を選択し、

空き容量を増やしたいボリューム(ここだと DataVol1)を選択して『管理』を選択します。表示された管理画面で、

『アクション』->『ボリュームのサイズ変更』を選択します。

下記のような設定画面が表示されるので、拡張後のサイズを入力します。

上記画面では、拡張後の容量を、 1TB として入力したところです。『適用』ボタンを押すと、すぐに作業が開始され、ステータスがいろいろと変化していきます。最終的に、ステータスが

のように『準備完了』となれば終了です。

QNAP 側の設定

さて、ボリュームに空き容量ができたら、やっと QNAP 側の設定です。設定は、下記の記事

Time Machine を使用して Mac を QNAP NAS にバックアップする方法 | QNAP

を見ると2通りの方法があるようです。

あとから、設定していることが追いかけやすいように、HBS3 を使う方法でやってみます。

QNAP のコンソールにログインして、 HBS3 を起動します。あれ?

早速、さきほどの記事と異なる画面が出てきました。どうも、ユーザーインターフェースが更新されたみたいですね。とりあえず、 TimeMachine を有効にします。

『バックアップ設定』のところに、『一意の共有フォルダーとNASアカウントを作成してください』とありますが、自分で作るんでしょうか?

とりあえず、チェックを入れてみると、

となりました。なるほど、画面の設定項目の感じからすると、内容としては上記の記事の説明の2つ目の『共有Time MachineアカウントとMacをHBS 3でバックアップ』と同じようです。

なお、『共有フォルダー名』と『ユーザー名』は固定のようです。なので、パスワードを入力して、保存先のボリュームを選択すればよさそうですね。とりあえず今回は保存先の使用量の制限はかけませんでした。

で、『適用』ボタンを押すと

という警告が表示されたので、そのまま『Proceed』(続行)を押します。すると、とくにエラーも表示されずに終了します。問題ないのかな?

念のため、共有フォルダが作られたか確認してみます。『コントロールパネル』を開いて、『権限設定』->『共有フォルダー』と進むと、

ちゃんと『TMBackup』フォルダが作られましたね。

Mac mini 側の設定

では、次に Mac mini 側の設定をします。公式の設定方法は、

Time Machine で Mac をバックアップする - Apple サポート (日本)

にあります。

まず、 Mac mini から共有フォルダへ接続します。

ファインダーの『移動』->『サーバーへ接続』

を選択し、 NAS 上の共有フォルダへのパスを入力します。

『接続』を押すと、パスワードを求められるので、さきほど設定したパスワードで接続します。

問題なく接続できたら、アップルメニューから『システム設定』->『一般』->『TimeMachine』を選択します。

表示された画面で『バックアップディスクを追加』を選択します。

2つ表示されてますが、何か違いあるのかな?とりあえず上側( .local が無いほう)で設定してみます。共有フォルダを選択した状態で『ディスクを設定』を押すと、

のような設定画面が表示されます。今回は暗号化はしないので、暗号化をオフにしておきます。

『完了』を押すと、これで設定が完了です。で、すぐに下記のような画面に切り替わり、

自動的にバックアップが始まりました。これで、問題なさそうですね。

TimeMachine の設定について

TimeMachine の画面から『オプション』を選択すると、バックアップ頻度を選択できます。

  • 1 時間ごと
  • 1 日ごと
  • 1 週間ごと

が選択できるようです。

ただ、手元の環境で動きをみていると、バックアップ間隔を 1 時間とかにしていても、スリープしてるときはバックアップが動かずに、次にスリープが解除されたときにバックアップを取るようです。そう考えると、 1 時間ごとにしておいても大きな問題なさそうです。

あと、 TimeMachine は設定しなければ、ドライブ内のデータすべてをバックアップするようです。NAS 上のフォルダとかは除外されるのかな?このあたりはちょっと定かではありませんが、バックアップ後の容量を見た感じでは除外されているっぽいです。

バックアップの方法は、単純な差分バックアップとかではないようです。まあ、気が向いたら調べてみますが、よしなにやってくれているとしておきましょう。

バックアップ先の容量制限(クォータ)も設定できますね。

問題点

さて、ちょっとづつ試しているのですが、現時点で困ったことが一つ出てきました。 QNAP 側の保存先の TMBackup という共有フォルダですが、これが QNAP の File Station とかでも中身が見れない、という状態になってます。

ディスク使用量は、コントロールパネルの『共有フォルダ』の設定画面から確認できるのですが、ちょっと気持ち悪いですね。

困ったな。おいおい調べてみたいと思います。

とりあえず、これで当面運用していきたいと思います。