Open16

Flutter開発メモ

やまやま

Flutterアプリ開発を始め,新しく学んだ知識や取り入れた仕組みを書き綴ります。

やまやま

時間に紐付いたタスクをCloud Functionsで実行する

FirebaseのCloud Functionsを用いて、時間に紐付いた不定期実行のタスクを処理する方法を調べた。
(時間に紐付いた不定期実行のタスク = ある操作の48時間後にPush通知を送信する、募集の締切日時になったら募集終了処理を行う、など)

Cloud Tasks
https://cloud.google.com/tasks/docs/tutorial-gcf?hl=ja
Cloud Scheduler
https://dev.classmethod.jp/articles/cloud-functions-create-scheduler-job-for-next-function/
https://qiita.com/takusamar/items/c9872867fdab0fe7302a
一分おきにタスク探索&実行を行う例。ゴリ押し感が少々
https://qiita.com/hirothings/items/37430b2408a5a7a85972

やまやま

MVVMアーキテクチャについて

FlutterではMVVMアーキテクチャが推奨されているが,どうにもこれがReactのHooks + 関数コンポーネントの書き味と全然違ってみえて,難しい。

難しさの言語化

Reactではpropsを用いた値の受け渡しが基本なので,コンポーネントを小さく区切りやすく,個別の実装・テストも容易である。
一方で私の観測範囲では,MVVMアーキテクチャを採用したFlutterのコードは,Viewの最小単位がページ単位になり,そのページ毎に状態管理用のViewModelを用いていることが多い。これ自体が直ちに問題となるわけではないが,Viewが全く区切られずに巨大なウィジェットになるから難しくみえるのである。

巨大なウィジェットができる原因

ではなぜMVVMのFlutterのViewは巨大化するのか?それは「ViewModelによるグローバルな状態管理に頼りすぎるから」だと私は思う。
ページの状態管理やビジネスロジックをすべてViewModelに委譲すると,Viewを小分けにしようにも相互依存を抱えることになり,うまくいかないのだ。

対処法

私なりに考えた対処法は次の通りである。

  • 小さなコンポーネント単位では,値の受け渡しにコンストラクタ注入を利用し,さらに状態管理と直接関係のないビジネスロジックは,そのコンポーネントクラスに委譲する。
  • 一方で全体としては,グローバルな状態管理には依然ViewModelを用い,ページ構築のViewはあくまでコンポーネントを組み立てるだけの調停役としての役割にとどめる。

まず,何でもかんでもViewModelのグローバルな状態を直接取りに行くことをやめ,コンストラクタ引数で渡すことにする。すると羃等でテストしやすい小さなコンポーネントに分けることができる。
一方で全体としては,コンポーネントを組み立てる「調停役」としてのViewが,適切にViewModelの状態を利用することで,変更時の再描画等を可能にする

やまやま

アイコンを探す方法

'package:flutter/material.dart'にはデフォルトでIconsというクラスがあり,そのアイコンをWEB上で探すことができる。ほかにも,FlutterIcon.comというサイトがあるみたい。
https://zenn.dev/tama8021/articles/dbc931e23120bb

やまやま

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で作られたオブジェクトでもいくつでも渡すことができる。

https://docs-v2.riverpod.dev/docs/concepts/modifiers/family

追記: わかりやすい記事を発見
https://qiita.com/TakahiroOta/items/ed37a33fe0ee8b06bc29

追記2: こんな記事をみつけたけど,通常版のproviderも用意してoverrideする意図は何なのだろうか? わざわざoverrideをしなくても,遷移先のView側で「引数を渡してViewModelを取得する」という処理を書けば十分なのでは?
https://qiita.com/yusuke-kobayashi0117/items/be3410ff391065783990#providerscope-override
https://torikatsu923.hatenablog.com/entry/2021/04/24/185346

→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(

結局,使っているバージョンが古いだけだった
https://stackoverflow.com/questions/76500308/flutter-web-fails-with-firebase-too-many-positional-arguments-1-allowed-but-2