Open85

Flutter

nicopinnicopin

Setup

https://docs.flutter.dev/get-started/install/macos

sudo softwareupdate --install-rosetta --agree-to-license

By using the agreetolicense option, you are agreeing that you have run this tool with the license only option and have read and agreed to the terms.
If you do not agree, press CTRL-C and cancel this process immediately.
2023-05-20 16:46:26.740 softwareupdate[48391:14457880] Package Authoring Error: 032-66590: Package reference com.apple.pkg.RosettaUpdateAuto is missing installKBytes attribute
Install of Rosetta 2 finished successfully

作業ディレクトリに移動に移動し解凍してから、flutterディレクトリの直上で

export PATH="$PATH:`pwd`/flutter/bin"

flutter doctor

> Found an existing Dart Analysis Server cache at
/Users/hiratsukatomohiro/.dartServer.
It can be reset by deleting /Users/hiratsukatomohiro/.dartServer.
Doctor summary (to see all details, run flutter doctor -v):
[!] Flutter (Channel stable, 3.10.1, on macOS 13.3.1 22E772610a darwin-arm64,
    locale ja-JP)
    ! Warning: `dart` on your path resolves to
      /usr/local/Cellar/dart/2.10.5/libexec/bin/dart, which is not inside your
      current Flutter SDK checkout at /Users/hiratsukatomohiro/Projects/flutter.
      Consider adding /Users/hiratsukatomohiro/Projects/flutter/bin to the front
      of your path.
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 14.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.1)
[✓] Android Studio (version 2022.2)
[✓] VS Code (version 1.76.2)
[✓] Connected device (2 available)
[✓] Network resources

! Doctor found issues in 1 category.
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-telemetry.
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).

前入れたDartのパスが違うので書き換えて再実行

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.10.1, on macOS 13.3.1 22E772610a darwin-arm64,
    locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 14.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.1)
[✓] Android Studio (version 2022.2)
[✓] VS Code (version 1.76.2)
[✓] Connected device (2 available)            
[✓] Network resources

• No issues found!
nicopinnicopin

AndroidStudio setup

https://docs.flutter.dev/get-started/test-drive?tab=androidstudio
Flutterを選択してSDKのパスを通す
Flutter
指示通り必要情報を入れる

AndroidStudioの指示通りエミュレータ入れようとしたらエラー

"Install Android SDK Platform-Tools (revision: 34.0.1)" complete.
"Install Android SDK Platform-Tools (revision: 34.0.1)" finished.
Installing Intel x86 Emulator Accelerator (HAXM installer) in /Users/hiratsukatomohiro/Library/Android/sdk/extras/intel/Hardware_Accelerated_Execution_Manager
"Install Intel x86 Emulator Accelerator (HAXM installer) (revision: 7.6.5)" complete.
Failed to update status to COMPLETE
"Install Intel x86 Emulator Accelerator (HAXM installer) (revision: 7.6.5)" failed.
Installing Android Emulator in /Users/hiratsukatomohiro/Library/Android/sdk/emulator
"Install Android Emulator (revision: 32.1.12)" complete.
"Install Android Emulator (revision: 32.1.12)" finished.
Installing Android SDK Command-line Tools (latest) in /Users/hiratsukatomohiro/Library/Android/sdk/cmdline-tools/latest
"Install Android SDK Command-line Tools (latest) (revision: 9.0)" complete.
"Install Android SDK Command-line Tools (latest) (revision: 9.0)" finished.
Failed packages:

  • Intel x86 Emulator Accelerator (HAXM installer) (extras;intel;Hardware_Accelerated_Execution_Manager)

上記だとエミュレータが動かないので、一度仮想デバイスを削除して、再度設定する。
設定する際にImage選択があるのでARMの方を選択して、インストールする。

W/OpenGLRenderer( 5836): Failed to initialize 101010-2 format, error = EGL_SUCCESS
I/Gralloc4( 5836): mapper 4.x is not supported
E/OpenGLRenderer( 5836): Unable to match the desired swap behavior.

グラフィックスがダメっぽいエラー
エミュレータのグラフィックス設定をAutomaticからHardwareへ変更してリトライ

Xcodeのシミュレータ見たいの想像していたら違ったが正常に起動

nicopinnicopin

First Flutter App

https://codelabs.developers.google.com/codelabs/flutter-codelab-first?hl=ja#8
Done

書き方のお作法が違う(Dart)なだけで、reactみたいな状態管理と、Widgetの独特なLayoutを覚えていけば書けそう
ネットワークリクエストやDB部分については含まれていなかったのでそこチェックと、ミドルウェア的な部分(認証)も含まれてないのでそこらへん整理する必要あり。

nicopinnicopin

Riverpod

状態管理だけどまずは閉じたStateで試してみて、なぜこれが必要か明確にできてから使用する。
https://riverpod.dev/ja/

nicopinnicopin

なんかリビルドされないなという時は

✅ ウィジェット分割時に意識すべき原則

項目 内容
状態をwatchするのは本当に必要な場所だけ 不要な親ウィジェットでstateをwatchしない。必要な子ウィジェットに任せる。
データは可能な限り渡さず、Providerを直接watchさせる 親がuserなどデータを引き出して渡すのではなく、子が直接ref.watch()する。
Provider経由で常に最新の状態を得る 「初期値をpropsで渡す→後から更新されない」を防ぐため、状態は参照元を保つ。

