Open54

Flutter 使ってモバイルの世界さ行くべ 🐦

maechanmaechan

Flutter を覚えることに相成った。過去にネイティブアプリ(Objective-C/Swift)を作っていた頃からかなりブランクがあるので、Flutter を使っていて気付いたことや躓いたことをメモしていく。

maechanmaechan

Flutter SDK, Xcode, Android Studio を入れた後の大事な環境構築

一通りの開発ツールを入れた後の各 IDE での必要な設定は flutter doctor で確認できる。このコマンドがやるべきことをリストアップしてくれる。

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.19.3, on macOS 14.4 23E214 darwin-arm64, locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    ✗ cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    ✗ Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.
[✗] Xcode - develop for iOS and macOS
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS and macOS development.
      Download at: https://developer.apple.com/xcode/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
        sudo xcodebuild -runFirstLaunch
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To install see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.2)
[✓] VS Code (version 1.87.1)
[✓] Connected device (2 available)
[✓] Network resources

! Doctor found issues in 2 categories.
The Flutter CLI developer tool uses Google Analytics to report usage and diagnostic
data along with package dependencies, and crash reporting to send basic crash
reports. This data is used to help improve the Dart platform, Flutter framework,
and related tools.

Telemetry is not sent on the very first run. To disable reporting of telemetry,
run this terminal command:

    flutter --disable-analytics

If you opt out of telemetry, an opt-out event will be sent, and then no further
information will be sent. This data is collected in accordance with the Google
Privacy Policy (https://policies.google.com/privacy)

いくつかエラーが出ているので対応していく。

cmdline-tools component is missing.

コマンドラインツールが無いのでインストールする。


Android Studio

Android license status unknown.

ライセンスが不足しているので以下のコマンドでライセンスの承認をおこなう。ライセンスについての巻物みたいに長い文章がいくつか表示されるので y 連打。

$ flutter doctor --android-licenses
[=======================================] 100% Computing updates...
4 of 6 SDK package licenses not accepted.
Review licenses that have not been accepted (y/N)? y

Xcode installation is incomplete; a full installation is necessary for iOS and macOS development.

Xcode の準備が足りてない。以下のコマンドを実行。

$ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
$ sudo xcodebuild -runFirstLaunch

CocoaPods not installed.

CocoaPods が必要なのでインストールする。

$ brew update && brew upgrade
$ sudo gem install cocoapods
$ pod --version
1.15.2

https://guides.cocoapods.org/using/getting-started.html

Cocoapods のインストール中にエラー

Cocoapods のインストール中に「うわ、私の Ruby のバージョン、低すぎ・・・」という状況になりました。

ERROR:  Error installing cocoapods:
The last version of drb (>= 0) to support your Ruby & RubyGems was 2.0.6. Try installing it with `gem install drb -v 2.0.6` and then running the current command again
drb requires Ruby version >= 2.7.0. The current ruby version is 2.6.10.210.

私の環境では Ruby は rbenv で管理しているので、それを通して環境を整備しました。

// 安定版を表示
$ rbenv install -l
3.0.7
3.1.5
3.2.4
3.3.1
jruby-9.4.6.0
mruby-3.3.0
picoruby-3.0.0
truffleruby-24.0.1
truffleruby+graalvm-24.0.1

// 適当な Ruby のインストールと適用
$ rbenv install 3.3.1
$ rbenv global 3.3.1
$ exec $SHELL -l

先生、再診お願いします。

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.19.3, on macOS 14.4 23E214 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.2)
[✓] VS Code (version 1.87.1)
[✓] Connected device (2 available)
[✓] Network resources

• No issues found!

大事なことって面倒くさい。

maechanmaechan

flutter doctor -v は細かい仕様を確認できる。

[✓] Flutter (Channel stable, 3.19.3, on macOS 14.4 23E214 darwin-arm64, locale ja-JP)
    • Flutter version 3.19.3 on channel stable at /Users/maechan/Dev/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision ba39319843 (10 days ago), 2024-03-07 15:22:21 -0600
    • Engine revision 2e4ba9c6fb
    • Dart version 3.3.1
    • DevTools version 2.31.1

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/maechan/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15E204a
    • CocoaPods version 1.15.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)

[✓] VS Code (version 1.87.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.84.0

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-arm64   • macOS 14.4 23E214 darwin-arm64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 122.0.6261.129

[✓] Network resources
    • All expected network resources are available.

• No issues found!
maechanmaechan

VSCode でデバッグ実行。

To use 'SeiyaのiPad' for development, enable Developer Mode in Settings → Privacy & Security.

Exited (1).

iPad の設定から開発者モードを ON にする。

maechanmaechan

VSCode のデバッグ実行したら警告っぽいものが2つほど。

Launching lib/main.dart on macOS in debug mode...
--- xcodebuild: WARNING: Using the first of multiple matching destinations:
{ platform:macOS, arch:arm64, id:xxxxxxxxxx-xxxxxxxxxx, name:My Mac }
{ platform:macOS, arch:x86_64, id:xxxxxxxxxx-xxxxxxxxxx, name:My Mac }
warning: Run script build phase 'Run Script' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'Flutter Assemble' from project 'Runner')

WARNING: Using the first of multiple matching destinations は、Xcode が複数の実行可能なターゲット(異なるアーキテクチャを持つ macOS デバイス)を検出して、その中から最初のものを自動的に選択したお知らせっぽいもの。

Run script build phase 'Run Script' will be run during every build because it does not specify any outputs. は、Xcode の「Run Script」ビルドフェーズが出力の依存関係を指定していないから、毎回のビルドで実行されるという内容。

どっちも初期の段階では無視して大丈夫そう。2つ目は Xcode の「Run Script」の output の依存もしくは Based on dependency analysis の設定をする。


