Flutter: Async function in Getx Controller takes no effect when initialized - flutter

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.

Related

how can I get the other controller's variable inside one controller in flutter using getx

This is an issue related to the getx in flutter.
I have 2 controllers. ContractsController and NotificationController.
In ContractsController I have put the value into observer variable by calling the Api request.
What I want now is to get that variable's data in another controller - NotificationController.
How to get that value using getx functions?
ContractsController
class ContractsController extends GetxController {
ExpiringContractRepository _expiringContractRepository;
final expiringContracts = <ExpiringContract>[].obs; // This is the value what I want in another controller
ContractsController() {
_expiringContractRepository = new ExpiringContractRepository();
}
#override
Future<void> onInit() async {
await refreshContracts();
super.onInit();
}
Future refreshContracts({bool showMessage}) async {
await getExpiringContracts();
if (showMessage == true) {
Get.showSnackbar(Ui.SuccessSnackBar(message: "List of expiring contracts refreshed successfully".tr));
}
}
Future getExpiringContracts() async {
try {
expiringContracts.value = await _expiringContractRepository.getAll(); // put the value from the api
} catch (e) {
Get.showSnackbar(Ui.ErrorSnackBar(message: e.toString()));
}
}
}
The expiringContracts is updated successfully with data after the api request.
Now, I want to get that value in NotificationController
NotificationController
class NotificationsController extends GetxController {
final notifications = <Notification>[].obs;
ContractsController contractsController;
NotificationsController() {
}
#override
void onInit() async {
contractsController = Get.find<ContractsController>();
print(contractsController.expiringContracts); // This shows an empty list ?????
super.onInit();
}
}
Overview
A couple solutions come to mind:
pass the expiringContracts list as a constructor argument to NotificationsController if you only need this done once at instantiation, or
use a GetX worker to update NotificationsController every time expiringContracts is updated
The first solution isn't related to GetX, rather it's just async coordination between ContractsController and NotificationsController, so lets focus on the 2nd solution: GetX Workers.
Details
In NotificationsController, create a method that will receive expiringContracts.
Something like:
class NotificationsController extends GetxController {
void refreshContracts(List<ExpiringContract> contracts) {
// do something
}
}
Please note: none of this code is tested. I'm writing this purely in StackOverflow, so consider this pseudo-code.
In ContractsController we'll supply the above callback method as a constructor arg:
In ContractsController, something like:
class ContractsController {
final expiringContracts = <ExpiringContract>[].obs
final Function(List<ExpiringContract>) refreshContractsCallback;
ContractsController(this.refreshContractsCallback);
#override
void onInit() {
super.onInit();
refreshContracts(); // do your stuff after super.onInit
ever(expiringContracts, refreshContractsCallback);
// ↑ contracts → refreshContractsCallback(contracts)
// when expiringContracts updates, run callback with them
}
}
Here the GetX ever worker takes the observable as first argument, and a function as 2nd argument. That function must take an argument of type that matches the observed variable, i.e. List<ExpiringContract>, hence the Type of refreshContractsCallback was defined as Function(List<ExpiringContract>).
Now whenever the observable expiringContracts is updated in ContractsController, refreshContractsCallback(contracts) will be called, which supplies the list of expiring contracts to NotificationsController via refreshContracts.
Finally, when instantiating the two controllers inside the build() method of your route/page:
NotificationsController nx = Get.put(NotificationsController());
ContractsController cx = Get.put(ContractsController(nx.refreshContracts));
Timeline of Events
NotificationsController gets created as nx.
nx.onInit() runs, slow call of refreshContracts() starts
ContractsController gets created, with nx.refreshContracts callback
your page paints
nx has no contracts data at this point, so you'll prob. need a FutureBuilder or an Obx/ GetX + StatelessWidget that'll rebuild when data eventually arrives
when refreshContracts() finishes, ever worker runs, sending contracts to nx
nx.refreshContracts(contracts) is run, doing something with contracts
Notes
async/await was removed from nx.onInit
ever worker will run when refreshContract finishes
There were some powerful approaches in GetX. I solved this issue with Get.put and Get.find
Here is the code that I added.
ContractsController
class ContractsController extends GetxController {
ExpiringContractRepository _expiringContractRepository;
final expiringContracts = <ExpiringContract>[].obs; // This is the value what I want in another controller
ContractsController() {
_expiringContractRepository = new ExpiringContractRepository();
}
#override
Future<void> onInit() async {
await refreshContracts();
super.onInit();
}
Future refreshContracts({bool showMessage}) async {
await getExpiringContracts();
if (showMessage == true) {
Get.showSnackbar(Ui.SuccessSnackBar(message: "List of expiring contracts refreshed successfully".tr));
}
}
Future getExpiringContracts() async {
try {
expiringContracts.value = await _expiringContractRepository.getAll(); // put the value from the API
// ******************************** //
Get.put(ContractsController()); // Added here
} catch (e) {
Get.showSnackbar(Ui.ErrorSnackBar(message: e.toString()));
}
}
}
NotificationController
class NotificationsController extends GetxController {
final notifications = <Notification>[].obs;
ContractsController contractsController;
NotificationsController() {
}
#override
void onInit() async {
// ******************************** //
contractsController = Get.find<ContractsController>(); // Added here.
print(contractsController.expiringContracts); // This shows the updated value
super.onInit();
}
}
Finally, I have found that GetX is simple but powerful for state management in flutter.
Thanks.