🚨 リビルドされない場合のチェックリスト

チェック項目 確認内容
① ref.watchしているか? 子ウィジェットでref.watch(provider)をしているか確認。データ渡しのみは危険。
② watchするProviderは正しいか? 適切なproviderをwatchしているか?(違うものをwatchしていないか)
③ Providerのスコープは正しいか? ルートのProviderScope配下に存在するか。別ツリーのProviderScopeを参照していないか。
④ State更新は適切か? state = state.copyWith(...)でimmutable更新しているか。直接書き換えていないか。
⑤ 非同期更新のタイミングを考慮しているか? await直後に即ref.read()してないか。build経由の反映を待つ設計になっているか。

🧠 注意するべきケース
• 親がref.watchしてしまうと子が最新状態をもらえない問題が起きやすい
➔ できるだけ子に直接責務を持たせる
• 値渡し(props渡し)すると「初期値だけ」になりがち
➔ 常にproviderをwatchして「リアルタイムの状態」を参照する
• 遅延反映を意識する
➔ state = ...した直後にstateを見るのではなく、次のbuildを信じる設計にする

📄 まとめ
原則 設計指針
必要な場所だけwatch 親でref.watchしすぎない
値渡しを減らす 子が自分でref.watchする
更新はimmutable state.copyWith()必須
非同期性を意識する buildタイミングに任せる

nicopinnicopin

notifer mock

するなって言われてるので、状態持ちの場合には内部で使ってるユースケースをモックして状態変化もNotifierのメソッドで状態変化させる。

nicopinnicopin

image mock

いまいち、綺麗に動かないので400で落ちることを前提にImage.networkはerrorBuilderを必ず書いておくようにしておいてClipOvalなどのラッパー部分で検証する

nicopinnicopin

依存関係の解決タイミングとモック


class AuthNotifier extends _$AuthNotifier {}

上記のようなNotifierでメソッド内部でRefしているときに、notifierをref.watchとかした時に依存関係が解決されるのではなく、メソッド呼び出し時に解決される。
そのためゴールデンテストなどでは Notifierをモックしなくても通る

一方で自分で作成したカスタムプロバイダーでそれ自身がさらに深い依存関係を持つもののような場合にはref.watchの時点で依存関係を解決しようとするので適切にモックしてあげないと、深いところでアプリ起動後に呼ばれるはずのものなどが引っかかって落ちる

nicopinnicopin
nicopinnicopin

Service

Bluetooth Low Energy (BLE) における「サービス」は、特定の機能やデータのグループを表します。BLEデバイスは通常、複数のサービスを持ち、各サービスはそのデバイスの特定の機能に対応します。

サービスの役割

機能のグループ化: サービスは、関連するデータや機能をグループ化するための単位です。たとえば、心拍数を測定するデバイスには「心拍数サービス」があり、そのサービス内に心拍数を測定するためのキャラクタリスティックが含まれています。

UUIDで識別: 各サービスは、ユニークなUUID (Universally Unique Identifier) によって識別されます。UUIDは128ビットの長さを持ち、各サービスやキャラクタリスティックをユニークに識別するために使用されます。

nicopinnicopin

Characteristic

Bluetooth Low Energy (BLE) のプロトコルにおいて、キャラクタリスティック(Characteristic)は、特定のデータや機能を表す最小単位のデータ構造です。キャラクタリスティックは、BLEデバイスの機能を提供するサービス内に含まれます。

1. キャラクタリスティックの構造

キャラクタリスティックは、次のような要素で構成されています:

UUID (Universally Unique Identifier):

各キャラクタリスティックはユニークなUUIDを持っています。このUUIDは128ビットの識別子で、特定のキャラクタリスティックを他と区別するために使用されます。

プロパティ (Properties):

キャラクタリスティックが持つ操作の種類を示します。例えば、データの「読み取り」や「書き込み」、変更時の「通知」などです。
値 (Value):

キャラクタリスティックが保持するデータそのものです。値は、デバイスから読み取ったり、デバイスに書き込んだりすることができます。

ディスクリプタ (Descriptors):

キャラクタリスティックに追加情報を提供する補助的な構造です。たとえば、キャラクタリスティックの値が何を表しているかを説明するテキスト情報などを持つことができます。

2. キャラクタリスティックのプロパティ

キャラクタリスティックのプロパティは、そのキャラクタリスティックがどのように操作できるかを定義しています。代表的なプロパティには以下があります:
Read:
キャラクタリスティックの値を読み取ることができます。例えば、バッテリーレベルのキャラクタリスティックを読み取ることで、現在のバッテリーレベルを取得できます。
Write:
キャラクタリスティックの値を書き込むことができます。たとえば、LEDのオン/オフを制御するキャラクタリスティックに対して値を書き込むことで、LEDを制御します。
Notify:
キャラクタリスティックの値が変更されたときに、デバイスが通知を送信することができます。これにより、アプリケーションはリアルタイムで値の変化を監視することができます。
Indicate:
Notifyと似ていますが、通知が送信されるたびに、受信側から確認応答が必要です。
3. キャラクタリスティックの役割
キャラクタリスティックは、サービス内で特定のデータや機能を提供します。例えば、以下のようなキャラクタリスティックが考えられます:

