Open9

riverpodで初学者が躓くポイント(Claude出力)

muranakarmuranakar

Riverpodで初学者がよく陥る実装の落とし穴と解決方法

1. Providerの過剰な作成

❌ よくある間違い

// 各状態ごとに個別のProviderを作成
final nameProvider = StateProvider((ref) => '');
final ageProvider = StateProvider((ref) => 0);
final addressProvider = StateProvider((ref) => '');

// これらを使用するWidget
class UserProfileWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final name = ref.watch(nameProvider);
    final age = ref.watch(ageProvider);
    final address = ref.watch(addressProvider);
    
    return Column(
      children: [
        Text('Name: $name'),
        Text('Age: $age'),
        Text('Address: $address'),
      ],
    );
  }
}

✅ 推奨される実装

// ユーザー情報を一つのクラスにまとめる
class UserState {
  final String name;
  final int age;
  final String address;

  UserState({
    required this.name,
    required this.age,
    required this.address,
  });

  UserState copyWith({
    String? name,
    int? age,
    String? address,
  }) {
    return UserState(
      name: name ?? this.name,
      age: age ?? this.age,
      address: address ?? this.address,
    );
  }
}

// 一つのProviderにまとめる
final userProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
  return UserNotifier();
});

class UserNotifier extends StateNotifier<UserState> {
  UserNotifier()
      : super(UserState(name: '', age: 0, address: ''));
  
  void updateName(String name) {
    state = state.copyWith(name: name);
  }
  
  void updateAge(int age) {
    state = state.copyWith(age: age);
  }
  
  void updateAddress(String address) {
    state = state.copyWith(address: address);
  }
}

2. watchとreadの誤用

❌ よくある間違い

class ProductListWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // buildメソッド内でreadを使用
    final products = ref.read(productsProvider);
    
    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        return Text(products[index].name);
      },
    );
  }
}

✅ 推奨される実装

class ProductListWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // watchを使用して値の変更を監視
    final products = ref.watch(productsProvider);
    
    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        return Text(products[index].name);
      },
    );
  }
}

// ボタンクリックなどのイベントハンドラでは read を使用
void onButtonPressed(WidgetRef ref) {
  // readを使用して値を一度だけ取得
  final products = ref.read(productsProvider.notifier).addProduct();
}

3. 非同期処理の不適切な実装

❌ よくある間違い

final dataProvider = StateProvider<List<String>>((ref) => []);

class DataWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final data = ref.watch(dataProvider);
    
    return FutureBuilder(
      future: fetchData(), // 直接Future を使用
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return ListView.builder(
            itemCount: data.length,
            itemBuilder: (context, index) {
              return Text(data[index]);
            },
          );
        }
        return CircularProgressIndicator();
      },
    );
  }
}

✅ 推奨される実装

final dataProvider = FutureProvider<List<String>>((ref) async {
  // 非同期処理をProvider内で実装
  return await fetchData();
});

class DataWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncData = ref.watch(dataProvider);
    
    return asyncData.when(
      data: (data) {
        return ListView.builder(
          itemCount: data.length,
          itemBuilder: (context, index) {
            return Text(data[index]);
          },
        );
      },
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

4. 依存関係の誤った管理

❌ よくある間違い

final userProvider = StateProvider<User>((ref) => User());
final cartProvider = StateProvider<Cart>((ref) {
  // 直接別のProviderの値を参照
  final user = ref.read(userProvider);
  return Cart(userId: user.id);
});

✅ 推奨される実装

final userProvider = StateProvider<User>((ref) => User());
final cartProvider = StateProvider<Cart>((ref) {
  // watchを使用して依存関係を適切に管理
  final user = ref.watch(userProvider);
  return Cart(userId: user.id);
});

5. dispose処理の忘れ

❌ よくある間違い

final streamProvider = StreamProvider<int>((ref) {
  final controller = StreamController<int>();
  // disposeの処理を忘れている
  return controller.stream;
});

✅ 推奨される実装

final streamProvider = StreamProvider<int>((ref) {
  final controller = StreamController<int>();
  
  ref.onDispose(() {
    // 適切にリソースを解放
    controller.close();
  });
  
  return controller.stream;
});

まとめ

  1. 状態管理は適切な粒度でまとめる
  2. watchとreadは用途に応じて使い分ける
  3. 非同期処理はFutureProvider/StreamProviderを活用する
  4. Provider間の依存関係は適切に管理する
  5. リソースの解放を忘れない

これらのポイントを意識することで、より保守性が高く、パフォーマンスの良いアプリケーションを開発することができます。

muranakarmuranakar

Riverpodで初学者がよく陥る実装の落とし穴と解決方法 パート2

1. ConsumerWidgetの過剰な使用

❌ よくある間違い

// 全てのWidgetをConsumerWidgetにしてしまう
class ParentWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(
      children: [
        ChildWidget1(),
        ChildWidget2(),
        ChildWidget3(),
      ],
    );
  }
}

class ChildWidget1 extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    return Text('Child 1');
  }
}

class ChildWidget2 extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    return Text('Child 2');
  }
}

✅ 推奨される実装

// 必要なWidgetのみConsumerWidgetにする
class ParentWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        ChildWidget1(),
        ChildWidget2(),
        StateConsumingWidget(),
      ],
    );
  }
}

// 状態を使用するWidgetのみConsumerWidgetを使用
class StateConsumingWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(someProvider);
    return Text('State: $state');
  }
}

2. select修飾子の未使用

❌ よくある間違い

class UserProfileWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // 必要のない再ビルドが発生する
    final user = ref.watch(userProvider);
    
    return Text(user.name); // 名前だけを表示したいのに全状態を監視
  }
}

✅ 推奨される実装

class UserProfileWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // 必要な値のみを監視
    final userName = ref.watch(userProvider.select((user) => user.name));
    
    return Text(userName);
  }
}

3. StateNotifierの状態更新の誤り

❌ よくある間違い

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() {
    // 直接状態を変更
    state++;
  }

  void updateValue(int newValue) {
    if (state == newValue) {
      state = newValue; // 同じ値でも更新してしまう
    }
  }
}

✅ 推奨される実装

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() {
    state = state + 1; // 新しい値を代入
  }

  void updateValue(int newValue) {
    if (state != newValue) {
      state = newValue; // 値が異なる場合のみ更新
    }
  }
}

4. autoDisposeの適切な使用

❌ よくある間違い

// 必要のないautoDisposeの使用
final constantDataProvider = Provider.autoDispose<String>((ref) {
  return 'Constant Value'; // 定数値なのにautoDisposeを使用
});

// autoDisposeが必要な場面で使用していない
final searchResultProvider = FutureProvider<List<String>>((ref) async {
  return await searchAPI();
});

✅ 推奨される実装

// 定数値の場合は通常のProvider
final constantDataProvider = Provider<String>((ref) {
  return 'Constant Value';
});

// 検索結果など一時的なデータにはautoDisposeを使用
final searchResultProvider = FutureProvider.autoDispose<List<String>>((ref) async {
  return await searchAPI();
});

5. family修飾子の誤った使用方法

❌ よくある間違い

// パラメータごとに新しいインスタンスが作られる
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
  return await fetchUser(userId);
});

class UserWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // 毎回新しいインスタンスを生成
    final user = ref.watch(userProvider('123'));
    
    return user.when(
      data: (data) => Text(data.name),
      loading: () => CircularProgressIndicator(),
      error: (e, s) => Text('Error'),
    );
  }
}

✅ 推奨される実装

// パラメータを状態として管理
final selectedUserIdProvider = StateProvider<String?>((ref) => null);

final userProvider = FutureProvider<User?>((ref) async {
  final userId = ref.watch(selectedUserIdProvider);
  if (userId == null) return null;
  return await fetchUser(userId);
});

class UserWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProvider);
    
    return user.when(
      data: (data) => data != null ? Text(data.name) : Text('Select user'),
      loading: () => CircularProgressIndicator(),
      error: (e, s) => Text('Error'),
    );
  }
}

6. エラーハンドリングの不適切な実装

❌ よくある間違い

final dataProvider = FutureProvider<Data>((ref) async {
  try {
    return await fetchData();
  } catch (e) {
    print(e); // エラーをログ出力のみ
    return Data.empty(); // エラーを隠蔽
  }
});

✅ 推奨される実装

final dataProvider = FutureProvider<Data>((ref) async {
  try {
    return await fetchData();
  } catch (e, stackTrace) {
    // エラーを適切に処理
    ref.read(errorLoggerProvider).logError(e, stackTrace);
    throw Exception('Failed to fetch data: $e');
  }
});

class DataWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final data = ref.watch(dataProvider);
    
    return data.when(
      data: (data) => DataView(data),
      loading: () => LoadingWidget(),
      error: (error, stackTrace) => ErrorWidget(
        error: error,
        onRetry: () => ref.refresh(dataProvider),
      ),
    );
  }
}

まとめ

  1. ConsumerWidgetは必要な場所でのみ使用する
  2. selectを使用して必要な状態のみを監視する
  3. StateNotifierでの状態更新は適切に行う
  4. autoDisposeは必要な場合のみ使用する
  5. family修飾子は適切なユースケースで使用する
  6. エラーハンドリングは適切に実装する

これらのベストプラクティスを意識することで、より効率的で保守性の高いアプリケーションを開発することができます。

muranakarmuranakar

Riverpodで初学者がよく陥る実装の落とし穴と解決方法 パート3

1. UIとビジネスロジックの混在

❌ よくある間違い

final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
  return CartNotifier();
});

