Search code examples
flutterdartriverpodflutter-riverpod

Flutter Riverpod StateNotifierProvider Async value still loading


I'm new to Flutter. I use the Riverpod state management library. I am calling a remote API to get a list of board games. If I don't pass a parameter to my query, it returns me a list of popular games, and if I pass a games name parameter, it returns me a list of games containing the parameter in its name. So I want to display a list of popular games when the user lands on the page, and update the list if the user searches for a game by writing its name in the search bar. Problem: My StateNotifier is not working and stays in loading state. Any help will be appreciated, I'm really lost.

Here is my remote_api.dart:

final boardGamesListProvider = FutureProvider.family.autoDispose<List<BoardGame>, String>((ref, request) => RemoteApi().getBoardGames(request));

class RemoteApi {

  Future<List<BoardGame>> getBoardGames(String request) async {
    // Appel WS
    try {
      final response = await Dio().get('https://api.boardgameatlas.com/api/search?name=$request&client_id=JLBr5npPhV');

      if (response.statusCode == 200) {
        final data = Map<String, dynamic>.from(response.data);
        final results = List<Map<String, dynamic>>.from(data['games']);
        if (results.isNotEmpty) {
          return results.map((e) => BoardGame.fromMap(e)).toList();
        }
      }
      return [];
    } on DioError catch (err) {
      print(err);
      throw ErrorHandler(message: err.response?.statusMessage ?? 'Something went wrong!');
    } on SocketException catch (err) {
      print(err);
      throw const ErrorHandler(message: 'Please check your connection.');
    }
  }
}

My search_game_controller.dart:

final boardGamesListControllerProvider =
    StateNotifierProvider<BoardGameList, AsyncValue<List<BoardGame>>>((ref) {
  return BoardGameList(const AsyncValue.data([]), ref);
});

class BoardGameList extends StateNotifier<AsyncValue<List<BoardGame>>> {
  BoardGameList(AsyncValue<List<BoardGame>> items, this.ref) : super(items);

  final Ref ref;

  Future<void> search(String request) async {
    state = const AsyncValue.loading();
    ref.read(boardGamesListProvider(request)).when(data: (data) {
      AsyncValue.data(data);
    }, error: (err, stackTrace) {
      state = AsyncValue.error(err, stackTrace: stackTrace);
    }, loading: () {
      state = const AsyncValue.loading();
    });
  }
}

My search_game_screen.dart:

class SearchGameScreen extends HookConsumerWidget {
  const SearchGameScreen({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final searchController = TextEditingController();
    final boardGameListAsync = ref.watch(boardGamesListControllerProvider);
    return Scaffold(
      body: Column(
        children: [
          Row(
            children: [
              Expanded(
                child: Container(
                  padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
                  margin: const EdgeInsets.only(bottom: 2),
                  child: TextFormField(
                    controller: searchController,
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      labelText: 'Search a game',
                    ),
                  ),
                ),
              ),
              Container(
                height: 50,
                padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
                margin: const EdgeInsets.only(bottom: 2),
                child: ElevatedButton(
                  child: const Text('Search',
                      style: TextStyle(color: Colors.white)),
                  onPressed: () {
                    ref
                    .read(boardGamesListControllerProvider.notifier).search(searchController.text);
                    print(boardGameListAsync);
                  },
                ),
              ),
            ],
          ),
          Expanded(
            child: boardGameListAsync
                .when(
                  data: (boardGamesList) {
                    return BoardGamesList(boardGames: boardGamesList);
                  },
                  loading: () =>
                      const Center(child: CircularProgressIndicator()),
                  error: (error, _) => ErrorScreen(message: error.toString()),
                ),
          )
        ],
      ),
    );
  }
}

class BoardGamesList extends HookConsumerWidget {
  const BoardGamesList({Key? key, required this.boardGames}) : super(key: key);
  final List<BoardGame> boardGames;
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ListView.builder(
      itemCount: boardGames.length,
      itemBuilder: (context, index) {
        final boardGame = boardGames[index];
        return BoardGameItemWidget(boardGame: boardGame);
      },
    );
  }
}

class BoardGameItemWidget extends ConsumerWidget {
  const BoardGameItemWidget({Key? key, required this.boardGame})
      : super(key: key);
  final BoardGame boardGame;
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return GestureDetector(
      onTap: () {
        context.go('/game/details/${boardGame.idFromApi}');
      },
      child: Card(
        margin: const EdgeInsets.all(8),
        elevation: 8,
        child: Row(
          children: [
            Hero(
              tag: boardGame.title,
              child: CachedNetworkImage(
                imageUrl: boardGame.image,
                placeholder: (context, url) =>
                    const Center(child: CircularProgressIndicator()),
                errorWidget: (context, url, error) => const Icon(Icons.error),
                width: 100,
                height: 100,
                fit: BoxFit.cover,
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Container(
                      padding: const EdgeInsets.only(bottom: 8),
                      child: Text(boardGame.title,
                          style: const TextStyle(
                              fontWeight: FontWeight.bold, fontSize: 20))),
                ],
              ),
            )
          ],
        ),
      ),
    );
  }
}

Thanks !


Solution

  • I found the solution :

    My screen :

    class SearchGameScreen extends HookConsumerWidget {
      const SearchGameScreen({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final searchController = TextEditingController();
        AsyncValue<List<BoardGame>> search = ref.watch(boardGamesListControllerProvider);
    
        return Scaffold(
          body: Column(
            children: [
              Row(
                children: [
                  Expanded(
                    child: Container(
                      padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
                      margin: const EdgeInsets.only(bottom: 2),
                      child: TextFormField(
                        controller: searchController,
                        decoration: const InputDecoration(
                          border: OutlineInputBorder(),
                          labelText: 'Search a game',
                        ),
                      ),
                    ),
                  ),
                  Container(
                    height: 50,
                    padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
                    margin: const EdgeInsets.only(bottom: 2),
                    child: ElevatedButton(
                      child: const Text('Search',
                          style: TextStyle(color: Colors.white)),
                      onPressed: () {
                        ref.read(boardGamesListControllerProvider.notifier).search(searchController.text);
                      },
                    ),
                  ),
                ],
              ),
              Expanded(
                child: search.when(
                  data: (data) => BoardGamesList(boardGames: data),
                  loading: () => const Center(
                      child: CircularProgressIndicator(),
                  ),
                  error: (error, _) => const Center(
                    child: Text('Uh oh... Something went wrong...',
                        style: TextStyle(color: Colors.white)),
                  ),
                )
              )
            ],
          ),
        );
      }
    }
    

    My view model :

    final boardGamesListControllerProvider =
    StateNotifierProvider<BoardGameList, AsyncValue<List<BoardGame>>>((ref) {
      return BoardGameList(const AsyncValue.data([]), ref);
    });
    
    class BoardGameList extends StateNotifier<AsyncValue<List<BoardGame>>> {
      BoardGameList(AsyncValue<List<BoardGame>> items, this.ref) : super(items){
        init();
      }
    
      final Ref ref;
    
      Future<void> init() async {
        state = const AsyncValue.loading();
        try {
          final search = await ref.watch(boardGamesListProvider('').future);
          state = AsyncValue.data(search);
        } catch (e) {
          state = AsyncValue.error(e);
        }
      }
    
      Future<void> search(String request) async {
        state = const AsyncValue.loading();
        try {
          final search = await ref.watch(boardGamesListProvider(request).future);
          state = AsyncValue.data(search);
        } catch (e) {
          state = AsyncValue.error(e);
        }
      }
    }