Open29

Flutterめも

kabikirakabikira

option+Enter
便利
ラップしたり、リムーブしたり

kabikirakabikira

VSCodeでの代替ショートカット

  1. クイックフィックスやリファクタリングを呼び出す
    ショートカット: Cmd + . (Mac) / Ctrl + . (Windows/Linux)
kabikirakabikira

スクロール可能な内容に変更する

import 'package:flutter/material.dart';

void main() {
  const body = SafeArea(
    child: SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: Row(
        children: [
          Text("あいうえおかきくけこさしすせそ"),
          Text("アイウエロカキくけこさしすせそ"),
        ],
      ),
    ),
  );

  const sc = Scaffold(body: body);

  const app = MaterialApp(home: sc);
  runApp(app);
}

kabikirakabikira

デバッグラベルを消す

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
kabikirakabikira

https://www.youtube.com/watch?v=0HVru4WtdWo

使いたいウィジェットによってパッケージをきめる

ステートフルは他のウィジェットと共有しにくいのでリバーポッドを使う
それがConsumerウィジェット
さらに新しいのがHookウィジェット

中にState Hook
外にState Consumer

公式はHooksは認めてないが支持されている

Stateless
Stateful
Hook
Consumer
の4つのウェジェットがある

以下は開発者が見極めて使う
Stateful
Hook

合体ウィジェットもある
ConsumerStateful
StatefulHookConsumer
HookConsumer

このウェジェットの組み合わせを決める

kabikirakabikira

https://www.youtube.com/watch?v=OLYcFlPp_rM

状態の作り方

State
変化していくデータの本体
状態

Provider
状態を守る壁
refを持ってないと状態がみれない

Notfier
状態を変える編集者
Consumerはrefで見れるが編集はできない

State→Notifier→Provider の順でコードを書いていく

State

単純データ

int, string

複雑データ

List, class

Future系

Future<String> Future.delayed()

Stream

Stream<String> StreamController().stream
急にデータが飛んでくる系

この4つのパターンをつかい分ける
Riverpod1と2で違う
これは2

2のRiverpod_generatorで自動生成するのがprovider

kabikirakabikira

状態の使い方

「Riverpod (リバーポッド) 完全ガイド #Flutter 3/4」の要約

  1. Riverpodの進化とプロバイダの自動生成(00:01 - 01:54)

    • Riverpod Generatorによりプロバイダコードが自動生成されるようになり、プロバイダの名前付けが重要になった。
    • @riverpodアノテーションを使用して、プロバイダの名前をカスタマイズする方法を紹介。
  2. ウィジェット内での状態の取得と表示(02:33 - 03:06)

    • ref.watchを使用してプロバイダからデータを取得し、ウィジェット内で活用する方法を説明。
    • 取得したデータを画面に表示する具体的なコード例を示す。
  3. Notifierを用いた状態の更新(03:06 - 04:06)

    • ボタンを作成し、押下時にref.readでNotifierを取得して状態を更新する方法を解説。
    • updateState関数を使用してデータを変更し、UIに反映させる手順を示す。
  4. watchlistenreadの使い分け(05:03 - 05:47)

    • watchは状態の継続的な監視、readは一度きりの値の取得、listenは状態変化時のコマンド実行に使用。
    • 適切なメソッドを選択することで、バグを防ぎ効率的な状態管理が可能になる。
  5. スコープとライフサイクルの重要性(06:30 - 07:43)

    • 関数スコープとwatchlistenのライフサイクルについて解説。
    • 不適切なスコープの使用がバグの原因となるため、build関数内での適切な使用を推奨。
  6. AsyncValueによる非同期状態の管理(14:22 - 16:14)

    • 非同期データ(読み込み中、エラー、データ取得済み)を管理するためのAsyncValueの紹介。
    • FutureBuilderStreamBuilderを使わずに、統一的に非同期状態を扱う方法を説明。
  7. 実践的なコード例と応用(11:30 - 17:37)

    • ボタンの実装、リストの表示、Notifierの使用など、具体的なコード例を提供。
    • 状態に応じたウィジェットの表示(ローディングインジケーター、エラーメッセージ)の方法を示す。
  8. 学習の継続とフィードバックの促進(18:56 - 19:52)

    • 視聴者に練習と継続的な学習を促し、疑問点や難しかった点のコメントを歓迎。
    • 将来的な進化にも注目し、最新情報への適応を推奨。
  9. 次回予告とさらなる発展(19:52 - 20:17)

    • 次回はより高度なアプリケーションでのRiverpodの活用方法を解説予定。
    • 現時点での知識でも多くのアプリが作成可能であることを強調。
  10. Flutterの宣言的フレームワークとしての特性とRiverpodの統合(Throughout)

    • Flutterの宣言的な特性を活かし、Riverpodを効果的に統合する方法を強調。
    • バグを避けるためのベストプラクティスと、効率的な状態管理の重要性を再確認。
kabikirakabikira

buildの中で状態をwatch画面触ったらNotifierをread

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:test_rivepod/s1.dart';