class CartNotifier extends StateNotifier<List<CartItem>> {
  CartNotifier() : super([]);
  
  void addItem(CartItem item) {
    state = [...state, item];
    // UIに関する処理を含めてしまう
    showToast('商品をカートに追加しました');
    navigateToCart();
  }
}

class CartScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final cart = ref.watch(cartProvider);
    
    return Scaffold(
      body: ListView.builder(
        itemCount: cart.length,
        itemBuilder: (context, index) {
          final item = cart[index];
          return ListTile(
            title: Text(item.name),
            trailing: IconButton(
              icon: Icon(Icons.delete),
              onPressed: () {
                // Widgetで直接ビジネスロジックを実行
                ref.read(cartProvider.notifier).removeItem(item);
                calculateTotalPrice();
                updateInventory();
              },
            ),
          );
        },
      ),
    );
  }
}

✅ 推奨される実装

// ビジネスロジックを担当するService層
class CartService {
  Future<void> addItemToCart(CartItem item) async {
    await updateInventory(item);
    await saveToDatabase(item);
    return item;
  }
  
  Future<void> removeItemFromCart(CartItem item) async {
    await updateInventory(item, isRemoval: true);
    await removeFromDatabase(item);
  }
}

// UIの状態管理を担当するProvider
final cartServiceProvider = Provider((ref) => CartService());

final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
  return CartNotifier(ref.read(cartServiceProvider));
});

class CartNotifier extends StateNotifier<List<CartItem>> {
  final CartService _cartService;
  
  CartNotifier(this._cartService) : super([]);
  
  Future<void> addItem(CartItem item) async {
    await _cartService.addItemToCart(item);
    state = [...state, item];
  }
  
  Future<void> removeItem(CartItem item) async {
    await _cartService.removeItemFromCart(item);
    state = state.where((i) => i.id != item.id).toList();
  }
}

// UIレイヤー
class CartScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final cart = ref.watch(cartProvider);
    
    return Scaffold(
      body: ListView.builder(
        itemCount: cart.length,
        itemBuilder: (context, index) {
          final item = cart[index];
          return ListTile(
            title: Text(item.name),
            trailing: IconButton(
              icon: Icon(Icons.delete),
              onPressed: () async {
                try {
                  await ref.read(cartProvider.notifier).removeItem(item);
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('商品を削除しました')),
                  );
                } catch (e) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('エラーが発生しました')),
                  );
                }
              },
            ),
          );
        },
      ),
    );
  }
}

2. 非同期状態の不適切な管理

❌ よくある間違い

final isLoadingProvider = StateProvider<bool>((ref) => false);
final errorProvider = StateProvider<String?>((ref) => null);
final dataProvider = StateProvider<List<Data>>((ref) => []);

class DataNotifier extends StateNotifier<List<Data>> {
  DataNotifier() : super([]);
  
  Future<void> fetchData() async {
    ref.read(isLoadingProvider.notifier).state = true;
    ref.read(errorProvider.notifier).state = null;
    
    try {
      final result = await api.fetchData();
      ref.read(dataProvider.notifier).state = result;
    } catch (e) {
      ref.read(errorProvider.notifier).state = e.toString();
    } finally {
      ref.read(isLoadingProvider.notifier).state = false;
    }
  }
}

✅ 推奨される実装

// 非同期状態を表現するクラス
class AsyncState<T> {
  final T? data;
  final bool isLoading;
  final String? error;

  const AsyncState({
    this.data,
    this.isLoading = false,
    this.error,
  });

  AsyncState<T> copyWith({
    T? data,
    bool? isLoading,
    String? error,
  }) {
    return AsyncState<T>(
      data: data ?? this.data,
      isLoading: isLoading ?? this.isLoading,
      error: error ?? this.error,
    );
  }
}

// 非同期状態を管理するProvider
final dataProvider = StateNotifierProvider<DataNotifier, AsyncState<List<Data>>>((ref) {
  return DataNotifier();
});

class DataNotifier extends StateNotifier<AsyncState<List<Data>>> {
  DataNotifier() : super(AsyncState());
  
  Future<void> fetchData() async {
    state = state.copyWith(isLoading: true, error: null);
    
    try {
      final result = await api.fetchData();
      state = state.copyWith(
        data: result,
        isLoading: false,
      );
    } catch (e) {
      state = state.copyWith(
        error: e.toString(),
        isLoading: false,
      );
    }
  }
}

// UI実装
class DataScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncState = ref.watch(dataProvider);
    
    if (asyncState.isLoading) {
      return CircularProgressIndicator();
    }
    
    if (asyncState.error != null) {
      return ErrorWidget(
        message: asyncState.error!,
        onRetry: () => ref.read(dataProvider.notifier).fetchData(),
      );
    }
    
    final data = asyncState.data;
    if (data == null || data.isEmpty) {
      return EmptyStateWidget();
    }
    
    return ListView.builder(
      itemCount: data.length,
      itemBuilder: (context, index) => DataItem(data: data[index]),
    );
  }
}

3. キャッシュ制御の不適切な実装

❌ よくある間違い

// キャッシュ制御がない実装
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
  return await api.fetchUser(userId);
});

✅ 推奨される実装

// キャッシュを制御するProvider
final userCacheProvider = StateProvider<Map<String, User>>((ref) => {});

final userProvider = FutureProvider.family<User, String>((ref, userId) async {
  // キャッシュをチェック
  final cache = ref.watch(userCacheProvider);
  if (cache.containsKey(userId)) {
    return cache[userId]!;
  }
  
  // キャッシュがない場合はAPIから取得
  final user = await api.fetchUser(userId);
  
  // キャッシュを更新
  ref.read(userCacheProvider.notifier).state = {
    ...cache,
    userId: user,
  };
  
  return user;
});

// キャッシュを制御するメソッド
void clearUserCache(WidgetRef ref) {
  ref.read(userCacheProvider.notifier).state = {};
}

void removeUserFromCache(WidgetRef ref, String userId) {
  final cache = ref.read(userCacheProvider);
  final newCache = Map<String, User>.from(cache)..remove(userId);
  ref.read(userCacheProvider.notifier).state = newCache;
}

4. Providerの依存関係の循環参照

❌ よくある間違い

// 循環参照を引き起こす実装
final providerA = Provider((ref) {
  final valueB = ref.watch(providerB);
  return 'A: $valueB';
});

final providerB = Provider((ref) {
  final valueA = ref.watch(providerA);
  return 'B: $valueA';
});

✅ 推奨される実装

// 共通の状態を持つProviderを作成
final sharedStateProvider = StateProvider<String>((ref) => '');

// 依存関係を明確にした実装
final providerA = Provider((ref) {
  final sharedState = ref.watch(sharedStateProvider);
  return 'A: $sharedState';
});

final providerB = Provider((ref) {
  final sharedState = ref.watch(sharedStateProvider);
  return 'B: $sharedState';
});

// または、一方向の依存関係にする
final baseProvider = StateProvider<String>((ref) => 'base');

final derivedProviderA = Provider((ref) {
  final base = ref.watch(baseProvider);
  return 'A: $base';
});

final derivedProviderB = Provider((ref) {
  final derivedA = ref.watch(derivedProviderA);
  return 'B: $derivedA';
});

まとめ

  1. UIとビジネスロジックは適切に分離する

    • Service層を導入してビジネスロジックを分離
    • UIの状態管理とビジネスロジックを明確に分ける
  2. 非同期状態は統合的に管理する

    • 非同期状態を表現する専用のクラスを作成
    • loading/error/dataの状態を一元管理
  3. キャッシュは適切に制御する

    • キャッシュの有効期限を設定
    • メモリ使用量を考慮した実装
    • キャッシュのクリア機能を提供
  4. Provider間の依存関係は慎重に設計する

    • 循環参照を避ける
    • 依存関係を明確にする
    • 共通の状態を適切に管理する
muranakarmuranakar

Riverpodで初学者がよく陥る実装の落とし穴と解決方法 パート4

1. リフレッシュ処理の不適切な実装

❌ よくある間違い

class HomeScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final data = ref.watch(dataProvider);
    
    return RefreshIndicator(
      onRefresh: () async {
        // 直接Providerを再作成
        ref.refresh(dataProvider);
        // 他の関連Providerの更新を忘れている
      },
      child: ListView(
        children: [
          for (final item in data) ListTile(title: Text(item.title))
        ],
      ),
    );
  }
}

✅ 推奨される実装

// リフレッシュ用のProviderグループを作成
final refreshNotifierProvider = StateNotifierProvider<RefreshNotifier, void>((ref) {
  return RefreshNotifier(ref);
});

class RefreshNotifier extends StateNotifier<void> {
  final Ref ref;
  
  RefreshNotifier(this.ref) : super(null);
  
  Future<void> refresh() async {
    // 関連する全てのProviderを更新
    await Future.wait([
      ref.refresh(dataProvider.future),
      ref.refresh(userProvider.future),
      ref.refresh(settingsProvider.future),
    ]);
    
    // キャッシュのクリア
    ref.read(cacheProvider.notifier).clear();
    
    // 必要に応じて追加の処理
    await ref.read(analyticsProvider).logRefresh();
  }
}

class HomeScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final data = ref.watch(dataProvider);
    
    return RefreshIndicator(
      onRefresh: () => ref.read(refreshNotifierProvider.notifier).refresh(),
      child: data.when(
        data: (items) => ListView(
          children: [
            for (final item in items) ListTile(title: Text(item.title))
          ],
        ),
        loading: () => CircularProgressIndicator(),
        error: (error, stack) => ErrorWidget(error: error),
      ),
    );
  }
}