Heart Rate Measurement Characteristic:

UUID: 00002a37-0000-1000-8000-00805f9b34fb
プロパティ: Notify
役割: 心拍数測定値を通知します。これにより、アプリケーションはリアルタイムで心拍数を受信できます。
Battery Level Characteristic:

UUID: 00002a19-0000-1000-8000-00805f9b34fb
プロパティ: Read, Notify
役割: デバイスのバッテリー残量を表します。ユーザーは、バッテリー残量を読み取ったり、バッテリー残量が変化した際に通知を受け取ったりできます。

nicopinnicopin

const と final の違い

初期化のタイミング:

const: コンパイル時に初期化される必要があります。
final: 実行時に一度だけ初期化され、その後は変更できません。
使用可能な場面:

const: コンパイル時に既に値が分かっている定数に使用します。定数式のみを含むオブジェクトや値に使用されます。
final: 実行時に決定される値(例えば、現在の日時やユーザーからの入力)に使用します。
メモリ管理:

const は、同じ値を持つ複数の const オブジェクトがあれば、それらはメモリ上で共有されます。
final はメモリ上で共有されず、final 変数ごとに固有のメモリ領域が確保されます。

nicopinnicopin
nicopinnicopin

scope

結論から言うと:

❌ Flutter VMでのウィジェットテストは、

「全プラットフォームでの動作保証」にはなりません。

🔍 なぜなら?

Flutterのウィジェットテスト(testWidgets)は:
• Flutterエンジンのテスト用バージョン上で動く
• 実際のプラットフォーム固有の レンダリングエンジン や ネイティブ API は通さない
• Platform.isAndroid, Theme.of(context).platform, MediaQuery.of(context).padding などの「プラットフォームごとに変わる動作」は正確に再現されない場合がある

✅ 何を保証しているか?
• FlutterのUIロジック(Widgetツリー・状態遷移・イベント処理)
• ボタンのタップやテキスト入力、表示の変化など ユーザーインタラクションの基本挙動

🚫 何が保証されないか?

要素 説明
プラットフォーム固有の挙動 iOSとAndroidで異なるUIやフォントサイズの微差など
ネイティブコードとのブリッジ MethodChannel, PlatformView, camera, google_maps_flutter など
実際のデバイススペック依存の動作 DPI、フォントレンダリング、センサーデータなど

✅ どうすればよいか?

テスト種別 目的 実行対象 使用パッケージ
ウィジェットテスト (testWidgets) UIロジックとイベントの単体検証 Dart VM(ローカル) flutter_test
インテグレーションテスト 実機 or エミュでのE2E動作確認 Android / iOS 実機 or エミュレータ integration_test
ゴールデンテスト 見た目のピクセル比較 基本的にはローカルVM(ただしCI対応時は環境統一が必要) flutter_test + 画像ファイル

✅ 結論:どう考えれば良いか

FlutterのtestWidgetsで保証されるのは FlutterのUIロジックが設計通りに動くかどうか です。
しかし「AndroidやiOSで実際にどう見えるか」「ネイティブAPIとうまく連携するか」をテストしたいなら、Integration Testを併用するのがベストです。

nicopinnicopin

Mockito Type

クラスをモックするときにsetUpなどを利用してlateで変数を宣言する場合にタイプをそのクラス自体にするとmockSomeClass.call()が必須の名前付き引数を取る場合にマッチャーを渡せなくなる。
タイプはモックとして生成されるクラスそのものを付与する必要がある。
https://github.com/dart-lang/mockito/issues/608