class MyWidget extends ConsumerWidget {
  const MyWidget({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final s1 = ref.watch(s1NotifierProvider);

    final button = ElevatedButton(
      onPressed: () {
        final notifier = ref.read(s1NotifierProvider.notifier);
        notifier.updateState();
      },
      child: const Text('ボタン'),
    );

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$s1'),
        button,
      ],
    );
  }
}
kabikirakabikira

listenは命令を足す感じ

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:test_rivepod/s1.dart';

class MyWidget extends ConsumerWidget {
  const MyWidget({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final s1 = ref.watch(s1NotifierProvider);

    ref.listen(s1NotifierProvider, (oldState, newState) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('$oldState から $newState へ変化しました')),
      );
    });

    final button = ElevatedButton(
      onPressed: () {
        final notifier = ref.read(s1NotifierProvider.notifier);
        notifier.updateState();
      },
      child: const Text('ボタン'),
    );

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$s1'),
        button,
      ],
    );
  }
}

kabikirakabikira

「Riverpod (リバーポッド) 完全ガイド #Flutter 4/4」の要約

  1. 部分的な状態の監視(00:00 - 01:51)

    • watchlisten を使い、状態の一部だけを監視する方法を解説。
    • 状態管理の例として四国の県(香川、徳島、高知、愛媛)のデータを使用。
    • 状態を細分化して管理することで効率的な状態管理が可能。
  2. 状態の初期化と破棄(02:31 - 04:00)

    • 状態は初期化(build)され、誰も監視しなくなると自動的に破棄(onDispose)される仕組み。
    • ウィジェットのライフサイクルに基づくデータ管理の重要性を説明。
  3. keep-aliveauto-dispose(04:50 - 06:17)

    • データを破棄せず保持する keep-alive と、不要なデータを自動で破棄する auto-dispose の使い分けを紹介。
    • 過剰な keep-alive の使用はリソースの無駄になるため注意が必要。
  4. 基本プロバイダとプロキシプロバイダ(07:30 - 11:49)

    • 基本プロバイダを用いたデータの管理と、複数の状態を結合するプロキシプロバイダの作成方法を解説。
    • 状態を結合することで管理が簡単になり、ウィジェットごとの監視が効率化。
  5. ロジックプロバイダ(13:54 - 14:39)

    • 状態の変更頻度を最適化し、不要なウィジェットの再描画を防ぐロジックプロバイダを紹介。
    • 必要最低限の処理だけを行うことでアプリのパフォーマンスを向上。
  6. テストや複数の状態管理(20:22 - 22:00)

    • テスト用にプロバイダスコープを分離し、異なる「仮想世界」の状態を作成する方法を解説。
    • テスト時に使用するフェイクデータの活用例を紹介。
  7. プロバイダファミリーの活用(18:02 - 19:53)

    • プロバイダファミリーを使用して、同じ型の複数のプロバイダを動的に生成する方法を説明。
    • 独立した状態管理が可能になり、柔軟なアプリ設計に役立つ。
  8. テストでのオーバーライド機能(22:31 - 23:35)

    • プロバイダをフェイクデータでオーバーライドし、動作確認やテストに活用する方法を解説。
    • 実際のコードを変更せずにテストデータを差し替え可能。
  9. 学習への励ましと今後の更新(24:43 - 25:23)

    • 状態プロバイダやステートノティファイアについて、基本を忘れないことが重要と強調。
    • 今後の動画更新予定を伝え、さらなる学習を促進。
  10. まとめと視聴者への感謝(25:23 - 終了)

    • Riverpodの基礎から応用までを体系的に解説し、視聴者の学習をサポートする姿勢を表明。
kabikirakabikira

調べたいこと
VScodeでのデバッグ
DevTool

kabikirakabikira
void main() async {
  final database = [
    {"id": "1", "date": "2025-01-21T10:45:00Z", "code": 1},
    {"id": "2", "date": "2025-01-22T10:45:00Z", "code": 2},
    {"id": "3", "date": "2025-01-23T10:45:00Z", "code": 3},
  ];

  Future<bool> isIdDuplicate(String id) async {
    return database.any((data) => data['id'] == id);
  }

  // テスト
  print(await isIdDuplicate("2")); // true
  print(await isIdDuplicate("4")); // false
}

kabikirakabikira

Riverpodの状態を意識するプログラミングとは?

Riverpodの状態管理を意識したプログラミングとは、アプリの状態(State)の流れや変更を適切に管理し、効率的な状態管理を実現することを指します。以下のポイントを意識すると、Riverpodを活かした設計ができます。

  1. 状態の種類を意識する

Riverpodでは、状態の種類に応じて適切なプロバイダを選ぶことが重要です。

(1) シンプルな状態

✅ Provider

•	用途: 不変のデータ、計算結果のキャッシュ
•	例: 定数や計算済みのデータ

final greetingProvider = Provider((ref) => "Hello, Riverpod!");

✅ StateProvider

•	用途: UIで変更される単純な状態
•	例: カウンターの値、チェックボックスの選択状態

final counterProvider = StateProvider((ref) => 0);

•	更新方法

ref.read(counterProvider.notifier).state++;

(2) 非同期処理

