Open33

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の状態を利用することで,変更時の再描画等を可能にする

やまやま

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

解決策

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
}

まとめ

型安全なフォームの状態管理を行うために、以下の方法を使用できます:

  1. Enumを使用してフィールド名を定義し、型安全なアクセスメソッドを提供する。
  2. データクラスを使用して、各フィールドに対応するプロパティを定義する。

どちらの方法も、FormGroupの操作を型安全に行うための有効な手段です。

やまやま

Providerが思うようにoverrideできない

これを試したのだが,やはり思ったようにオーバーライドはされず,もとのコードが参照されてしまう。
https://zenn.dev/aomi/articles/37a029a84cc69b

このissueにあるような状況に陥っていないだろうか?
https://github.com/rrousselGit/riverpod/issues/669
https://github.com/rrousselGit/riverpod/issues/575
https://github.com/rrousselGit/riverpod/issues/2458

やまやま

ページ遷移の手段まとめ

Navigatorを使ったページ遷移には、pushpopのほかにpushAndRemoveUntilなど様々な選択肢がある。この記事ではそれが実際のユースケースを例にとって解説されており、非常にわかりやすい。

https://qiita.com/granoeste/items/19c119ffc36a016e6223

やまやま

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という表示が出てきた。
https://qiita.com/kanatatsu64/items/bd33c85fbf890743cfc5

プロビジョニングファイルの登録が必要とのことで、その詳細を調べていたところで、次に開いたのがこの記事。
1.のUDIDをコピーするところまでできた (ただし私の環境ではUDIDとの表示ではなくIdentifierとの表示であった)
続いてApple Developer Portalにログインして、"左側のメニューで「Device」を選択し"たかったのだが、そのようなメニューが見つからない…
https://note.com/shiokara_okome/n/n9a55b5f6ef06

新たに探し出した記事がこれ。
端末の登録のためには、Certificates, Identifiers & Profilesにアクセスすればよいとわかった。
https://zenn.dev/doshirote/articles/40b8f3e346e70a

そしたら、次のような表示がでてきた。

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 と入力して選択します。
使用可能なデバイスの一覧が表示されるので、選びたいデバイスを選択します。