//bad
late SignInWithEmailUseCase mockSignInWithEmailUseCase;
//error:  Error: The argument type 'Null' can't be assigned to the parameter type 'String' because 'String' is not nullable.
when(mockSignInWithEmailUseCase.call(email: anyNamed('email'));
//good
late MockSignInWithEmailUseCase mockSignInWithEmailUseCase;
when(mockSignInWithEmailUseCase.call(email: anyNamed('email'));
nicopinnicopin

TextFormField

Overlayが必要とかのエラーが出たら、Scaffold→Overlay->OverlayEntryとしてオーバーレイが使える状態でレンダリングするように指示する。

void main() {
  group('Validation', () {
    testWidgets('Email should be email format', (tester) async {
      // Arrange
      await tester.pumpWidget(
        WidgetTestWrapper(
          child: Scaffold(
            body: Overlay(
              initialEntries: [
                OverlayEntry(
                  builder:
                      (context) => SignInFormEmail(
                        onSubmit: (_, __) async {},
                        isSubmitting: false,
                      ),
                ),
              ],
            ),
          ),
        ),
      );

      // Act
      final emailField = find.byType(TextFormField).first;
      await tester.enterText(emailField, 'invalid-email');
      await tester.pumpAndSettle();
      await tester.tap(find.byType(FilledButton));
      await tester.pumpAndSettle();

      // Assert
      final context = tester.element(find.byType(SignInFormEmail));
      final t = AppLocalizations.of(context);
      expect(find.text(t!.isInvalidEmail), findsOneWidget);
    });
  });
}

nicopinnicopin

native channel mock

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  setUp(() {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(MethodChannel('flutter_native_timezone'), (
          methodCall,
        ) async {
          if (methodCall.method == 'getLocalTimezone') {
            return 'Asia/Tokyo';
          }
          return null;
        });
  });
  tearDown(() {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(MethodChannel('flutter_native_timezone'), null)
  });
}

The "instance" getter on the TestDefaultBinaryMessengerBinding binding mixin is only available once that binding has been initialized.
Typically, this is done by calling "WidgetsFlutterBinding.ensureInitialized()"

TestWidgetsFlutterBinding.ensureInitialized();を使って初期化が必要

nicopinnicopin

static class method

UIのビルドでファクトリパターンを使う時に純粋なクラスを使って対応するなどのケースで、class.buildのような静的メソッドを使うとGenerateNiceMocksで対応できなくなるのでインスタンスメソッドにしておくのがベター。

nicopinnicopin
nicopinnicopin

✅ なぜ2つ使い分ける必要があるのか?

#####################################################
## Google provider config
#####################################################
provider "google-beta" {
  user_project_override = true
}
provider "google-beta" {
  alias                 = "no_user_project_override"
  user_project_override = false
}

☑️ 理由:リソースの種類によって、許可される挙動が異なるから
✅ 通常のリソース(Cloud Functions / IAM / Firestoreなど)

user_project_override = true を使うことで、リソースが属するプロジェクトで正しく課金・クォータが適用される。

⚠️ プロジェクト作成時や初期化系リソース

GCPでは プロジェクト作成時点ではまだ課金プロジェクトが存在しない ため、

user_project_override = true を使うとエラーになることがある。

→ このときは user_project_override = false のプロバイダーで実行すべき。

nicopinnicopin

あるプロジェクトで使うterraform用のSAに組織で定義したカスタムロールを付与する

gcloud auth login
gcloud projects add-iam-policy-binding PROJECT_ID \
 --member="serviceAccount:serviceAccount@gmail.com" \
 --role="organizations/ORGANIZATION_ID/roles/ROLE_ID"
nicopinnicopin

impersonate_service_accountをterraformのプロバイダーで利用できるように自身のGoogleアカウントに対してロールを追加する

gcloud iam service-accounts add-iam-policy-binding SERVICE_ACCOUNT_EMAIL \
  --member="user:MY_GOOGLE_ACCOUNT_EMAIL" \
  --role="roles/iam.serviceAccountTokenCreator" \
  --project=PROJECT_ID
nicopinnicopin

assets

flutter
 assets:
  - assets/images

images直下に画像があるならその画像は読み込める、iamges/some/some.pngは読み込めない
常に直下の画像しかとれないので、階層を深くするならその階層までのアセットへの追加が必要

nicopinnicopin

l10n

https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization

flutter pub add flutter_localizations --sdk=flutter
flutter pub add intl:^0.19.0

Resolving dependencies...
Note: intl is pinned to version 0.19.0 by flutter_localizations from the flutter SDK.
See https://dart.dev/go/sdk-version-pinning for details.
Because mobile depends on flutter_localizations from sdk which depends on intl 0.19.0, intl 0.19.0 is
required.
So, because mobile depends on intl ^0.20.2, version solving failed.
You can try the following suggestion to make the pubspec resolve:

  • Consider downgrading your constraint on intl: flutter pub add intl:^0.19.0
pubspec.yml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0
l10n.yml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
synthetic-package: false

synthetic-package:falseにしてlib/l10n以下に生成されたファイルを置くようにする(インポートしやすい)

l10n/app_en.arb
{
  "@@locale": "en",
  "signInWithGoogle": "Sign in with Google",
  "signUpWithGoogle": "Sign up with Google"
}
flutter gen-l10n
main.dart
return MaterialApp.router(
      //~
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      //~
    );
widget.dart
Text(AppLocalizations.of(context)!.signInWithGoogle)
nicopinnicopin

go_router

ShellRouteは、ナビゲーションコンテナ(タブ画面やナビゲーションバー)に関係する部分に限定して使い、
UI構造や見た目の共通化はウィジェットで行うのが Flutter の一般的な設計指針です。

nicopinnicopin

root layout

main.dart
return MaterialApp.router(
      builder:
          (BuildContext context, Widget? child) =>
              ResponsiveBreakpoints.builder(
                child: SafeArea(child: child!),
                breakpoints: [
                  const Breakpoint(start: 0, end: 599, name: MOBILE),
                  const Breakpoint(start: 600, end: 1023, name: TABLET),
                  const Breakpoint(start: 1024, end: 1439, name: DESKTOP),
                  const Breakpoint(
                    start: 1440,
                    end: double.infinity,
                    name: 'LARGE_DESKTOP',
                  ),
                ],
              ),
    );
nicopinnicopin

Don't use dash in path param

:user-idとかのパスパラメータを使うと適切にマッチせずnot found exceptionになる

nicopinnicopin

form

✅ 標準機能でできること(再確認)
項目 説明
Form と GlobalKey<FormState> フォーム全体の状態を管理(検証、リセットなど)
TextFormField の validator: フィールドごとのバリデーションロジック
FormState.validate() 全ての validator: を実行し、true/false を返す
自動エラー表示 validator が文字列を返すと、そのまま UI に表示される
入力フォーカス制御やリアクティブなフォーム連携も可能 必要に応じて FocusNode や onChanged: も使える

rhf+zodなどと違って、自前である程度できる

nicopinnicopin

状態管理

結論から言うと、Flutterで StateNotifier などの「グローバルに見えるステート管理」が一般的に使われるのは、FlutterのUIツリーと状態の結びつきが React よりも「疎結合」だからです。


✅ なぜ StateNotifier のようなグローバルステートが使われるのか?

1. Flutter の UI はツリーだが、ツリーに依存しないステート注入がしやすい

  • Flutter のウィジェットはすべてクラスであり、状態管理や構造化において柔軟な設計が可能です。
  • React のように「親→子にしか状態が渡せない」わけではなく、Riverpod や Provider を使えば「任意の深さに直接アクセスできる」という特性があります。

Flutter は「どこにいても使える仕組みがある」ため、Lift State の必要性が低くなっています。


2. ステートが「UIの責務」である必要がない

React は UI ロジックを中心に設計されており、状態はその延長線上で構築されます。
一方で Flutter はアーキテクチャ的に以下のような分離をよく行います:

  • Widget: UI を構築するだけ
  • StateNotifier: ロジック・状態を集中管理する場所
  • Repository: API や DB とのやり取りを抽象化
  • Provider: 上記を UI に注入する橋渡し

つまり、状態は UI の中にあるのではなく、UI から切り離して管理するのが一般的です。


3. Riverpod は「明示的な依存解決」を持つためトレースしやすい

ご指摘のように「どこで使われているかわからない」状態になることもありますが、Riverpod は以下の特徴によって可視性を保っています:

  • ref.watch()ref.listen() の場所でのみリアクティブに依存
  • IDE で依存関係が追いやすい
  • ProviderGraph や DevTool などで可視化もできる(Riverpod DevTools など)

✅ Flutter のステート管理は「単一の正解」がない

実は Flutter コミュニティでも、「ローカル vs グローバル」の議論はずっとあります。
以下のような判断基準が一般的です:

状況 推奨される手法
単一画面・状態は画面内で完結 StatefulWidget, useState などのローカルステート
状態が複数画面・複数Widgetで共有される StateNotifier, Notifier, ChangeNotifier, Bloc など
ユーザー情報、認証状態、設定値など アプリ全体の Provider(実質グローバル)で管理するのが自然

✅ React と Flutter の違いによる設計選択

比較項目 React Flutter
状態の渡し方 親→子(props)に流すのが基本 Provider を使えばどこでも inject できる
UIの基本形 関数コンポーネント(データ+表示が密接) クラスベースのウィジェット(疎結合が可能)
状態の重視度 UI と状態の結合が強い 状態と UI を分離する設計が一般的
複雑な状態管理 Context API, Redux, Recoil, Zustand など Riverpod, Bloc, Provider, GetX など

✅ 最後に:状態の責任を持つのはどこか?

  • UI(Widget) が状態を持つ → 手軽だが、使い回しや画面をまたいだ連携に弱い
  • コントローラ(StateNotifier) が状態を持つ → 責任分離、再利用性、テストがしやすい

つまり、状態が「誰の責任なのか?」を明確にできるなら、FlutterでもLift Upと同じ思想で設計は可能です。

nicopinnicopin

Dart grammer

cascade

これは Dart の cascade notation(カスケード記法) と呼ばれる構文で、

(eb) => eb
  ..type = ...
  ..email = ...
  ..password = ...

という書き方は、eb に複数のプロパティを連続して設定するために使われます。

🔍 カスケード記法とは?

Dart のカスケード記法 .. は、同じオブジェクトに対して複数の操作を連続して行うのに使われる構文です。

📘 通常の書き方

final eb = SigninRequestProviderEmailBuilder();
eb.type = IdentityProviderTypeEnum.email;
eb.email = 'example@example.com';
eb.password = 'yourPassword';

✅ カスケード記法(短く書ける)

final eb = SigninRequestProviderEmailBuilder()
  ..type = IdentityProviderTypeEnum.email
  ..email = 'example@example.com'
  ..password = 'yourPassword';

📦 関数の中でも使える

SigninRequestProviderEmail((eb) => eb
  ..type = IdentityProviderTypeEnum.email
  ..email = 'example@example.com'
  ..password = 'yourPassword'
)

これは「eb を受け取り、いくつかのプロパティを設定して返す」という関数です。

💡 いつ使うの?

Dart では、built_value などの builder パターンにおいて

SomeObject((b) => b..prop1 = ..prop2 = ..)

のようにカスケード記法を使って、イミュータブルオブジェクトの初期値設定を行うことが推奨されてます。

🔁 応用:配列の追加などもできる

final list = <int>[]
  ..add(1)
  ..add(2)
  ..add(3);
nicopinnicopin

いい質問です![void Function(SigninRequestBuilder)? updates] は 配列ではありません。

✅ 正しくは「省略可能な位置引数」

これは Dart の関数やコンストラクタの構文的な特徴で、**「角括弧 [] に囲まれた引数は省略可能(optional positional parameters)」**を意味します。

🔸 なぜ配列に見える?

たしかに JavaScript や TypeScript に慣れていると、

[void Function(SigninRequestBuilder)? updates]

は「関数のリストっぽく」見えるかもしれませんが、Dart ではこれは:

updates という名前の1つの省略可能な位置引数(型は void Function(SigninRequestBuilder)?)

という意味です。

🧠 まとめ表

構文 意味
(void Function(FooBuilder)) 必須の位置引数
([void Function(FooBuilder)?]) 省略可能な位置引数(nullも許容)
{void Function(FooBuilder)? updates} 省略可能な名前付き引数
List<void Function(FooBuilder)> 関数のリスト型(まさに配列)

✅ built_value でこう書かれる理由

factory Foo([void Function(FooBuilder)? updates]) =>
(FooBuilder()..update(updates)).build();

この形にすることで、以下のようにオプションでビルダーを渡せる便利な構文が実現できるんです:

final foo = Foo((b) => b.name = 'Tom'); // ← updatesに渡ってる
final foo2 = Foo(); // ← 省略可能

質問者さんのように「配列に見える」と感じるのは自然なことです!
Dart 特有の構文に慣れてくると、「省略可能な位置引数」として納得できるようになると思います 🙌

nicopinnicopin
nicopinnicopin

Android

https://docs.flutter.dev/cookbook/navigation/set-up-app-links

android/app/src/main/Androidanifest.xml
            <!-- Deep Link Local -->
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:host="localhost"
                    android:port="5173"
                    android:scheme="http" />
                />
                <data android:scheme="https" />
            </intent-filter>

hostを環境ごとに変えて複数のintent-filterを持たせるっぽい

assetlinks.json

必要なもの

  • Package name
  • sha256 fingerprint
build.gradle.kts
    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId = "example.com.mobile"
        // You can update the following values to match your application needs.
        // For more information, see: https://flutter.dev/to/review-gradle-config.
        minSdk = flutter.minSdkVersion
        targetSdk = flutter.targetSdkVersion
        versionCode = flutter.versionCode
        versionName = flutter.versionName
    }