✅ FutureProvider

•	用途: API呼び出し、非同期のデータ取得
•	例: FirestoreやAPIからデータを取得する

final userProvider = FutureProvider((ref) async {
return await fetchUserData();
});

•	非同期データの取得

ref.watch(userProvider).when(
data: (user) => Text(user.name),
loading: () => CircularProgressIndicator(),
error: (e, _) => Text("Error: $e"),
);

✅ StreamProvider

•	用途: Firestoreのリアルタイムデータ、WebSocket
•	例: Firestoreのドキュメントをリアルタイムで監視

final messagesProvider = StreamProvider((ref) {
return FirebaseFirestore.instance.collection("messages").snapshots();
});

(3) グローバルな状態管理

✅ NotifierProvider

•	用途: MVVMのViewModelのような状態管理
•	例: 画面をまたぐデータ、複雑な状態管理

class CounterNotifier extends Notifier<int> {
@override
int build() => 0;

void increment() => state++;
}
final counterNotifierProvider = NotifierProvider<CounterNotifier, int>(() => CounterNotifier());

•	更新方法

ref.read(counterNotifierProvider.notifier).increment();

  1. 不要なリビルドを抑える

Riverpodのref.watchは、プロバイダの状態が変更されるたびに再ビルドされます。不要なリビルドを抑える方法として、以下を意識すると良いです。

(1) ref.read() を使う

•	watch を使うとUIが再ビルドされるが、read は再ビルドを抑えられる。
•	例: ボタンのクリック時にのみ状態を変更

ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: Text("Increment"),
);

(2) select() を使う

•	オブジェクトの一部のプロパティだけを監視することで、不要なリビルドを抑える。
•	例: User オブジェクトの name だけを監視

final userProvider = StateProvider((ref) => User(id: 1, name: "Alice"));

Text(ref.watch(userProvider.select((user) => user.name)));

  1. 適切なスコープを意識する

Riverpodのプロバイダは、どのスコープに置くかが重要です。

(1) グローバルスコープ

•	main.dart で ProviderScope を定義
•	アプリ全体で管理すべき状態(ログイン状態、ユーザー情報)

void main() {
runApp(ProviderScope(child: MyApp()));
}

(2) ローカルスコープ

•	画面ごとに ProviderScope を分けることで、不要なプロバイダを破棄できる
•	例: あるページでのみ必要な状態

ProviderScope(
overrides: [
counterProvider.overrideWith((ref) => StateProvider((ref) => 0)),
],
child: CounterPage(),
);

  1. ファミリー (family) と自動破棄 (autoDispose) の活用

Riverpodでは family や autoDispose を使うことで、状態の管理を最適化できます。

(1) family: 引数を受け取るプロバイダ

•	例: IDごとに異なるユーザー情報を取得

final userProvider = FutureProvider.family<User, String>((ref, userId) async {
return fetchUser(userId);
});

•	利用方法

final user = ref.watch(userProvider("123"));

(2) autoDispose: 不要な状態を自動破棄

•	一時的なデータに使う(ページ遷移時に破棄)
•	例: 画面が閉じられたらキャッシュを削除

final searchResultsProvider = FutureProvider.autoDispose((ref) async {
return fetchSearchResults();
});

まとめ

Riverpodの状態を意識するプログラミングとは、以下の点を考慮して状態管理を適切に行うことです。

✅ 状態の種類を適切に選ぶ

•	Provider: 計算済みのデータ
•	StateProvider: シンプルな状態
•	FutureProvider: 非同期データ
•	NotifierProvider: ビジネスロジックを持つ状態管理

✅ 不要なリビルドを抑える

•	read() や select() を活用

✅ スコープを適切に管理

•	グローバルとローカルの使い分け

✅ family や autoDispose を活用

•	動的に引数を受け取る場合は family
•	一時的な状態管理には autoDispose

これらを意識することで、Riverpodを最大限に活かした状態管理ができます!

kabikirakabikira

NavigatorObserverRouteObserver の使い分けに関する情報源をお探しですね。下の公式ドキュメントが参考になります。

  • NavigatorObserver クラス:
    • NavigatorObserver は、ナビゲーターの動作を監視するためのインターフェースで、didPop` メソッドをオーバーライドすることで、ルートがポップされた際のイベントを検知できます。 - 式ドキュメント: citeturn0search5
  • RouteObserver クラス:
    • RouteObserver は、NavigatorObserver を継承したクラスで、RouteAware を実装したウィジェットに対して、ルートの状態変化(例えば、別のルートがプッシュまたはポップされたとき)を通知します。 - 式ドキュメント: citeturn0search1 れらのドキュメントを参照することで、NavigatorObserverRouteObserver` の違いや適切な使い分けについて、より深く理解できると思います。
kabikirakabikira

didPop 内で特定のパス(例: /home)のときのみ dateTimeProvider を更新するように修正します。


修正後の TransitionObserver

変更点

  1. didPop 内で route.settings.name をチェックし、/home の場合のみ dateTimeProvider を更新する
  2. pageName == '/home' の条件を追加

修正コード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:intl/intl.dart';

