Provider in `didChangeDependencies` does not update data - flutter

I'm initializing the data in my provider in didChangeDependencies in the parent widget.
#override
void didChangeDependencies() {
super.didChangeDependencies();
final provider = Provider.of<NewArrivalsProvider>(context);
FirebaseFirestore.instance.collection(CurrentUser.getCurrentUser().uid).doc('newArrivals').get().then(
(snapshot) {
Map<String, dynamic> data = snapshot.data();
provider.init(data);
},
);
}
Then updating the data in the child widget.
The change in the data is does not persist however.
Switch(
value: item.value,
onChanged: (state) => provider.update(key: item.key, state: state),
)
Only one switch changes value at a time.
class NewArrivalsProvider extends ChangeNotifier {
Map<String, dynamic> _items = {};
Map<String, dynamic> get items => _items;
int get length => _items.length;
void init(Map<String, dynamic> data) {
_items = data['mapUrls'];
}
void update({#required String key, #required bool state}) {
_items.update(key, (value) => value = state);
notifyListeners();
}
}
Since this is the first I used Provider in the didChangeDependencies method, I'm suspecting that's where the error is. Am I correct?

I solved it.
I changed the provider declaration in the didChangeDependencies to
final provider = Provider.of<NewArrivalsProvider>(context, listen: false);
When listen: true, this line listens to the changes I make to the data.
Then it downloads the data again from Firestore (which is set to false initially)
The change to listen: false makes line run only when the user navigates to the page and does not update every time I change the data

Related

Riverpod StateNotifier and a Stream

I've got a Stream<UserProfile> being returned form a firebase service.
I'm using MVVM architecture and have a ProfileViewModel which is extended by a freezed state class:
class ProfileModel extends StateNotifier<ProfileState> {
ProfileModel({
required this.authService,
required this.databaseService,
}) : super(const ProfileState.loading());
late AuthService authService;
late FirestoreDatabase databaseService;
Stream<UserProfile?> get userProfile {
return databaseService.profileStream();
}
}
The above results in the following view:
final profileModelProvider =
StateNotifierProvider.autoDispose<ProfileModel, ProfileState>((ref) {
final authService = ref.watch(authServiceProvider);
final databaseService = ref.watch(databaseProvider)!;
return ProfileModel(
authService: authService, databaseService: databaseService);
});
class ProfilePageBuilder extends ConsumerWidget {
const ProfilePageBuilder({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(profileModelProvider);
final model = ref.read(profileModelProvider.notifier);
final up = ref.read(userProfileProvider);
return ProfilePage(
onSubmit: () => {},
name: up.value?.uid ?? "Empty",
canSubmit: state.maybeWhen(
canSubmit: () => true,
success: () => true,
orElse: () => false,
),
isLoading: state.maybeWhen(
loading: () => true,
orElse: () => false,
),
errorText: state.maybeWhen(
error: (error) => error,
orElse: () => null,
),
);
}
}
I would like to know the correct way (using riverpod) to pass the firebase stream to the UI without mixing up UI/BL without loosing functionality of real time data.
I was able to create a StreamProvider which referenced the profile model but it doesnt feel right.
final userProfileProvider = StreamProvider.autoDispose<UserProfile?>((ref) {
return ref.watch(profileModelProvider.notifier).userProfile;
});
My alternative is to convert streams to futures within the view model and then update the state as the function runs.
I'm really quite stuck here, any help would be appreciated
My guess is you want to
listen to a stream from Firebase
When the latest value changes, you want any dependencies to update
You only want the latest value of the stream.
INTRODUCING BehaviorSubject!
You'll need package:rxdart though you may already have it installed.
import 'package:rxdart/rxdart.dart';
#riverpod
BehaviorSubject<ProfileState> userProfileSubject(
UserProfileSubjectRef ref) {
final stream = ....;
// Get the stream and wrap it in a BehaviorSubject
return BehaviorSubject()..addStream(stream);
}
#riverpod
UserProfile? userProfile(
UserProfileRef ref) {
final behaviorSubject = ref.watch(userProfileSubjectProvider);
// when the underlying stream updates,
// invalidate self so we read the new value
behaviorSubject.doOnData((newProfileState) { ref.invalidateSelf(); });
// note that value could be null while stream
// emits a value. You can wait for that
// and convert this provider to return a Future<UserProfile>
// or in the UI handle the null.
// note that firebase could also have a null value.
return behaviorSubject.value?.userProfile;
}

Blocbuilder not updating when list in map changes

I'm trying to trigger an update when a list in my map changes. Type is Map<String, List<int>>. Basically one of the integers is changing in the list but not triggering the blocbuilder. Although when I print the state the value is updated. I'm using freezed. From what I understand freezed only provides deep copies for nested #freezed objects but not for Iterables. I've seen a few solutions for this kind of problem. For example create a new Map with Map.from and emit that map. But that doesn't trigger a rebuild. Any suggestions!
My freezed state is
onst factory RiskAttitudeState.loaded({
required int customerId,
required RiskAttitudeQuestionsInfo riskAttitude,
required Map<String, List<int>> answerIds,
#Default(FormzStatus.pure) FormzStatus status,
int? finalRisk,
}) = RiskAttitudeLoaded;
And I'm updating an integer in the list type List<int> in the map answerIds
Here is the bloc
Future _mapAnswerToState(
String id, List<int> answerIds, Emitter<RiskAttitudeState> emit) async {
await state.maybeMap(
loaded: (RiskAttitudeLoaded loaded) async {
if (loaded.answerIds.containsKey(id)) {
loaded.answerIds.update(
id,
(_) => answerIds,
ifAbsent: () {
add(RiskAttitudeEvent.error(Exception('unknown Question ID: $id')));
return answerIds;
},
);
}
emit(loaded.copyWith(answerIds: loaded.answerIds));
},
orElse: () async {},
);
}
For contest if I pass an empty map like this emit(loaded.copyWith(answerIds:{}));
the builder gets triggered.
unfortunality i came accross this problem too. if your algorithm requires change one item of list maybe you can remove this item from your list and then change its properties. after that if you add the item to the list, builder will be triggered..
I tried a small code with cubit and Equatable and it worked. the key note is that you should override props method and add answerIds and other fields if exists to props and all fields must be final.
also notice to use Map<String, List<int>>.from to fill the map.
so the state class looks like this:
class UcHandleState extends Equatable {
final Map<String, List<int>> answerIds;
const UcHandleState({
required this.answerIds,
});
#override
List<Object> get props => [
answerIds,
];
UcHandleState copyWith({
Map<String, List<int>>? answerIds,
}) {
return UcHandleState(
answerIds: answerIds != null
? Map<String, List<int>>.from(answerIds)
: this.answerIds,
);
}
}
and a simple cubit class for managing events is like below. in valueChanged I'm just passing List<int>.
class TestCubit extends Cubit<TestState> {
TestCubit() : super(const TestState(answerIds: {'1': [1, 1]}));
void valueChanged(List<int> newValues ) {
Map<String, List<int>> test = Map<String, List<int>>.from(state.answerIds);
test['1'] = newValues;
emit(state.copyWith(
answerIds: test,
));
}
}
so in UI I call valueChanged() method of cubit:
cubit.valueChanged(newValues:[ Random().nextInt(50), Random().nextInt(70)]);
and the blocBuilder gets triggered:
return BlocBuilder<UcHandleCubit, UcHandleState>(
buildWhen: (prev, cur) =>
prev.answerIds!= cur.answerIds,
builder: (context, state) {
print(state.answerIds.values);
....

How to re-render a Widget based on another widget using riverpod in flutter?

I want to know how can I refresh a table data (which is fetched from an API using a future provider) and re-render the table widget based on dropdown value change.
Following is the Repo file with providers:
import 'package:ct_analyst_app/src/features/dashboard/domain/dashboard_data.dart';
import 'package:dio/dio.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../authentication/application/auth_local_service.dart';
abstract class IDashboardRepository {
Future<void> fetchDashboard(String name);
Future<void> fetchNames();
}
final clientProvider = Provider.family((ref, token) => Dio(BaseOptions(
baseUrl: "http://${dotenv.env['IP']}/excel/",
headers: {"authorization": token})));
class DashboardRepository implements IDashboardRepository {
DashboardRepository(this.read);
final Reader read;
DashboardData? _data;
DashboardData? get dashboardData => _data;
List<dynamic>? _names;
List<dynamic>? get names => _names;
#override
Future<DashboardData?> fetchDashboard(String name) async {
final token = await read(authServiceProvider).getToken();
final response = await read(clientProvider(token))
.get('/getData', queryParameters: {"name": name});
_data = DashboardData.fromJson(response.data);
print(name);
return _data;
}
#override
Future<void> fetchNames() async {
final token = await read(authServiceProvider).getToken();
final response = await read(clientProvider(token)).get('/analystNames');
_names = response.data["names"];
}
}
final dashboardRepositoryProvider =
Provider((ref) => DashboardRepository(ref.read));
final fetchDashboardData = FutureProvider.family<void, String>((ref, name) {
final repoProvider = ref.watch(dashboardRepositoryProvider);
return repoProvider.fetchDashboard(name);
});
final fetchAnalystNames = FutureProvider((ref) {
final repoProvider = ref.watch(dashboardRepositoryProvider);
return repoProvider.fetchNames();
});
I have tried to refresh the future provider in the dropdown onChange and it does fetch the new table data from the API. However, the widget which renders the data in the table is not getting re-rendered when the refresh is called.
Done as following:
onChanged: (String? newValue) {
ref.read(dropItemProvider.notifier).state = newValue as String;
ref.refresh(fetchDashboardData(newValue));
setState(() {
widget.value = newValue;
});
},
I am using ref.watch on the data, still it does not re-render the widget if the data is changed.
class TableGenerator extends ConsumerWidget {
const TableGenerator({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dashboardRepositoryProvider);
return data.dashboardData != null
? SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Row(
children: [
const FixedColumnWidget(data: [
"one",
"two",
"three",
"four",
"fifth",
]),
ScrollableColumnWidget(
data: data.dashboardData as DashboardData)
],
))
: const CircularProgressIndicator();
}
}
Am I missing something or how should I approach this problem? like different providers or something else?
Thanks!
Your Widget is watching dashboardRepositoryProvider, which doesn't update after the ref.refresh call.
There's two things to consider:
dashboardRepository should just expose your repo / services, and instead it is used to observe actual data. This is not affecting your app directly, but it is part of the problem imho. I'd expect your Widget to observe a FutureProvider that exposes (and caches, etc.) the data by calling the methods inside your repository;
Then, let's analyze why your Widget isn't updating: dashboardRepository isn't depending, i.e. performing a watch, on the Provider you're refreshing, which is fetchDashboardData, nor it is depending on dropItemProvider (I am specifying this since your onChanged callback updates / refreshes two different Providers).
I think your should refactor your code so that it will expose actual data from a FutureProvider which exploits your repositories and can be simply refreshed similarly as what you already are doing.
Quick FutureProvider example:
// WARNING: PSEUDOCODE
final myDataProvider = FutureProvider<MyClass>((ref) {
final repo = ref.watch(myRepo);
final response = repo.getSomeData(...);
// TODO: add error handling, debouncing, cancel tokens, etc.
return MyClass.fromJson(response.data); // e.g.
});
Quick usage:
// WARNING: PSEUDOCODE
#override
Widget build(BuildContext context, WidgetRef ref) {
final myData = ref.watch(myDataProvider);
return ElevatedButton(
onTap: () {
ref.refresh(myDataProvider);
},
child: Text("Click me to refresh me (data: $myData)"),
);
}

Displaying CircularProgressIndicator() will not stop after API call is completed

I'm attempting to have a CircularProgressIndicator display while the API call is made. When navigating to the OrdersScreen the CircularProgressIndicator displays and does not stop.
When clicking on the error it is directing me to my catch in my try{} catch{} block in my API call.
Here is the error I'm encountering:
I/flutter (22500): Invalid argument(s) (input): Must not be null
E/flutter (22500): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: Invalid argument(s) (input): Must not be null
[38;5;248mE/flutter (22500): #0 Orders.getOrders[39;49m
E/flutter (22500): <asynchronous suspension>
[38;5;248mE/flutter (22500): #1 _OrdersScreenState.initState.<anonymous closure> (package:shop_app/screens/order_screen.dart)[39;49m
E/flutter (22500): <asynchronous suspension>
E/flutter (22500):
Here is my API call:
class Orders with ChangeNotifier {
List<OrderItem> _orders = [];
List<OrderItem> get orders {
return [..._orders];
}
//make a copy of private class _orders
//establishing so that we cannot modify the private class
//READ API call
Future<void> getOrders() async {
final url = Uri.https(
'shop-app-flutter-49ad1-default-rtdb.firebaseio.com', '/products.json');
//note that for the post URL when using this https package we had to remove the special characters (https://) in order to properly post via the API
//establish the URL where the API call will be made
try {
final response = await http.get(url);
// print(json.decode(response.body));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
//retrieve the json response data stored in firebase, translate to a Map, and store that map in the jsonResponse variable
if (jsonResponse == null) {
return;
}
//if there is no data returned in the jsonResponse (the db is empty) then we do nothing, avoiding an app crash on an empty API call
final List<OrderItem> orderProducts = [];
//establish an empty list in preparation to store the new Order values retrieved from the API call
jsonResponse.forEach((orderID, orderData) {
//forEach will exectue a function on every value that is housed within that Map
orderProducts.insert(
0, //insert at index 0 inserts the newest added product at the beginning of the list
OrderItem(
id: orderID,
amount: orderData['amount'],
dateTime: DateTime.parse(orderData['dateTime']),
products: (orderData['products'] as List<dynamic>)
.map(
(item) => CartItem(
id: item['id'],
title: item['title'],
quantity: item['quantity'],
price: item['price'],
),
)
.toList(),
//since products is stored on the db as a map, we have to retrieve those values and define how the properties of the items stored in the db should be mapped --> recreating our CartItem as it's stored in the db
));
//retrieve the values for each of the given properties and Map them according to the values stored on the server
});
_orders = orderProducts;
notifyListeners();
//set the value of the _items list - that is the primary data of the ProductsProvider to tell the different areas of the app the data to show - equal to the values retrieved from the API call
} catch (error) {
print(error);
throw (error);
}
}
}
Code with CircularProgressIndicator:
class OrdersScreen extends StatefulWidget {
static const routeName = '/orders';
#override
_OrdersScreenState createState() => _OrdersScreenState();
}
class _OrdersScreenState extends State<OrdersScreen> {
bool _isLoading = false;
#override
void initState() {
setState(() {
_isLoading = true;
});
// when the state of the screen is initialized set the value of _isLoading to true
// by setting _isLoading to true we are establishing another state while the API call is being made
Provider.of<Orders>(context, listen: false).getOrders().then((_) {
setState(() {
_isLoading = false;
});
});
// we are making the API call and then setting the state of _isLoading back to false indicating the change of the _isLoading variable means a completed API call
// --> by changing the value of _isLoading prior to and after the API call it allows us to put additional functionality while the API call is made --> we established a CircularProgressIndicator which may be found in the body
super.initState();
}
#override
Widget build(BuildContext context) {
final orderData = Provider.of<Orders>(context);
return Scaffold(
appBar: AppBar(
title: Text('Your Order'),
),
body: _isLoading == true
? Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).primaryColor),
)
: ListView.builder(
itemCount: orderData.orders.length,
itemBuilder: (ctx, index) => OrderCard(
order: orderData.orders[index],
),
//populate the order card UI element with data provided by the orders method within orders.dart
//this data is retrieved by calling the provider of type orders
),
drawer: SideDrawer(),
);
}
}
For reference:
OrderItem:
class OrderItem {
OrderItem({
#required this.id,
#required this.amount,
#required this.products,
#required this.dateTime,
});
final String id;
final double amount;
final List<CartItem> products; //CartItem from cart.dart
final DateTime dateTime;
}
CartItem:
class CartItem {
CartItem({
#required this.id,
#required this.title,
#required this.quantity,
#required this.price,
});
final String id;
final String title;
final int quantity;
final double price;
}
To fully take advantage of the Provider you already have setup, you should make the body of your scaffold a Consumer<Orders> widget. Keep the same logic inside, but it would need to be based on a bool (initialized to true) that lives within the Orders class.
Consumer<Orders>(builder: (context, orderData, child) {
return orderData.isLoading == true
? Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).primaryColor),
)
: ListView.builder(
itemCount: orderData.orders.length,
itemBuilder: (ctx, index) => OrderCard(
order: orderData.orders[index],
),
//populate the order card UI element with data provided by the orders method within orders.dart
//this data is retrieved by calling the provider of type orders
);
});
Handle the value of isLoading in your getOrders() function and that will notify the Consumer<Orders> widget to either return a CircularProgressIndicator or the ListView.builder once isLoading is updated to false.
You still call that function in initState but the local bool in that class would go away.