うおお、久しぶりの Xcode や・・・懐かしくて鼻血でそう

maechanmaechan

なんか上手くいかないときのおまじない flutter clean

不要なデータやキャッシュなどの削除に。

$ flutter clean
Cleaning Xcode workspace...
Cleaning Xcode workspace...
Deleting build...
Deleting .dart_tool...
Deleting Generated.xcconfig...
Deleting flutter_export_environment.sh...
Deleting ephemeral...
maechanmaechan

必要最低限、まずは覚えておくべきディレクトリやファイル

  • 普段実装で使うコードを管理 lib/
  • パッケージ管理は pubspec.yaml
    • .lock ファイルもあるよ
  • プラットフォーム毎の設定を管理する ios/, android/, web/ etc...
maechanmaechan

エミュレータでアプリを立ち上げる

  • VSCode の下部バーにあるエミュレータ選択画面から適当なエミュレータを選択
    • ここでエミュレータが立ち上がる
  • VSCode の「実行」->「デバッグ開始」でエミュレータでアプリが起動


Android, iOS のエミュレータを起動してみた

ちなみにエミュレータ起動時にログに出るこちら。

Connecting to VM Service at ws://127.0.0.1:56477/QUrtiBrqCtw=/ws

これは Flutter が Dart の仮想マシン(VM)サービスに接続しているログ。具体的には WebSocket を使ってローカル 56477 ポートに、パスとセッション固有トークン(今回は QUrtiBrqCtw=/ws)を付与して接続をしている。

ちなみにこの URL のスキームを http に変えてブラウザでアクセスすると DartVM という仮想マシンにアクセスできる。アプリの中身を覗ける。


DartVM

maechanmaechan

Flutter/Dart を少し眺めて触ってみての感想は「JavaScript っぽく書ける Java(とかC言語)の書き心地と React みたいな宣言的UIでコンポーネントベースにインターフェースを構築できる言語」っぽさが見え隠れ。

温故知新、不易流行。

React ライクな雰囲気はウェブやってた身としてはよく手に馴染んでくれそうと期待。

ちなみにアプリの基盤になるエンジン部分は C++ 製。
https://qiita.com/kurun_pan/items/02b46e4b330b137da3db

エンジンはちょっと前から Skia から Impeller というやつに変わったらしい。
https://docs.flutter.dev/perf/impeller

maechanmaechan

基本的な画面構造

かなり基本的な構造にはなるけど、Scaffold が親となって各クラス(コンポーネントかウィジェットという表現でもいいのかな)がそれにぶら下がる。

Scaffold
│
├── AppBar
│    └── Text ("タイトル")
│
├── Center
│    └── Text ("メインコンテンツ")
│
└── FloatingActionButton
     └── Icon (Icons.add)

各ウィジェットの説明

  • Scaffold
    • Scaffoldウィジェットは、マテリアルデザインの基本的なレイアウト構造を提供。このウィジェットを使うことで、アプリバー、フローティングアクションボタン、ドロワーなどの要素を簡単に追加できる
  • AppBar:
    • AppBarは、ScaffoldのappBarプロパティに設定される。アプリの上部に表示されるバーで、タイトルやアクションアイテムを含むことができる
  • Center:
    • Centerウィジェットは、その子ウィジェットを中央に配置する。画面の中央にコンテンツを表示したい場合に使用する
  • FloatingActionButton:
    • FloatingActionButtonは、ScaffoldのfloatingActionButtonプロパティに設定される。画面上に浮かぶアクションボタンで、主にプライマリなアクションを配置するのに使用される

Center ウィジェットはそれ以外にも Container, Row, Stack などいろいろある。

https://docs.flutter.dev/ui/layout

コードサンプル

  Scaffold(
  appBar: AppBar(
    title: Text('タイトル'),
  ),
  body: Center(
    child: Text('メインコンテンツ'),
  ),
  floatingActionButton: FloatingActionButton(
    onPressed: () {
      // アクション
    },
    child: Icon(Icons.add),
  ),
)
maechanmaechan

Flutter/Dart のモバイル開発の世界観

Flutter で用意されたウィジェットを置いていって UI を作っていき、Dart でアプリケーションのビジネスロジック、データモデル、ネットワーク通信、状態管理とか、UI 以外の部分を記述していく。

ただ、UI 構築の部分で、ウィジェットで提供されていないもの(そういう状況は少なそうだけど)については、ネイティブ言語で頑張ることになるのかな?

自分の経験してきた価値観で感じることは、少し窮屈な開発体験になるのでは?と思ってしまうけど、フレームワークとしての普及率や評価を見るとたぶんそんなことないんだろうなあ。マルチプラットフォームにコンパイルするからこれくらいのレールを用意しないといけないのかも。

とにかく、ウィジェットの役割や使い方を覚えておく必要はありそう。

maechanmaechan

テスト

Integration, Unit, Widget の3種類が用意されている。
https://docs.flutter.dev/cookbook/testing

テストコードを眺めているとよく目にする tester.pump() 関数。この関数は、ウィジェットツリーに対してフレームを描画するプロセスをシミュレートするためのもの。

ウィジェットの状態の変更やアニメーションの進行など、時間の経過に伴う変更をテスト中のウィジェットツリーに反映させるために利用される。pump(Duration duration) のように期間も指定できる。

https://api.flutter.dev/flutter/flutter_test/WidgetTester/pump.html

ポンプさせていこう。

あと setUp()tearDown() などの事前処理や事後処理は見ておこう。

https://zenn.dev/ncdc/articles/flutter_unit_test

maechanmaechan

ちょっと Firebase との連携を調べてる。導入は公式を参考に進めるのが無難。flutterfire_cli コマンドが優秀らしい。

https://firebase.google.com/docs/flutter/setup?hl=ja&platform=ios#initialize-firebase