keytool -list -v -keystore ~/.android/debug.keystore

デフォルトでデバックモードの時は上記にデフォルトキーがある。パスワード入力を求められるがこちらもデフォルトのandroidと入力する。

  1. エミューレーターを起動
  2. エミュレーターでアプリをデバッグモードで実行
  3. adb reverseを使ってホストのローカルホストからエミュレータのローカルホストへ
  4. adb shellを使って遷移確認
adb reverse tcp:5173 tcp:5173
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "http://localhost:5173/auth/sign-up" example.com.app
nicopinnicopin

実際にassetlinks.jsonを使った検証はステージング以降じゃないとダメそう

nicopinnicopin
<activity
            android:name=".MainActivity"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:exported="true"
            android:hardwareAccelerated="true"
            android:launchMode="singleTop"
            android:taskAffinity=""
            android:theme="@style/LaunchTheme"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
                android:name="io.flutter.embedding.android.NormalTheme"
                android:resource="@style/NormalTheme" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <!-- Deep Link Stg -->
            <!-- Make sure you explicitly set android:autoVerify to "true". -->
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <!-- If a user clicks on a shared link that uses the "http" scheme, your
                     app should be able to delegate that traffic to "https". -->
                <!-- Do not include other schemes. -->
                <data android:scheme="http" />
                <data android:scheme="https" />

                <!-- Include one or more domains that should be verified. -->
                <data android:host="stg-dl.example.com" />
            </intent-filter>
        </activity>