Best way to handle async backend call Flutter

I have a class called FeedbackEdit that requires data in a variable called feedback to run correctly. I must get feedback from a backend API call. Currently, the code I have runs, but it shows an error for one second while it is retrieving data from the back-end. What would be the best way to fix this so it runs continuously?
class FeedbackEdit extends StatefulWidget {
#override
_FeedbackEditState createState() => _FeedbackEditState();
}
class _FeedbackEditState extends State<FeedbackEdit> {
MyFeedback feedback;
void initState() {
super.initState();
asyncGetFeedback();
}
void asyncGetFeedback() async {
MyFeedback data = await fetchFeedback(http.Client());
setState(() {
feedback = data;
});
}
Widget build(BuildContext context) { ...
it's because you are rendering your view while still fetching data from the backend. To solve the issue, you should use FutureBuilder (see) in your build method. That will make your view to wait the response being fetched from backend.
A sample code I wrote in one of my projects:
FutureBuilder<List<SingleQuestion>>(
future: retrieveFavedQuestions(questionIds),
builder: (context, favQuestionssnapshot) {
if (favQuestionssnapshot.connectionState ==
ConnectionState.done) {
if (favQuestionssnapshot.hasError) {
// check error
}
if (favQuestionssnapshot.hasData) {
// continue working with your data
}
}
);

Flutter Provider setState() or markNeedsBuild() called during build

I want to load a list of events and display a loading indicator while fetching data.
I'm trying Provider pattern (actually refactoring an existing application).
So the event list display is conditional according to a status managed in the provider.
Problem is when I make a call to notifyListeners() too quickly, I get this exception :
════════ Exception caught by foundation library ════════
The following assertion was thrown while dispatching notifications for EventProvider:
setState() or markNeedsBuild() called during build.
...
The EventProvider sending notification was: Instance of 'EventProvider'
════════════════════════════════════════
Waiting for some milliseconds before calling notifyListeners() solve the problem (see commented line in the provider class below).
This is a simple example based on my code (hope not over simplified) :
main function :
Future<void> main() async {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => LoginProvider()),
ChangeNotifierProvider(create: (_) => EventProvider()),
],
child: MyApp(),
),
);
}
root widget :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final LoginProvider _loginProvider = Provider.of<LoginProvider>(context, listen: true);
final EventProvider _eventProvider = Provider.of<EventProvider>(context, listen: false);
// load user events when user is logged
if (_loginProvider.loggedUser != null) {
_eventProvider.fetchEvents(_loginProvider.loggedUser.email);
}
return MaterialApp(
home: switch (_loginProvider.status) {
case AuthStatus.Unauthenticated:
return MyLoginPage();
case AuthStatus.Authenticated:
return MyHomePage();
},
);
}
}
home page :
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final EventProvider _eventProvider = Provider.of<EventProvider>(context, listen: true);
return Scaffold(
body: _eventProvider.status == EventLoadingStatus.Loading ? CircularProgressIndicator() : ListView.builder(...)
)
}
}
event provider :
enum EventLoadingStatus { NotLoaded, Loading, Loaded }
class EventProvider extends ChangeNotifier {
final List<Event> _events = [];
EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.NotLoaded;
EventLoadingStatus get status => _eventLoadingStatus;
Future<void> fetchEvents(String email) async {
//await Future.delayed(const Duration(milliseconds: 100), (){});
_eventLoadingStatus = EventLoadingStatus.Loading;
notifyListeners();
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
}
Can someone explain what happens?
You are calling fetchEvents from within your build code for the root widget. Within fetchEvents, you call notifyListeners, which, among other things, calls setState on widgets that are listening to the event provider. This is a problem because you cannot call setState on a widget when the widget is in the middle of rebuilding.
Now at this point, you might be thinking "but the fetchEvents method is marked as async so it should be running asynchronous for later". And the answer to that is "yes and no". The way async works in Dart is that when you call an async method, Dart attempts to run as much of the code in the method as possible synchronously. In a nutshell, that means any code in your async method that comes before an await is going to get run as normal synchronous code. If we take a look at your fetchEvents method:
Future<void> fetchEvents(String email) async {
//await Future.delayed(const Duration(milliseconds: 100), (){});
_eventLoadingStatus = EventLoadingStatus.Loading;
notifyListeners();
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
We can see that the first await happens at the call to EventService().getEventsByUser(email). There's a notifyListeners before that, so that is going to get called synchronously. Which means calling this method from the build method of a widget will be as though you called notifyListeners in the build method itself, which as I've said, is forbidden.
The reason why it works when you add the call to Future.delayed is because now there is an await at the top of the method, causing everything underneath it to run asynchronously. Once the execution gets to the part of the code that calls notifyListeners, Flutter is no longer in a state of rebuilding widgets, so it is safe to call that method at that point.
You could instead call fetchEvents from the initState method, but that runs into another similar issue: you also can't call setState before the widget has been initialized.
The solution, then, is this. Instead of notifying all the widgets listening to the event provider that it is loading, have it be loading by default when it is created. (This is fine since the first thing it does after being created is load all the events, so there shouldn't ever be a scenario where it needs to not be loading when it's first created.) This eliminates the need to mark the provider as loading at the start of the method, which in turn eliminates the need to call notifyListeners there:
EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.Loading;
// or
late EventLoadingStatus _eventLoadingStatus;
#override
void initState() {
super.initState();
_eventLoadingStatus = EventLoadingStatus.Loading;
}
...
Future<void> fetchEvents(String email) async {
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
The issue is you calling notifyListeners twice in one function. I get it, you want to change the state. However, it should not be the responsibility of the EventProvider to notify the app when it's loading. All you have to do is if it's not loaded, assume that it's loading and just put a CircularProgressIndicator. Don't call notifyListeners twice in the same function, it doesn't do you any good.
If you really want to do it, try this:
Future<void> fetchEvents(String email) async {
markAsLoading();
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
void markAsLoading() {
_eventLoadingStatus = EventLoadingStatus.Loading;
notifyListeners();
}
You are calling Apis from within your code for the root widget. Within Apis, you call notifyListeners, which, among other things, calls setState on widgets that are listening to the event provider. So firstly remove setState in your code and make sure Future use when call apis in init state
#override
void initState() {
super.initState();
Future.microtask(() => context.read<SellCarProvider>().getBrandService(context));
}

Flutter provider in initState

I'm currently trying Provider as a state management solution, and I understand that it can't be used inside the initState function.
All examples that I've seen call a method inside a derived ChangeNotifier class upon user action (user clicks a button, for example), but what if I need to call a method when initialising my state?
Motivation:
Creating a screen which loads assets (async) and shows progress
An example for the ChangeNotifier class (can't call add from initState):
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
}
You can call such methods from the constructor of your ChangeNotifier:
class MyNotifier with ChangeNotifier {
MyNotifier() {
someMethod();
}
void someMethod() {
// TODO: do something
}
}
Change your code to this
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) async {
// Loading Assets maybe async process with its network call, etc.
_progress += dProgress;
notifyListeners();
}
ProgressData() {
add();
}
}
In initState all the of(context) things don't work correctly, because the widget is not fully wired up with every thing in initState.
You can use this code:
Provider.of<ProgressData>(context, listen: false).add(progress)
Or this code:
Future.delayed(Duration.zero).then(_){
Provider.of<ProgressData>(context).add(progress)
}):
So an AssetLoader class which reports on its progress will look something like this, I guess:
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
ProgressData() {
_loadFake();
}
Future<void> _loadFake() async {
await _delayed(true, Duration(seconds: 1));
_add(1.0);
await _delayed(true, Duration(seconds: 2));
_add(2.0);
await _delayed(true, Duration(seconds: 3));
_add(3.0);
}
// progress
double get progress => _progress;
// add
void _add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
// _delayed
Future<dynamic> _delayed(dynamic returnVal, Duration duration) {
return Future.delayed(duration, () => returnVal);
}
}
As Fateme said:
the widget is not fully wired up with everything in initState
Also, you can use something like this in your initState
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
Provider.of<ProgressData>(context, listen: false).add(5);
});
I think it's more standard!
Be aware that you should use the correct context! I mean the context of the Builder!
The problem here lies with the fact that context does not exist yet in initState as extensively explained by the other answers. It doesn't exist because it hasn't yet been made a part of the widget tree.
Calling a method
If you're not assigning any state and only calling a method then initState would be the best place to get this done.
// The key here is the listen: false
Provider.of<MyProvider>(context, listen: false).mymethod();
The code above is allowed by Flutter because it doesn't have to listen for anything. In short, it's a one off. Use it where you only want to do something instead of read/listen to something.
Listening to changes
Alternatively, if you need to listen to changes from Provider then the use of didChangeDependencies would be the best place to do so as context would exist here as in the docs.
This method is also called immediately after initState.
int? myState;
#override
void didChangeDependencies() {
// No listen: false
myState = Provider.of<MyProvider>(context).data;
super.didChangeDependencies();
}
If you've never used didChangeDependencies before, what it does is get called whenever updateShouldNotify() returns true. This in turn lets any widgets that requested an inherited widget in build() respond as needed.
I'd usually use this method in a FutureBuilder to prevent reloading data when data already exists in Provider after switching screens. This way I can just check Provider for myState and skip the preloader (if any) entirely.
Hope this helps.