2. ページネーションの実装ミス

❌ よくある間違い

final pagedDataProvider = StateNotifierProvider<PagedDataNotifier, List<Item>>((ref) {
  return PagedDataNotifier();
});

class PagedDataNotifier extends StateNotifier<List<Item>> {
  int _page = 1;
  bool _isLoading = false;
  
  PagedDataNotifier() : super([]);
  
  Future<void> loadMore() async {
    if (_isLoading) return;
    _isLoading = true;
    
    try {
      final newItems = await api.fetchItems(page: _page);
      state = [...state, ...newItems];
      _page++;
    } finally {
      _isLoading = false;
    }
  }
}

✅ 推奨される実装

// ページネーションの状態を管理するクラス
class PaginationState<T> {
  final List<T> items;
  final bool isLoading;
  final String? error;
  final bool hasMoreItems;
  final int currentPage;

  PaginationState({
    required this.items,
    this.isLoading = false,
    this.error,
    this.hasMoreItems = true,
    this.currentPage = 1,
  });

  PaginationState<T> copyWith({
    List<T>? items,
    bool? isLoading,
    String? error,
    bool? hasMoreItems,
    int? currentPage,
  }) {
    return PaginationState<T>(
      items: items ?? this.items,
      isLoading: isLoading ?? this.isLoading,
      error: error,
      hasMoreItems: hasMoreItems ?? this.hasMoreItems,
      currentPage: currentPage ?? this.currentPage,
    );
  }
}

final pagedDataProvider = StateNotifierProvider<PagedDataNotifier, PaginationState<Item>>((ref) {
  return PagedDataNotifier();
});

class PagedDataNotifier extends StateNotifier<PaginationState<Item>> {
  static const int _pageSize = 20;
  
  PagedDataNotifier() : super(PaginationState(items: []));
  
  Future<void> loadMore() async {
    if (state.isLoading || !state.hasMoreItems) return;
    
    state = state.copyWith(isLoading: true, error: null);
    
    try {
      final newItems = await api.fetchItems(
        page: state.currentPage,
        pageSize: _pageSize,
      );
      
      final hasMore = newItems.length == _pageSize;
      
      state = state.copyWith(
        items: [...state.items, ...newItems],
        currentPage: state.currentPage + 1,
        hasMoreItems: hasMore,
        isLoading: false,
      );
    } catch (e) {
      state = state.copyWith(
        error: e.toString(),
        isLoading: false,
      );
    }
  }
  
  Future<void> refresh() async {
    state = PaginationState(items: []);
    await loadMore();
  }
}

// UI実装
class PagedListView extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(pagedDataProvider);
    
    return RefreshIndicator(
      onRefresh: () => ref.read(pagedDataProvider.notifier).refresh(),
      child: ListView.builder(
        itemCount: state.items.length + (state.hasMoreItems ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == state.items.length) {
            if (state.error != null) {
              return ErrorItem(
                error: state.error!,
                onRetry: () => ref.read(pagedDataProvider.notifier).loadMore(),
              );
            }
            
            ref.read(pagedDataProvider.notifier).loadMore();
            return LoadingItem();
          }
          
          return ItemWidget(item: state.items[index]);
        },
      ),
    );
  }
}

3. 状態の永続化の不適切な実装

❌ よくある間違い

final persistentDataProvider = StateNotifierProvider<PersistentDataNotifier, Map<String, dynamic>>((ref) {
  return PersistentDataNotifier();
});

class PersistentDataNotifier extends StateNotifier<Map<String, dynamic>> {
  PersistentDataNotifier() : super({}) {
    _loadData();
  }
  
  Future<void> _loadData() async {
    final prefs = await SharedPreferences.getInstance();
    final data = prefs.getString('data');
    if (data != null) {
      state = json.decode(data);
    }
  }
  
  Future<void> saveData(String key, dynamic value) async {
    final prefs = await SharedPreferences.getInstance();
    state = {...state, key: value};
    await prefs.setString('data', json.encode(state));
  }
}

✅ 推奨される実装

// 永続化するデータのモデル

class PersistentData with _$PersistentData {
  const factory PersistentData({
    required Map<String, dynamic> data,
    required DateTime lastUpdated,
    String? error,
  }) = _PersistentData;
  
  factory PersistentData.fromJson(Map<String, dynamic> json) =>
      _$PersistentDataFromJson(json);
}

// 永続化を担当するService
class StorageService {
  final SharedPreferences _prefs;
  
  StorageService(this._prefs);
  
  static Future<StorageService> init() async {
    final prefs = await SharedPreferences.getInstance();
    return StorageService(prefs);
  }
  
  Future<void> saveData(String key, String value) async {
    await _prefs.setString(key, value);
  }
  
  String? getData(String key) {
    return _prefs.getString(key);
  }
  
  Future<void> removeData(String key) async {
    await _prefs.remove(key);
  }
}

// StorageServiceのProvider
final storageServiceProvider = Provider<StorageService>((ref) {
  throw UnimplementedError();
});

// 永続化データのProvider
final persistentDataProvider = StateNotifierProvider<PersistentDataNotifier, AsyncValue<PersistentData>>((ref) {
  return PersistentDataNotifier(ref.watch(storageServiceProvider));
});

class PersistentDataNotifier extends StateNotifier<AsyncValue<PersistentData>> {
  final StorageService _storage;
  static const String _storageKey = 'persistent_data';
  
  PersistentDataNotifier(this._storage) : super(const AsyncValue.loading()) {
    _loadData();
  }
  
  Future<void> _loadData() async {
    try {
      final jsonStr = _storage.getData(_storageKey);
      if (jsonStr != null) {
        final data = PersistentData.fromJson(json.decode(jsonStr));
        state = AsyncValue.data(data);
      } else {
        state = AsyncValue.data(PersistentData(
          data: {},
          lastUpdated: DateTime.now(),
        ));
      }
    } catch (e, stack) {
      state = AsyncValue.error(e, stack);
    }
  }
  
  Future<void> updateData(String key, dynamic value) async {
    state.whenData((currentData) async {
      try {
        final newData = PersistentData(
          data: {...currentData.data, key: value},
          lastUpdated: DateTime.now(),
        );
        
        await _storage.saveData(
          _storageKey,
          json.encode(newData.toJson()),
        );
        
        state = AsyncValue.data(newData);
      } catch (e, stack) {
        state = AsyncValue.error(e, stack);
      }
    });
  }
  
  Future<void> clear() async {
    try {
      await _storage.removeData(_storageKey);
      state = AsyncValue.data(PersistentData(
        data: {},
        lastUpdated: DateTime.now(),
      ));
    } catch (e, stack) {
      state = AsyncValue.error(e, stack);
    }
  }
}

// アプリケーションの初期化
Future<void> initializeApp() async {
  final storage = await StorageService.init();
  
  runApp(
    ProviderScope(
      overrides: [
        storageServiceProvider.overrideWithValue(storage),
      ],
      child: MyApp(),
    ),
  );
}

まとめ

  1. リフレッシュ処理は包括的に実装する

    • 関連する全てのProviderを更新
    • キャッシュの制御も忘れずに
    • エラーハンドリングを適切に実装
  2. ページネーションは状態を明確に管理する

    • ローディング状態、エラー状態、データの有無を管理
    • 重複読み込みを防ぐ
    • リフレッシュ機能も実装
  3. 永続化は堅牢に実装する

    • データの型を明確に定義
    • エラーハンドリングを適切に実装
    • 非同期処理を適切に管理
    • 依存関係の注入を適切に行う
muranakarmuranakar

Riverpodで初学者がよく陥る実装の落とし穴と解決方法 パート5

1. Providerの状態監視における共通のミス

❌ 不適切なwatch/readの使用例

// 例1: buildメソッド内でのread使用
class ProductWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // 誤り: buildメソッド内でreadを使用
    final product = ref.read(productProvider);
    return Text(product.name); // 更新が反映されない
  }
}

// 例2: イベントハンドラ内でのwatch使用
class CartButton extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () {
        // 誤り: イベントハンドラ内でwatchを使用
        final cart = ref.watch(cartProvider);
        cart.addItem(product);
      },
      child: Text('カートに追加'),
    );
  }
}

// 例3: 必要以上の監視
class PriceWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // 誤り: 製品全体を監視している
    final product = ref.watch(productProvider);
    // 価格のみを表示
    return Text(${product.price}');
  }
}

✅ 適切な実装例

// 例1: buildメソッドでの適切なwatch使用
class ProductWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // 正しい: buildメソッド内でwatchを使用
    final product = ref.watch(productProvider);
    return Text(product.name); // 更新が反映される
  }
}

// 例2: イベントハンドラでの適切なread使用
class CartButton extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () {
        // 正しい: イベントハンドラ内でreadを使用
        ref.read(cartProvider.notifier).addItem(product);
      },
      child: Text('カートに追加'),
    );
  }
}

// 例3: 必要な値のみの監視
class PriceWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // 正しい: 価格のみを監視
    final price = ref.watch(productProvider.select((product) => product.price));
    return Text($price');
  }
}

