Flutter - RiverPod Newbie. How to watch StateNotifier in my view - flutter

I have a Recipe Repository that get recipes from FireStore
class RecipeRepository {
Future<List<Recipe>> readAll() async {
final snap = await _recipeRef.get();
return snap.docs.map((doc) => doc.data()).toList();
}
}
Here I'm returning the Repository as a Provider
final recipeRepositoryProvider =
Provider<RecipeRepository>((ref) => RecipeRepository());
Here I have a Class that I want to use to control the state of the UI
final recipeAsyncController =
StateNotifierProvider<RecipeAsyncNotifier, AsyncValue<List<Recipe>>>(
(ref) => RecipeAsyncNotifier(ref.read));
class RecipeAsyncNotifier extends StateNotifier<AsyncValue<List<Recipe>>> {
RecipeAsyncNotifier(this._read) : super(const AsyncLoading()) {
init();
}
final Reader _read;
init() async {
final recipes = await _read(recipeRepositoryProvider).readAll();
state = AsyncData(recipes);
}
}
As you can see I'm wrapping the recipeRepositoryProvider on a read.
In my UI I want to View the recipe list
return Consumer(
builder: (context, watch, child) {
return watch(recipeAsyncController).when();
},
);
The problem is I'm getting the following error.
When trying to access the when async call.

https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/Consumer-class.html
the second parameter in builder function is actually a ref object.
return Consumer(
builder: (context, ref, child) {
return ref.watch(recipeAsyncController).when();
},
);

Related

How to set multiple StateNotifierProvider (s) with dynamicaly loaded async data?

I'm completely stuck with the task below.
So, the idea is to solve these steps using Riverpod
Fetch data from db with some kind of Future async while pausing the app (display SomeLoadingPage() etc.)
Once the data has loaded:
2.1 initialize multiple global StateNotifierProviders which utilize the data in their constructors and can further be used throughout the app with methods to update their states.
2.2 then show MainScreen() and the rest of UI
So far I've tried something like this:
class UserData extends StateNotifier<AsyncValue<Map>> { // just <Map> for now, for simplicity
UserData() : super(const AsyncValue.loading()) {
init();
}
Future<void> init() async {
state = const AsyncValue.loading();
try {
final HttpsCallableResult response =
await FirebaseFunctions.instance.httpsCallable('getUserData').call();
state = AsyncValue.data(response.data as Map<String, dynamic>);
} catch (e) {
state = AsyncValue.error(e);
}}}
final userDataProvider = StateNotifierProvider<UserData, AsyncValue<Map>>((ref) => UserData());
final loadingAppDataProvider = FutureProvider<bool>((ref) async {
final userData = await ref.watch(userDataProvider.future);
return userData.isNotEmpty;
});
class LoadingPage extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
return FutureBuilder(
future: ref.watch(loadingAppDataProvider.future),
builder: (ctx, AsyncSnapshot snap) {
// everything here is simplified for the sake of a question
final Widget toReturn;
if (snap.connectionState == ConnectionState.waiting) {
toReturn = const SomeLoadingPage();
} else {
snap.error != null
? toReturn = Text(snap.error.toString())
: toReturn = const SafeArea(child: MainPage());
}
return toReturn;},);}}
I intentionally use FutureBuilder and not .when() because in future i may intend to use Future.wait([]) with multiple futures
This works so far, but the troubles come when I want to implement some kind of update() methods inside UserData and listen to its variables through the entire app. Something like
late Map userData = state.value ?? {};
late Map<String, dynamic> settings = userData['settings'] as Map<String, dynamic>;
void changeLang(String lang) {
print('change');
for (final key in settings.keys) {
if (key == 'lang') settings[key] = lang;
state = state.whenData((data) => {...data});
}
}
SomeLoadingPage() appears on each changeLang() method call.
In short:
I really want to have several StateNotifierProviders with the ability to modify their state from the inside and listen to it from outside. But fetch the initial state from database and make the intire app wait for this data to be fetched and these providers to be initilized.
So, I guess I figured how to solve this:
final futureExampleProvider = FutureProvider<Map>((ref) async {
final HttpsCallableResult response =
await FirebaseFunctions.instance.httpsCallable('getUserData').call();
return response.data as Map;
});
final exampleProvider = StateNotifierProvider<Example, Map>((ref) {
// we get AsyncValue from FutureNotifier
final data = ref.read(futureExampleProvider);
// and wait for it to load
return data.when(
// in fact we never get loading state because of FutureBuilder in UI
loading: () => Example({'loading': 'yes'}),
error: (e, st) => Example({'error': 'yes'}),
data: (data) => Example(data),
);
});
class LoadingPage extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
return FutureBuilder(
// future: ref.watch(userDataProvider.future),
future: ref.watch(futureExampleProvider.future),
builder: (ctx, AsyncSnapshot snap) {
final Widget toReturn;
if (snap.data != null) {
snap.error != null
? toReturn = Text(snap.error.toString())
: toReturn = const SafeArea(child: MainPage());
} else {
// this is the only 'Loading' UI the user see before everything get loaded
toReturn = const Text('loading');
}
return toReturn;
},
);
}
}
class Example extends StateNotifier<Map> {
Example(this.initData) : super({}) {
// here comes initial data loaded from FutureProvider
state = initData;
}
// it can be used further to refer to the initial data, kinda like cache
Map initData;
// this way we can extract any parts of initData
late Map aaa = state['bbb'] as Map
// this method can be called from UI
void ccc() {
// modify and update data
aaa = {'someKey':'someValue'};
// trigger update
state = {...state};
}
}
This works for me, at least on this level of complexity.
I'll leave question unsolved in case there are some better suggestions.