このコマンドを実行すると ios/Runner/GoogleService-Info.plist, android/app/google-services.json を各プロジェクトの中に良しなに置いてくれる。また、Firebase の設定を格納した firebase_options.dart も作ってくれる。

$ flutterfire configure
i Found 4 Firebase projects.

✔ Select a Firebase project to configure your Flutter application with xxxxxx.
✔ Which platforms should your configuration support (use arrow keys & space to select)? · android, ios
i Firebase android app com.example.helloworld is not registered on Firebase project xxxxxx. 

i Registered a new Firebase android app on Firebase project xxxxxx. 
i Firebase ios app com.example.helloworld is not registered on Firebase project xxxxxx.
i Registered a new Firebase ios app on Firebase project xxxxxx.
Firebase configuration file lib/firebase_options.dart generated successfully with the following Firebase apps:
Platform  Firebase App Id
android   n:xxxxxxxxx:android:xxxxxxxxxxxx
ios       n:xxxxxxxxxx:ios:xxxxxxxxxxxxxxx

Learn more about using this file and next steps from the documentation:
 > https://firebase.google.com/docs/flutter/setup
maechanmaechan

pod install が終わらない

Flutter launching... でずっと止まっていたから何事かと調べたら pod install でかなり時間を使うようになっていた。

$ flutter run --verbose
...
[ +394 ms] 1.15.2
[        ] Running pod install...  // ここで数分待っている・・・

SDK のビルドが鬼遅いらしいので、タグを指定して対応か初回ビルドのタイミングをズラす方法を取る。後者の方がいいのかな。

注意点として、Firebase導入後に初回ビルドすると、プリコンパイルが実行されないようです。2回目以降のビルドから実行されるようです。(自分はこれが原因で詰まっていた)
なので最初はFirebaseを導入せずに初回ビルドし、2回目のビルドでFireabseを導入する必要があります。

https://zenn.dev/hino2ton/articles/2e2654f628de80
https://zenn.dev/nagaho/articles/012e9ac3b0dfd1

自分の環境ではこの手順で解消された

  • (1) をした後に一旦アプリをビルド
  • アンインストールしたライブラリを再びインストールしてその後に (2) を実行
// Firebase 関連のライブラリを一旦アンインストール (1)
$ flutter pub remove firebase_ui_auth
$ flutter pub remove cloud_firestore
$ flutter pub remove firebase_auth
$ flutter pub remove firebase_core
// 依存関係を再構築 (2)
$ flutter clean // 念の為おまじない
$ flutter pub get
maechanmaechan

Enum の中で const が書ける

こういう書き方は TypeScript では見ないなあと思ったのでメモ。

https://dart.dev/language#enums

表現豊かな enum を書けるのはいいですね。定数のほうがメモリ効率も良い。

enum PlanetType { terrestrial, gas, ice }

