Fabio Ebner
Fabio Ebner

Reputation: 2783

Flutter Riverpod FutureProvider

I trying to use riverpod to do a search but without success, I create one FutureProvider and when I create my Widget I run watch a FutureProvider with a default value, so then I need to click in a button and execute the search again with another value. the logic work byt why the CircularProgressIndicator dont show when I click a button?

tks

my service

import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:i_condominio/models/Condominio.dart';

final condominioPRovider = Provider((ref) => CondominioService());

final condFuture =
    FutureProvider.autoDispose.family<List<Condominio>, String>((ref, arg) {
  final serviceProvider = ref.watch(condominioPRovider);
  return serviceProvider.findByCepFuture(arg);
});

class CondominioService {
  Future<List<Condominio>> findByCepFuture(String cepProcura) async {
    List<Condominio> listCondominio = [
      Condominio(
          nome: 'Porto panorama',
          cep: Cep(
              cep: "11075-350",
              logradouro: "Rua Monsenhor Paula Rodrigues",
              complemento: "",
              bairro: "Vila Belmiro",
              localidade: "Santos",
              uf: "SP",
              ibge: "3548500"),
          numero: "129"),
      Condominio(
          nome: 'Central Park',
          cep: Cep(
              cep: "11075-350",
              logradouro: "Rua Monsenhor Paula Rodrigues",
              complemento: "",
              bairro: "Vila Belmiro",
              localidade: "Santos",
              uf: "SP",
              ibge: "3548500"),
          numero: "126"),
      Condominio(
          nome: 'Sao Vicente park',
          cep: Cep(
              cep: "11380-120",
              logradouro: "Rua Monsenhor Paula Rodrigues",
              complemento: "",
              bairro: "Vila Belmiro",
              localidade: "Santos",
              uf: "SP",
              ibge: "3548500"),
          numero: "130")
    ];
    print('buscando $cepProcura');
    await Future.delayed(const Duration(seconds: 1));
    final retorno = listCondominio
        .where((element) => element.cep.cep == cepProcura)
        .toList();
    print('acabou ${retorno.length}');
    return Future.value(retorno);
  }
}

and my widget

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:i_condominio/models/Condominio.dart';
import 'package:i_condominio/services/CondominioService.dart';
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';

class SelecionarCondominioPage extends ConsumerWidget {
  const SelecionarCondominioPage({super.key});
  selecionarCondoninio(Condominio e) {
    print('condominio Selecionado ${e.nome}');
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final cf = ref.watch(condFuture('11380-120'));

    var maskFormatter = MaskTextInputFormatter(
        mask: '#####-###',
        filter: {"#": RegExp(r'[0-9]')},
        type: MaskAutoCompletionType.lazy);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Novo usuario'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              inputFormatters: [maskFormatter],
              decoration: const InputDecoration(
                labelText: 'CEP',
              ),
              keyboardType: TextInputType.number,
            ),
          ),
          const Padding(
            padding: EdgeInsets.all(8.0),
            child: Text(
              "Condominios encontrados",
              style: TextStyle(fontSize: 22),
            ),
          ),
          ElevatedButton(
            onPressed: () => ref.watch(condFuture('11075-350')),
            child: const Text('Buscar 11075-350'),
          ),
          cf.when(
              data: (data) => Expanded(
                    child: ListView(
                      children: [
                        ...data.map((e) => ListTile(
                            title: Text(e.nome),
                            subtitle: Text(e.enderecoCompleto),
                            onTap: () => showDialog(
                                  context: context,
                                  builder: ((ctx) => AlertDialog(
                                        title: const Text(
                                            'Condominio selecionado'),
                                        content: Text(
                                            'Confirma o condominio ${e.nome}'),
                                        actions: [
                                          const TextButton(
                                              onPressed: null,
                                              child: Text("OK")),
                                          TextButton(
                                              onPressed: () =>
                                                  Navigator.of(context).pop(),
                                              child: const Text("CANCELAR")),
                                        ],
                                      )),
                                )))
                      ],
                    ),
                  ),
              error: (error, n) => Text(error.toString()),
              loading: () => const Center(child: CircularProgressIndicator())),
        ],
      ),
    );
  }
}

Upvotes: 1

Views: 4200

Answers (2)

enchance
enchance