// 例4: 複雑な状態変更を伴うケース
class ComplexProductWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // 複数の状態を監視
    final product = ref.watch(productProvider);
    final isInCart = ref.watch(cartProvider.select(
      (cart) => cart.items.any((item) => item.id == product.id)
    ));
    final isFavorite = ref.watch(favoriteProvider.select(
      (favorites) => favorites.contains(product.id)
    ));

    return Card(
      child: Column(
        children: [
          Text(product.name),
          Text(${product.price}'),
          Row(
            children: [
              IconButton(
                icon: Icon(
                  isFavorite ? Icons.favorite : Icons.favorite_border,
                  color: isFavorite ? Colors.red : null,
                ),
                onPressed: () {
                  // 状態更新はnotifierを通じて行う
                  ref.read(favoriteProvider.notifier).toggleFavorite(product.id);
                },
              ),
              ElevatedButton(
                onPressed: isInCart
                    ? null
                    : () => ref.read(cartProvider.notifier).addItem(product),
                child: Text(isInCart ? 'カート追加済み' : 'カートに追加'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

2. 非同期データの詳細な制御

❌ 不適切な非同期処理の実装

// 例1: ローディング状態の不適切な管理
final productsProvider = FutureProvider((ref) async {
  final products = await fetchProducts();
  return products;
});

// 例2: エラーハンドリングの欠如
final searchProvider = FutureProvider.family<List<Product>, String>((ref, query) async {
  return await api.searchProducts(query);
});

// 例3: キャッシュ制御の欠如
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
  return await api.fetchUser(userId);
});

✅ 適切な実装例

// 例1: 詳細な非同期状態管理
final productsProvider = FutureProvider.autoDispose((ref) async {
  // キャンセルトークンの使用
  final cancelToken = CancellationToken();
  ref.onDispose(() => cancelToken.cancel());

  // デバウンス処理
  await Future.delayed(const Duration(milliseconds: 300));
  
  try {
    final products = await fetchProducts(cancelToken: cancelToken);
    
    // 結果が空の場合の処理
    if (products.isEmpty) {
      throw const NoProductsFoundException();
    }
    
    return products;
  } on DioError catch (e) {
    // ネットワークエラーの変換
    throw NetworkException(e.message);
  } catch (e) {
    throw UnexpectedError(e.toString());
  }
});

// 例2: 高度な検索機能の実装
final searchProvider = StateNotifierProvider.autoDispose<SearchNotifier, AsyncValue<List<Product>>>((ref) {
  return SearchNotifier(ref);
});

class SearchNotifier extends StateNotifier<AsyncValue<List<Product>>> {
  final AutoDisposeStateNotifierProviderRef _ref;
  Timer? _debounceTimer;
  
  SearchNotifier(this._ref) : super(const AsyncValue.loading()) {
    _ref.onDispose(() {
      _debounceTimer?.cancel();
    });
  }
  
  Future<void> search(String query) async {
    _debounceTimer?.cancel();
    if (query.isEmpty) {
      state = const AsyncValue.data([]);
      return;
    }
    
    state = const AsyncValue.loading();
    _debounceTimer = Timer(const Duration(milliseconds: 500), () async {
      try {
        final results = await api.searchProducts(query);
        if (!mounted) return;
        state = AsyncValue.data(results);
      } catch (e, stack) {
        if (!mounted) return;
        state = AsyncValue.error(e, stack);
      }
    });
  }
}

// 例3: キャッシュを含む高度なユーザー情報管理
final userCacheProvider = StateProvider<Map<String, User>>((ref) => {});

final userProvider = FutureProvider.family<User, String>((ref, userId) async {
  final cache = ref.watch(userCacheProvider);
  
  // キャッシュチェック
  if (cache.containsKey(userId)) {
    final cachedUser = cache[userId]!;
    final cacheAge = DateTime.now().difference(cachedUser.lastFetched);
    
    // キャッシュが新しい場合はそれを使用
    if (cacheAge < const Duration(minutes: 5)) {
      return cachedUser;
    }
  }
  
  // 新しいデータをフェッチ
  final user = await api.fetchUser(userId);
  
  // キャッシュを更新
  ref.read(userCacheProvider.notifier).state = {
    ...cache,
    userId: user.copyWith(lastFetched: DateTime.now()),
  };
  
  return user;
});

// 使用例
class UserProfileWidget extends ConsumerWidget {
  final String userId;
  
  const UserProfileWidget({required this.userId});
  
  
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider(userId));
    
    return userAsync.when(
      data: (user) => Column(
        children: [
          Text(user.name),
          Text(user.email),
          ElevatedButton(
            onPressed: () {
              // キャッシュを強制的にリフレッシュ
              ref.refresh(userProvider(userId));
            },
            child: Text('更新'),
          ),
        ],
      ),
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => ErrorWidget(
        error: error,
        onRetry: () => ref.refresh(userProvider(userId)),
      ),
    );
  }
}

// エラーウィジェット
class ErrorWidget extends StatelessWidget {
  final Object error;
  final VoidCallback onRetry;

  const ErrorWidget({
    required this.error,
    required this.onRetry,
  });

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          error is NetworkException
              ? 'ネットワークエラーが発生しました'
              : error is NoProductsFoundException
                  ? '商品が見つかりませんでした'
                  : 'エラーが発生しました',
          style: TextStyle(color: Colors.red),
        ),
        ElevatedButton(
          onPressed: onRetry,
          child: Text('再試行'),
        ),
      ],
    );
  }
}

3. Providerの依存関係の適切な管理

❌ 不適切な依存関係の管理

// 例1: 密結合な依存関係
final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
  final products = ref.watch(productsProvider);
  final user = ref.watch(userProvider);
  return CartNotifier(products, user); // 直接依存
});

// 例2: グローバルな状態アクセス
class ProductService {
  void updateProduct(Product product) {
    // グローバル変数としてProviderContainerにアクセス
    container.read(productsProvider.notifier).updateProduct(product);
  }
}

✅ 適切な実装例

// 例1: 依存性の注入を使用した実装
abstract class ProductRepository {
  Future<List<Product>> getProducts();
  Future<void> updateProduct(Product product);
}

class ApiProductRepository implements ProductRepository {
  final Dio _dio;
  
  ApiProductRepository(this._dio);
  
  
  Future<List<Product>> getProducts() async {
    final response = await _dio.get('/products');
    return (response.data as List)
        .map((json) => Product.fromJson(json))
        .toList();
  }
  
  
  Future<void> updateProduct(Product product) async {
    await _dio.put('/products/${product.id}', data: product.toJson());
  }
}

final productRepositoryProvider = Provider<ProductRepository>((ref) {
  final dio = ref.watch(dioProvider);
  return ApiProductRepository(dio);
});

// 例2: 疎結合な依存関係
final productServiceProvider = Provider<ProductService>((ref) {
  final repository = ref.watch(productRepositoryProvider);
  return ProductService(repository);
});

class ProductService {
  final ProductRepository _repository;
  
  ProductService(this._repository);
  
  Future<void> updateProduct(Product product) async {
    await _repository.updateProduct(product);
  }
}

// 例3: 複雑な依存関係の管理
final cartServiceProvider = Provider<CartService>((ref) {
  final productRepo = ref.watch(productRepositoryProvider);
  final userRepo = ref.watch(userRepositoryProvider);
  final analytics = ref.watch(analyticsServiceProvider);
  
  return CartService(
    productRepository: productRepo,
    userRepository: userRepo,
    analytics: analytics,
  );
});

class CartService {
  final ProductRepository _productRepository;
  final UserRepository _userRepository;
  final AnalyticsService _analytics;
  
  CartService({
    required ProductRepository productRepository,
    required UserRepository userRepository,
    required AnalyticsService analytics,
  })  : _productRepository = productRepository,
        _userRepository = userRepository,
        _analytics = analytics;
  
  Future<void> addToCart(String productId) async {
    try {
      final product = await _productRepository.getProduct(productId);
      final user = await _userRepository.getCurrentUser();
      
      await _productRepository.updateStock(productId, -1);
      await _userRepository.addToCart(user.id, product);
      
      _analytics.logAddToCart(product);
    } catch (e) {
      // エラーハンドリング
      rethrow;
    }
  }
}

// 使用例
class AddToCartButton extends ConsumerWidget {
  final String productId;
  
  const AddToCartButton({required this.productId});
  
  
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () async {
        try {
          await ref.read(cartServiceProvider).addToCart(productId);
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('カートに追加しました')),
          );
        } catch (e) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('エラーが発生しました')),
          );
        }
      },
      child: Text('カートに追加'),
    );
  }
}

まとめ

  1. 状態監視の基本ルール

    • buildメソッド内ではwatchを使用
    • イベントハンドラ内ではreadを使用
    • 必要な値のみをselectで監視
    • 状態更新は必ずnotifierを通じて行う
  2. 非同期データの扱い方

    • 適切なローディング状態の管理
    • エラーハンドリングの実装
    • キャンセル処理の実装
    • キャッシュ制御の実装
    • デバウンス処理の実装
  3. 依存関係の管理

    • 依存性の注入を活用
    • インターフェースを使用した疎結合な設計
    • 適切な責任分離
muranakarmuranakar

Riverpodで初学者がよく悩む実装パターンと解決方法

1. 基本的なPODOの実装パターン

❌ よくある実装ミス

// 例1: 不完全なモデルクラス
class User {
  final String name;
  final String email;
  
  User(this.name, this.email); // コピーメソッドがない
}

// 例2: バリデーションの欠如
class Product {
  final String name;
  final int price;
  
  Product(this.name, this.price); // 値の検証がない
}

// 例3: 不適切なJSONシリアライズ
class Order {
  final String id;
  final DateTime createdAt;
  
  Order(this.id, this.createdAt);
  
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'createdAt': createdAt.toString(), // 不適切な日付形式
    };
  }
}

✅ 推奨される実装