enum AppTransitionType {
  push,
  pop,
}

// 現在の日時を保持するプロバイダー
final dateTimeProvider = StateProvider<String>((ref) {
  return _getCurrentTime();
});

String _getCurrentTime() {
  return DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
}

// TransitionObserver のプロバイダー
final transitionObserverProvider = Provider<TransitionObserver>((ref) {
  return TransitionObserver(ref);
});

class TransitionObserver extends NavigatorObserver {
  final Ref ref;
  static FirebaseAnalytics analytics = FirebaseAnalytics.instance;

  TransitionObserver(this.ref);

  
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    super.didPush(route, previousRoute);
    _onTransition(route, previousRoute, AppTransitionType.push);
  }

  
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    super.didPop(route, previousRoute);

    final pageName = previousRoute?.settings.name ?? 'unknown';

    // `/home` に戻ったときのみ `dateTimeProvider` を更新
    if (pageName == '/home') {
      ref.read(dateTimeProvider.notifier).state = _getCurrentTime();
    }

    _onTransition(route, previousRoute, AppTransitionType.pop);
  }

  void _onTransition(
    Route<dynamic> route,
    Route<dynamic>? previousRoute,
    AppTransitionType transitionType,
  ) async {
    final pageName = route.settings.name ?? 'unknown';
    debugPrint('pageName: $pageName, ${transitionType.name}');
    await analytics.logEvent(name: 'page_transition', parameters: {
      'page_name': pageName,
      'transition_type': transitionType.name,
    });
  }
}

動作の流れ

  1. Navigator.pop() で前の画面に戻る
  2. didPop が発火
  3. previousRoute.settings.name == '/home' の場合のみ dateTimeProvider を更新
  4. HomeScreendateTimeProvider をリッスンして UI に反映

修正後の HomeScreen

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class HomeScreen extends ConsumerWidget {
  static const String routeName = '/home'; // ルート名を指定

  
  Widget build(BuildContext context, WidgetRef ref) {
    final currentTime = ref.watch(dateTimeProvider);

    return Scaffold(
      appBar: AppBar(title: Text("Home")),
      body: Center(
        child: Text(
          "Last Popped: $currentTime",
          style: TextStyle(fontSize: 18),
        ),
      ),
    );
  }
}

この方法のメリット

NavigatorObserver を利用して /home に戻ったときだけ dateTimeProvider を更新
Riverpod の状態管理を適切に活用し、UI を動的に更新
他の画面に戻る場合は dateTimeProvider を変更しない

これで 特定のパス /home に戻ったときのみ dateTimeProvider を更新できる実装 になりました! 🚀🎉

kabikirakabikira

送っていただいたコードと修正案を確認しました。TransitionObserverをRiverpodと組み合わせて実装するために、以下のように修正することをお勧めします:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';

// 日時用のプロバイダー
final dateTimeProvider = StateProvider<String>((ref) {
  return _getCurrentTime();
});

String _getCurrentTime() {
  return DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
}

enum AppTransitionType {
  push,
  pop,
}

class TransitionObserver extends NavigatorObserver {
  final Ref ref;
  
  TransitionObserver(this.ref);

  
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    super.didPush(route, previousRoute);
    _onTransition(route, previousRoute, AppTransitionType.push);
  }

  
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    super.didPop(route, previousRoute);
    final pageName = previousRoute?.settings.name;
    
    // HomeRouteに戻ったときだけ更新
    if (pageName == const HomeRoute().location) {
      ref.read(dateTimeProvider.notifier).state = _getCurrentTime();
    }
    
    _onTransition(route, previousRoute, AppTransitionType.pop);
  }

  void _onTransition(
    Route<dynamic> route,
    Route<dynamic>? previousRoute,
    AppTransitionType transitionType,
  ) async {
    final pageName = route.settings.name ?? 'unknown';
    debugPrint('pageName: $pageName, ${transitionType.name}');
  }
}

// TransitionObserverのプロバイダー
final transitionObserverProvider = Provider<TransitionObserver>((ref) {
  return TransitionObserver(ref);
});

そして、App クラスでRouterの設定を以下のように修正します:

class App extends ConsumerWidget {
  App({super.key});

  final LoginInfo loginInfo = LoginInfo();
  static const String title = 'GoRouter Example: Named Routes';

  
  Widget build(BuildContext context, WidgetRef ref) {
    final observer = ref.watch(transitionObserverProvider);
    
    return ChangeNotifierProvider<LoginInfo>.value(
      value: loginInfo,
      child: MaterialApp.router(
        routerConfig: _configureRouter(observer),
        title: title,
        debugShowCheckedModeBanner: false,
      ),
    );
  }

  GoRouter _configureRouter(TransitionObserver observer) {
    return GoRouter(
      debugLogDiagnostics: true,
      observers: [observer],
      routes: $appRoutes,
      redirect: (BuildContext context, GoRouterState state) {
        // 既存のリダイレクトロジック
        ...
      },
      refreshListenable: loginInfo,
    );
  }
}

