Flutter開発メモ
Flutterアプリ開発を始め,新しく学んだ知識や取り入れた仕組みを書き綴ります。
開発環境と本番環境を分ける
Dart-define-from-fileを用いて開発環境と本番環境を分ける
Freezedという,イミュータブルクラスを作るためのライブラリ
最も多いユースケースは,Provider/StateNotifierやRiverpodなどの状態管理ライブラリと組み合わせて使うこと。その他,DDDにおける値オブジェクトの実装に用いることもある。
ドメイン駆動設計を取り入れたFlutter開発
テスト方法
単体テスト (Unit Test), ウィジェットテスト (Widget Test), 結合テスト (Integration Test)
Provider + StateNotifier + Freezed な MVVMパターン
時間に紐付いたタスクをCloud Functionsで実行する
FirebaseのCloud Functionsを用いて、時間に紐付いた不定期実行のタスクを処理する方法を調べた。
(時間に紐付いた不定期実行のタスク = ある操作の48時間後にPush通知を送信する、募集の締切日時になったら募集終了処理を行う、など)
Cloud Tasks
Cloud Scheduler 一分おきにタスク探索&実行を行う例。ゴリ押し感が少々MVVMアーキテクチャについて
FlutterではMVVMアーキテクチャが推奨されているが,どうにもこれがReactのHooks + 関数コンポーネントの書き味と全然違ってみえて,難しい。
難しさの言語化
Reactではpropsを用いた値の受け渡しが基本なので,コンポーネントを小さく区切りやすく,個別の実装・テストも容易である。
一方で私の観測範囲では,MVVMアーキテクチャを採用したFlutterのコードは,Viewの最小単位がページ単位になり,そのページ毎に状態管理用のViewModelを用いていることが多い。これ自体が直ちに問題となるわけではないが,Viewが全く区切られずに巨大なウィジェットになるから難しくみえるのである。
巨大なウィジェットができる原因
ではなぜMVVMのFlutterのViewは巨大化するのか?それは「ViewModelによるグローバルな状態管理に頼りすぎるから」だと私は思う。
ページの状態管理やビジネスロジックをすべてViewModelに委譲すると,Viewを小分けにしようにも相互依存を抱えることになり,うまくいかないのだ。
対処法
私なりに考えた対処法は次の通りである。
- 小さなコンポーネント単位では,値の受け渡しにコンストラクタ注入を利用し,さらに状態管理と直接関係のないビジネスロジックは,そのコンポーネントクラスに委譲する。
- 一方で全体としては,グローバルな状態管理には依然ViewModelを用い,ページ構築のViewはあくまでコンポーネントを組み立てるだけの調停役としての役割にとどめる。
まず,何でもかんでもViewModelのグローバルな状態を直接取りに行くことをやめ,コンストラクタ引数で渡すことにする。すると羃等でテストしやすい小さなコンポーネントに分けることができる。
一方で全体としては,コンポーネントを組み立てる「調停役」としてのViewが,適切にViewModelの状態を利用することで,変更時の再描画等を可能にする
MVVMを理解するのに,Flutterのコードだけ見ていても仕方がない。Reactのほうが絶対自分にとってはわかりやすいな…
RiverpodのProviderを利用する方法
Riverpodの基本的な使い方
Providerのオーバーライドなど,重要だが案外知らなかったことも書いてある
Providerがあると何が嬉しいのか,サンプルコードで解説した記事
アイコンを探す方法
'package:flutter/material.dart'
にはデフォルトでIcons
というクラスがあり,そのアイコンをWEB上で探すことができる。ほかにも,FlutterIcon.comというサイトがあるみたい。
Providerの値を動的に(ランタイムに)設定する
追記:これならもっとスマートに管理できるかも?
Riverpodのfamilyについて
familyをつけると,外部から引数を与えることができる。
それだけでなく,その引数は同時にインスタンスを管理するためのIDともなるのである。
Parameter restrictions
For families to work correctly, it is critical for the parameter passed to a provider to have a consistent hashCode and ==.
Ideally, the parameter should either be a primitive (bool/int/double/String), a constant (providers), or an immutable object that overrides == and hashCode.
また,引数として与えられるのは一個のみだが,上述のParameter restrictionsを満たしていれば,タプルでもFreezedで作られたオブジェクトでもいくつでも渡すことができる。
追記: わかりやすい記事を発見
追記2: こんな記事をみつけたけど,通常版のproviderも用意してoverrideする意図は何なのだろうか? わざわざoverrideをしなくても,遷移先のView側で「引数を渡してViewModelを取得する」という処理を書けば十分なのでは?
→MVVMにおいては,Viewに直接コンストラクタ引数で値を渡すのが嫌だから,では?状態管理はViewModelの責務なのに,状態に関連したデータをまずViewに渡すのが嫌,みたいな。
でも一方で,ページ遷移時に遷移先のViewModelの中身を知る必要がある,というのにも違和感がある(これは自分の考え)。ページ遷移時には遷移先のViewだけを意識して,値を渡す必要があればその際に渡せばよく,引数を渡してViewModelを取得する作業はView内部の最初のほうでササッとやってもらえれば十分だと思う。
ViewModel (ステート・ビジネスロジック管理) とView (見た目) の分離をより重視したいのであれば,前者のoverrideを使用したパターンが向いていて,より直感的で宣言的なページ遷移を重視したいのであれば,後者のView内部で引数ありのViewModelを作るパターンが向いている。ということではなかろうか?
ちなみに,familyという道具について知る前にはこういう強引な作り方をしていた
// このmaxTransportationExpensesProviderを呼び出し元でoverrideして,与えたい値を返すようにする
final maxTransportationExpensesProvider = StateProvider<int>((ref) {
// throw Exceptionをしてはならない。
// あとから結局オーバーライドするとしても,一度この中身は読まれる(変数の初期化は不可欠なので。)
return -1;
});
final studentTaskApplyProvider = StateNotifierProvider.autoDispose<
StudentTaskApplyViewModel, StudentTaskApplyState>((ref) {
final maxTransportationExpenses =
ref.watch(maxTransportationExpensesProvider);
return StudentTaskApplyViewModel(ref, maxTransportationExpenses);
});
firebase_auth
がWebビューのビルド時にエラーを吐く
Firebase Authを使って普段Android, iOS用に作っているアプリをWebビルドしようとしたら,エラーが出た。
Launching lib/main.dart on Chrome in debug mode...
../../../.pub-cache/hosted/pub.dev/firebase_auth_web-5.5.1/lib/firebase_auth_web
.dart:94:36: Error: Too many positional arguments: 1 allowed, but 2 found.
Try removing the extra positional arguments.
FirebaseCoreWeb.registerService('auth', (firebaseApp) async {
^
../../../.pub-cache/hosted/pub.dev/firebase_core_web-2.6.0/lib/src/firebase_core
_web.dart:43:15: Context: Found this candidate, but the arguments don't match.
static void registerService(
結局,使っているバージョンが古いだけだった
解決策
flutter clean
flutter pub get
flutter pub upgrade firebase_core_web
flutter pub upgrade firebase_auth_web
flutter pub upgrade
FormGroup
型安全なFlutterでフォームの状態管理をする際にはFormGroup
が有用であるが,これは中でMap
を使っており,キーに対してのアクセスが型安全ではない。もっと型安全にFormGroup
でフォームを管理する方法はないか?とChatGPTに聞いてみた。
「型安全なFormGroup
」とWEB検索しても出てこなかったけど,これ案外需要あるのでは??
FormGroup
を型安全に使うためには、型安全なアクセスを提供する方法を考える必要があります。Dartには、型安全にマップを操作するためのいくつかの方法があります。その一つが、データクラスとEnumを使用してキーの安全性を確保する方法です。
Enumを使用して型安全にアクセスする方法
まず、Enumを使用してフィールド名を定義します。次に、拡張メソッドやカスタムメソッドを使用して型安全にアクセスします。
import 'package:reactive_forms/reactive_forms.dart';
// Enumでフィールド名を定義
enum FormFields { name, email }
// 型安全なキーアクセスを提供するクラス
class TypedFormGroup {
final FormGroup _formGroup;
TypedFormGroup(this._formGroup);
FormControl<String> get name => _formGroup.control(FormFields.name.name) as FormControl<String>;
FormControl<String> get email => _formGroup.control(FormFields.email.name) as FormControl<String>;
}
void main() {
// Enumのnameプロパティを使用してフォームを作成
final form = FormGroup({
FormFields.name.name: FormControl<String>(value: 'John Doe'),
FormFields.email.name: FormControl<String>(value: ''),
});
final typedForm = TypedFormGroup(form);
// 型安全なアクセス
print(typedForm.name.value); // 出力: John Doe
print(typedForm.email.value); // 出力:
// 値の設定
typedForm.name.value = 'Jane Doe';
print(typedForm.name.value); // 出力: Jane Doe
}
データクラスを使用する方法
データクラスを使用して型安全にアクセスする方法も考えられます。この方法では、カスタムクラスを作成し、そのクラスのプロパティを通じてアクセスします。
import 'package:reactive_forms/reactive_forms.dart';
// データクラスでフィールド名を定義
class MyForm {
final FormControl<String> name;
final FormControl<String> email;
MyForm({required this.name, required this.email});
}
void main() {
final form = FormGroup({
'name': FormControl<String>(value: 'John Doe'),
'email': FormControl<String>(value: ''),
});
final myForm = MyForm(
name: form.control('name') as FormControl<String>,
email: form.control('email') as FormControl<String>,
);
// 型安全なアクセス
print(myForm.name.value); // 出力: John Doe
print(myForm.email.value); // 出力:
// 値の設定
myForm.name.value = 'Jane Doe';
print(myForm.name.value); // 出力: Jane Doe
}
まとめ
型安全なフォームの状態管理を行うために、以下の方法を使用できます:
- Enumを使用してフィールド名を定義し、型安全なアクセスメソッドを提供する。
- データクラスを使用して、各フィールドに対応するプロパティを定義する。
どちらの方法も、FormGroup
の操作を型安全に行うための有効な手段です。
Flutterキャッチアップ記事
まだ自分も知らないこともたくさん書いてあるし,Flutter未経験者にキャッチアップしてもらう上でも参考になりそう
Providerが思うようにoverrideできない
これを試したのだが,やはり思ったようにオーバーライドはされず,もとのコードが参照されてしまう。
このissueにあるような状況に陥っていないだろうか?
ここに書いてあった。「オーバーライドしたProvider」にアクセスする(依存している)Providerがあれば、あわせてそちらも、overridesに加える必要があるのだ。
ページ遷移の手段まとめ
Navigator
を使ったページ遷移には、push
やpop
のほかにpushAndRemoveUntil
など様々な選択肢がある。この記事ではそれが実際のユースケースを例にとって解説されており、非常にわかりやすい。
というか、名前付きでルーティングできるのか。知らなかった。
RiverpodとMVVM
riverpod_annotation
という道具があるとは知らなかった。
Material WidgetsとCupertino Widgetsを同時に使いたい
Cupertino Widgetsのサンプルを探すためのTips
YouTubeでcupertino channel: 'Flutter Mapp'
と検索すると、Cupertino Widgetsの実際の見た目・動きがいろいろ見れる。
WidgetbookのWebホスティングをパスワードで守る
Flutter版のStorybookであるWidgetbookは、Webビルドしてホスティングすることで、開発環境のない関係者にも手軽に各機能を試してもらううえで便利である。
そんなWidgetbookを、URLが流出した場合でも守れるようにパスワードで守ってはどうか?と作ってみた。AIに書かせたコードだが、実際に動く。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
import 'widgetbook.directories.g.dart';
void main() {
runApp(const ProviderScope(child: WidgetbookApp()));
}
.App()
class WidgetbookApp extends StatelessWidget {
const WidgetbookApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
Widget _buildPasswordScreen(BuildContext context) {
return MaterialApp(home: PasswordScreen());
}
return _buildPasswordScreen(context);
}
}
class PasswordScreen extends StatelessWidget {
const PasswordScreen({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final TextEditingController passwordController = TextEditingController();
final String correctPassword = "your_password"; // ここにパスワードを設定してください
return Scaffold(
appBar: AppBar(title: Text('Password Required')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: passwordController,
decoration: InputDecoration(
labelText: 'Enter Password',
border: OutlineInputBorder(),
),
obscureText: true,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (passwordController.text == correctPassword) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => Widgetbook.material(
directories: directories,
addons: [],
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Incorrect Password')),
);
}
},
child: Text('Submit'),
),
],
),
),
),
);
}
}
ちなみに、Webホスティングのgithub actionsもFirebase CLIを使ってfirebase init hosting
で指示通りにやっていくことで、自動的に生成された。すごく便利。
preview channels
にデプロイされたWebアプリは、デフォルトでは7日間で期限が切れるそうなので、わざわざパスワードで守りに行かなくても良いのかもしれない。
live channel
(本番環境) についてはパスワードで守っても良いのだろうが、そもそも簡単にURL流出が想定されるような運用が良くないか。
iPhoneで実機デバッグしたい!
まず参考にしたのがこの記事。4. のBundle Identifierを変更する、というところではまった。
記事にある「被っている場合」のようなエラーではなくて、Cannot create a iOS App Development provisioning profile for "<com.foo.bar>"
, No Profiles for 'com.foo.bar' were found
という表示が出てきた。
プロビジョニングファイルの登録が必要とのことで、その詳細を調べていたところで、次に開いたのがこの記事。
1.のUDIDをコピーするところまでできた (ただし私の環境ではUDID
との表示ではなくIdentifier
との表示であった)
続いてApple Developer Portalにログインして、"左側のメニューで「Device」を選択し"たかったのだが、そのようなメニューが見つからない…
新たに探し出した記事がこれ。
端末の登録のためには、Certificates, Identifiers & Profilesにアクセスすればよいとわかった。
そしたら、次のような表示がでてきた。
Access Unavailable
You currently don't have access to this membership resource. To resolve this issue, your team's Account Holder, John Doe, must agree to the latest Program License Agreement.
なるほど。Apple Developer Portalに入ったときに表示される「本プログラムの使用許諾契約が更新されました。~(中略)~に同意する必要があります。」の表示のやつか。
まずはアカウント管理者にお願いしないことには始まらないな。
「デバイスを個別に登録する」ページを見た感じだと、もしかして"左側のメニューで「Device」を選択"しようにもそのメニューが見つけられなかったのも、契約の承諾やアカウント権限がなかったからであり、それらが適切な状態にあれば、そのメニューが出てくるのかな?
デバッグに使用するデバイスを変えたい
VSCodeで開発をしていると、F5キー等でデバッグを呼び出すとき、起動後初回にはiOS SimulatorやAndroid Emulator, Chromeなどデバッグに用いるデバイスを変更することができるが、2回目以降は直前に使用したデバイスで自動的にデバッグが開始される。その状態でも、VSCodeを再起動することなく使用デバイスを変更できるようにしたい。
ということでChatGPTに聞いたら簡単にかえってきた
Ctrl + Shift + P(Macの場合は Cmd + Shift + P)を押して、コマンドパレットを開きます。
Flutter: Select Device と入力して選択します。
使用可能なデバイスの一覧が表示されるので、選びたいデバイスを選択します。
ブログ
こちらのブログ記事、良い。様々なユースケースが端的に解説されている
こちらもとても良い。
クローズドなパッケージの読み込み
アプリの検索キーワード設定
そういえばどこで設定できるんだろう?と思っていたが、Info.plistでいけるのか。