stg-dl.example.com/.well-known/assetlinks.json
[
  {
    "relation": [
      "delegate_permission/common.handle_all_urls"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app.stg",
      "sha256_cert_fingerprints": ["<KEY>"]
    }
  }
]

s3+cloudfrontでカスタムドメイン設定してホスティングしてhttps設定周りしておけばおk

nicopinnicopin

ちゃんとインストール時に検証されているかどうかをチェックする場合
USB接続+開発者モード

adb shell pm get-app-links <PACKAGE_NAME>

正常に通っていればDomain verification stateのパートで期待するドメインがverifiedになってるはず。
firebase app distribution経由でインストールしたデバイスの検証状態をチェック

nicopinnicopin
  1. Bundle Idを事前に決めておく
  2. Apple DveloperでApp Id を登録
    a. App IdのCapabilityにAssociated Domainsを登録(*これAppIDのCapabilityを変えるとこのIDに紐づくプロビジョニングファイルが無効化されて作り直さなければいけなくなるので必ず先にやっておくこと。)
  3. 認証ファイルを作成
  4. プロビジョニングプロファイルを作成
    a. 実機のUDIDが必要になる。Macに接続したことのある端末なら、XCode > window > device and simulatorからデバイス情報が手元になくても見れる
  5. プロビジョニングプロファイルをダウンロード
  6. AASAファイルの作成と配置
    https://<fully qualified domain>/.well-known/apple-app-site-association
   {
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.app.stg",
        "paths": [ "*" ]
      }
    ]
  }
}
  1. Info.plistのAssociated Domains設定
    a.
nicopinnicopin
nicopinnicopin

Android

android/app.build.gradle.kts
android {
    flavorDimensions += "env"
    productFlavors {
        create("dev") {
            dimension = "env"
            applicationIdSuffix = ".dev"
            versionNameSuffix = "-dev"
        }
        create("stg") {
            dimension = "env"
            applicationIdSuffix = ".stg"
            versionNameSuffix = "-stg"
        }
        create("prod") {
            dimension = "env"
        }
    }
}

productFlavaorsを使うとflutter build apk --flavor stgとかで予約語を使ったカスタマイズが可能っぽい
applicationIdSuffixを使えば、defaultConfig.applicationIdのサフィックスがつくようになるなど。