主な変更点:

  1. TransitionObserverにRiverpodのRefを注入してdateTimeProviderを更新できるようにしました
  2. HomeRoute().locationを使用して正確なパスマッチングを実現
  3. AppをConsumerWidgetに変更し、TransitionObserverプロバイダーを注入
  4. Routerの設定を別メソッドに分離して可読性を向上

この実装により:

  • 型安全なルート管理
  • 適切な状態管理
  • クリーンな依存性注入
    が実現できます。
kabikirakabikira

以下に、Riverpod を適用しながら TransitionObserver を正しく適用するように コード1(TransitionObserver)とコード2(FirstBranch / SecondBranch)を修正 しました。

修正後のコード1: TransitionObserver

修正点

•	FirebaseAnalytics に加えて、Riverpod の dateTimeProvider を更新する処理を追加
•	NavigatorObserver の didPop で現在の日時を更新し、dateTimeProvider を通じて UI に反映させる

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:intl/intl.dart';

enum AppTransitionType {
push,
pop,
}

// 現在の日時を保持するプロバイダー
final dateTimeProvider = StateProvider<String>((ref) {
return _getCurrentTime();
});

String _getCurrentTime() {
return DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
}

// TransitionObserver のプロバイダー
final transitionObserverProvider = Provider<TransitionObserver>((ref) {
return TransitionObserver(ref);
});

class TransitionObserver extends NavigatorObserver {
final Ref ref;
static FirebaseAnalytics analytics = FirebaseAnalytics.instance;

TransitionObserver(this.ref);

@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
_onTransition(route, previousRoute, AppTransitionType.push);
}

@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPop(route, previousRoute);
ref.read(dateTimeProvider.notifier).state = _getCurrentTime(); // 日時を更新
_onTransition(route, previousRoute, AppTransitionType.pop);
}

void _onTransition(
Route<dynamic> route,
Route<dynamic>? previousRoute,
AppTransitionType transitionType,
) async {
final pageName = route.settings.name ?? 'unknown';
debugPrint('pageName: $pageName, ${transitionType.name}');
await analytics.logEvent(name: 'page_transition', parameters: {
'page_name': pageName,
'transition_type': transitionType.name,
});
}
}

修正後のコード2: FirstBranch / SecondBranch

修正点

•	static final List<NavigatorObserver> を getter に変更
•	ProviderScope.containerOf($navigatorKey.currentContext!).read(transitionObserverProvider) を使用して TransitionObserver を取得

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// bottom tab1 branch
class FirstBranch extends StatefulShellBranchData {
const FirstBranch();

static final GlobalKey<NavigatorState> $navigatorKey = firstNavigatorKey;

@override
List<NavigatorObserver> get observers => [
ProviderScope.containerOf($navigatorKey.currentContext!).read(transitionObserverProvider),
];
}

/// bottom tab2 branch
class SecoundBranch extends StatefulShellBranchData {
const SecoundBranch();

static final GlobalKey<NavigatorState> $navigatorKey = secoundNavigatorKey;

@override
List<NavigatorObserver> get observers => [
ProviderScope.containerOf($navigatorKey.currentContext!).read(transitionObserverProvider),
];
}

追加: HomeScreen 側での UI 更新

この変更によって、didPop が実行された際に dateTimeProvider の値が更新されます。
そのため、HomeScreen などの ConsumerWidget で ref.watch(dateTimeProvider) を使えば、画面上に更新された日時を表示できます。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class HomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentTime = ref.watch(dateTimeProvider);

return Scaffold(
  appBar: AppBar(title: Text("Home")),
  body: Center(
    child: Text(
      "Last Popped: $currentTime",
      style: TextStyle(fontSize: 18),
    ),
  ),
);

}
}

まとめ

修正前の問題点

❌ TransitionObserver() のインスタンスを静的リスト static final List<NavigatorObserver> で管理していたため、
  Riverpod の ref を渡せなかった
❌ StatefulShellBranchData の $observers を 静的に定義 していたため、
  TransitionObserver を Provider から取得できなかった

修正後の改善点

✅ TransitionObserver を Provider で管理 することで ref を取得できるように修正
✅ $observers を getter に変更 し、 ProviderScope.containerOf() を使って 動的に TransitionObserver を取得
✅ didPop で dateTimeProvider を更新し、 UI(HomeScreen)で ref.watch() を使って日時を反映

これで Riverpod を活用しながら TransitionObserver を適用する 方法が 正しく動作する形 になっています! 🚀🎉

kabikirakabikira

フォアグラウンドでプッシュ通知を受け取ったときに、ダイアログのような通知を表示し、それをタップした際の処理を取得する方法はいくつかあります。

方法 1: Flutter Local Notifications を使用

Flutter の flutter_local_notifications パッケージを使えば、フォアグラウンド時の通知をシステム通知として表示し、それをタップしたときのイベントを処理できます。

手順

1.	flutter_local_notifications をインストール

flutter pub add flutter_local_notifications

2.	プッシュ通知を受信したときにローカル通知を表示

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();

void setupForegroundNotification() {
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
print("フォアグラウンド通知: ${message.notification?.title}");

// ローカル通知を表示
showLocalNotification(message);

});
}