enum Planet {
  mercury(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
  venus(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
  uranus(planetType: PlanetType.ice, moons: 27, hasRings: true),
  neptune(planetType: PlanetType.ice, moons: 14, hasRings: true);

  const Planet({required this.planetType, required this.moons, required this.hasRings});

  final PlanetType planetType;
  final int moons;
  final bool hasRings;
  
  bool get isGiant => planetType == PlanetType.gas || planetType == PlanetType.ice;
}

void main() {
  List<Planet> planets = Planet.values;
  for (var planet in planets) {
    print('Planet: ${planet.name}');
    print('Type: ${planet.planetType}');
    print('Moons: ${planet.moons}');
    print('Has Rings: ${planet.hasRings}');
    print('Is Giant: ${planet.isGiant}');
    print('---');
  }
}
maechanmaechan

React を使っているとクラスの扱いを忘れてしまう。クラスの基礎的なことだけどメモ。

implements キーワード

Dart では、implements キーワードはあるクラスが他のクラスやインターフェースの全てのメソッドを実装することを強制する。implements を使用すると、指定したインターフェースのすべてのメソッドをクラス内で定義しないといけない。この機能は主にインターフェースを定義するのに使われますが、Dart では明示的なインターフェースがないので、任意のクラスをインターフェースとして使用することができる。

例えば、MockSpaceship クラスが Spacecraft クラスのすべてのメソッドを実装する必要がある場合、以下のように記述します:

class MockSpaceship implements Spacecraft {
  @override
  // Spacecraftクラスの全てのメソッドをここで実装する必要がある
}

抽象クラス (abstract class)

抽象クラスは、一部または全てのメソッドが実装されていないクラスのこと。これらのクラスは直接インスタンス化することはできず、サブクラスで具体的な実装を提供する必要がある。抽象クラスは、一連のメソッドを持つが、それらのメソッドの実装が異なる可能性があるクラスの共通の基底クラスとして使われることが多い。

例えば、Describable は抽象クラスで、describe メソッドが抽象メソッドとして定義されているけど、describeWithEmphasis は具体的な実装が提供されている:

abstract class Describable {
  void describe();  // 抽象メソッド

  void describeWithEmphasis() {
    print('=========');
    describe();
    print('=========');
  }
}

この Describable クラスを継承するサブクラスでは、describe メソッドを実装する必要があるが、describeWithEmphasis メソッドは継承され、そのまま使用することができる。サブクラスは describe メソッドの具体的な実装を提供し、その実装は describeWithEmphasis メソッドによって利用される。

maechanmaechan

Dart のコンセプト

公式に載っていた Dart を使う上で重要なコンセプトを GPT 先生を使って翻訳した。

https://dart.dev/language#important-concepts

  • 変数に格納できるすべてのものはオブジェクトであり、すべてのオブジェクトはクラスのインスタンスです。数値、関数、nullもすべてオブジェクトです。nullを除き(サウンドnull安全を有効にした場合)、すべてのオブジェクトはObjectクラスから継承されます。
    • バージョンノート:Null安全はDart 2.12で導入されました。Null安全を使用するには、言語バージョンが少なくとも2.12である必要があります。
  • Dartは強く型付けされていますが、型注釈はオプションです。Dartは型を推論することができるためです。例えば、var number = 101;では、numberはint型と推論されます。
  • Null安全を有効にすると、変数はそれが可能であると明示しない限りnullを含むことができません。変数をnull許容にするには、その型の末尾に疑問符(?)を付けます。例えば、int?型の変数は整数かもしれませんし、nullかもしれません。式がnullに評価されないことが分かっているがDartが同意しない場合は、それがnullではないことをアサートするために!を追加することができます。例:int x = nullableButNotNullInt!;
  • 任意の型が許容されることを明示的に示したい場合は、型Object?(null安全が有効な場合)、Object、または実行時まで型チェックを遅延させる必要がある場合は特別な型dynamicを使用します。
  • Dartはジェネリック型をサポートしており、List<int>(整数のリスト)やList<Object>(任意の型のオブジェクトのリスト)のような型があります。
  • Dartはトップレベル関数(main()など)をサポートしており、クラスやオブジェクトに紐付けられた関数(静的およびインスタンスメソッド)もサポートしています。関数内に関数を作成することもできます(ネストされた関数またはローカル関数)。
  • 同様に、Dartはトップレベル変数と、クラスやオブジェクトに紐付けられた変数(静的およびインスタンス変数)をサポートしています。インスタンス変数はフィールドやプロパティとしても知られています。
  • Javaとは異なり、Dartにはpublic、protected、privateといったキーワードはありません。識別子がアンダースコア(_)で始まる場合、それはそのライブラリにプライベートです。詳細については、ライブラリとインポートを参照してください。
  • 識別子は文字またはアンダースコア(_)で始まり、それらの文字に加えて数字を任意の組み合わせで続けることができます。
  • Dartには式(実行時に値を持つ)と文(値を持たない)があります。例えば、条件式condition ? expr1 : expr2はexpr1またはexpr2の値を持ちます。これをif-else文と比較してください。if-else文には値がありません。文はしばしば一つ以上の式を含むことができますが、式が直接文を含むことはできません。
  • Dartツールは、警告とエラーの2種類の問題を報告することができます。警告はコードが機能しない可能性があることを示すだけで、プログラムの実行を妨げるものではありません。エラーはコンパイル時または実行時のいずれかであり、コンパイル時エラーはコードの実行を全く阻止し、実行時エラーはコードの実行中に例外が発生することを意味します。
maechanmaechan

標準アノテーション

Dart では、コードに追加情報を提供するためのメタデータアノテーションを使用する。

https://dart.dev/language/metadata

  1. @Deprecated@deprecated:

    • @Deprecated は、コードの要素(クラス、メソッド、プロパティなど)が非推奨であると示し、将来的に削除される可能性があることを警告する。@Deprecated を使用すると、その要素を使用している箇所で警告が表示される。
    • @deprecated@Deprecated と同じ目的で使用されるが、こちらは Dart の組み込みアノテーションである。@Deprecated('message') の形でメッセージを提供し、非推奨の理由や代替案についての情報を提供するのに役立つ。
  2. @override:

    • @override アノテーションは、サブクラスがスーパークラスまたはインターフェースのメソッドをオーバーライドしていることを示す。このアノテーションを使用することで、意図しないメソッド名の誤りやオーバーライドの不備をコンパイル時に検出するのに役立つ。
  3. @pragma:

    • @pragma アノテーションは、コンパイラに特定の命令や振る舞いを伝えるために使用される。例えば、Dart VM でのインライン展開の抑制や、特定の最適化の無効化などに使うことができる。パフォーマンス最適化や特定のコンパイラの振る舞いを制御するために使用されることが多い。
maechanmaechan

プロデューサーとコンシューマーのルール

このルールは、オブジェクト指向プログラミングにおけるポリモーフィズムとサブタイピングの原則に基づいており、型安全性を保ちながらメソッドの柔軟性を高めることができる。

https://dart.dev/language/type-system#methods

  • コンシューマー (消費者): メソッドのパラメーターは「コンシューマー」と見なされる。コンシューマーに関しては、パラメーターの型をスーパータイプ(より一般的な型)に置き換えることができる。例えば、 chase(Animal) メソッドがあり、Animal の代わりに Animal のスーパータイプをパラメーターとして受け取るように変更することが可能。
  • プロデューサー (生産者): メソッドの戻り値は「プロデューサー」と見なされる。プロデューサーに関しては、戻り値の型をサブタイプ(より特化した型)に置き換えることができる。例えば、parent ゲッターメソッドが Animal を返す場合、その戻り値を Animal のサブタイプである Cat などに置き換えることができる。
class Animal {
  Animal get parent => ...
  void chase(Animal a) { ... }
}

class Cat extends Animal {
  @override
  Cat get parent => ...

  @override
  void chase(Object a) { ... } // Animal から Object へ変更は可能
}
  • Cat クラスでは、parent ゲッターの戻り値を Animal から Cat に変更しています(プロデューサールール)。
  • chase メソッドでは、Animal 型のパラメーターをより一般的な Object 型に変更しています(コンシューマールール)。
maechanmaechan

Generators

https://dart.dev/language/functions#generators

ジェネレータは、大きなデータセットを扱う場合や、計算コストの高いシーケンスを生成する場合に特に有用。値が必要になるまで計算が遅延されるため、メモリ効率が良く、パフォーマンスが向上する。アプリの要件次第では使うことがありそうなので覚えておきたい。

maechanmaechan

クラス修飾子

  • abstract
  • base
  • final
  • interface
  • sealed
  • mixin

https://dart.dev/language/class-modifiers

https://dart.dev/language/modifier-reference

sealed は初めて見た気がする。

finalconst の違い

final と、定数定義で使う const との違いをメモ。

finalとconstの違いは、それらが値を割り当てるタイミングと、その値がコンパイル時に決定されるか実行時に決定されるかにあります。

final: finalキーワードは、変数が一度初期化された後、その値を変更できないことを示します。しかし、final変数の値は実行時に初期化されます。つまり、final変数は、実行時に決定される値を持つことができます。たとえば、ユーザーの入力やデータベースからの読み取りなど、実行時にのみ利用可能なデータに基づいてfinal変数を初期化できます。

const: constキーワードは、変数がコンパイル時に定数として設定されることを示します。これは、その値がコンパイル時に既に決定されており、実行時には変更できないことを意味します。const変数はリテラル値やコンパイル時にその値が確定する他のconst変数によってのみ初期化できます。

要するに、finalは実行時に一度だけ設定される変数であり、constはコンパイル時に定数として設定される変数です。これにより、constはfinalよりも厳格な制約を持ち、その値がコンパイル時に既に固定されている必要があります。

maechanmaechan

Flutter と Firebase の連携を Codelabs を参考にやってみた。

ログイン機能、データ作成、データ表示をするシンプルなアプリ。

アプリを一つ作ってみて Flutter の大枠は理解できたと思うけど、まだ「ツールに使われてる感」が拭えないので、もう少し Flutter を使って手に馴染ませていく必要がありそう。

https://firebase.google.com/codelabs/firebase-get-to-know-flutter?hl=ja#0

maechanmaechan

とりあえず簡単なアプリ作ったので、これを言わないといけない

「Flutter 完全に理解した」

maechanmaechan

バージョン管理

「fvm」がおすすめらしい。

https://fvm.app/

インストールと使い方

// Homebrew 経由で fvm をインストール
$ brew tap leoafarias/fvm
$ brew install fvm
$ fvm -v
3.1.1

// インストール可能な Flutter のバージョンを出力
$ fvm releases 
┌───────────────────┬──────────────┬──────────┐
│ Version           │ Release Date │ Channel  │
├───────────────────┼──────────────┼──────────┤
│ v1.0.0            │ Dec 4, 2018  │ stable   │
├───────────────────┼──────────────┼──────────┤
│ v1.2.1            │ Feb 26, 2019 │ stable   │
├───────────────────┼──────────────┼──────────┤
...
│ 3.19.2            │ Feb 28, 2024 │ stable   │
├───────────────────┼──────────────┼──────────┤
│ 3.19.3            │ Mar 8, 2024  │ stable   │
├───────────────────┼──────────────┼──────────┤
│ 3.19.4            │ Mar 21, 2024 │ stable ✓ │
└───────────────────┴──────────────┴──────────┘
Channel:
┌─────────┬─────────┬──────────────┐
│ Channel │ Version │ Release Date │
├─────────┼─────────┼──────────────┤
│ stable  │ 3.19.4  │ Mar 21, 2024 │
└─────────┴─────────┴──────────────┘

// 現状の安定版をインストール
$ fvm install 3.19.4
Creating local mirror...
 Compressing objects: [██████████████████████████████████████████████████] 100%
 Receiving objects:   [██████████████████████████████████████████████████] 100%
 Resolving deltas:    [██████████████████████████████████████████████████] 100%
✓ Clone complete
✓ Flutter SDK: SDK Version : 3.19.4 installed! (2.6s)

// バージョンを指定
$ fvm use 3.19.4 // 安定版を指定したい場合は `stable` と入力してもいい

fvm コマンドを使いやすくするためにシェル(例:fish シェル)にエイリアスを追加。

~/.config/fish/config.fish
# Flutter alias
alias flutter 'fvm flutter'

エイリアス効いている

// エイリアス `flutter` で実行
$ flutter pub add http
Resolving dependencies... 
+ http 1.2.1
+ http_parser 4.0.2
  leak_tracker 10.0.0 (10.0.4 available)
  leak_tracker_flutter_testing 2.0.1 (3.0.3 available)
  leak_tracker_testing 2.0.1 (3.0.1 available)
  material_color_utilities 0.8.0 (0.11.1 available)
  meta 1.11.0 (1.12.0 available)
  test_api 0.6.1 (0.7.0 available)
+ typed_data 1.3.2
  vm_service 13.0.0 (14.2.0 available)
+ web 0.5.1
Changed 4 dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

// エイリアスを使わない元のコマンドで実行
$ fvm flutter pub add http
"http" is already in "dependencies". Will try to update the constraint.
Resolving dependencies... 
  leak_tracker 10.0.0 (10.0.4 available)
  leak_tracker_flutter_testing 2.0.1 (3.0.3 available)
  leak_tracker_testing 2.0.1 (3.0.1 available)
  material_color_utilities 0.8.0 (0.11.1 available)
  meta 1.11.0 (1.12.0 available)
  test_api 0.6.1 (0.7.0 available)
  vm_service 13.0.0 (14.2.0 available)
Got dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

fvm で管理しているバージョンをプロジェクトに適用させる(VSCode)

プロジェクト直下で fvm use [バージョン番号] と入力する。
コマンドが自動的に .vscode/settings.json を作成して、そこに fvm のパスを書き込んでくれる。
また .fvm が作られ、ここに fvm で指定した Flutter が格納されている。

$ fvm use 3.19.4
Setting up Flutter SDK: 3.19.4

Downloading Darwin arm64 Dart SDK from Flutter engine a5c24f538d05aaf66f7972fb23959d8cafb9f95a...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  212M  100  212M    0     0  18.1M      0  0:00:11  0:00:11 --:--:-- 15.7M
Building flutter tool...
Resolving dependencies... (2.1s)
Got dependencies.
Flutter 3.19.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 68bfaea224 (2 days ago) • 2024-03-20 15:36:31 -0700
Engine • revision a5c24f538d
Tools • Dart 3.3.2 • DevTools 2.31.1

✓ Flutter SDK: SDK Version : 3.19.4 is setup
[WARN] Project is not a git repository. 
 But will set .gitignore as IDEs may use it,to determine what to index and display on searches,
You should add the fvm version directory ".fvm/" to .gitignore.
✔ Would you like to do that now? · yes                                                      
✓ Added .fvm/ to .gitignore

✓ Dependencies resolved. (8.4s)
✓ Project now uses Flutter SDK : SDK Version : 3.19.4
┌────────────────────────────────────────────────────────────────────┐
│ ✓ Running on VsCode, please restart the terminal to apply changes. │
└────────────────────────────────────────────────────────────────────┘
You can then use "flutter" command within the VsCode terminal.
.vscode/settings.json
{
  "dart.flutterSdkPath": ".fvm/versions/3.19.4"
}

fvm で指定したバージョンになっている。

$ flutter  --version
Flutter 3.19.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 68bfaea224 (2 days ago) • 2024-03-20 15:36:31 -0700
Engine • revision a5c24f538d
Tools • Dart 3.3.2 • DevTools 2.31.1

// Dart は Flutter をインストールしたら自動的に入るけど念のため出力してみる
$ dart --version
Dart SDK version: 3.3.1 (stable) (Wed Mar 6 13:09:19 2024 +0000) on "macos_arm64"

Dart のバージョンがちがう??

maechanmaechan

Dart のバージョンが違うぞ?

↑の投稿の気になってた Dart のバージョンの違い。

flutter --version コマンドで返ってきた Dart のバージョンは 3.3.2 で、dart --version コマンドで返ってきた Dart のバージョンは 3.3.1

この 3.3.1 の方の Dart は Flutter SDK を公式から zip ファイルでダウンロードしてきた Dart SDK のバージョンだった。

// `Dev` というディレクトリをユーザーディレクトリの下に作ってそこで管理していた
$ which dart
/Users/maechan/Dev/flutter/bin/dart

このバージョンの乖離を見て「大丈夫なのか?」と思ったので調べると、これについては問題はない模様。以下、GPT 先生のお言葉。

dart コマンドは /Users/maechan/Dev/flutter/bin/dart から実行されています。これは、Dart SDKがFlutter SDKの一部としてインストールされていることを示しています。つまり、このDartはFlutterと共にインストールされたものであり、Flutterのインストールに伴ってシステムに追加されたものです。

この場合、システム上にはFlutterにバンドルされたDart SDKのみが存在し、別途インストールされたDart SDKはないことになります。従って、Flutterの開発に使用しているDart SDKに関する混乱は生じないため、特にアンインストールする必要はありません。

Flutterと共にインストールされたDartを使用している限り、Flutterのアップデートに合わせてDart SDKも更新されるため、手動でのDart SDKの管理は必要ありません。Flutterの開発に専念している場合は、このセットアップで問題なく開発を続けることができます。

ただこれは少し心地よくないと思ったのと、今後、Flutter のバージョンは fvm で管理する方針を取るので要らないものは削除しておいたほうが良さそうかなと思ったので掃除をする。

// Flutter ディレクトリを削除(シェルで通していたパスがあればそれも削除)
$ rm -rf /Users/maechan/Dev/flutter/
// Dart コマンドが使えないことを確認
$ dart --version
fish: Unknown command: dart

fvm で管理しているプロジェクトの Dart のバージョンがどっちも 3.3.2 になった!

$ flutter --version
Flutter 3.19.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 68bfaea224 (2 days ago) • 2024-03-20 15:36:31 -0700
Engine • revision a5c24f538d
Tools • Dart 3.3.2 • DevTools 2.31.1

$ dart --version
Dart SDK version: 3.3.2 (stable) (Tue Mar 19 20:44:48 2024 +0000) on "macos_arm64"
maechanmaechan

StatelessWidget, StatefulWidget

  • StatelessWidget:状態を持たないウィジェット
  • StatefulWidget:状態を持つウィジェット

StatelessWidget は build メソッドをオーバーライドして、1つ以上の Widget を組み合わせて UI を構成する。自身では表示更新の仕組みを持たない。

StatefulWidget は build メソッドを持たない。createState をオーバーライドして State オブジェクトを返せる。State オブジェクトは状態が変化したことをフレームワークに知らせる setState メソッドを持っている。

import 'package:flutter/material.dart';

void main() {
  runApp(const Center(
    child: Counter(),
  ));
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('tapped!');
        setState(() {
          _count += 1;
        });
      },
      child: Container(
          color: Colors.red,
          width: 100,
          height: 100,
          child: Center(child: Text('$_count', textDirection: TextDirection.ltr))),
    );
  }
}
maechanmaechan