Reputation: 30501

You can use a button event to trigger a provider. The only difference is you don't use an async provider but instead use a notifier provider. This way you can call the provider only when you need it to run such as when a button is pressed.

Here's a snippet using Riverpod annotation while making a Firebase query.

Create the service

Create the service that will call all Firebase commands:

class MyService {

  ...

  static Future<Map<String, dynamic>?> myAsyncMethod() async {
    try {
      var db = FirebaseFirestore.instance;
      var ref = db.collection('MY-COLLECTION').doc('MY-DOC-ID');
      var snap = await ref.get();

      if(!snap.exists) return null;
      return snap.data();
    } catch (err, _) {
      print(err);
      rethrow;
    }
  }

  ...
}

Create the provider that uses the service

Once you have the service set up use the provider to set things up so you can monitor its loading state:

@riverpod
class MyNotifier extends _$MyNotifier {

  // We don't care about build() so just return an empty map
  @override
  FutureOr<Map<String, dynamic>> build() => {};

  // This is what matters to us
  Future<void> foo() async {
    // Set state as loading
    state = const AsyncLoading<Map<String, dynamic>>();

    // Will get the value or throw an error courtesy of `AsyncValue.guard()`
    state = await AsyncValue.guard<Map<String, dynamic>>(() => MyService.myAsyncMethod());
  }
}

Trigger it with a button

@override
Widget build(BuildContext context) {
  // We watch it but it doesn't do much because we know it's an empty map
  // We still need this so we can monitor its progress
  final asyncFoo = ref.watch(myNotifierProvider);


  return Scaffold(
    body: Column(
      children: [
        

        // Show different states of the button
        asyncFoo.maybeWhen(
          // This is why we set state to `AsyncLoading()`
          loading: () => ElevatedButton(
            onPressed: null,
            child: Text("Pls wait, I'm working..."),
          ),
          orElse: () => ElevatedButton(
            onPressed: () => ref.read(myNotifierProvider.notifier).foo(),
            child: Text('Click me'),
          ),
        ),

        
      ],
    ),
  );
}

Optional: Returning a value

Should MyService.foo() return a value and you need access to that value then all you have to do is create a listener to check if there is a value.

@override
Widget build(BuildContext context) {
  final asyncFoo = ref.watch(myNotifierProvider);

  // Listener that's waiting for a value
  ref.listen(myNotifierProvider, (prev, next) {
    // Check that it's not in the loading state and has a value
    if (!next.isLoading && next.hasValue) {
      // Here fooData has the value you need
      final fooData = next.value;
    }
  });

}

If you don't check for next.isLoading (which I initially didn't do) you'll end up with 2 values since the loading state will always precede the actual data. Basically, the first value is due to AsyncLoading() (which we don't want) and the other one is due to AsyncValue.data() (which we want).

Upvotes: 0

Kasymbek R. Tashbaev
Kasymbek R. Tashbaev

Reputation: 1473

In this case, it is better to use the StateNotifierProvider, as indicated in the documentation

FutureProvider does not offer a way of directly modifying the computation after a user interaction. It is designed to solve simple use-cases. For more advanced scenarios, consider using StateNotifierProvider.

Provider

final condominioNotifierProvider = StateNotifierProvider<CondominioNotifier,
    AsyncValue<List<Condominio>>>((ref) {
  final service = ref.watch(condominioPRovider);

  return CondominioNotifier(service);
});


class CondominioNotifier extends StateNotifier<AsyncValue<List<Condominio>>> {
  CondominioNotifier(this.service) : super(const AsyncValue.loading()) {
    load('11380-120');
  }

  final CondominioService service;

  Future<void> load(String name) async {
    state = AsyncValue.loading();

    try {
      final data = await service.findByCepFuture(name);
      state = AsyncValue.data(data);
    }  on Exception catch(e) {
      state = AsyncValue.error(e, StackTrace.current);
    }
  }
}

Widget

Widget build(BuildContext context, WidgetRef ref) {
    final cf = ref.watch(condominioNotifierProvider);
    ...
    ElevatedButton(
      onPressed: () => ref.read(condominioNotifierProvider.notifier).load('11075-350'),
      child: const Text('Buscar 11075-350'),
    ),
    ...
}

Upvotes: 0

Related Questions