ステージング環境ビルド想定コマンド

flutter build apk --release --flavor stg

ステージングで使うようの鍵情報を適当に作っておく

keytool -genkeypair -v \
  -keystore keystore.jks \
  -alias stg \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000
キーストアのパスワードを入力してください:  
新規パスワードを再入力してください: 
姓名は何ですか。
  [Unknown]:  
組織単位名は何ですか。
  [Unknown]:  
組織名は何ですか。
  [Unknown]:  
都市名または地域名は何ですか。
  [Unknown]:  
都道府県名または州名は何ですか。
  [Unknown]:  
この単位に該当する2文字の国コードは何ですか。
  [Unknown]:  
CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknownでよろしいですか。
  [いいえ]:

github action のシークレットに渡しやすい形式に変換

base64 -i keystore.jks -o keystore.jks.base64

github action env secrets
KEYSTORE_BASE64 -> ローカルで生成したファイルのBase64エンコード文字列
KEY_PROPERTIES -> keystore.propertiesのファイル内容文字列
*storePasswordとkeyPasswordはkeypassを明示しない限りおんなじ

あとは環境変数からkeystore.jksとkeystore.propertiesを復元してビルドコマンドを流すようにすればCIでのビルドはOK

name: Release Android

on:
  push:
    branches:
      - main
      - dev

jobs:
  build-stg:
    if: github.ref_name == 'dev'
    uses: ./.github/workflows/build-apk.yml
    with:
      github_action_env: Preview
      flutter_flavor: stg
    secrets: inherit
name: Build APK

on:
  workflow_call:
    inputs:
      github_action_env:
        required: true
        type: string
      flutter_flavor:
        required: true
        type: string

jobs:
  build:
    environment: ${{ inputs.github_action_env }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.29.x'

      - name: Restore keystore.jks
        run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > mobile/android/keystore.jks

      - name: Restore key.properties
        run: echo -e "${{ secrets.KEY_PROPERTIES }}" > mobile/android/app/key.properties

      - name: Flutter pub get
        run: flutter pub get
        working-directory: mobile

      - name: Build APK
        run: flutter build apk --release --flavor ${{ inputs.flutter_flavor }}
        working-directory: mobile
nicopinnicopin

OAuth Google

  1. OAuth同意画面の作成
  2. クライアントIDを対応OSに対して発行
    a. androidの場合sha1必要
  3. google_sign_inのインストール

https://github.com/flutter/flutter/issues/36673
https://github.com/flutter/flutter/issues/20903
https://github.com/flutter/flutter/issues/111051
add firebase project and android app is not working
https://stackoverflow.com/questions/79009273/flutter-platformexception-when-trying-to-get-google-sign-in-access-token
not working
https://medium.com/codebrew/flutter-google-sign-in-without-firebase-3680713966fb

firebase authenticationを使うと確かにidTokenが取れるようになる
->なんか、firebase authenticationを有効にするとgoogle-service.jsonに新しいoauth_clientが追加されてる。これをGoogle cloudの方で確認するとAndroidクライアントではなくWebクライアントで作成されていることが確認できる。この状態であればidTokenを多分このWebクライアント経由でとってきているように見える。

webと同じ感覚でやってみたらidTokenが取れないが上記のようにドキュメント通り動くのはFirebase projectがあり、gradle pluginでgoogle serviceが有効になっている時だけっぽい。

nicopinnicopin
  1. firebase authenticationを有効化
  2. sha1をfirebase android appに追加
  3. google-services.jsonをDLしてフレーバーに対応したディレクトリに保存(ex, android/src/dev/google-services.json)
  4. google_sign_inパッケージでサインイン
  5. idToken取得

iosはios同意画面のクライアントIDでバックエンド検証するのに、Androidの場合にはAndroidの同意画面のクライアントIDではなくAudがWebクライアントIDの方になっている。
https://developers.google.com/identity/sign-in/android/backend-auth

nicopinnicopin

firabase authenticationを有効化してある上で、iosの場合もまずはapple appとして追加する。
→GCPの方でiOS用のクライアントができてるかチェック

Your app is missing support for the following URL schemes: com.googleusercontent.apps.~

info.plistも以下のように更新してあげる必要があるっぽいので、先ほど確認したクライアントのurlをチェックして、Xcodeからinfo.plist更新
https://github.com/googlesamples/google-services/issues/81#issuecomment-302976984

環境別に切り替える方法は以下の方式
https://qiita.com/ko2ic/items/53f97bb7c28632268b5a#環境ごとの設定

nicopinnicopin

このやり方複数のGoogle Client Idで検証しなければいけなくなるが本当に合ってるのか調査

nicopinnicopin

markdwon

https://github.com/flutter/flutter/issues/162966

flutterの公式マークダウンパッケージが終わるらしい

nicopinnicopin

markdown_widgetはMarkdownWidgetを使うと1ページレンダリング想定で指定の高さが必要になるので、ListViewとかの内部で使う場合はMarkdownBlockを使うとベター

nicopinnicopin

List

📝 FlutterチャットUI実装メモ:ListView vs CustomScrollView

■ 問題発生時の構成(ListView)
• ListView.builder の itemBuilder にステートフルな AIMessageStream を含めていた。
• チャットメッセージとストリーム処理UIをすべて ListView のアイテムとして扱っていた。

🔥 発生した問題
• AIMessageStream がスクロールで一時的に画面外に出ると破棄される。
• StatefulWidget の initState() → dispose() が何度も呼ばれ、StreamSubscription がキャンセルされてしまう。
• 再レンダリング時に null 参照エラー (NoSuchMethodError) が発生。
• 理由:一度 dispose されたステートが復活せずに参照されたため。

■ 解決アプローチ(CustomScrollView + Slivers)

CustomScrollView(
slivers: [
SliverList(...), // ステートレスなチャットバブルのみ
SliverToBoxAdapter(child: AIMessageStream(...)), // ステートフルなストリーム処理UI
],
);

✅ 解決内容
• SliverList にステートレスなメッセージリストのみを配置。
• AIMessageStream を SliverToBoxAdapter でリスト外の「独立要素」として分離。
• これにより、スクロールによって破棄されず、常に initState → dispose が正しく制御される。
• CustomScrollView により、チャットUI全体が1つのスクロールコンテキストに統一されており、自然なスクロール体験も維持。

✔️ 結論

比較項目 ListView CustomScrollView + Slivers
ステートフル要素の維持 ❌ スクロールで破棄される ✅ 常に維持される
レンダリング制御 ⛔ 一括処理しにくい ✅ 部分分離可能
チャットUI適性 △ 単純構造ならOK ◎ 複雑構造に対応

nicopinnicopin

Websocket

go_router 使ってる場合の構成
gateパターンで特定ディレクトリ以下の場合にはinitStateまたはdidUpdateWidgetで接続
pushなどウィジェットツリーから除外されない移動の場合であっても不要なws接続の切断と再接続をしたい場合にはobserverを使って特定の接続が必要なスクリーンの移動の前後をチェクしてnotifier経由でchannelをクローズする

nicopinnicopin
nicopinnicopin

MacBook Proをクラムシェルモードで使用している場合、内蔵マイクは使用できません。クラムシェルモードとは、ディスプレイを閉じた状態で外部ディスプレイやキーボード、マウスなどを使用してMacBook Proを使用する状態のことです。このモードでは、内蔵カメラやマイクは使用できなくなるため、別途外付けのマイクを用意する必要があります。外付けマイクの入力レベルは、システム設定の「サウンド」で調整できます。

nicopinnicopin

XCode Schema

Xcodeで新しいビルド構成を追加する手順を詳しく説明します。


手順1: Xcodeでプロジェクトを開く

  1. Xcodeを起動
  2. ios/Runner.xcworkspaceを開く.xcodeprojではなく.xcworkspaceを開く)