環境変数

define/env.json を作成する。

define/env.json
{
  "apiEndpoint": "http://example.com/api",
  "logLevel": 1,
  "enableDebugMenu": true
}
const endpoint = String.fromEnvironment('apiEndpoint');
const endpoint = Int.fromEnvironment('logLevel');
const endpoint = bool.fromEnvironment('enableDebugMenu');

注意としては、変数に代入するときは const 修飾子を使うこと(それか呼び出し側に const キーワードを付与)。

maechanmaechan

iOS 向けのデザインに特化したアプリケーション開発に "CupertinoApp"

https://api.flutter.dev/flutter/cupertino/CupertinoApp-class.html

iOSデザインを対象としたアプリケーションに一般的に必要とされる多数のウィジェットをラップする便利なウィジェットです。フォントやスクロール物理など、iOS固有のデフォルト設定によりWidgetsAppを拡張して構築します。

maechanmaechan

Flutter を車に例えて

過去に Objective-C/Swift で仕事していたので、この表現はすごいしっくりくる。

教習でMT(マニュアル)を試運転して次にATを運転したときのような感覚をFlutterで学び始めたときに感じました。

https://zenn.dev/yuuki0206/articles/9de7dc295380e7

maechanmaechan

State と Element の関係

  • ウィジェット(Widget): UI の一部を表すイミュータブルなオブジェクト。ウィジェット自体に状態を持つことはない。
  • 状態(State): 状態を持つウィジェット(StatefulWidget)は、対応する State オブジェクトによってその状態が管理される。この State オブジェクトは、ウィジェットがツリーに存在する間、維持される。
  • エレメント(Element): ウィジェットツリーを実際のインスタンスに変換する役割を持ち、StatefulWidget に対しては、その State オブジェクトを保持する。