How do I get mobx to update when I change a property in an ObservableList?

I currently have been using mobx for my flutter app, and I'm trying to update a ListTile to change it's colour onTap. Right now I have I have an ObservableList marked with #observable, and an #action that changes a property on an item in that list.
class TestStore = TestStoreBase with _$TestStore;
abstract class TestStoreBase with Store {
final DataService _dataService;
TestStoreBase({
#required DataService dataService,
}) : assert(dataService != null),
_dataService = dataService,
players = ObservableList<Player>();
#observable
ObservableList<Player> players;
#action
Future<void> loadPlayers(User user) async {
final userPlayers = await _dataService.getUserPlayers(user);
players.addAll(userPlayers);
}
#action
void selectPlayer(int index) {
players[index].isSelected = !players[index].isSelected;
);
}
}
in my UI I have this inside of a listbuilder:
return Observer(builder: (_) {
return Container(
color: widget.testStore.players[index].isSelected != null &&
widget.testStore.players[index].isSelected
? Colors.pink
: Colors.transparent,
child: ListTile(
leading: Text(widget.testStore.players[index].id),
onTap: () => widget.testStore.selectPlayer(index),
),
);
});
but it doesn't redraw when I call widget.testStore.selectPlayer(index);
The second thing I tried was to add #observable in the 'Players' class on the isSelected bool, but it doesn't seem to work either.
#JsonSerializable()
class Player {
final String id;
final bool isUser;
#observable
bool isSelected;
Player(this.id, this.isUser, this.isSelected);
factory Player.fromJson(Map<String, dynamic> data) => _$PlayerFromJson(data);
Map<String, dynamic> toJson() => _$PlayerToJson(this);
}
any help would be greatly appreciated, thanks!
Your are trying to take actions on the isSelected property, so basically you have to define the Player class as a MobX store as well to create a mixin that triggers reportWrite() on modifying isSelected.
Adding #observable annotation to players property only means to watch on the property itself, and typing players as a ObservableList means to watch on the list elements of the property, i.e. to watch on players[0], players[1]...and so on.
For example
#JsonSerializable()
class Player = _Player with _$Player;
abstract class _Player with Store {
final String id;
final bool isUser;
#observable
bool isSelected;
_Player(this.id, this.isUser, this.isSelected);
factory _Player.fromJson(Map<String, dynamic> data) => _$PlayerFromJson(data);
Map<String, dynamic> toJson() => _$PlayerToJson(this);
}
Here is a similar issue from MobX's GitHub repo: https://github.com/mobxjs/mobx.dart/issues/129