Reputation: 2783
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
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 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;
}
}
...
}
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());
}
}
@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'),
),
),
],
),
);
}
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
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