StatefulWidget がウィジェットツリーに挿入されると、Flutter フレームワークは対応する State オブジェクトを作成し、それを Element ツリー内の対応する Element に関連付ける。この State オブジェクトは、Element がツリーから削除されるまで、または状態が明示的に破棄されるまで維持される。

Element の再利用

Element が再利用されるかどうかは、Flutter のウィジェット再利用ロジックに依存する。ウィジェットのキー(Key)とウィジェットのタイプが主な要因になる。

  • キーなしのウィジェット: キーがない場合、フレームワークはウィジェットのタイプと位置に基づいて再利用を試みる。ウィジェットの型が同じで、ツリー内での位置が同じであれば、Element は再利用される可能性がある。
  • キー付きのウィジェット: ウィジェットにユニークなキーが割り当てられている場合、そのキーはウィジェット(およびその状態)の同一性をフレームワークに伝えるため、ウィジェットの型が同じで、さらに同じキーを持つウィジェットが再利用される。これは、例えばリストやグリッド内でウィジェットが動的に移動する場合に特に有用。

Element の再利用は、特に状態を持つウィジェット(StatefulWidget)で重要。再利用により、ウィジェットの状態が適切に保持され、ユーザーインタラクションやデータの変更が反映されたスムーズな UI を提供できる。

