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;
}
Related
I'm using flutter and firebase realtime database.I'm trying to read data from a specific node.I'm saving the data that I am collecting in the Orderlist class and then I return a Future List of Ordelist.This Future function I am trying to use on another widget.I want to display on screen every time data is updated.
Future<List<Orderlist>> order() async{
String business =await businessname();
List table = await tables();
List<Orderlist> list = [];
table.forEach((element) async{
String payment_method = '';
String payment_state ='';
var snapshot = ref.child(business).child(element.toString()).onValue.listen((event) {
event.snapshot.children.forEach((method) {
if(method.key=='payment_method') payment_method=method.value.toString();
if(method.key=='payment_state') payment_state = method.value.toString();
});
final order = Orderlist(payment_method: payment_method,payment_state: payment_state);
list.add(order);
});
});
return list;
}
The problem is that at first place the data are loaded on screen but when I am trying to update the data for some reason the list is appended whereas I just want to replace the previous data with the updated data.To be more specific if I want to listen to 2 nodes to be updated I will have a list with 2 Orderlist items.But the problem is when I update one of them the list is expanded to 3 Orderlist items.
Here is the widget where I am trying to use the Future function
first data loaded Updated da
class TempSettings extends StatefulWidget {
const TempSettings({super.key});
#override
State<TempSettings> createState() => _TempSettingsState();
}
class _TempSettingsState extends State<TempSettings> {
String? business;
List<Orderlist> list=[];
final user = FirebaseAuth.instance.currentUser;
#override
void initState() {
// TODO: implement initState
g();
super.initState();
}
void g() async{
list = await DatabaseManager(user_uid: user!.uid).order();[![[![enter image description here](https://i.stack.imgur.com/hn2NQ.png)](https://i.stack.imgur.com/hn2NQ.png)](https://i.stack.imgur.com/SJ4M1.png)](https://i.stack.imgur.com/SJ4M1.png)
}
#override
Widget build(BuildContext context) {
return Column(children: list.map((e) => ListTile(title: Text(e.payment_method!),)).toList(),);
}
}
When you listen to data in Firebase with onValue, the event you get contains a snapshot of all data. Even when only one child node was added/changed/removed, the snapshot will contain all data. So when you add the data from the snapshot to list, you are initially adding all child nodes once - but then on an update, you're adding most of them again.
The easiest way to fix this is to empty the list every time onValue fires an event by calling list.clear().
I am new to GetX and want to get some concepts right. This is my process to get the groups that a current user is in using Firebase Realtime Database:
Create an AuthController to get current user id (works perfectly)
class AuthController extends GetxController {
FirebaseAuth auth = FirebaseAuth.instance;
Rx<User?> firebaseUser = Rx<User?>(FirebaseAuth.instance.currentUser);
User? get user => firebaseUser.value;
#override
void onInit() {
firebaseUser.bindStream(auth.authStateChanges());
super.onInit();
}
} ```
Create a UserController to get ids of groups that the user has (working partially)
class FrediUserController extends GetxController {
Rx<List<String>> groupIdList = Rx<List<String>>([]);
List<String> get groupIds => groupIdList.value;
#override
void onInit() {
User? user = Get.find<AuthController>().user;
if (user != null) {
String uid = user.uid;
groupIdList.bindStream(DatabaseManager().userGroupIdsStream(uid));
print(groupIds); //prints [] when it should be populated
}
super.onInit();
}
}
Create a GroupsController to get the groups from those ids (not working) --> Dependant on UserController to have been populated with the id's.
class FrediGroupController extends GetxController {
Rx<List<FrediUserGroup>> groupList = Rx<List<FrediUserGroup>>([]);
List<FrediUserGroup> get groups => groupList.value;
void bindStream() {}
#override
void onInit() {
final c = Get.find<FrediUserController>();
List<String> groupIds = c.groupIds;
print(groupIds); //prints [], when it should have ids
groupList.bindStream(DatabaseManager().groupsStream(groupIds)); //won't load anything without id's
super.onInit();
}
}
Observations
Get.put is called sequentially in the main.dart file:
Get.put(AuthController());
Get.put(FrediUserController());
Get.put(FrediGroupController());
Inside my HomePage() Stateful Widget, if I call the UserController, the data loads correctly:
GetX<FrediUserController>(
builder: (controller) {
List<String> groups = controller.groupIds;
print(groups); //PRINTS the list of correct ids. THE DATA LOADS.
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: groups.length,
itemBuilder: (context, index) {
return Text('${groups[index]}',
style: TextStyle(color: Colors.white));
},),); },),
QUESTION
It is as if the stream takes some time to populate, but the UserController doesn't wait for it and initializes the controller as empty at first, but after some time it populates (not in time to pass the data to the GroupController.
How can I fix this? I have tried async but not with much luck.
Behaviour I would Like:
Streams may/may not be ready, so it can initialize as empty or not.
HOWEVER, if the stream arrives, everything should be updted, including the initialization of controllers that depend on UserController like GroupController.
Consequently, the UI is rebuilt with new values.
THANK YOU!
There are two things that you can add:
Future Builder to show some loading screen while it fetch data from RTDB
ever function
class AuthController extends GetxController {
late Rx<User?> firebaseuser;
#override
void onReady() {
super.onReady();
firebaseuser = Rx<User?>(FirebaseAuth.instance.currentUser);
firebaseuser.bindStream(FirebaseAuth.instance.idTokenChanges());
ever(firebaseuser, _setInitialScreen);
}
_setInitialScreen(User? user) {
if (user != null) {
//User Logged IN
} else {
//User Logged out
}
}
}
You only take the user once, in the method onInit. You are not getting user changes. To get every change you would have to use "ever" function. For example, "firebaseUser.value" is like a photography of the firebaseUser observable in the moment.
If I can make a sugestion, don't mistake controllers with providers. Think Firebase as a provider and the controller as a mid point between the UI and the provider. You can listen to Firebase Streams at the controller to update UI and make calls from the UI change parameters in your Firebase provider. Separate your concerns into two distinct classes and you'll, potentially, have a better design.
Use of "ever" function example:
ever(firebaseUser, (user) {
// do something
});
"Ever" assigned function runs whenever the observable emits a new value
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 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
}
}
);
I have a Map() called myData that holds multiple lists. I want to use a Stream to populate one of the lists in the Map. For this StreamBuilder will not work as it requires a return and I would like to use List.add() functionality.
Map<String, List<Widget>> myData = {
'list1': [],
'list2': [],
'list3': [],
'list4': []
};
How can I fetch information from FireStore but add it to the list instead of returning data?
Like this but this wouldn't work.
StreamBuilder<QuerySnapshot>(
stream: // my snapshot from firestore,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
snapshot.data.documents.map((DocumentSnapshot doc) {
myData['list1'].add(Text(doc['color']));
});
},
),
Any help would be appreciated!
StreamBuilder does not fit for this task. Even if you manage to do it (actually there is a way :) )- it might be rebuilt by higher level widgets without new data and you will end up with duplicates in list.
All the WidgetBuilders and build methods in widgets serve only for displaying UI
You need to subscribe to a stream. If you want to do it using widget, then you need to create a custom widget extending StatefulWidget. StatefulWidget state has lifecycle methods (initState and dispose) so it will allow to correctly manage StreamSubscription.
Here is example code:
class StreamReader extends StatefulWidget {
#override
_StreamReaderState createState() => _StreamReaderState();
}
class _StreamReaderState extends State<StreamReader> {
StreamSubscription _subscription;
#override
void initState() {
super.initState();
_subscription = myStream.listen((data) {
// do whatever you want with stream data event here
});
}
#override
void dispose() {
_subscription?.cancel(); // don't forget to close subscription
super.dispose();
}
#override
Widget build(BuildContext context) {
// return your widgets here
}
}