// 例1: 完全なモデルクラス

class User with _$User {
  const factory User({
    required String id,
    required String name,
    required String email,
    required DateTime createdAt,
    String? photoUrl,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

// 例2: バリデーションを含むモデル

class Product with _$Product {
  const factory Product({
    required String id,
    required String name,
    required int price,
    required int stock,
    (false) bool isPublished,
  }) = _Product;

  factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
  
  // カスタムバリデーションメソッド
  static Product? validate({
    required String name,
    required int price,
    required int stock,
  }) {
    if (name.isEmpty) return null;
    if (price < 0) return null;
    if (stock < 0) return null;
    
    return Product(
      id: const Uuid().v4(),
      name: name,
      price: price,
      stock: stock,
    );
  }
}

// 例3: 適切なJSONシリアライズ

class Order with _$Order {
  const factory Order({
    required String id,
    required List<OrderItem> items,
    required DateTime createdAt,
    required OrderStatus status,
    String? couponCode,
  }) = _Order;

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  
  // カスタムゲッター
  int get totalAmount => items.fold(
    0,
    (sum, item) => sum + (item.price * item.quantity),
  );
  
  bool get canCancel => status == OrderStatus.pending;
}

// 列挙型の定義
enum OrderStatus {
  pending,
  processing,
  shipped,
  delivered,
  cancelled;
  
  String get displayName {
    switch (this) {
      case OrderStatus.pending:
        return '注文受付';
      case OrderStatus.processing:
        return '処理中';
      case OrderStatus.shipped:
        return '発送済み';
      case OrderStatus.delivered:
        return '配達完了';
      case OrderStatus.cancelled:
        return 'キャンセル';
    }
  }
}

2. Provider定義のベストプラクティス

❌ よくある実装ミス

// 例1: グローバル変数としてのProvider定義
final userProvider = StateNotifierProvider<UserNotifier, User>((ref) => UserNotifier());

// 例2: 過度に複雑なProvider
final complexProvider = StateNotifierProvider<ComplexNotifier, ComplexState>((ref) {
  final service = ref.watch(serviceProvider);
  final repository = ref.watch(repositoryProvider);
  final analytics = ref.watch(analyticsProvider);
  final cache = ref.watch(cacheProvider);
  return ComplexNotifier(service, repository, analytics, cache);
});

// 例3: 不適切なProviderの種類選択
final counterProvider = Provider((ref) {
  return 0; // 変更可能な値なのにProviderを使用
});

✅ 推奨される実装

// 例1: 適切なProvider定義
// Providerの定義をクラスとしてまとめる
class Providers {
  // プライベートコンストラクタでインスタンス化を防ぐ
  Providers._();
  
  // ユーザー関連のProvider
  static final userProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
    final repository = ref.watch(repositoryProvider);
    final analytics = ref.watch(analyticsProvider);
    return UserNotifier(repository, analytics);
  });
  
  // 認証状態のProvider
  static final authStateProvider = StreamProvider<AuthState>((ref) {
    return ref.watch(firebaseAuthProvider).authStateChanges().map((user) {
      if (user == null) return const AuthState.unauthenticated();
      return AuthState.authenticated(user);
    });
  });
  
  // ユーザー設定のProvider
  static final userSettingsProvider = StateNotifierProvider<UserSettingsNotifier, UserSettings>((ref) {
    // 認証状態を監視して設定を更新
    ref.listen<AuthState>(authStateProvider, (previous, next) {
      next.whenOrNull(
        unauthenticated: () => ref.invalidate(userSettingsProvider),
      );
    });
    
    return UserSettingsNotifier(ref.watch(settingsRepositoryProvider));
  });
}

// 例2: 適切な粒度のProvider
class UserNotifier extends StateNotifier<AsyncValue<User?>> {
  final UserRepository _repository;
  final AnalyticsService _analytics;
  
  UserNotifier(this._repository, this._analytics)
      : super(const AsyncValue.loading()) {
    _init();
  }
  
  Future<void> _init() async {
    state = const AsyncValue.loading();
    try {
      final user = await _repository.getCurrentUser();
      state = AsyncValue.data(user);
    } catch (error, stackTrace) {
      state = AsyncValue.error(error, stackTrace);
    }
  }
  
  Future<void> updateProfile({
    String? name,
    String? photoUrl,
  }) async {
    final currentUser = state.valueOrNull;
    if (currentUser == null) return;
    
    state = const AsyncValue.loading();
    try {
      final updatedUser = await _repository.updateUser(
        currentUser.id,
        name: name,
        photoUrl: photoUrl,
      );
      
      _analytics.logProfileUpdate();
      state = AsyncValue.data(updatedUser);
    } catch (error, stackTrace) {
      state = AsyncValue.error(error, stackTrace);
    }
  }
}

// 例3: 適切なProviderの種類選択
// 単純な値の場合
final counterProvider = StateProvider<int>((ref) => 0);

// 複雑な状態管理の場合
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
  return CartNotifier(ref.watch(cartRepositoryProvider));
});

// 非同期データの場合
final productsProvider = FutureProvider.autoDispose((ref) async {
  ref.keepAlive(); // 必要に応じてキャッシュを維持
  return ref.watch(productRepositoryProvider).getProducts();
});

// ストリームデータの場合
final messagesProvider = StreamProvider.autoDispose<List<Message>>((ref) {
  final chatId = ref.watch(currentChatIdProvider);
  return ref.watch(messageRepositoryProvider).watchMessages(chatId);
});

3. Provider間の依存関係の管理

❌ よくある実装ミス

// 例1: 直接的な依存関係
final dataProvider = Provider((ref) {
  final service = ServiceImpl(); // 直接インスタンス化
  return service.getData();
});

// 例2: 循環依存
final providerA = Provider((ref) {
  final b = ref.watch(providerB);
  return 'A: $b';
});

final providerB = Provider((ref) {
  final a = ref.watch(providerA); // 循環依存
  return 'B: $a';
});

// 例3: 不適切な状態共有
final globalStateProvider = StateProvider((ref) => 0);

✅ 推奨される実装

// 例1: レイヤー分けされた依存関係
// 1. 設定Provider
final configProvider = Provider((ref) {
  return AppConfig(
    apiUrl: 'https://api.example.com',
    timeout: const Duration(seconds: 30),
  );
});

// 2. HTTP ClientのProvider
final dioProvider = Provider((ref) {
  final config = ref.watch(configProvider);
  
  final dio = Dio()
    ..options.baseUrl = config.apiUrl
    ..options.connectTimeout = config.timeout
    ..options.receiveTimeout = config.timeout
    ..interceptors.addAll([
      LogInterceptor(),
      AuthInterceptor(),
    ]);
  
  ref.onDispose(() {
    dio.close();
  });
  
  return dio;
});

// 3. RepositoryのProvider
final userRepositoryProvider = Provider<UserRepository>((ref) {
  return UserRepositoryImpl(ref.watch(dioProvider));
});

// 4. ServiceのProvider
final userServiceProvider = Provider<UserService>((ref) {
  return UserServiceImpl(ref.watch(userRepositoryProvider));
});

// 5. 状態管理のProvider
final userStateProvider = StateNotifierProvider<UserStateNotifier, AsyncValue<User?>>((ref) {
  return UserStateNotifier(ref.watch(userServiceProvider));
});

// 例2: 適切な状態分離
// 認証状態
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
  return AuthNotifier(ref.watch(authRepositoryProvider));
});

// ユーザープロフィール
final profileProvider = FutureProvider<Profile?>((ref) {
  final authState = ref.watch(authProvider);
  
  return authState.maybeWhen(
    authenticated: (user) => ref.watch(profileRepositoryProvider).getProfile(user.id),
    orElse: () => null,
  );
});

// 設定
final settingsProvider = StateNotifierProvider<SettingsNotifier, Settings>((ref) {
  ref.listen<AuthState>(authProvider, (previous, next) {
    // 認証状態が変更されたら設定をリセット
    next.maybeWhen(
      unauthenticated: () => ref.invalidate(settingsProvider),
      orElse: () {},
    );
  });
  
  return SettingsNotifier(ref.watch(settingsRepositoryProvider));
});

// 例3: 機能ごとの状態管理
class CartState {
  final List<CartItem> items;
  final bool isLoading;
  final String? error;
  
  const CartState({
    required this.items,
    this.isLoading = false,
    this.error,
  });
  
  CartState copyWith({
    List<CartItem>? items,
    bool? isLoading,
    String? error,
  }) {
    return CartState(
      items: items ?? this.items,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }
  
  int get totalQuantity => items.fold(0, (sum, item) => sum + item.quantity);
  int get totalAmount => items.fold(0, (sum, item) => sum + item.totalPrice);
}

final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
  // 依存するProviderを注入
  final repository = ref.watch(cartRepositoryProvider);
  final analytics = ref.watch(analyticsProvider);
  
  // 認証状態の変更を監視
  ref.listen<AuthState>(authProvider, (previous, next) {
    next.maybeWhen(
      unauthenticated: () => ref.invalidate(cartProvider),
      orElse: () {},
    );
  });
  
  return CartNotifier(repository, analytics);
});

class CartNotifier extends StateNotifier<CartState> {
  final CartRepository _repository;
  final AnalyticsService _analytics;
  
  CartNotifier(this._repository, this._analytics)
      : super(const CartState(items: [])) {
    _loadCart();
  }
  