maechanmaechan

InheritedWidget

https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html

ウィジェットツリーを通じてデータを効率的に伝播させるための特別な種類のウィジェット。このウィジェットを使うことで、ウィジェットツリーのどのレベルからでも、共有データへのアクセスやそのデータに依存するウィジェットの再構築を行うことができる。

InheritedWidget の特徴は以下:

  • InheritedWidget は、状態管理のために使用され、特に上位のウィジェットから下位のウィジェットへデータを効率的に伝達する際に便利。
  • BuildContext を介して、ウィジェットツリーの任意の場所から InheritedWidget にアクセスできる。これは context.dependOnInheritedWidgetOfExactType<Type>() メソッドを使用して行われる。
  • InheritedWidget を継承したクラスは、依存するウィジェットがそのデータに依存するかどうかを判断し、データが変更された場合にのみ依存するウィジェットを再構築することができる。
    状態管理のために使われることが多く、例えばテーマやロケール情報など、アプリケーション全体で共有されるべきデータを管理するのに適している。

InheritedWidget は Flutterの状態管理において重要な役割を果たし、効率的かつ直感的にデータをウィジェットツリーを通じて伝播させるメカニズムを提供する。

maechanmaechan

公式の "パフォーマンスに関する考察"

https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html#performance-considerations

大事そうなので、GPT 先生に翻訳してもらった。以下、その翻訳文。

Performance considerations

StatelessWidget の build メソッドは、通常、3つの状況でのみ呼び出されます:ウィジェットがツリーに初めて挿入されたとき、ウィジェットの親がその構成を変更したとき (Element.rebuild を参照)、そしてウィジェットが依存する InheritedWidget が変更されたときです。

ウィジェットの親が定期的にウィジェットの構成を変更する場合、または頻繁に変更される InheritedWidget に依存している場合、流動的なレンダリングパフォーマンスを維持するために、build メソッドのパフォーマンスを最適化することが重要です。

StatelessWidget を再構築する影響を最小限に抑えるために使用できるいくつかのテクニックがあります:

  • build メソッドとそれが作成するウィジェットによって間接的に作成されるノードの数を最小限に抑えます。例えば、特に洗練された方法で1つの子を配置するために Rows、Columns、Paddings、SizedBoxes の複雑な配置の代わりに、単に Align や CustomSingleChildLayout を使用することを検討してください。ちょうどいいグラフィカルエフェクトを描画するために複数の Containers と Decorations を複雑に重ねる代わりに、単一の CustomPaint ウィジェットを検討してください。
  • 可能な限り const ウィジェットを使用し、ウィジェットのユーザーもそうできるようにウィジェットに const コンストラクタを提供します。
  • StatelessWidget を StatefulWidget にリファクタリングして、 StatefulWidget で説明されているテクニック、たとえばサブツリーの一般的な部分のキャッシュやツリー構造の変更時の GlobalKeys の使用などを利用できるように検討してください。
  • InheritedWidget の使用によりウィジェットが頻繁に再構築される可能性が高い場合、StatelessWidget を複数のウィジェットにリファクタリングし、変更されるツリーの部分を葉にプッシュすることを検討してください。たとえば、4つのウィジェットでツリーを構築し、一番内側のウィジェットがテーマに依存している場合、一番内側のウィジェットを構築する build 関数の一部を独自のウィジェットに分割して、テーマが変更されたときに一番内側のウィジェットのみを再構築する必要があるように検討してください。
  • 再利用可能な UI の一部を作成しようとするときは、ヘルパーメソッドではなくウィジェットを使用することを優先してください。たとえば、ウィジェットを構築するために使用される関数があった場合、State.setState 呼び出しは Flutter に返されたラッピングウィジェットを完全に再構築するよう要求します。ウィジェットが代わりに使用された場合、Flutterは必要な部分のみを効率的に再レンダリングすることができます。さらに良いことに、作成されたウィジェットが const であれば、Flutterはほとんどの再構築作業を省略することができます。