Future<void> showLocalNotification(RemoteMessage message) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'high_importance_channel', // 任意のチャンネルID
'High Importance Notifications',
importance: Importance.high,
priority: Priority.high,
);

const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);

await flutterLocalNotificationsPlugin.show(
0, // 通知ID
message.notification?.title,
message.notification?.body,
platformChannelSpecifics,
payload: message.data.toString(), // ここにデータを入れる
);
}

3.	通知をタップした際の処理

void setupNotificationTapHandler() {
flutterLocalNotificationsPlugin.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
),
onSelectNotification: (String? payload) async {
if (payload != null) {
print("ローカル通知をタップ: $payload");
// ここで画面遷移を行う
}
},
);
}

4.	initState でセットアップ

@override
void initState() {
super.initState();
setupForegroundNotification();
setupNotificationTapHandler();
}

方法 2: SnackBar や AlertDialog を使う

Flutter の Snackbar や AlertDialog を使って通知を表示し、タップ時のイベントを取得する方法もあります。

void setupForegroundNotification(BuildContext context) {
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print("フォアグラウンド通知: ${message.notification?.title}");

// スナックバーで通知
ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text(message.notification?.body ?? "通知が届きました"),
    action: SnackBarAction(
      label: "開く",
      onPressed: () {
        print("スナックバーをタップ");
        // ここで画面遷移を行う
      },
    ),
  ),
);

});
}

この方法のポイント

•	ScaffoldMessenger.of(context).showSnackBar() を使ってスナックバーを表示
•	onPressed で通知タップ時のイベントを処理

結論

方法 通知の表示方法 タップ時の処理取得
flutter_local_notifications を使う システムの通知領域に表示 onSelectNotification を使う
Snackbar や AlertDialog を使う 画面上に Snackbar / ダイアログを表示 onPressed で取得

フォアグラウンドでプッシュ通知を処理し、タップ時の処理を取得するには、flutter_local_notifications を使う方法が最も自然 ですが、アプリ内での通知表示にこだわる場合は Snackbar でも可能 です。

kabikirakabikira

フォアグラウンドでプッシュ通知を受け取ったときに、ダイアログのような通知を表示し、それをタップした際の処理を取得する方法はいくつかあります。

方法 1: Flutter Local Notifications を使用

Flutter の flutter_local_notifications パッケージを使えば、フォアグラウンド時の通知をシステム通知として表示し、それをタップしたときのイベントを処理できます。

手順

1.	flutter_local_notifications をインストール

flutter pub add flutter_local_notifications

2.	プッシュ通知を受信したときにローカル通知を表示

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();

void setupForegroundNotification() {
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
print("フォアグラウンド通知: ${message.notification?.title}");

// ローカル通知を表示
showLocalNotification(message);

});
}

Future<void> showLocalNotification(RemoteMessage message) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'high_importance_channel', // 任意のチャンネルID
'High Importance Notifications',
importance: Importance.high,
priority: Priority.high,
);

const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);

await flutterLocalNotificationsPlugin.show(
0, // 通知ID
message.notification?.title,
message.notification?.body,
platformChannelSpecifics,
payload: message.data.toString(), // ここにデータを入れる
);
}

3.	通知をタップした際の処理

void setupNotificationTapHandler() {
flutterLocalNotificationsPlugin.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
),
onSelectNotification: (String? payload) async {
if (payload != null) {
print("ローカル通知をタップ: $payload");
// ここで画面遷移を行う
}
},
);
}

4.	initState でセットアップ

@override
void initState() {
super.initState();
setupForegroundNotification();
setupNotificationTapHandler();
}

方法 2: SnackBar や AlertDialog を使う

Flutter の Snackbar や AlertDialog を使って通知を表示し、タップ時のイベントを取得する方法もあります。

void setupForegroundNotification(BuildContext context) {
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print("フォアグラウンド通知: ${message.notification?.title}");

// スナックバーで通知
ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text(message.notification?.body ?? "通知が届きました"),
    action: SnackBarAction(
      label: "開く",
      onPressed: () {
        print("スナックバーをタップ");
        // ここで画面遷移を行う
      },
    ),
  ),
);

});
}

この方法のポイント

•	ScaffoldMessenger.of(context).showSnackBar() を使ってスナックバーを表示
•	onPressed で通知タップ時のイベントを処理

結論

方法 通知の表示方法 タップ時の処理取得
flutter_local_notifications を使う システムの通知領域に表示 onSelectNotification を使う
Snackbar や AlertDialog を使う 画面上に Snackbar / ダイアログを表示 onPressed で取得

フォアグラウンドでプッシュ通知を処理し、タップ時の処理を取得するには、flutter_local_notifications を使う方法が最も自然 ですが、アプリ内での通知表示にこだわる場合は Snackbar でも可能 です。

kabikirakabikira

go_routerとShellRouteを使っている場合、プッシュ通知から直接ディープリンク的に/test/a/bに飛ぶと、スタック構造が/home → /test/a/bとなってしまい、pop()で/test/aに戻れない状態ですね。

解決アプローチ