  Future<void> _loadCart() async {
    state = state.copyWith(isLoading: true, error: null);
    
    try {
      final items = await _repository.getCartItems();
      state = state.copyWith(items: items, isLoading: false);
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: 'カートの読み込みに失敗しました',
      );
    }
  }
  
  Future<void> addItem(Product product, [int quantity = 1]) async {
    state = state.copyWith(isLoading: true, error: null);
    
    try {
      await _repository.addToCart(product.id, quantity);
      final items = await _repository.getCartItems();
      
      _analytics.logAddToCart(product, quantity);
      
      state = state.copyWith(items: items, isLoading: false);
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: '商品の追加に失敗しました',
      );
    }
  }
}

まとめのポイント

  1. モデルクラスの実装

    • freezedを活用した不変オブジェクトの作成
    • 適切なバリデーション
    • JSONシリアライズ/デシリアライズの実装
    • カスタムメソッドやゲッターの活用
  2. Provider定義のベストプラクティス

    • 適切なスコープでの定義
    • 責任の分離
    • 適切なProvider種類の選択
    • エラーハンドリング
    • 依存関係の注入
  3. Provider間の依存関係

    • レイヤー分けによる依存関係の整理
    • 適切な状態分離
    • 機能ごとの状態管理
    • リスナーを使用した状態の連携

これらの実装パターンを意識することで、より保守性が高く、理解しやすいコードを書くことができます。と

muranakarmuranakar

Riverpodでよくある間違いと正しい実装 - より実践的なパターン

1. ログイン状態の管理

❌ 間違い1: 単純なログイン状態管理

final isLoggedInProvider = StateProvider<bool>((ref) => false);

class LoginButton extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final isLoggedIn = ref.watch(isLoggedInProvider);

    return ElevatedButton(
      onPressed: () async {
        await FirebaseAuth.instance.signInWithEmailAndPassword(...);
        ref.read(isLoggedInProvider.notifier).state = true;  // 単純なbool値の管理
      },
      child: Text(isLoggedIn ? 'ログアウト' : 'ログイン'),
    );
  }
}

✅ 正解1: 包括的な認証状態管理

// 認証状態を表現する
sealed class AuthState {
  const AuthState();
  
  const factory AuthState.initial() = _Initial;
  const factory AuthState.authenticated(User user) = _Authenticated;
  const factory AuthState.unauthenticated() = _Unauthenticated;
  const factory AuthState.loading() = _Loading;
  const factory AuthState.error(String message) = _Error;
}

final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
  return AuthNotifier(ref.watch(authRepositoryProvider));
});

class AuthNotifier extends StateNotifier<AuthState> {
  final AuthRepository _repository;
  StreamSubscription? _authStateSubscription;

  AuthNotifier(this._repository) : super(const AuthState.initial()) {
    _initialize();
  }

  void _initialize() {
    _authStateSubscription = _repository.authStateChanges().listen(
      (user) {
        if (user != null) {
          state = AuthState.authenticated(user);
        } else {
          state = const AuthState.unauthenticated();
        }
      },
      onError: (error) {
        state = AuthState.error(error.toString());
      },
    );
  }

  Future<void> signIn(String email, String password) async {
    state = const AuthState.loading();
    try {
      await _repository.signIn(email, password);
      // 状態の更新は_initializeのリスナーで行われる
    } catch (e) {
      state = AuthState.error('ログインに失敗しました: ${e.toString()}');
    }
  }

  
  void dispose() {
    _authStateSubscription?.cancel();
    super.dispose();
  }
}

❌ 間違い2: 認証後のユーザー情報取得

final userProvider = FutureProvider<User?>((ref) async {
  final auth = FirebaseAuth.instance;
  return auth.currentUser;  // 単純なユーザー情報の取得
});

✅ 正解2: 詳細なユーザー情報管理

final userProvider = FutureProvider.family<UserProfile, String>((ref, userId) async {
  // 認証状態を監視
  final authState = ref.watch(authProvider);
  
  return authState.maybeWhen(
    authenticated: (authUser) async {
      // キャッシュをチェック
      final cached = ref.read(userCacheProvider)[userId];
      if (cached != null && cached.isValid) {
        return cached.data;
      }

      final userProfile = await ref.read(userRepositoryProvider).getUserProfile(userId);
      
      // キャッシュを更新
      ref.read(userCacheProvider.notifier).cacheUser(userId, userProfile);
      
      // 追加情報の取得
      ref.read(userPreferencesProvider.notifier).loadPreferences(userId);
      
      return userProfile;
    },
    orElse: () => throw UnauthorizedException(),
  );
});

// キャッシュ管理
final userCacheProvider = StateNotifierProvider<UserCacheNotifier, Map<String, CachedData<UserProfile>>>((ref) {
  return UserCacheNotifier();
});

class CachedData<T> {
  final T data;
  final DateTime cachedAt;
  final Duration validity;

  CachedData({
    required this.data,
    required this.cachedAt,
    this.validity = const Duration(minutes: 5),
  });

  bool get isValid => 
    DateTime.now().difference(cachedAt) < validity;
}

2. フォーム状態の管理

❌ 間違い3: 個別のフォームフィールド管理

final emailProvider = StateProvider<String>((ref) => '');
final passwordProvider = StateProvider<String>((ref) => '');
final isLoadingProvider = StateProvider<bool>((ref) => false);

class LoginForm extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final email = ref.watch(emailProvider);
    final password = ref.watch(passwordProvider);
    final isLoading = ref.watch(isLoadingProvider);

    return Column(
      children: [
        TextField(
          onChanged: (value) => ref.read(emailProvider.notifier).state = value,
        ),
        TextField(
          onChanged: (value) => ref.read(passwordProvider.notifier).state = value,
        ),
        ElevatedButton(
          onPressed: isLoading ? null : () async {
            ref.read(isLoadingProvider.notifier).state = true;
            // ログイン処理
            ref.read(isLoadingProvider.notifier).state = false;
          },
          child: Text('ログイン'),
        ),
      ],
    );
  }
}

✅ 正解3: 統合的なフォーム状態管理

// フォームの状態を定義

class LoginFormState with _$LoginFormState {
  const factory LoginFormState({
    ('') String email,
    ('') String password,
    (false) bool isSubmitting,
    (false) bool isValid,
    String? errorMessage,
  }) = _LoginFormState;
}

final loginFormProvider = StateNotifierProvider<LoginFormNotifier, LoginFormState>((ref) {
  return LoginFormNotifier(ref.watch(authRepositoryProvider));
});

class LoginFormNotifier extends StateNotifier<LoginFormState> {
  final AuthRepository _authRepository;

  LoginFormNotifier(this._authRepository) : super(const LoginFormState());

  void emailChanged(String email) {
    state = state.copyWith(
      email: email,
      isValid: _validateForm(email: email, password: state.password),
      errorMessage: null,
    );
  }

  void passwordChanged(String password) {
    state = state.copyWith(
      password: password,
      isValid: _validateForm(email: state.email, password: password),
      errorMessage: null,
    );
  }

  bool _validateForm({required String email, required String password}) {
    if (email.isEmpty || !email.contains('@')) return false;
    if (password.isEmpty || password.length < 6) return false;
    return true;
  }

  Future<void> submit() async {
    if (!state.isValid || state.isSubmitting) return;

    state = state.copyWith(isSubmitting: true, errorMessage: null);

    try {
      await _authRepository.signIn(
        email: state.email,
        password: state.password,
      );
    } catch (e) {
      state = state.copyWith(
        isSubmitting: false,
        errorMessage: e.toString(),
      );
      return;
    }

    state = state.copyWith(isSubmitting: false);
  }
}

// UI実装
class LoginForm extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final formState = ref.watch(loginFormProvider);

    return Form(
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(
              labelText: 'メールアドレス',
              errorText: !formState.email.contains('@') && formState.email.isNotEmpty
                  ? '有効なメールアドレスを入力してください'
                  : null,
            ),
            onChanged: ref.read(loginFormProvider.notifier).emailChanged,
          ),
          TextFormField(
            decoration: InputDecoration(
              labelText: 'パスワード',
              errorText: formState.password.length < 6 && formState.password.isNotEmpty
                  ? 'パスワードは6文字以上必要です'
                  : null,
            ),
            obscureText: true,
            onChanged: ref.read(loginFormProvider.notifier).passwordChanged,
          ),
          if (formState.errorMessage != null)
            Text(
              formState.errorMessage!,
              style: TextStyle(color: Colors.red),
            ),
          ElevatedButton(
            onPressed: formState.isValid && !formState.isSubmitting
                ? () => ref.read(loginFormProvider.notifier).submit()
                : null,
            child: formState.isSubmitting
                ? CircularProgressIndicator()
                : Text('ログイン'),
          ),
        ],
      ),
    );
  }
}

3. API通信の状態管理

❌ 間違い4: 単純なAPI通信

final productsProvider = FutureProvider<List<Product>>((ref) async {
  final response = await http.get('https://api.example.com/products');
  return (jsonDecode(response.body) as List)
      .map((json) => Product.fromJson(json))
      .toList();
});

✅ 正解4: エラーハンドリングとキャッシュを含むAPI通信

// APIの状態を表現

class ApiState<T> with _$ApiState<T> {
  const factory ApiState.initial() = _Initial;
  const factory ApiState.loading() = _Loading;
  const factory ApiState.success(T data) = _Success;
  const factory ApiState.error(String message) = _Error;
}

final productsProvider = StateNotifierProvider<ProductsNotifier, ApiState<List<Product>>>((ref) {
  return ProductsNotifier(ref.watch(productRepositoryProvider));
});