個人的メモ

  • build は再描画される機会が多いので、そこで重い処理を実装するな
  • ウィジェットツリーはできるだけ浅く
  • const 修飾子は付与できる部分には必ず付けて
    • 定数として扱われるから、常に同じインスタンスが使われるようになる。先祖の再構築の影響を受けない効果がある
  • 独自ウィジェットクラスには constant コンストラクタを実装する
    • 上の const 修飾子の効果と、ミュータブルにする必要があって状態を持たないようになるので堅牢性が高まる
  • 状態は末端のウィジェットに持たせる
    • 不要な再構築が行われなくなり、クラスの債務も明確になる
    • Riverpod の監視も末端で
maechanmaechan

不要なプラットフォームを削除

iOS と Android の2つにしたかったので、それ以外のプラットフォームを削除したときのメモ。

// 不要なプラットフォームディレクトリを削除
$ rm -rf linux/ && rm -rf macos/ && rm -rf windows/ && rm -rf web/
// お掃除
$ flutter clean
// 依存関係を再取得
$ flutter pub get
maechanmaechan

Firebase + Riverpod + freezed のライブラリ

ライブラリのインストールコマンド。

// Hooks を入れておきたい
$ flutter pub add flutter_hooks

// Firebase のインストール
$ flutter pub add firebase_core firebase_auth cloud_firestore provider firebase_ui_auth

// Riverpod のインストール(`flutter_riverpod` を使う場合は `hooks_riverpod` と交換)
$ flutter pub add hooks_riverpod riverpod_annotation
// Riverpod のアノテーションと定義の自動生成と整形用ライブラリ
$ flutter pub add --dev riverpod_generator riverpod_lint custom_lint

// freezed のインストール
$ flutter pub add freezed_annotation json_annotation
$ flutter pub add --dev freezed json_serializable 

// コード生成のために build_runner を導入
$ flutter pub add --dev build_runner
// コード生成コマンド
$ flutter packages pub run build_runner build

Riverpod と freezed の使い方を覚えたい。

https://riverpod.dev/ja/

https://pub.dev/packages/freezed

maechanmaechan

Riverpod 関連で読んでよかった記事(公式以外)

https://medium.com/flutter-jp/state-1daa7fd66b94

Provider/Riverpodで状態の伝達を行えますが、その伝達される側のクラスとしてStateNotifierをメインに使っています。

状態をimmutableに扱いたいかどうかが肝
まず、状態値をmutableで扱ってしまっても良いという考えで実際にそれで課題も感じていないなら、ChangeNotifierを使うのが楽です。

一方、immutableにする利点は以下によくまとまっています。
簡単な具体例で言うと、例えばあるアイテムの編集画面でそのオブジェクトの状態を弄って確定せずにキャンセルしたとします。immutableに扱っていれば単にそれを破棄すれば良いだけですが、mutableに扱っていて特に何もケアせず単純に組んでいたらその中途半端な操作が別の箇所で反映されてしまうバグを生む可能性があります。もちろん、mutable中心に扱っていてもその編集画面の実装次第で問題を起こさないようにはできますが、immutableに扱っている場合は何の工夫をせずともその類のバグを引き起こすことはあり得ません。

なので、個人的な結論としては、以下です。

mutableで簡単に済ませたいし、それで問題を感じない → ChangeNotifier
状態クラスの扱いが多少煩わしくなるのを許容してでもimmutableの恩恵を得たい → StateNotifier
そもそも、WidgetもFlutter標準のInheritedWidgetを介してアクセスできるもの、例えばTheme.of(context)で取れるThemeDataなどもimmutableですし、そちらに寄せた方がFlutterらしい気もしています。

仮にimmutableなクラスが扱いやすいように言語機能としてサポートされていればそちら一択で良いと思いますが、後述の通りDartの場合は残念ながらそうではないので、ある程度トレードオフが発生します。

StateNotifier については、この記事を見ると、現在は NotifierProvider で代替することを推奨されているようです。

https://naokikosuge.hashnode.dev/riverpod-2x-provider
Riverpod 関連パッケージの説明は有り難い。

https://dev.classmethod.jp/articles/flutter-provider-riverpod/
3つの状態管理をコードで書いている。

https://zenn.dev/naoya_maeda/articles/a8bbf40a202c74
Riverpod の各 Provider の使い方が網羅されている。

https://deku.posstree.com/flutter/provider/#google_vignette
Riverpod ではく「Provider」の記事だけど、Provider の説明が分かりやすかった。

maechanmaechan

Riverpod: FutureProvider と StreamProvider の違い

"Future" と "Stream"。

FlutterやDartでのストリームは、例えばユーザー入力イベント、ファイルの読み込み、ネットワーク通信など、連続的かつ非同期に発生するデータやイベントを扱う際に非常に便利です。StreamProviderは、このようなストリームを扱うためにRiverpodが提供するツールで、ストリームからのデータフローを監視し、最新のデータに基づいてUIを更新することができます。

StreamProviderがFutureProviderの「ストリーム版」であると述べられているのは、FutureProviderが単一の非同期値を扱うのに対し、StreamProviderは連続するデータの流れを扱うためです。FutureProviderは一度きりの非同期操作(例えば、APIからの単一のデータフェッチ)に適していますが、StreamProviderは時間の経過とともに繰り返しデータが生成される場合(例えば、リアルタイムで更新されるデータフィード)に適しています。