My app starts with retrieving data that is important throughout the flow of the app mainContent. Most of this data is static
Navigation screens
Widget stackPages(WidgetRef ref) {
AsyncValue<Map<String, Object>> mainContent = ref.watch(mainContentFutureProvider);
return mainContent.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (e, st) => Center(child: Text("Error: " + e.toString() + " " + st.toString())),
data: (content) {
return Stack(
children: [
_buildOffstageNavigator(ref, "Home", content),
_buildOffstageNavigator(ref, "Page1", content),
_buildOffstageNavigator(ref, "Page2", content),
_buildOffstageNavigator(ref, "Page3", content)
],
);
},
);
}
content retrieval (mainContentFutureProvider)
final mainContentFutureProvider= FutureProvider<Map<String, Object>>((ref) async {
List response = await Future.wait([
DataController.userInfoDB.getUsers(),
DataController.userInfoDB.getAnotherList(),
DataController.userInfoDB.getAnotherList,
]);
return {
"users": response[0],
"some_list": response[1],
"some_list": response[2],
};
},
);
User class (simplified)
class User{
String id;
String email;
List<Vehicle> vehicles = [];
User(this.email, this.vehicles);
User.fromJson(Map<String, dynamic> json)
: id = json['id'],
displayName = json['display_name'],
}
problem
in the garage screen of the app the user can add or remove vehicles. When a user adds or removes a vehicle this affects the entire flow of the app. So this User needs to have its Notifier class
CurrentUserNotifier
class CurrentUserNotifier extends StateNotifier<User> {
final User user;
CurrentUserNotifier(this.user) : super(null);
void addUserVehicle(Vehicle vehicle) {
state..vehicles.add(vehicle);
}
void removeUserVehicle(int vehicleId) {
state..vehicles.removeWhere((v) => v.id == vehicleId);
}
}
currentUserProvider
final currentUserProvider = StateNotifierProvider.family<CurrentUserNotifier, User, User>((ref, user) {
return CurrentUserNotifier(user);
});
Currently I am retrieving a List<User> and want only to have the current user to be coming from a provider in my app. As you see I have made a .family from StateNotifierProvider so I can perform the following thing:
content retrieval (mainContentFutureProvider)
final mainContentFutureProvider= FutureProvider<Map<String, Object>>((ref) async {
List response = await Future.wait([
DataController.userInfoDB.getUsers(),
DataController.userInfoDB.getAnotherList(),
DataController.userInfoDB.getAnotherList,
]);
---> currentUserProvider(response[0].first);
return {
"users": response[0],
"some_list": response[1],
"some_list": response[2],
};
},
);
But for any page that deals with my User object it needs to pass through the user object as parameter to my currentUserProvider
like:
press: () async {
ref.read(currentUserProvider(user).notifier).addUserVehicle(vehicle);
}
I want the provider just set the value of the StateNotifierProvider once, am I making a pattern/flow mistake here?
Try this:
Have your CurrentUserNotifier like so.
final currentUserProvider = StateNotifierProvider<CurrentUserNotifier, User>((ref) {
return CurrentUserNotifier();
});
class CurrentUserNotifier extends StateNotifier<User?> {
CurrentUserNotifier() : super(null);
void setUser(User user){
state = user;
}
void addUserVehicle(Vehicle vehicle) {
state = state..vehicles.add(vehicle);
}
void removeUserVehicle(int vehicleId) {
state = state..vehicles.removeWhere((v) => v.id == vehicleId);
}
}
Then set the user like so:
final mainContentFutureProvider= FutureProvider<Map<String, Object>>((ref) async {
List response = await Future.wait([
DataController.userInfoDB.getUsers(),
DataController.userInfoDB.getAnotherList(),
DataController.userInfoDB.getAnotherList,
]);
ref.read(currentUserProvider.notifier).setUser(response[0].first);
return {
"users": response[0],
"some_list": response[1],
"some_list": response[2],
};
},
);
Then you can do:
press: () async {
ref.read(currentUserProvider.notifier).addUserVehicle(vehicle);
}
Related
I have a simple controller like this
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
user = user;
print(user.sId);
notifyListeners();
}
login(data) async {
var response = await ApiService().login(data);
final databody = json.decode(response);
if (databody['success']) {
UserData authUser = UserData.fromJson(databody['data']);
setUser(authUser);
notifyListeners();
return true;
} else {
return false;
}
}
}
I am trying to just print it like this on both widget and in initstate function but values are showing null. I can see in set function value is not null.
print('id ${context.watch<UserController>().user.sId.toString()}');
print(
'id2 ${Provider.of<UserController>(context, listen: false).user.sId.toString()}');
I already have added
ChangeNotifierProvider(create: (_) => UserController()),
],
in main.dart in MultiProvider
Also on Tap of login button I am doing this
showLoader(context);
UserController auth = Provider.of<UserController>(
context,
listen: false);
var data = {
"userEmail":
emailController.text.trim().toLowerCase(),
"userPassword": passwordController.text.trim(),
};
auth.login(data).then((v) {
if (v) {
hideLoader(context);
context.go('/homeroot');
} else {
hideLoader(context);
Fluttertoast.showToast(
backgroundColor: green,
textColor: Colors.white,
msg:
'Please enter correct email and password');
}
});
Try to include this while naming is same,
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Follow this structure
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Future<bool> login(String data) async {
await Future.delayed(Duration(seconds: 1));
UserData authUser = UserData(sId: data);
setUser(authUser);
notifyListeners();
return true;
}
}
class HPTest extends StatelessWidget {
const HPTest({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<UserController>(
builder: (context, value, child) {
return Text(value.user.sId);
},
),
floatingActionButton: FloatingActionButton(onPressed: () async {
final result = await Provider.of<UserController>(context, listen: false)
.login("new ID");
print("login $result");
;
}),
);
}
}
I have a scenario, where I want to change state of loading class while I load my data on screen. So for that I am trying to switch the initial state of a provider from another provider but throws me an error. "Providers are not allowed to modify other providers during their initialisation." I need to know the very best practice to handle this kind of scenarios. My
classes are as follow:
class CleansingServices extends StateNotifier<List<CleansingBaseModel>> {
CleansingServices() : super([]);
void setServices(List<CleansingBaseModel> data) {
state = data;
}
}
final cleansingServicesProvider = StateNotifierProvider<CleansingServices, List<CleansingBaseModel>>((ref) {
final data = ref.watch(loadServicesProvider);
final dataLoading = ref.watch(cleansingLoadingStateProvider.notifier);
data.when(
data: (data) {
ref.notifier.setServices(data);
dataLoading.setNotLoading();
},
error: (error, str) {
dataLoading.setNotLoadingWithError(error);
},
loading: () {
dataLoading.setLoading();
},
);
return CleansingServices();
});
class CleansingServices extends StateNotifier<List<CleansingBaseModel>> {
var data ;
CleansingServices(this.data) : super([]){
data.when(
data: (data) {
ref.notifier.setServices(data);
dataLoading.setNotLoading();
},
error: (error, str) {
dataLoading.setNotLoadingWithError(error);
},
loading: () {
dataLoading.setLoading();
},
);
}
void setServices(List<CleansingBaseModel> data) {
state = data;
}
}
final cleansingServicesProvider = StateNotifierProvider<CleansingServices, List<CleansingBaseModel>>((ref) {
final data = ref.watch(loadServicesProvider);
final dataLoading = ref.watch(cleansingLoadingStateProvider.notifier);
return CleansingServices(data );
});
I have implemented StateNotifierProvider with ".family" modifier:
class OrderReviewNotifier extends StateNotifier<OrderReviewState> {
final OrderReviewRepository repository;
OrderReviewNotifier(
this.repository,
int orderId,
) : super(OrderReviewState.initial(orderId));
Future<void> getOrderItems() async {
//.....
}
}
final orderReviewProvider = StateNotifierProvider.autoDispose
.family<OrderReviewNotifier, OrderReviewState, int>(
(ref, orderId) {
return OrderReviewNotifier(
ref.watch(orderReviewRepositoryProvider),
orderId,
);
},
);
Then in Consumer I watch it:
Consumer(
builder: (context, watch, child) {
final state = watch(orderReviewProvider(order.id));
//.....
},
);
But when I want to read it, I need to pass order.id too:
onTap: () {
context
.read(orderReviewProvider(order.id).notifier)
.getOrderItems();
},
When I want to send events to notifier from another file, I don't have order.id.
How to get out of this situation?
Thanks for any help!
I figured out.
All I needed was StateProvider.
final selectedOrderProvider = StateProvider<Order?>((ref) => null);
Then in orderReviewProvider I can easily get orderId.
final orderReviewProvider =
StateNotifierProvider.autoDispose<OrderReviewNotifier, OrderReviewState>(
(ref) {
return OrderReviewNotifier(
ref.read,
orderId: ref.watch(selectedOrderProvider).state!.id,
repository: ref.watch(orderReviewRepositoryProvider),
);
},
);
class OrderReviewNotifier extends StateNotifier<OrderReviewState> {
OrderReviewNotifier(
this.read, {
required int orderId,
required this.repository,
}) : super(OrderReviewState.initial(orderId)) {
getOrderItems();
}
final Reader read;
final OrderReviewRepository repository;
Future<void> getOrderItems() async {
state = state.copyWith(
isLoading: true,
error: null,
);
final result = await repository.getOrderItems(state.orderId);
final checkedItemIds = await repository.getCheckedItemIds(state.orderId);
if (!mounted) {
return;
}
result.when(
data: (data) {
final isAllItemsChecked = !checkedItemIds.containsValue(false) &&
checkedItemIds.length >= data.length;
state = state.copyWith(
orderItems: data,
checkedItemIds: checkedItemIds,
isAllItemsChecked: isAllItemsChecked,
);
},
error: (message) {
state = state.copyWith(
error: message,
);
},
);
state = state.copyWith(
isLoading: false,
);
}
}
The documentation describes the work with this well: link.
I using Flutter Riverpod package to handling http request. I have simple Http get request to show all user from server, and i using manage it using FutureProvider from Flutter Riverpod package.
API
class UserGoogleApi {
Future<List<UserGoogleModel>> getAllUser() async {
final result = await reusableRequestServer.requestServer(() async {
final response =
await http.get('${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getAllUser');
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
}
User Provider
class UserProvider extends StateNotifier<UserGoogleModel> {
UserProvider([UserGoogleModel state]) : super(UserGoogleModel());
Future<UserGoogleModel> searchUserByIdOrEmail({
String idUser,
String emailuser,
String idOrEmail = 'email_user',
}) async {
final result = await _userGoogleApi.getUserByIdOrEmail(
idUser: idUser,
emailUser: emailuser,
idOrEmail: idOrEmail,
);
UserGoogleModel temp;
for (var item in result) {
temp = item;
}
state = UserGoogleModel(
idUser: temp.idUser,
createdDate: temp.createdDate,
emailUser: temp.emailUser,
imageUser: temp.emailUser,
nameUser: temp.nameUser,
tokenFcm: temp.tokenFcm,
listUser: state.listUser,
);
return temp;
}
Future<List<UserGoogleModel>> showAllUser() async {
final result = await _userGoogleApi.getAllUser();
state.listUser = result;
return result;
}
}
final userProvider = StateNotifierProvider((ref) => UserProvider());
final showAllUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.showAllUser();
return result;
});
After that setup, i simply can call showAllUser like this :
Consumer((ctx, read) {
final provider = read(showAllUser);
return provider.when(
data: (value) {
return ListView.builder(
itemCount: value.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final result = value[index];
return Text(result.nameUser);
},
);
},
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error $error'),
);
}),
it's no problem if http request don't have required parameter, but i got problem if my http request required parameter. I don't know how to handle this.
Let's say , i have another http get to show specific user from id user or email user. Then API look like :
API
Future<List<UserGoogleModel>> getUserByIdOrEmail({
#required String idUser,
#required String emailUser,
#required String idOrEmail,
}) async {
final result = await reusableRequestServer.requestServer(() async {
final baseUrl =
'${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getUserByIdOrEmail';
final chooseURL = idOrEmail == 'id_user'
? '$baseUrl?id_or_email=$idOrEmail&id_user=$idUser'
: '$baseUrl?id_or_email=$idOrEmail&email_user=$emailUser';
final response = await http.get(chooseURL);
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
User Provider
final showSpecificUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.searchUserByIdOrEmail(
idOrEmail: 'id_user',
idUser: usrProvider.state.idUser, // => warning on "state"
);
return result;
});
When i access idUser from userProvider using usrProvider.state.idUser , i got this warning.
The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'.
It's similiar problem with my question on this, but on that problem i already know to solved using read(userProvider.state) , but in FutureProvider i can't achieved same result using ref(userProvider).
I missed something ?
Warning: This is not a long-term solution
Assuming that your FutureProvider is being properly disposed after each use that should be a suitable workaround until the new changes to Riverpod are live. I did a quick test to see and it does work. Make sure you define a getter like this and don't override the default defined by StateNotifier.
class A extends StateNotifier<B> {
...
static final provider = StateNotifierProvider((ref) => A());
getState() => state;
...
}
final provider = FutureProvider.autoDispose((ref) async {
final a = ref.read(A.provider);
final t = a.getState();
print(t);
});
Not ideal but seems like a fine workaround. I believe the intention of state being inaccessible outside is to ensure state manipulations are handled by the StateNotifier itself, so using a getter in the meantime wouldn't be the end of the world.
from this link on my web server as
http://instamaker.ir/api/v1/getPersons
i'm trying to get result and printing avatar from that result, unfortunately my implementation with rxDart and Bloc don't get result from this response and i don't get any error
server response this simplified result:
{
"active": 1,
"name": "my name",
"email": " 3 ",
"loginType": " 3 ",
"mobile_number": " 3 ",
...
"api_token": "1yK3PvAsBA6r",
"created_at": "2019-02-12 19:06:34",
"updated_at": "2019-02-12 19:06:34"
}
main.dart file: (click on button to get result from server)
StreamBuilder(
stream: bloc.login,
builder: (context,
AsyncSnapshot<UserInfo>
snapshot) {
if (snapshot.hasData) {
parseResponse(snapshot);
}
},
);
void parseResponse(AsyncSnapshot<UserInfo> snapshot) {
debugPrint(snapshot.data.avatar);
}
LoginBlock class:
class LoginBlock{
final _repository = Repository();
final _login_fetcher = PublishSubject<UserInfo>();
Observable<UserInfo> get login=>_login_fetcher.stream;
fetchLogin() async{
UserInfo userInfo = await _repository.userInfo();
_login_fetcher.sink.add(userInfo);
}
dispose(){
_login_fetcher.close();
}
}
final bloc = LoginBlock();
Repository class:
class Repository {
final userInformation = InstagramApiProviders();
Future<UserInfo> userInfo() => userInformation.checkUserLogin();
}
my model:
class UserInfo {
int _active;
String _name;
...
UserInfo.fromJsonMap(Map<String, dynamic> map)
: _active = map["active"],
_name = map["name"],
...
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['active'] = _active;
data['name'] = _name;
...
return data;
}
//GETTERS
}
BaseUrl class:
class BaseUrl {
static const url = 'http://instamaker.ir';
}
and then InstagramApiProviders class:
class InstagramApiProviders {
Client client = Client();
Future<UserInfo> checkUserLogin() async {
final response = await client.get(BaseUrl.url+'/api/v1/getPersons');
print("entered "+BaseUrl.url+'/api/v1/getPersons');
if (response.statusCode == 200) {
return UserInfo.fromJsonMap(json.decode(response.body));
} else
throw Exception('Failed to load');
}
}
Well the answer here is part of the test that I make to get this done. I can put my all test here but I think that the problem cause was because as StreamBuilder is a widget his builder method callback is only called when the widget is in flutter widget tree. As in your sample you're just creating a StreamBuilder the builder method will never be called bacause this widget isn't in widget tree.
As advice first test your code changing only UI layer... do somenthing like:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
IconButton(icon: Icon(Icons.assessment), onPressed: () => loginBlock.fetchLogin()),
],
),
body: StreamBuilder<UserInfo>(
stream: loginBlock.login,
builder: (context, snapshot){
if (snapshot.hasData){
parseResponse(snapshot);
return Text('user: ${snapshot.data.name} ');
}
if (snapshot.hasError)
return Text('${snapshot.error}');
else return Text('There is no data');
},
),
);
Here we're putting the StreamBuilder in widget tree so the builder callback is called and maybe you will see the results. If it fails, please comment that I update my answer with my full test code with this working.
Updating the answer with sources that I made tests.
Basic model
class UserInfo {
int _active;
String name;
UserInfo.fromJsonMap(Map<String, dynamic> map) {
_active = map["active"];
name = map["name"];
}
Map<String, dynamic> toJson() => {
'active' : _active,
'name' : name,
};
}
The provider class
class InstagramApiProviders {
Future<UserInfo> checkUserLogin() async {
UserInfo info;
try {
http.Response resp = await http.get("http://instamaker.ir/api/v1/getPersons");
if (resp.statusCode == 200){
print('get response');
print( resp.body );
info = UserInfo.fromJsonMap( Map.from( json.decode(resp.body ) ));
}
}
catch (ex) {
throw ex;
}
print('returning $info');
return info;
}
}
Repository
class Repository {
final userInformation = InstagramApiProviders();
Future<UserInfo> userInfo() => userInformation.checkUserLogin().then((user) => user);
}
BLoC class
class LoginBlock{
final _repository = Repository();
final _login_fetcher = PublishSubject<UserInfo>();
Observable<UserInfo> get login=>_login_fetcher.stream;
fetchLogin() async {
UserInfo info = await _repository.userInfo();
_login_fetcher.sink.add(info);
}
dispose(){
_login_fetcher.close();
}
}
Widget UI
This starts showing There is no data message but when you hit appBar button wait a little and then the data is fetched and updates the UI.
class WidgetToShowData extends StatelessWidget {
final LoginBlock bloc = LoginBlock();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
IconButton(icon: Icon(Icons.assessment), onPressed: () => loginBlock.fetchLogin()),
],
),
body: StreamBuilder<UserInfo>(
stream: loginBlock.login,
builder: (context, snapshot){
if (snapshot.hasData){
parseResponse(snapshot);
return Text('user: ${snapshot.data.name} ');
}
if (snapshot.hasError)
return Text('${snapshot.error}');
else return Text('There is no data');
},
),
);
}
void parseResponse(AsyncSnapshot<UserInfo> snapshot) {
debugPrint(snapshot.data.name);
}
}