手順2: プロジェクト設定を開く

  1. 左側のナビゲーターペインで「Runner」プロジェクトをクリック
  2. 「Runner」プロジェクト(青いアイコン)を選択
  3. 「Info」タブをクリック

手順3: 新しいビルド構成を追加

3-1. 構成一覧を確認

  • 「Configurations」セクションで現在の構成を確認
  • 通常はDebugReleaseProfileの3つ

3-2. 新しい構成を追加

  1. 「+」ボタンをクリック
  2. 「Duplicate "Debug" Configuration」を選択
  3. 新しい構成名を入力
    • Debug-Staging
    • Release-Staging
    • Debug-Development
    • Release-Development

3-3. 複数の構成を追加

  • 同様の手順で必要な構成をすべて追加
  • 例:
    • Debug-Development
    • Release-Development
    • Debug-Staging
    • Release-Staging

手順4: 各構成でBundle IDを設定

4-1. Target設定を開く

  1. 「TARGETS」セクションの「Runner」を選択
  2. 「Build Settings」タブをクリック

4-2. Bundle IDを検索

  1. 検索ボックスに「Bundle Identifier」と入力
  2. 「Product Bundle Identifier」を探す

4-3. 各構成でBundle IDを設定

  • Debug-Development: com.example.app.dev
  • Release-Development: com.example.app.dev
  • Debug-Staging: com.example.app.stg
  • Release-Staging: com.example.app.stg
  • Debug: com.example.app(本番)
  • Release: com.example.app(本番)

手順5: スキームでビルド構成を指定

5-1. スキームを編集

  1. Xcodeのツールバーでスキーム名をクリック
  2. 「Edit Scheme...」を選択

5-2. ビルド構成を設定

  1. 左側の「Run」を選択
  2. 「Build Configuration」ドロップダウンで適切な構成を選択
    • Runner-Devスキーム: Debug-DevelopmentまたはRelease-Development
    • Runner-Stgスキーム: Debug-StagingまたはRelease-Staging
    • Runnerスキーム: DebugまたはRelease

手順6: 設定の確認

6-1. project.pbxprojファイルの確認

  • 設定後、ios/Runner.xcodeproj/project.pbxprojファイルに新しい構成が追加されていることを確認

6-2. Bundle IDの確認

  • 各構成で正しいBundle IDが設定されていることを確認

注意点

  • 新しいBundle IDはApple DeveloperサイトでApp IDとして登録する必要があります
  • 各App IDにAssociated Domains権限を有効化する必要があります
  • 新しいプロビジョニングプロファイルを作成する必要があります

要点:
Xcodeのプロジェクト設定で新しいビルド構成を追加し、各構成で異なるBundle IDを設定することで、環境ごとにUniversal Linkを分離できます。