Flutter set state not updating my UI with new data

I have a ListView.builder widget wrapped inside a RefreshIndicator and then a FutureBuilder. Refreshing does not update my list, I have to close the app and open it again but the refresh code does the same as my FutureBuilder.
Please see my code below, when I read it I expect the widget tree to definitely update.
#override
void initState() {
super.initState();
taskListFuture= TaskService().getTasks();
}
#override
Widget build(BuildContext context) {
return Consumer<TaskData>(builder: (context, taskData, child) {
return FutureBuilder(
future: taskListFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
taskData.tasks = (snapshot.data as ApiResponseModel).responseBody;
return RefreshIndicator(
onRefresh: () async {
var responseModel = await TaskService().getTasks();
setState(() {
taskData.tasks = responseModel.responseBody;
});
},
child: ListView.builder(
...
...
Let me know if more code is required, thanks in advance!
Points
I am using a StatefulWidget
Task data is a class that extends ChangeNotifier
When I debug the refresh I can see the new data in the list, but the UI does not update
getTasks()
Future<ApiResponseModel> getTasks() async {
try {
var _sharedPreferences = await SharedPreferences.getInstance();
var userId = _sharedPreferences.getString(PreferencesModel.userId);
var response = await http.get(
Uri.parse("$apiBaseUrl/$_controllerRoute?userId=$userId"),
headers: await authorizeHttpRequest(),
);
var jsonTaskDtos = jsonDecode(response.body);
var taskDtos= List<TaskDto>.from(
jsonTaskDtos.map((jsonTaskDto) => TaskDto.fromJson(jsonTaskDto)));
return ApiResponseModel(
responseBody: taskDtos,
isSuccessStatusCode: isSuccessStatusCode(response.statusCode));
} catch (e) {
return null;
}
}
The issue here seems to be that you are updating a property that is not part of your StatefulWidget state.
setState(() {
taskData.tasks = responseModel.responseBody;
});
That sets a property part of TaskData.
My suggestion is to only use the Consumer and refactor TaskService so it controls a list of TaskData or similar. Something like:
Provider
class TaskService extends ChangeNotifier {
List<TaskData> _data;
load() async {
this.data = await _fetchData();
}
List<TaskData> get data => _data;
set data(List<TaskData> data) {
_data = data;
notifyListeners();
}
}
Widget
class MyTaskList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<TaskService>(builder: (context, service, child) {
return RefreshIndicator(
onRefresh: () {
service.getTasks();
},
child: ListView.builder(
itemCount: service.data.length,
itemBuilder: (BuildContext context, int index) {
return MyTaskItem(data:service.data[index]);
},
),
);
});
}
}
and make sure to call notifyListeners() in the service.getTasks() method to make the Consumer rebuild
I think (someone will correct me if I'm wrong) the problem is that you are using the FutureBuilder, once it's built, you need to refresh to whole widget for the FutureBuilder to listen to changes. I can suggest a StreamBuilder that listens to any changes provided from the data model/api/any kind of stream of data. Or better yet, you can use some sort of state management like Provider and use Consumer from the Provider package that notifies the widget of any changes that may occurred.

Getx is not working properly with FutureBuilder for update list

I'm using the Getx controller in my project. I have create the controller for FutureBuilder for displaying list but .Obs is not set on Future Function. I'm sharing the code.
class PPHomeController extends GetxController {
Future<List<PPProductRenterModel>> listNearProduct;
// i want to set .Obs end of the "listNearProduct" but it's not working because of Future.
FetchNearProductList({#required int price}) async {
listNearProduct = CallGetNearProducts();// Http API Result
}
}
{
PPHomeController _homeController = Get.put(PPHomeController());
Widget mainProductListView() {
return FutureBuilder<List<PPProductRenterModel>>
(builder: (context, AsyncSnapshot<List<PPProductRenterModel>> projectSnap){
if(!projectSnap.hasData){
if(projectSnap.connectionState == ConnectionState.waiting){
return Container(
child: Loading(),
);
}
}
return ListView.builder(
itemCount: projectSnap.data.length,
itemBuilder: (context, index) {
PPProductRenterModel model = projectSnap.data[index];
PPPrint(tag: "CheckId",value: model.productId);
return ProductMainItemRow(model);
});
},
future: _homeController.listNearProduct,);
There is a cleaner way for implementing List in GetX without worrying about Type-Casting:
Instantiate it:
final myList = [].obs;
Assign it:
myList.assignAll( listOfAnyType );
(Reference) Flutter error when using List.value :
'value' is deprecated and shouldn't be used. List.value is deprecated.
use [yourList.assignAll(newList)]. Try replacing the use of the
deprecated member with the replacement.
Detailed code example
ProductController.dart
class ProductController extends GetxController {
final productList = [].obs;
#override
void onInit() {
fetchProducts();
super.onInit();
}
void fetchProducts() async {
var products = await HttpServices.fetchProducts();
if (products != null) {
productList.assignAll(products);
}
}
}
HttpServices.dart
class HttpServices {
static var client = http.Client();
static Future<List<Product>> fetchProducts() async {
var url = 'https://link_to_your_api';
var response = await client.get(url);
if (response.statusCode == 200) {
return productFromJson(response.body);
} else {
return null;
}
}
}
product.dart
class Product {
Product({
this.id,
this.brand,
this.title,
this.price,
....
});
....
}
Form the docs:
3 - The third, more practical, easier and preferred approach, just add
.obs as a property of your value:
final items = <String>[].obs;
Following that instruction, this should work:
final listNearProduct = Future.value(<PPProductRenterModel>[]).obs;
E.g.:
// controller
final list = Future.value(<String>[]).obs;
#override
void onInit() {
super.onInit();
fetchList();
}
Future<List<String>> callApi() async {
await Future.delayed(Duration(seconds: 2));
return ['test'];
}
void fetchList() async {
list.value = callApi();
}
// screen
#override
Widget build(BuildContext context) {
return GetX<Controller>(
init: Controller(),
builder: (controller) {
return FutureBuilder<List<String>>(
future: controller.list.value,
builder: (context, snapshot) {
if (snapshot.hasData) {
print(snapshot.data[0]); // Output: test
return Text(snapshot.data[0]);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
);
},
);
};
You never actually call FetchNearProductList.
You need to call it in some place, preferably before the FutureBuilder uses that Future.

flutter_block: BlockProvider vs. RepositoryProvider

I want some clarification on BlocProvider and RepositoryProvider. From official API reference, they provide the same functionality - they provide an instance of an object to its descendant widgets.
However, judging by the names, I guess BlocProvider should be used for Bloc objects only, and RepositoryProvider for everything else. Is this correct?
RepositoryProvider acts like Repository pattern
It's Data Provider provide data to Bloc, so Bloc do not need to know data come from cloud or sqflite or ...
And do merge/filter, see official example below
In the following LoginForm example
Repository is part of Bloc and you can use _userRepository.login
class LoginFormBloc extends FormBloc<String, String> {
final emailField = TextFieldBloc(validators: [Validators.email]);
final passwordField = TextFieldBloc();
final UserRepository _userRepository;
LoginFormBloc(this._userRepository);
#override
List<FieldBloc> get fieldBlocs => [emailField, passwordField];
#override
Stream<FormBlocState<String, String>> onSubmitting() async* {
try {
_userRepository.login(
email: emailField.value,
password: passwordField.value,
);
yield currentState.toSuccess();
} catch (e) {
yield currentState.toFailure();
}
}
}
You can inject your Repository to Bloc
code snippet
class LoginForm extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider<LoginFormBloc>(
builder: (context) =>
LoginFormBloc(RepositoryProvider.of<UserRepository>(context)),
child: Builder(
builder: (context) {
final formBloc = BlocProvider.of<LoginFormBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Simple login')),
body: FormBlocListener<LoginFormBloc, String, String>(
onSubmitting: (context, state) => LoadingDialog.show(context),
onSuccess: (context, state) {
LoadingDialog.hide(context);
Navigator.of(context).pushReplacementNamed('success');
},
onFailure: (context, state) {
LoadingDialog.hide(context);
Notifications.showSnackBarWithError(
context, state.failureResponse);
},
you can reference https://flutterawesome.com/create-beautiful-forms-in-flutter/
for official example https://bloclibrary.dev/#/architecture
The bloc layer can depend on one or more repositories to retrieve data needed to build up the application state.
class Repository {
final DataProviderA dataProviderA;
final DataProviderB dataProviderB;
Future<Data> getAllDataThatMeetsRequirements() async {
final RawDataA dataSetA = await dataProviderA.readData();
final RawDataB dataSetB = await dataProviderB.readData();
final Data filteredData = _filterData(dataSetA, dataSetB);
return filteredData;
}
}
class BusinessLogicComponent extends Bloc<MyEvent, MyState> {
final Repository repository;
Stream mapEventToState(event) async* {
if (event is AppStarted) {
try {
final data = await repository.getAllDataThatMeetsRequirements();
yield Success(data);
} catch (error) {
yield Failure(error);
}
}
}
}
Simple Answer
Same as BlocProvider.
BlockProvider provides the bloc to its children through BlockProvider.of(context). In most cases, developers create a new BlocProvider and that gets available to the rest of the subtree.
Now come to RepositoryProvider
RepositoryProvider provides the repository to its children through RepositoryProvider.of(context). When developers create a new RepositoryProvider and that gets available to the rest of the subtree.

Flutter application freezes after notifyListeners

I'm trying to write a simple Flutter app using the Google Maps plugin. I need to use multiple BLoC/ChangeNotifier objects in order to manage the object shown on-screen.
The issue comes out when I call notifyListeners() on a ChangeNotifier. The method which calls notifyListeners() completes its execution, and then the app freezes completely (no widget update, unable to interact with existing widgets).
I've tried to understand where's the problem: the only thing I understood is that it works fine while CompaniesData (which is the ChangeNotifier that causes the problem) is empty.
class CompaniesData extends ChangeNotifier {
Map<MarkerId, Company> _companiesMap;
set companies(Set<Company> companies) {
_companiesMap = companies != null
? Map.fromIterable(
companies,
key: (company) => MarkerId(company.id.toString()),
value: (company) => company,
)
: null;
notifyListeners();
;
}
bool get available => _companiesMap != null;
Company companyWithId(MarkerId id) => available ? _companiesMap[id] : null;
Map<MarkerId, Company> get companiesIfAvailable =>
available ? _companiesMap : Map();
Iterable<Company> companiesFromIds(BuildContext context, Set<int> ids) {
Set<int> idsCopy = Set.from(ids);
return companiesIfAvailable.entries
.where((entry) => idsCopy.remove(entry.value.id))
.map<Company>((entry) => entry.value);
}
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Consumer<CompaniesData>(
builder: (context, data, child) {
return BlocBuilder(
bloc: BlocProvider.of<ShownCompaniesBloc>(context),
builder: (context, shownCompaniesState) {
return BlocBuilder(
bloc: BlocProvider.of<FavoriteCompaniesBloc>(context),
builder: (context, favoriteCompaniesState) {
return BlocBuilder(
bloc: BlocProvider.of<MapPropertiesBloc>(context),
builder: (context, mapPropertiesState) {
CompaniesData data =
Provider.of<CompaniesData>(context, listen: false);
// ...
As you can see, the build method contains multiple nested BLoC/Consumer objects.
#override
void initState() {
_fetchCompanies();
super.initState();
}
void _fetchCompanies() {
findUser().then((location) {
Set<Company> companies = Set.from([Company.fake()]);
// CompaniesData.companies is a setter, which calls
// notifyListeners
_companiesData.companies = companies;
});
}
I don't get error messages, exception, my app simply dies after the end of the execution of the callback given to findUser().then().
EDIT:
I changed the code a little bit, and I figured out that the problem isn't notifyListeners (or at least it isn't now).
final Completer<Map<MarkerId, Company>> _companiesData = Completer();
_AeroMainViewState() {
findUser()
.then(_fetchCompanies)
.then((companies) => _companiesData.complete(Map.fromIterable(
companies,
key: (company) => MarkerId(company.id.toString()),
value: (company) => company,
)));
}
Future<Set<Company>> _fetchCompanies(LatLng location) async =>
Set.from([Company.fake()]);
// ...
child: FutureBuilder<Map<MarkerId, Company>>(
future: _companiesData.future,
builder: (context, snapshot) {
// this builder function isn't called at all
// when the Completer _companiesData is completed
if (snapshot.connectionState == ConnectionState.done) {
return Provider<Map<MarkerId, Company>>.value(
value: snapshot.data,
child: // ...
} else {
return Center(child: CircularProgressIndicator());
}
}),
// ...
Removing the ChangeNotifier doesn't fix the issue.
I post my error for future reference. I was doing this in a class:
static Stream<Obj1> stream() async* {
while (true) {
yield Obj1();
}
}
_subscription = Obj1.stream().listen((event) {
// do something...
}
Since the Stream contains potentially an infinite number of objects, the subscription to that stream was blocking the main (and only) thread.