I'm using GetX to manage the state of my Flutter app, and the structure of my pages consists of an ItemsListPage that lists some items, and an ItemDetailsPage that shows details about an specific item and lets the user change it or delete it.
I'm managing state using GetX's Reactive state management, which means that, in order to update the state of my ItemsListPage, I need to update an observable variable itemsList that is inside the ItemsListController. This variable is updated on the onInit() method of the controller, which means that whenever the controller is instantiated the list should be updated with the data fetched from the API.
The controller of the ItemsListPage looks like this:
class ItemsListController extends GetxController {
var itemsList = <AppEntity>[].obs;
#override
void onInit() {
super.onInit();
// Fetch items from the API and store them on itemsList:
getApps();
}
//...
}
And the controller of the ItemDetailsPage is like this:
class ItemsDetailsController extends GetxController {
void deleteItem() async {
final response = await deleteItem();
response.fold((failure) {
// Deal with a failure on the request
}, (response) {
if (response.success) {
// I want to update the state of the list from here
Get.toNamed('/main/');
Get.snackbar('Deleted', 'Item deleted successfully!');
}
});
}
The problem is that when the user changes or deletes an item (on the ItemDetails page), he gets redirected to the main page via Get.toNamed('/main'), and sees the old version of the list, because the onInit() method isn't called again.
Is there a way for me to trigger an action everytime the user is redirected to the ItemsList page? This would allow me to fetch the fresh data from the API and update the page. Another solution would be a way of updating the state of the ItemsListController from the ItemDetailsController, which would be even better, as it would not require another call to the back-end.
If you are using get_cli, then you can pretty much generate and use named routes along with your view, controller and bindings. But if in case you are just using the state management without binding, then this problem may occur.
The thing happening in this scenario is, your controller is building one time and then your onInit method is called, after that you do some operation on your list like deleting or adding some data, and then when you go to another screen, your controller doesn't get disposed(Strange).
You can solve this by using these ways,
Whenever you switch to another screen, let's say using Get.toNamed(route) or Get.offNamed(route) or Get.off(route), what you can do is before navigating to other screen, you can remove the controller form the memory like this,
Get.delete<ControllerName>();
In this way, your controller will be again reinstantiated when you open your screen again(Provided that you have put the Get.put(Controller()) In your widget.
If you are using named routes then you can pretty much directly use them, you don't need to do anything in order to work with them. In case you're facing the error even if you have bindings and named routes then you can perform step 1.
If you want to minimize the API calls then whenever the user deleted some item from the list, you can delete it locally and update the UI according to it. This way user won't have to see the loaders again and again.
Related
I have a navbar, change between 2 screens nested with Provider create blocs. Whenever bloc constructor called. It's call api to get data and add to the stream. So the problem here, user can spam switch between 2 screens and make the bloc dispose and init => api was called multiple times.
Class Bloc1 {
const Bloc1(){
data = await fetch() //Call api
stream1.add(data) //Then add to stream
}
}
I have tried the lock. But it does not work because when recreate, the lock is recreate too -> useless.
Class Bloc1{
var lock = false;
const Bloc1(){
if(lock == false) {
data = await fetch() //Call api
stream1.add(data) //Then add to stream
}
}
}
In my opinion, the issue you are mentioning is a kind of a border case where the user will have an aggressive behavior with the switch.
That being said, I think you should clarify a few points:
Do you really want to change the switch behavior to avoid this scenario?
Does it make sense to fetch new data every time the user switches tabs? I mean, does the data change that often? Because if the data does not change, maybe it does not makes sense to make a new request to the API at that point, and you should consider fetching this data at some other point.
I think there is no way to avoid this scenario if you instantiate/dispose your bloc every time the user switches screens (unless you save the lock or previously retrieved data outside the bloc, so next time you instantiate this bloc you can provide this value via the constructor, and this way you can avoid making the API request).
Depending on your Bloc logic, a solution could be that you instantiate the Bloc above these two screens, by doing this the Bloc will not be disposed once you switch screens and therefore it won't be instantiated again. Take into account that this approach will make the Bloc to be alive no matter how many times the user switches screens, and it is possible that this is something that you want to avoid.
I am trying to call Future function which will fetch data from API from the ViewModel. But in view it is not changing my isLoading inside then function.But if I change the page and go back to the same page it is reflecting.
Make your View extends StateFulWidget and use setState function or try to use a State management library like Provider.
Provide your code if this does not solve your problem.
I have a custom object called item, which contains parameters such as a name, description, price etc. My app's home page is a streambuilder of item objects (stored in Firestore). When I click on an item, it takes me to a detail page, and from there, I can hit another button to edit the item's parameters. When navigating from the home page (item list) to the item detail page, I am using fromMap to easily create a local item from the database and passing that item into the detail page. Then, if the user decides to click the edit button, I am passing that same item into the item edit page (with the item's parameters already filled out). My edit page also contains a button 'save' that will save the updated parameters to Firestore.
The issue I am facing is that this local item is being shared between the detail page and the edit page. So for example if I edit the name of an item and hit the back button, those changes will be reflected in the detail page, when I actually don't want them to. What is the best way of going about handling these changes, more specifically updating the item parameter's in the detail page? Should I add a method in the detail page that every time I enter this page, refresh the page by pulling the most recent data from Firestore? Or, when navigating from the home page (item list) to the detail page, pass in the item's document ID and create the item in the detail page?
You can create a copy of the item object and edit the copy instead so they do not share the same reference.
At the end when you want to propagate the change you can just set the values of the newly instantiated object to the old one.
For example:
class EditItemDetailsPage extends StatefulWidget {
final Item item;
EditItemDetailsPage([this.item]);
#override
_EditItemDetailsPageState createState() => _EditItemDetailsPageState();
}
...
class _EditItemDetailsPageState extends State<EditItemDetailsPage> {
Item _itemCopy;
#override
void initState() {
super.initState();
// Make a copy of the instance you are editing
// An easy way to copy is to convert them into a map and back.
itemCopy = Item().fromMap(widget.item.toMap());
// Now your widgets here will edit _itemCopy instead of widget.item
}
void submit() {
// Send to firestore
bool success = sendToFirestore();
// Change were made to firestore successfully
if (success) {
// Apply change to original account instance.
widget.item.fromMap(_itemCopy.toMap());
}
}
}
When the user views the home screen for the first time - they are being sent there via this.nav.setRoot(Page). This presents an issue when I have three other pages settings root to a page. For example - I go to the home page, which is set root, so that home page data is loaded initially for the first time. Then the user navigates to the message page. Then the user goes back to the home page, the data is reloaded again. I don't want that to happen. I would like to only call it once, but due to setRoot, it refreshes the page. Just like how navCtrl.push(Page) and .pop, the data is not refreshed. I have a hamburger navigation style and that's why I have the set roots for each page in the hamburger navigation.
app.comp.ts
openPage() {
this.nav.setRoot(Page);
}
openPageTwo() {
this.nav.setRoot(MessagesPage);
}
How do I override the nav.setRoot refresh? Or use something else entirely?
Thanks
Exactly where are you refreshing your data? In what function?
Lifecycle events
I think your problem will be solved by using the correct lifecycle event. For example, ionViewDidLoad :
Runs when the page has loaded. This event only happens once per page being created. If a page leaves but is cached, then this event will not fire again on a subsequent viewing. The ionViewDidLoad event is good place to put your setup code for the page.
Have you tried refreshing your data there?
Global variable or service
As an alternative, you can create a global variable or a service that holds your data. Once you setRoot to a page for the first time, do the refresh and update the global value and do not refresh it the subsequent times that you setRoot to that page. This is not my favorite way, but will do the job for you.
Tabs:
Rather than setting roots several times, you may wanna try ionic tabs.
By setting a rootpage using navCtrl.setRoot(), you tell Ionic this page is the begin of the navigation tree of your application. For example, after a login, you should .setRoot(HomePage);
When you want to navigate from HomePage to MessagesPage, you should use navCtrl.push(), to pop the MessagesPage on top of the HomePage. eg; navCtrl.push(MessagesPage);
You can use a provider, since it is stored on memory once and every one that injects it will get its properties and data.
some-provider.ts
import { Injectable } from '#angular/core';
#Injectable()
export class SomeProvider {
public data: DataModel;
constructor(public http: Http) {
}
}
And then import that on your app's module providers variable.
After, get this provider on your component's constructor
some-page.ts
#Component({
selector: 'app-some-page',
templateUrl: 'app-some-page.html'
})
export class AppSomePage {
data: DataModel;
constructor(public someProvider: SomeProvider) {
this.data = this.someProvider.data;
}
}
I am wondering why binding a button inside the Loaded event in WPF page does not work and will only work after navigating to another page, and going back.
I have an inventory app and on the main page, most of the ViewModel are called because of a Back button which goes back to a specific lists and what causes that is, it will start binding the even if that command is not for that page and it will also load the collections for other pages.
So I used Loaded page event to call the necessary methods to populate the lists and also start binding commands for this specific page. I also used Unloaded page event for clean up like unsubscribing to some CRUD events.
The problem now though is, buttons does not get binding in Loaded page event. I do not know why..
I have made a miniature app to demo the problem. It can be downloaded here
(full source code included)
https://www.dropbox.com/s/qzumzyicuvrktsi/ICommandTest.zip?dl=0
This is because your views are not getting notified about the change of Command_ShowAddWindow and Command_ClickMe. Let me explain:
When your Page constructor is first run the bindings to your commands are initialized and transferred to the view, but by that time your commands are null, so the view binds both buttons' commands to null.
Then when your Loaded event is fired the commands are initialized, but the view is not getting notified about it, so it keeps command bindings to null.
The solutions to the problem are:
You manually call RaisePropertyChanged to notify the view about commands change when you initialize them:
void InitCommands()
{
Command_ShowAddWindow = new RelayCommand(Command_ShowAddWindow_Click);
Command_ClickMe = new RelayCommand(Command_ClickMe_Click);
RaisePropertyChanged("Command_ShowAddWindow");
RaisePropertyChanged("Command_ClickMe");
}
Or you initialize your commands in your ViewModel constructor before DataBindings are initialized:
public ViewModel_Page1()
{
InitCommands();
...
}