Tutorial do Flutter: Recursos de pesquisa com o Riverpod

Tempo de leitura: 4 minutes

Olá a todos! Já faz algum tempo desde minha última postagem. Sim, em outras mãos, tenho tempo para compartilhar mais tutoriais. Espero que sejamos um povo que adora o processo de aprendizado. Em outras palavras, eu nunca perderia meu leitor, LOL. Estou brincando, pessoal! Muito bem, o que aprenderemos hoje? Ainda estamos aprendendo sobre o Riverpod, um dos incríveis gerenciadores de estado do Flutter. Acho que você já está familiarizado com esse incrível gerenciamento de estado. Se não estiver familiarizado com esse gerenciamento de estado, você pode ir até meu perfil e ler minha série sobre o Riverpod.

Muito bem, hoje vamos aprender a criar recursos de busca no Flutter usando o Riverpod, incrível, não é? Antes de mais nada, vamos instalar nosso incrível gerenciamento de estado: https://pub.dev/packages/flutter_riverpod. Muito bem, vamos pular para o tutorial de hoje!

Neste tutorial, eu usaria a API pública do reqres: https://reqres.in/api/users?page=2 para os nossos dados. Muito bem, vamos criar o arquivo provider.dart em nosso projeto. Nesse arquivo, eu usaria para obter dados da Internet usando o FutureProvider.

final getUserProvider = FutureProvider<Map<String, dynamic>>((ref) async {
  final response =
      await http.get(Uri.parse('https://reqres.in/api/users?page=2'));
  final jsonResponse = jsonDecode(response.body);
  return jsonResponse;
});

Incrível, acabamos de criar um provedor para buscar dados da Internet. Como você pode ver, acabei de criar o FutureProvider com o tipo Map<String, dyanmic>. Você pode substituir seu FutureProvider por qualquer tipo que desejar, como UserModel, por exemplo.

Muito bem, vamos associar nosso FutureProvider à nossa tela da interface do usuário. E vamos ver se o nosso FutureProvider funciona.

import 'package:belajar_tiktok/learn/riverpod/search_riverpod/providers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final getAllUser = ref.watch(getUserProvider);
    return GestureDetector(
      child: Scaffold(
          appBar: AppBar(
            title: const Text('Search Features Riverpod'),
          ),
          body: getAllUser.when(
            data: (data) => Form(
              child: Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    SafeArea(
                      child: TextFormField(
                        validator: (val) {
                          if (val == null || val.isEmpty) {
                            return 'Please enter some text';
                          }
                          return null;
                        },
                        onChanged: (value) {},
                        onEditingComplete: () {},
                        decoration: InputDecoration(
                          hintText: 'Search',
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(7),
                          ),
                        ),
                      ),
                    ),
                    const SizedBox(
                      height: 10,
                    ),
                    Expanded(
                        child: ListView.builder(
                      padding: EdgeInsets.zero,
                      itemCount: data['data'].length,
                      itemBuilder: (context, index) {
                        final user = data['data'][index];
                        return ListTile(
                          title: Text(
                              '${user['first_name']} ${user['last_name']}'),
                          subtitle: Text(user['email']),
                          leading: CircleAvatar(
                            maxRadius: 30,
                            backgroundImage: NetworkImage(user['avatar']),
                          ),
                        );
                      },
                    )),
                  ],
                ),
              ),
            ),
            error: (error, stackTrace) => const SizedBox(
              height: double.infinity,
              child: Center(
                child: Text('Something went wrong'),
              ),
            ),
            loading: () => const SizedBox(
              height: double.infinity,
              child: Center(
                child: CircularProgressIndicator(),
              ),
            ),
          )),
    );
  }
}

Parabéns! Nosso FutureProvider está funcionando totalmente! Com o Future Provider, não precisamos lidar com o estado de carregamento, erro ou sucesso. O provedor do futuro já o forneceu para nós. Fantástico.

Se você vir o nosso código, temos as funções onChange e onEditingCompleted. Porém, ambas as funções estão vazias. Portanto, vamos trabalhar primeiro com a função onChange. Na função onChange, quero analisar cada consulta ou valor que o usuário digitar no nosso campo de pesquisa ou no nosso campo de formulário de texto. Portanto, precisamos de uma variável global, nesse caso, um provedor global. Como? Muito simples, basta criar o provedor de estado em nosso arquivo providers.dart:

final searchUserProvider = StateProvider<String>((ref) => '');

Acima, acabamos de criar uma variável global que mudará seu valor sempre que o usuário digitar algo em nosso campo de pesquisa. Então, como alterar o valor do nosso provedor de estado? Bastante simples. Primeiro, vamos associar nosso provedor à nossa função onChange:

onChanged: (value) {
  ref
  .read(searchUserProvider.notifier).update((state) => state = value);
},

Podemos usar o método update que o riverpod nos forneceu para alterar o valor do provedor de estado. Assim, estamos apenas atualizando o valor do provedor de estado com base no texto que o usuário digita.

Vamos criar nosso método de pesquisa, que podemos usar em nosso onEditingCompleted. Primeiro, vamos criar um arquivo search_controller.dart. Em nosso search_controller.dart, podemos criar um notificador de estado para substituir os dados que recebemos antes de usar o futureprovider pelos dados que recebemos em nossa função de pesquisa.

import 'package:flutter_riverpod/flutter_riverpod.dart';

final searchControllerProvider =
    StateNotifierProvider<SearchUserController, List>((ref) {
  return SearchUserController();
});

class SearchUserController extends StateNotifier<List> {
  SearchUserController() : super([]);

  void onSearchUser(String query, List<dynamic> data) {
    state = [];
    if (query.isNotEmpty) {
      final result = data
          .where((element) => element['email']
              .toString()
              .toLowerCase()
              .contains(query.toString().toLowerCase()))
          .toSet()
          .toList();
      state.addAll(result);
    }
  }
}

Acima, acabei de criar uma classe chamada SearchUserController que estende um notificador de estado com o tipo List<dynamic>. Por que meu notificador de estado tem o tipo List<dynamic>? Porque quero substituir meus dados de usuário pela nova lista de usuários que obtive da função de pesquisa. Bastante simples, certo? Por fim, em nosso search_controller.dart, precisamos expor nossa classe SearchUserController com StateNotfierProvider. Muito bem, nossa função de pesquisa já está funcionando, vamos associá-la à nossa função onEditingCompleted.

onEditingComplete: () {
 ref.read(searchControllerProvider.notifier).onSearchUser(searchText, data['data']);
},

Se você se lembra, criamos uma variável global que armazena todas as consultas que o usuário digita em nosso campo de pesquisa. Essa é a variável que passarei para os parâmetros de nossa função onSearchUser acima. E o segundo parâmetro são os dados que obtivemos em nosso FutureProvider. Perfeito! Na verdade, nossa função de pesquisa está funcionando, mas, se você tentar, nada mudou em nossa tela, porque precisamos alterar os dados do usuário a cada resultado que a função onSearch retorna. Muito bem, vamos atualizar nossa lista de usuários:

Expanded(
  child: ListView.builder(
          padding: EdgeInsets.zero,
          itemCount: searchController.isNotEmpty
                    ? searchController.length
                    : data['data'].length,
          itemBuilder: (context, index) {
               final user = searchController.isNotEmpty
                     ? searchController[index]
                     : data['data'][index];
              return ListTile(
                     title: Text(
                        '${user['first_name']} ${user['last_name']}'),
                     subtitle: Text(user['email']),
                     leading: CircleAvatar(
                       maxRadius: 30,
                       backgroundImage: NetworkImage(user['avatar']),
                     ),
                   );
                 },
               )),

Acima, acabei de atualizar minha lista de usuários com verificação condicional. Talvez você esteja se perguntando o que é searchController on itemCount, que é a nossa classe que acabamos de criar em nosso providers.dart. Vamos declarar nosso SearchUserController:

final searchController = ref.watch(searchControllerProvider);

Portanto, se você executar seu código, poderá ver que nossa função de pesquisa com o riverpod funciona totalmente! Ótimo trabalho! Muito bem, por hoje é só. Espero que você possa implementar este tutorial em seu próprio projeto. Dê seu feedback batendo palmas ou comentando abaixo e nos vemos em outro tutorial!

Código Fonte completo do Projeto GitHub.