I am using the Flutter provider package to manage all the states and separate the business logic from the UI part, and have all the API call present in the provider class that I need to call every time the user moves to that page.
But the issue is I don't want to hold the data even when the user moves to another screen that is the case when I declare provider in main.dart.
class _HomeScreenAppState extends State<HomeScreenApp> {
bool _isLoading;
int counter = 0;
String seller, user;
#override
void initState() {
_isLoading = true;
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_fetchHomedetails();
}
Future<void> _fetchHomedetails() async {
await Provider.of<HomeDetailProvider>(context, listen: false)
.getEarnignStatus(context);}
}
I have used ChangeNotifierProvider(create:(context) =>HomeProvider(),
builder:(context) => HomeScreen()
But if there is any dialog (bottomsheet, alertdialog) which is using HomeProvider, the dialog cannot access the HomeProvider data present on its parent widget.
Related
I want to add a Listener when the widget/state is visible and remove myself from the Listener when I leave the route.
But if the user clicks on the back button and returns to the current widget/state, I want to do the same thing again.
Currently initState, didChangeDependencies, didUpdateWidget and build are NOT called when the user clicks back from the next page, therefore I cannot detect when the user is returning and the widget was loaded from cache.
After much poking around the API, I've discovered that ModalRoute and RouteObserver is what I want.
https://api.flutter.dev/flutter/widgets/ModalRoute-class.html
https://api.flutter.dev/flutter/widgets/RouteObserver-class.html
If I just want to check if the current route is active I can call isCurrent on ModalRoute.of(context):
void onNetworkData(String data) {
if (ModalRoute.of(context).isCurrent) {
setState(() => list = data);
}
}
If I want to listen to route load/unload, I just create it and serve it up the hood with Provider like this:
class HomePage extends StatelessWidget {
final spy = RouteObserver<ModalRoute<void>>();
build(BuildContext context) {
return Provider<RouteObserver<ModalRoute<void>>>.value(
value: spy,
child: MaterialApp(
navigatorObservers: [spy],
),
);
}
}
Then somewhere in another widget:
class _AboutPageState extends State<AboutPage> with RouteAware {
RouteObserver<ModalRoute<void>>? spy;
void didChangeDependencies() {
super.didChangeDependencies();
spy = context.read<RouteObserver<ModalRoute<void>>>();
spy.subscribe(this, ModelRoute.of(context)!);
}
void dispose() {
spy.unsubscribe(this);
super.dispose();
}
void didPush() => attachListeners();
void didPopNext() => attachListeners();
void didPop() => removeListeners();
void didPushNext() => removeListeners();
attachListeners() {
}
removeListeners() {
}
}
Updates:
2021/06/11 After hours of debugging yesterday, I confirmed that the problem is caused by aws amplify configuration: _configureAmplify(). Because the location of the amplify server was set wrong, so _configureAmplify() takes several seconds to work... and therefore, the readPost() function did not work on initialization, as it must run after _configureAmplify()...
2021/06/10I made changes to my code according to S. M. JAHANGIR's advice, and updated the question. The issue still presists. The value of posts is not updated when called in initialization and the data only shows up after reload. (if I commented out the _controller.readPost() in UI, the value of posts is always empty.
I have this page that loads information from aws amplify with getx implemented. However, I found out the readPost() async funtion in getx controller dart file is not reading from database, when the controller instance is initialized. I have to add a _controller.readPost() in UI file to make it work. And the data only shows up after a reload of that UI page...
Getx Controller dart file:
class ReadPostController extends GetxController {
var isLoading = true.obs;
var posts = <Posty>[].obs;
#override
void onInit() {
_configureAmplify();
await readPost();
super.onInit();
// print('show post return value: $posts');
}
void _configureAmplify() {
final provider = ModelProvider();
final dataStorePlugin = AmplifyDataStore(modelProvider: provider);
AmplifyStorageS3 storage = new AmplifyStorageS3();
AmplifyAuthCognito auth = new AmplifyAuthCognito();
AmplifyAPI apiRest = AmplifyAPI();
// Amplify.addPlugin(dataStorePlugin);
Amplify..addPlugins([dataStorePlugin, storage, auth, apiRest]);
Amplify.configure(amplifyconfig);
print('Amplify configured');
}
// read all posts from databases
Future readPost() async {
try {
isLoading(true);
var result = await Amplify.DataStore.query(Posty.classType);
print('finish loading request');
result = result.sublist(1);
posts.assignAll(result);
// print(the value of posts is $posts');
} finally {
isLoading(false);
}
}
#override
void onClose() {
// called just before the Controller is deleted from memory
super.onClose();
}
}
And in the UI part:
class TabBody extends StatelessWidget {
TabBody({Key? key}) : super(key: key);
final ReadPostController _controller = Get.put(ReadPostController());
#override
Widget build(BuildContext context) {
_controller.readPost();//if commented out, _controller.post is empty
return Container(
child: Obx(
() => Text('showing:${_controller.posts[1].title}'),
));
}
}
In my understanding, the readPost() function should be called when the ReadPost_controller is initiallized. And the UI will update when the posts = <Posty>[].obs changes. Guys, what am I doing wrong here?
First, when you are calling readPost on onInit you are not awaiting. So change it to:
onInit() async{
...
await readPost();
...
}
Secondly, posts is a RxList so you need to use the assignAll method to update it.
Therefore, in your readPost method, instead of posts.value = reault you need to use posts.assignAll(result)
Calling from the UI works because readPost every time the build method is called by the Flutter framework and actually the UI shows the data from every previous call.
I think try with GetBuilder instead of Obx.
GetBuilder<ReadPostController>(
builder: (value) => Text('showing:${value.posts[1].title}'),
)
and also use update(). in readPost() method.
I'm building a Flutter app, and have a page with a table that is populated with data. I load the data like so:
class _AccountMenuState extends State<AccountMenu> { {
List<Account> accounts;
Future<List<Account>> getAccounts() async {
final response = await http.get('http://localhost:5000/accounts/' + globals.userId);
return jsonDecode(response);
}
setAccounts() async {
accounts = await getAccounts();
}
#override
void initState() {
setAccounts();
super.initState();
}
}
This works as expected when hot reloading the page, but when I route to this page via MaterialPageRoute,
like so: Navigator.push(context, MaterialPageRoute(builder: (context) => AccountMenu()));
then the data is not there.
What am I missing? I thought initState() gets called whenever a page loads?
You cannot do setState inside initState directly but you can wrap the initialization inside a PostFrameCallback to make sure that the initState lifecycle of the Widget is done.
class _AccountMenuState extends State<AccountMenu> { {
List<Account> accounts;
Future<List<Account>> getAccounts() async {
final response = await http.get('http://localhost:5000/accounts/' + globals.userId);
return jsonDecode(response);
}
setAccounts() async {
accounts = await getAccounts();
setState(() {})
}
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) => setAccounts());
super.initState();
}
}
initState() will not wait for setAccounts() to finish execution. In the method setAccounts() call setState after loading data.
setAccounts() async {
accounts = await getAccounts();
setState((){});
}
initState does not await. It only loads functions before the widget builder but it does not await.
you need to await loading widgets with data until accounts.length is not empty.
Show loading widget while data still loads or use FutureBuilder
List<Account> accounts;
#override
void initState() {
setAccounts();
super.initState();
}
#override
Widget build(BuildContext context) {
accounts.length > 0 ? SHOW_DATA_HERE : LOADING_WIDGET_HERE
}
I'm currently building a custom bottom bar for quick navigation.
I used the Navigation Service described in this article
Now I want to add highlighting based on which page the user has selected.
I tried to add RouteAware to my BottomNav widget to update the menu when the routing changed but I'm not receiving any events only when starting my app.
class _BottomNavState extends State<BottomNav> with RouteAware {
String _selectedRoute;
AppRouteObserver _routeObserver;
#override
void initState() {
super.initState();
_routeObserver = AppRouteObserver();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_routeObserver.subscribe(this, ModalRoute.of(context));
}
#override
void dispose() {
_routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPush() {
print('didPush');
}
The Route observer is a simple class:
class AppRouteObserver extends RouteObserver<PageRoute> {
factory AppRouteObserver() => _instance;
AppRouteObserver._private();
static final AppRouteObserver _instance = AppRouteObserver._private();
}
I'm guessing that it has to with me not using the Navigator.pushNamed but the direct implementation of the Navigation Service.
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName, {var content}) {
return navigatorKey.currentState.pushNamed(routeName, arguments: content);
}
bool goBack() {
return navigatorKey.currentState.pop();
}
}
The reason I created the NavigationService is because I want to show a consistent layout on every page (menubar / bottom bar / background).
Is there a better way to solve this problem?
I fixed the issue by extending the NavigationService with a ChangeNotifier.
Now when the user clicks the button I call a setSelected function and notify the menu items to redraw them self's.
setSelected(String newRoute) {
_showLeading = !checkIsHome(newRoute);
this._currentRoute = newRoute;
notifyListeners();
}
I have a Food object that contains properties like name, id, calories, etc. With a series of screens, the user populates the food object properties.
Once done, the user can press the submit button, that will call the addFood method in the store.
The problem is, after uploading the food to the server, i want to pop the screen or show error message in toast based on the response. I just don't know how to do this.
Following is my code (only the important bits):
FoodDetailStore.dart
class FoodDetailStore = _FoodDetailStore with _$FoodDetailStore;
abstract class _FoodDetailStore with Store {
Repository _repository;
Food _food;
#observable
String msg = '';
// ... Other Observables and actions
#action
addFood(bool toAdd) {
if (toAdd) {
_repository.addFood(food).then((docId) {
if (docId != null) {
// need to pop the screen
}
}).catchError((e) {
// show error to the user.
// I tried this, but it didn't work
msg = 'there was an error with message ${e.toString()}. please try again.';
});
}
// .. other helper methods.
}
FoodDetailScreen.dart (Ignore the bloc references, I am currently refactoring code to mobx)
class FoodDataScreen extends StatefulWidget {
final String foodId;
final Serving prevSelectedServing;
final bool fromNewRecipe;
FoodDataScreen({#required this.foodId, this.prevSelectedServing, this.fromNewRecipe});
#override
_FoodDataScreenState createState() => _FoodDataScreenState(
this.foodId,
this.prevSelectedServing,
this.fromNewRecipe,
);
}
class _FoodDataScreenState extends State<FoodDataScreen> {
final String foodId;
final Serving prevSelectedServing;
final bool fromNewRecipe;
FoodDataBloc _foodDataBloc;
_FoodDataScreenState(
this.foodId,
this.prevSelectedServing,
this.fromNewRecipe,
);
FoodDetailStore store;
#override
void initState() {
store = FoodDetailStore();
store.initReactions();
store.initializeFood(foodId);
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
// I know this is silly, but this is what i tried. Didn't worked
Observer(
builder: (_) {
_showMsg(store.msg);
}
);
}
#override
Widget build(BuildContext context) {
return Container(
// ... UI
);
}
_popScreen() {
_showMsg('Food Added');
Majesty.router.pop(context);
}
_showMsg(String msg) {
Fluttertoast.showToast(msg: msg);
}
#override
void dispose() {
store.dispose();
super.dispose();
}
}
Constructing an Observer instance inside the didChangeDependencies() is indeed "silly" as you have rightly noted already :)
Observer is a widget and widget needs to be inserted into the widgets tree in order to do something useful. In our case non-widget Mobx reactions come to the rescue.
I will show how I did it in my code for the case of showing a Snackbar upon observable change so you will get an idea how to transform your code.
First of all, import import 'package:mobx/mobx.dart';.
Then in the didChangeDependencies() create a reaction which will use some of your observables. In my case these observables are _authStore.registrationError and _authStore.loggedIn :
final List<ReactionDisposer> _disposers = [];
#override
void dispose(){
_disposers.forEach((disposer) => disposer());
super.dispose();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_authStore = Provider.of<AuthStore>(context);
_disposers.add(
autorun(
(_) {
if (_authStore.registrationError != null)
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text(_authStore.registrationError),
backgroundColor: Colors.redAccent,
duration: Duration(seconds: 4),
),
);
},
),
);
_disposers.add(
reaction(
(_) => _authStore.loggedIn,
(_) => Navigator.of(context).pop(),
),
);
}
I use two types of Mobx reactions here: autorun and reaction. autorun triggers the first time immediately after you crate it and then every time the observable changes its value. reaction does not trigger the first time, only when the observable change.
Also pay attention to dispose the created reactions in the dispose() method to avoid resources leak.
Here is a code of my Mobx store class with used observables to complete the picture:
import 'package:mobx/mobx.dart';
import 'dart:convert';
part "auth_store.g.dart";
class AuthStore = AuthStoreBase with _$AuthStore;
abstract class AuthStoreBase with Store{
#observable
String token;
#observable
String registrationError;
#observable
String loginError;
#action
void setToken(String newValue){
token = newValue;
}
#action
void setRegistrationError(String newValue){
registrationError = newValue;
}
#action
void setLoginError(String newValue){
loginError = newValue;
}
#action
void resetLoginError(){
loginError = null;
}
#computed
bool get loggedIn => token != null && token.length > 0;
#action
Future<void> logOut() async{
setToken(null);
}
}