class ProductsNotifier extends StateNotifier<ApiState<List<Product>>> {
  final ProductRepository _repository;
  Timer? _debounceTimer;
  
  ProductsNotifier(this._repository) : super(const ApiState.initial()) {
    fetchProducts();
  }

  Future<void> fetchProducts({String? search}) async {
    // 既存のデバウンスタイマーをキャンセル
    _debounceTimer?.cancel();
    
    // 検索クエリがある場合はデバウンス
    if (search != null) {
      _debounceTimer = Timer(const Duration(milliseconds: 300), () {
        _fetchProducts(search: search);
      });
      return;
    }
    
    await _fetchProducts(search: search);
  }

  Future<void> _fetchProducts({String? search}) async {
    state = const ApiState.loading();

    try {
      final products = await _repository.getProducts(search: search);
      
      if (!mounted) return;
      
      // キャッシュの更新
      ref.read(productCacheProvider.notifier).updateProducts(products);
      
      state = ApiState.success(products);
    } on NetworkException catch (e) {
      state = ApiState.error('ネットワークエラー: ${e.message}');
    } on ApiException catch (e) {
      state = ApiState.error('APIエラー: ${e.message}');
    } catch (e) {
      state = ApiState.error('予期せぬエラーが発生しました');
    }
  }

  
  void dispose() {
    _debounceTimer?.cancel();
    super.dispose();
  }
}

// UI実装
class ProductsScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final productsState = ref.watch(productsProvider);
    
    return Scaffold(
      appBar: AppBar(
        title: Text('商品一覧'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: () => ref.refresh(productsProvider),
          ),
        ],
      ),
      body: productsState.when(
        initial: () => const SizedBox(),
        loading: () => const Center(child: CircularProgressIndicator()),
        success: (products) => products.isEmpty
            ? const Center(child: Text('商品がありません'))
            : ListView.builder(
                itemCount: products.length,
                itemBuilder: (context, index) => ProductCard(
                  product: products[index],
                  onTap: () => _navigateToDetail(context, products[index]),
                ),
              ),
        error: (message) => ErrorView(
          message: message,
          onRetry: () => ref.refresh(productsProvider),
        ),
      ),
    );
  }
}

これらの実装例は、より実践的な状況での正しい実装パターンを示しています。要点は:

  1. 状態管理

    • 単純な値ではなく、詳細な状態を表現する
    • エラーハンドリングを適切に行う
    • ローディング状態を管理する
  2. キャッシュ管理

    • データの有効期限を設定
    • メモリ使用量を考慮
    • 適切なタイミングでの更新
  3. ユーザー体験

    • ローディング表示
    • エラーメッセージの表示
    • リトライ機能の提供

これらのパターンを意識することで、より堅牢なアプリケーションを開発することができます。

muranakarmuranakar

Riverpodの実践的な実装パターン

1. 検索機能の実装

❌ 間違い: 単純な検索実装

// 検索キーワードの保持
final searchQueryProvider = StateProvider<String>((ref) => '');

// 検索結果の取得
final searchResultsProvider = FutureProvider<List<Product>>((ref) async {
  final query = ref.watch(searchQueryProvider);
  if (query.isEmpty) return [];
  
  // 毎回APIを呼び出してしまう
  return await api.searchProducts(query);
});

class SearchScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final results = ref.watch(searchResultsProvider);
    
    return Column(
      children: [
        TextField(
          onChanged: (value) {
            // 入力の度にStateProviderを更新
            ref.read(searchQueryProvider.notifier).state = value;
          },
        ),
        Expanded(
          child: results.when(
            data: (data) => ListView.builder(
              itemCount: data.length,
              itemBuilder: (context, index) => ListTile(
                title: Text(data[index].name),
              ),
            ),
            loading: () => CircularProgressIndicator(),
            error: (_, __) => Text('エラーが発生しました'),
          ),
        ),
      ],
    );
  }
}

✅ 正解: 最適化された検索実装

// 検索の状態管理

class SearchState with _$SearchState {
  const factory SearchState({
    required String query,
    required bool isLoading,
    required List<Product> results,
    required List<Product> recentSearches,
    String? error,
  }) = _SearchState;

  factory SearchState.initial() => SearchState(
    query: '',
    isLoading: false,
    results: [],
    recentSearches: [],
  );
}

class SearchNotifier extends StateNotifier<SearchState> {
  final SearchRepository _repository;
  final AnalyticsService _analytics;
  Timer? _debounceTimer;
  
  SearchNotifier(this._repository, this._analytics)
      : super(SearchState.initial());
  
  Future<void> onQueryChanged(String query) async {
    state = state.copyWith(query: query);
    
    _debounceTimer?.cancel();
    
    if (query.isEmpty) {
      state = state.copyWith(results: [], isLoading: false);
      return;
    }
    
    // デバウンス処理
    _debounceTimer = Timer(const Duration(milliseconds: 300), () async {
      await _performSearch(query);
    });
  }
  
  Future<void> _performSearch(String query) async {
    if (query != state.query) return; // クエリが変更された場合は処理をスキップ
    
    state = state.copyWith(isLoading: true, error: null);
    
    try {
      final results = await _repository.searchProducts(
        query: query,
        limit: 20,
        offset: 0,
      );
      
      if (!mounted) return;
      
      _analytics.logSearch(query, results.length);
      
      state = state.copyWith(
        results: results,
        isLoading: false,
      );
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: '検索に失敗しました',
      );
    }
  }
  
  void addToRecentSearches(Product product) {
    final currentRecent = List<Product>.from(state.recentSearches);
    currentRecent.remove(product); // 重複を削除
    currentRecent.insert(0, product); // 先頭に追加
    
    // 最大10件まで保持
    if (currentRecent.length > 10) {
      currentRecent.removeLast();
    }
    
    state = state.copyWith(recentSearches: currentRecent);
  }
  
  
  void dispose() {
    _debounceTimer?.cancel();
    super.dispose();
  }
}

// UIの実装
class SearchScreen extends ConsumerStatefulWidget {
  
  _SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends ConsumerState<SearchScreen> {
  final _searchController = TextEditingController();
  
  
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }
  
  
  Widget build(BuildContext context) {
    final searchState = ref.watch(searchProvider);
    
    return Scaffold(
      appBar: AppBar(
        title: TextField(
          controller: _searchController,
          decoration: InputDecoration(
            hintText: '検索...',
            border: InputBorder.none,
            suffixIcon: searchState.query.isNotEmpty
                ? IconButton(
                    icon: Icon(Icons.clear),
                    onPressed: () {
                      _searchController.clear();
                      ref.read(searchProvider.notifier).onQueryChanged('');
                    },
                  )
                : null,
          ),
          onChanged: (value) {
            ref.read(searchProvider.notifier).onQueryChanged(value);
          },
        ),
      ),
      body: Column(
        children: [
          if (searchState.isLoading)
            LinearProgressIndicator(),
          if (searchState.error != null)
            ErrorBanner(
              message: searchState.error!,
              onDismiss: () {
                ref.read(searchProvider.notifier).clearError();
              },
            ),
          Expanded(
            child: searchState.query.isEmpty
                ? _buildRecentSearches(searchState.recentSearches)
                : _buildSearchResults(searchState.results),
          ),
        ],
      ),
    );
  }
  
  Widget _buildRecentSearches(List<Product> recentSearches) {
    if (recentSearches.isEmpty) {
      return Center(
        child: Text('最近の検索履歴はありません'),
      );
    }
    
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.all(16),
          child: Text(
            '最近の検索',
            style: Theme.of(context).textTheme.titleMedium,
          ),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: recentSearches.length,
            itemBuilder: (context, index) {
              final product = recentSearches[index];
              return ListTile(
                leading: CachedNetworkImage(
                  imageUrl: product.imageUrl,
                  placeholder: (context, url) => Shimmer.fromColors(
                    baseColor: Colors.grey[300]!,
                    highlightColor: Colors.grey[100]!,
                    child: Container(
                      width: 50,
                      height: 50,
                      color: Colors.white,
                    ),
                  ),
                ),
                title: Text(product.name),
                subtitle: Text(
                  ${product.price.toStringAsFixed(0)}',
                  style: TextStyle(color: Theme.of(context).primaryColor),
                ),
                onTap: () {
                  _searchController.text = product.name;
                  ref.read(searchProvider.notifier).onQueryChanged(product.name);
                },
              );
            },
          ),
        ),
      ],
    );
  }
  
  Widget _buildSearchResults(List<Product> results) {
    if (results.isEmpty && !searchState.isLoading) {
      return Center(
        child: Text('検索結果が見つかりません'),
      );
    }
    
    return ListView.builder(
      itemCount: results.length,
      itemBuilder: (context, index) {
        final product = results[index];
        return ProductListItem(
          product: product,
          onTap: () {
            ref.read(searchProvider.notifier).addToRecentSearches(product);
            Navigator.of(context).push(
              MaterialPageRoute(
                builder: (_) => ProductDetailScreen(product: product),
              ),
            );
          },
        );
      },
    );
  }
}

// 検索結果アイテムの表示
class ProductListItem extends StatelessWidget {
  final Product product;
  final VoidCallback onTap;