How to retain data while using ScopedModel in Flutter?

I am using ScopedModel to fetch some data from a Firebase database.
I am fetching a list of events.
I fetch the events from the endpoint inside the Model;
I store the events into a List<Event> inside the model;
I use that list to build my ListView.
mixin EventModel on Model {
List<Event> _events = [];
Future<http.Response> fetchEvents() async {
http.Response response = await http.get(//Url);
final List<Event> fetchedEvents = [];
... // decode response data into fetchedEvents
// Add the loaded data to my List
_events = fetchedEvents;
notifyListeners();
...
}
}
So, when opening the EventsPage the first thing I do is to fetch the data in initState().
class _EventPageState extends State<EventPage> {
#override
void initState() {
super.initState();
widget.model.fetchEvents();
}
}
}
After fetching the network data, my List inside my app has the network data so I can use it to build my ListView.
EventsPage.dart
Widget _buildListView(MainModel model) {
return Center(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ItemEventBig(model.events[index], index);
},
itemCount: model.events.length,
),
);
}
My problem is that, if I navigate to another page and then come back to EventsPage, initState() will be called again, so fetchEvents. Making the app reload all the events list again.
I would like to retain the downloaded data while my app is alive, so If the user go and come back to EventsPage the data will not be lost.
I was used to do it in Android using ViewModel, how to do it in Flutter?
I want to keep using ScopedModel to do my State Management in Flutter.
Possible Solution
I thought that a solution would be to store the events in a List<Event> as I am doing. Then, when calling fetchEvents() I could first check if my List<Event> is not null if so, I don't need to call it again because data was already loaded.
This seems a bad solution for me, especially when I have multiple pages fetching the data. Suppose I load the first one, when I go to the second one it will assume the data was already loaded because List<Event> is non null and it will not load again.
See Flutter Documentation - https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixin.html
class _EventPageState extends State<EventPage>
with AutomaticKeepAliveClientMixin<EventPage> {
#override
void initState() {
super.initState();
widget.model.fetchEvents();
}
}
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}