go_routerのroutes定義とpushの使い方を調整することで対応できます。以下のポイントを抑えます:

  1. goRouter.push()ではなくgoRouter.go()を使用する

push()は新しいスタックを作るので/homeの後に/test/a/bが積まれますが、go()なら指定のルートまで直接遷移します。

  1. initialLocationとShellRouteの定義を確認する

/test/a/bに飛ぶ際、親の/test/aを経由してナビゲーションスタックを構築する必要があります。

例:go_routerの設定例

final GoRouter router = GoRouter(
initialLocation: '/home',
routes: [
ShellRoute(
builder: (context, state, child) {
return MainShell(child: child);
},
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/test/a',
builder: (context, state) => const TestAScreen(),
routes: [
GoRoute(
path: 'b',
builder: (context, state) => const TestBScreen(),
),
],
),
],
),
],
);

プッシュ通知タップ時の遷移処理

onMessageOpenedAppでgo()を使用してスタックを正しく構成します:

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
if (message.data['route'] == '/test/a/b') {
GoRouter.of(context).go('/test/a/b');
}
});

結果

•	/test/a/bに遷移時に/test/aがスタックされます。
•	戻るボタンで/test/aに戻れます。

このアプローチで希望通りの動作になると思います!試してみてください。

kabikirakabikira

その挙動は、ShellRouteを使用しているため、/test/a/bが正しいスタックに載っているように見えても、実際には/homeのタブ内スタックの一部として処理されてしまっていることが原因です。

ShellRouteは、複数のタブを管理するため、直接/test/a/bに飛んだ場合、ホームタブ内の履歴に紐付けられてしまい、結果としてpop()で/homeに戻ってしまいます。

解決アプローチ

✅ 1. goRouter.go()ではなく、goRouter.push()で正しい親階層に遷移させる

push()を使って/test/a → /test/a/bというスタックを作成し、ShellRoute内で正しいヒエラルキーを維持するようにします。

✅ 2. extraを活用して、ShellRouteを横断した場合は明示的に親画面に戻す処理を行う

🌟 実装例

go_routerの設定

final GoRouter router = GoRouter(
initialLocation: '/home',
routes: [
ShellRoute(
builder: (context, state, child) {
return MainShell(child: child);
},
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/test/a',
builder: (context, state) => const TestAScreen(),
routes: [
GoRoute(
path: 'b',
builder: (context, state) => TestBScreen(
fromNotification: state.extra == 'notification',
),
),
],
),
],
),
],
);

📲 プッシュ通知タップ時のハンドラー

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
final router = GoRouter.of(context);
if (message.data['route'] == '/test/a/b') {
router.push('/test/a');
router.push('/test/a/b', extra: 'notification');
}
});

⬅️ 戻る処理 (TestBScreen)

class TestBScreen extends StatelessWidget {
final bool fromNotification;

const TestBScreen({super.key, this.fromNotification = false});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test B Screen'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (fromNotification) {
GoRouter.of(context).go('/test/a');
} else {
context.pop();
}
},
),
),
body: const Center(child: Text('This is Test B')),
);
}
}

📝 解説

•	router.push('/test/a')で/test/aを親階層として先に積む
•	次にrouter.push('/test/a/b', extra: 'notification')で/test/aの子として/test/a/bを積む
•	/test/a/bから戻る際、extraフラグがあればgo('/test/a')して親階層に正しく戻す

🏁 結果

•	プッシュ通知タップで/homeタブを飛び越えて/test/a/bへ
•	戻るボタンで/test/aに戻り、/homeに戻らない動作を実現

これでShellRouteを使ったタブ内でも正しくスタックが管理できるはずです!✨

kabikirakabikira

ShellRoute でのタブナビゲーションはスタック管理が独特なので、ヘッダーの戻るボタン (AppBarの自動的なナビゲーション) が期待通りに動かない原因は、ルートヒエラルキーが ShellRoute の配下に組み込まれていないことが問題です。

🚩 問題の原因

•	ShellRoute はタブごとに独立したナビゲーションスタックを管理します。
•	プッシュ通知から直接 /test/a/b に飛ぶと、/home タブ上に無理やりスタックが積まれた形になり、戻ると /home に戻ってしまいます。

✅ 解決アプローチ

1.	goRouter.push()で親ルートを明示的に積む (すでに試しているようですが、階層管理を補強します)
2.	Navigatorウィジェットを ShellRoute 内に独立して配置 することで、各タブの履歴を分離管理する
3.	pop()時の戻り先を制御する onPopPage コールバックを実装 する

🛠️ 解決方法: Navigator を各タブに導入する (ShellRouteの中にスタックを持つ)

1️⃣ ShellRoute の構造調整

final GoRouter router = GoRouter(
initialLocation: '/home',
routes: [
ShellRoute(
builder: (context, state, child) {
return MainShell(child: child);
},
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/test',
builder: (context, state) => TestNavigator(),
),
],
),
],
);

2️⃣ /test 用の独立 Navigator ウィジェット