  const ProductListItem({
    required this.product,
    required this.onTap,
  });

  
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: InkWell(
        onTap: onTap,
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Row(
            children: [
              CachedNetworkImage(
                imageUrl: product.imageUrl,
                width: 80,
                height: 80,
                fit: BoxFit.cover,
              ),
              SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      product.name,
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    SizedBox(height: 4),
                    Text(
                      ${product.price.toStringAsFixed(0)}',
                      style: TextStyle(
                        color: Theme.of(context).primaryColor,
                        fontSize: 14,
                      ),
                    ),
                    if (product.description != null) ...[
                      SizedBox(height: 4),
                      Text(
                        product.description!,
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.grey[600],
                        ),
                      ),
                    ],
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

このパターンの主なポイントは:

  1. 状態管理

    • デバウンス処理による API コール最適化
    • ローディング状態の適切な管理
    • エラーハンドリング
    • 検索履歴の管理
  2. UI/UX

    • クリアボタンの表示
    • ローディングインジケータ
    • エラーメッセージの表示
    • 検索履歴の表示と再検索機能
    • 画像のプレースホルダー表示
  3. パフォーマンス最適化

    • 不必要な API コールの防止
    • 画像のキャッシュ
    • リスト表示の最適化
  4. エラーハンドリング

    • ネットワークエラーの処理
    • ユーザーへのフィードバック
    • リトライ機能

このような実装により、より使いやすく、パフォーマンスの良い検索機能を提供することができます。

muranakarmuranakar

Riverpodの実践的な実装パターン - 無限スクロール&ページネーション

1. 無限スクロールの実装

❌ 間違い: 単純な無限スクロール実装

final postsProvider = StateNotifierProvider<PostsNotifier, List<Post>>((ref) {
  return PostsNotifier();
});

class PostsNotifier extends StateNotifier<List<Post>> {
  int _page = 1;
  bool _isLoading = false;
  
  PostsNotifier() : super([]) {
    loadMore();
  }
  
  Future<void> loadMore() async {
    if (_isLoading) return;
    
    _isLoading = true;
    final posts = await api.getPosts(page: _page);
    state = [...state, ...posts];
    _page++;
    _isLoading = false;
  }
}

class PostsScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final posts = ref.watch(postsProvider);
    
    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        if (index == posts.length - 1) {
          ref.read(postsProvider.notifier).loadMore();
        }
        return ListTile(title: Text(posts[index].title));
      },
    );
  }
}

✅ 正解: 最適化された無限スクロール実装

// ページネーション状態の定義

class PaginationState<T> with _$PaginationState<T> {
  const factory PaginationState({
    required List<T> items,
    required bool isLoading,
    required bool hasNextPage,
    required int currentPage,
    String? error,
  }) = _PaginationState;

  factory PaginationState.initial() => PaginationState(
    items: [],
    isLoading: false,
    hasNextPage: true,
    currentPage: 1,
  );
}

// ページネーション用のNotifier
class PaginatedPostsNotifier extends StateNotifier<PaginationState<Post>> {
  final PostRepository _repository;
  final int _pageSize;
  bool _isLoadingMore = false;

  PaginatedPostsNotifier(this._repository, {int pageSize = 20})
      : _pageSize = pageSize,
        super(PaginationState.initial()) {
    loadInitial();
  }

  Future<void> loadInitial() async {
    state = state.copyWith(isLoading: true, error: null);

    try {
      final response = await _repository.getPosts(
        page: 1,
        pageSize: _pageSize,
      );

      state = state.copyWith(
        items: response.items,
        hasNextPage: response.items.length >= _pageSize,
        currentPage: 1,
        isLoading: false,
      );
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: '投稿の読み込みに失敗しました',
      );
    }
  }

  Future<void> loadMore() async {
    if (_isLoadingMore || !state.hasNextPage) return;
    if (state.error != null) return;

    _isLoadingMore = true;
    state = state.copyWith(isLoading: true, error: null);

    try {
      final nextPage = state.currentPage + 1;
      final response = await _repository.getPosts(
        page: nextPage,
        pageSize: _pageSize,
      );

      state = state.copyWith(
        items: [...state.items, ...response.items],
        hasNextPage: response.items.length >= _pageSize,
        currentPage: nextPage,
        isLoading: false,
      );
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: '追加の投稿の読み込みに失敗しました',
      );
    } finally {
      _isLoadingMore = false;
    }
  }

  Future<void> refresh() async {
    state = PaginationState.initial();
    await loadInitial();
  }
}

// UIの実装
class PostsScreen extends ConsumerStatefulWidget {
  
  _PostsScreenState createState() => _PostsScreenState();
}

class _PostsScreenState extends ConsumerState<PostsScreen> {
  final _scrollController = ScrollController();
  
  
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }
  
  
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
  
  void _onScroll() {
    if (_isBottom) {
      ref.read(paginatedPostsProvider.notifier).loadMore();
    }
  }
  
  bool get _isBottom {
    if (!_scrollController.hasClients) return false;
    
    final maxScroll = _scrollController.position.maxScrollExtent;
    final currentScroll = _scrollController.offset;
    return currentScroll >= (maxScroll * 0.9); // 90%スクロールで次を読み込み
  }
  
  
  Widget build(BuildContext context) {
    final state = ref.watch(paginatedPostsProvider);
    
    return Scaffold(
      appBar: AppBar(
        title: Text('投稿一覧'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: () {
              ref.read(paginatedPostsProvider.notifier).refresh();
            },
          ),
        ],
      ),
      body: RefreshIndicator(
        onRefresh: () async {
          await ref.read(paginatedPostsProvider.notifier).refresh();
        },
        child: CustomScrollView(
          controller: _scrollController,
          slivers: [
            if (state.error != null)
              SliverToBoxAdapter(
                child: ErrorBanner(
                  message: state.error!,
                  onRetry: () {
                    ref.read(paginatedPostsProvider.notifier).loadMore();
                  },
                ),
              ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  if (index >= state.items.length) {
                    if (state.isLoading) {
                      return _buildLoader();
                    }
                    if (!state.hasNextPage) {
                      return _buildNoMoreContent();
                    }
                    return null;
                  }
                  
                  return PostListItem(
                    post: state.items[index],
                    onTap: () => _navigateToDetail(state.items[index]),
                  );
                },
                childCount: state.items.length + (state.hasNextPage ? 1 : 0),
              ),
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildLoader() {
    return Container(
      padding: EdgeInsets.symmetric(vertical: 16),
      alignment: Alignment.center,
      child: CircularProgressIndicator(),
    );
  }
  
  Widget _buildNoMoreContent() {
    return Container(
      padding: EdgeInsets.symmetric(vertical: 32),
      alignment: Alignment.center,
      child: Text(
        '全ての投稿を読み込みました',
        style: TextStyle(
          color: Colors.grey,
          fontSize: 14,
        ),
      ),
    );
  }
  
  void _navigateToDetail(Post post) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => PostDetailScreen(post: post),
      ),
    );
  }
}

// 投稿アイテムのUI
class PostListItem extends StatelessWidget {
  final Post post;
  final VoidCallback onTap;

  const PostListItem({
    required this.post,
    required this.onTap,
  });

  
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: InkWell(
        onTap: onTap,
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  CircleAvatar(
                    backgroundImage: NetworkImage(post.author.avatarUrl),
                  ),
                  SizedBox(width: 12),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        post.author.name,
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      Text(
                        _formatDate(post.createdAt),
                        style: TextStyle(
                          color: Colors.grey,
                          fontSize: 12,
                        ),
                      ),
                    ],
                  ),
                ],
              ),
              SizedBox(height: 12),
              Text(
                post.title,
                style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                ),
              ),
              if (post.content != null) ...[
                SizedBox(height: 8),
                Text(
                  post.content!,
                  maxLines: 3,
                  overflow: TextOverflow.ellipsis,
                ),
              ],
              if (post.imageUrl != null) ...[
                SizedBox(height: 12),
                CachedNetworkImage(
                  imageUrl: post.imageUrl!,
                  placeholder: (context, url) => Shimmer.fromColors(
                    baseColor: Colors.grey[300]!,
                    highlightColor: Colors.grey[100]!,
                    child: Container(
                      height: 200,
                      color: Colors.white,
                    ),
                  ),
                  errorWidget: (context, url, error) => Icon(Icons.error),
                  fit: BoxFit.cover,
                ),
              ],
              SizedBox(height: 12),
              Row(
                children: [
                  Icon(Icons.favorite_border, size: 16),
                  SizedBox(width: 4),
                  Text('${post.likesCount}'),
                  SizedBox(width: 16),
                  Icon(Icons.comment_outlined, size: 16),
                  SizedBox(width: 4),
                  Text('${post.commentsCount}'),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  String _formatDate(DateTime date) {
    final now = DateTime.now();
    final difference = now.difference(date);
    
    if (difference.inDays > 7) {
      return DateFormat.yMMMd().format(date);
    } else if (difference.inDays > 0) {
      return '${difference.inDays}日前';
    } else if (difference.inHours > 0) {
      return '${difference.inHours}時間前';
    } else if (difference.inMinutes > 0) {
      return '${difference.inMinutes}分前';
    } else {
      return 'たった今';
    }
  }
}

このパターンの主なポイントは:

  1. 状態管理

    • ページネーション状態の完全な管理
    • ローディング状態の適切な制御
    • エラーハンドリング
    • リフレッシュ機能
  2. UI/UX

    • スムーズなスクロール体験
    • ローディングインジケータ
    • エラーメッセージの表示
    • リフレッシュ機能
    • 最後までスクロールした時の表示
  3. パフォーマンス最適化

    • スクロール位置の適切な検出
    • 画像のレイジーローディング
    • 適切なキャッシュ管理
  4. エラーハンドリング

    • ネットワークエラーの処理
    • 再試行機能
    • ユーザーへのフィードバック

このような実装により、スムーズで使いやすい無限スクロール機能を提供することができます。