class TestNavigator extends StatelessWidget {
const TestNavigator({super.key});

@override
Widget build(BuildContext context) {
return Navigator(
initialRoute: '/test/a',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/test/a':
return MaterialPageRoute(
builder: () => const TestAScreen(),
);
case '/test/a/b':
return MaterialPageRoute(
builder: (
) => const TestBScreen(),
);
default:
return MaterialPageRoute(
builder: (_) => const TestAScreen(),
);
}
},
);
}
}

3️⃣ プッシュ通知タップ時の遷移処理 (onMessageOpenedApp)

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
final router = GoRouter.of(context);
if (message.data['route'] == '/test/a/b') {
router.go('/test'); // まず親に移動
Navigator.of(context).pushNamed('/test/a/b'); // 次に子へ
}
});

4️⃣ TestBScreen の戻るボタン動作制御

class TestBScreen extends StatelessWidget {
const TestBScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test B Screen'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop(); // これで /test/a に戻る
},
),
),
body: const Center(child: Text('This is Test B')),
);
}
}

📝 解説

•	ShellRoute内でタブごとにNavigatorを分離することで、

/test/a/b → /test/a → /test というスタックを保持できます。
• プッシュ通知からの遷移も、/home を経由せずに /test/a/b へ直接飛べます。
• ヘッダーの戻るボタン (AppBar の leading) も、期待通りのナビゲーションを実現します。

🏁 結果

•	プッシュ通知タップで /test/a/b に直接遷移
•	戻るボタンで /test/a に正しく戻る
•	/home に戻る問題が解消

これでShellRouteのタブ内スタックを正しく構築できます。ぜひお試しください!🔥🚀

kabikirakabikira

go_router での .go().push() の違いは、画面遷移時の履歴スタックの扱い にあります。


🎯 簡単まとめ

メソッド 動作 使うタイミング
.go() スタックをリセットして遷移(履歴が消える) 新しいトップ画面に移動する時、例えば「ホーム」や「ログイン画面」への遷移
.push() スタックにページを積んで遷移(履歴が残る) 詳細画面やモーダルを開く時、前の画面に戻れるようにする時

📌 詳しい解説

1️⃣ context.go():スタックをリセットして遷移

  • 現在の履歴スタックをクリアして、新しいページに移動します。
  • ブラウザのURL変更時のように、戻るボタンで前の画面に戻れません
// スタックをリセットして /home へ移動
context.go('/home');

📝 例:

  • ログイン後にホーム画面に移動する場合
  • スプラッシュ画面 → メイン画面への遷移

2️⃣ context.push():スタックにページを積んで遷移

  • 現在の履歴スタックに新しいページを追加します。
  • Navigator.pop()元の画面に戻れる 仕様です。
// スタックに積んで /details へ移動
context.push('/details');

📝 例:

  • 商品一覧から商品詳細画面に遷移する場合
  • プロフィール画面から設定画面に遷移する場合

💡 実行イメージ

// ホーム → 商品一覧 → 商品詳細
context.go('/home');        // スタック: [Home]
context.push('/products');  // スタック: [Home, Products]
context.push('/details');   // スタック: [Home, Products, Details]

Navigator.of(context).pop(); // スタック: [Home, Products] に戻る

📝 push()go()を使い分けるポイント

シナリオ go() push()
ホームやダッシュボードへの遷移
ログイン後のメイン画面への遷移
一覧 → 詳細 → 詳細2と進む場合
設定画面や詳細画面を開く場合

🦉 まとめ

  • go()履歴を捨てる:トップ画面への遷移など
  • push()履歴を積む:詳細画面や設定画面など

これで go_router の遷移がしっかり理解できると思います!🚀✨
何かサンプルコードが欲しければ気軽に言ってね!😊💙

kabikirakabikira

はい、その認識で正しいです!✨

await を使うことで、第1の処理が終わるまで次の処理は実行されません。 具体的には:

🔑 ポイント:

  • await非同期処理が終わるのを待つ キーワードです。
  • その後の処理は、await の完了後に順次実行 されます。

📌 実行順を解説:

Future<void> fetchData() async {
  // 第1の処理 (2秒待機)
  await Future.delayed(Duration(seconds: 2)); 
  print('処理1が終わった');

  // 第2の処理
  print('処理2を実行');
}

🛎️ 実行結果(出力順):

処理1が終わった
処理2を実行

なぜこの順番になるのか?

  • await Future.delayed():2秒間「この処理が終わるまで待つ」
  • 2秒後、次の行に進み print('処理1が終わった') を実行
  • その後、print('処理2を実行') を実行

⚠️ もしawaitをつけなかった場合:

await を外すと、非同期処理は待たずにすぐ次の処理が走る ため、順序が崩れます。

Future<void> fetchData() async {
  // awaitなし
  Future.delayed(Duration(seconds: 2));
  print('処理1が終わった');

  print('処理2を実行');
}

🚨 実行結果(順序が崩れる):

処理1が終わった
処理2を実行
(2秒後に非同期処理が完了するが無視される)

💡 まとめ:

  • awaitあり:処理が順番通りに実行される
  • 🚫 awaitなし:順序が崩れる(並列実行)

もし何か他にもわからないことがあれば教えてね!😊🚀