-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ViewModel's SaveState/ReloadState and NavigationService #2167
Comments
This comment was marked as abuse.
This comment was marked as abuse.
No, it isn't serialized. A reference to the ViewModel object is kept in a singleton cache.
MvxViewModel doesn't implement IDisposable currently. Are you suggesting we should add it? When a ViewModel is rehydrated, it is recreated and it receives all saved information in a bundle, specifically in The problem is, the ViewModel is completely recreated and it receives some parameters, but |
I bumped into this issue after investigating how to properly do tombstoning in Android / Mvx 5.3 and realized that indeed
The way I see it there are two options to solve this particular problem:
For me personally, (2) would be fine; I already have a neat |
Just to elaborate my first alternative a bit more, what I mean is to save ViewModels by default in the singleton cache, and give the user the ability to disable this. If disabled, a ViewModel could use SaveState/ReloadState (without Prepare and Initialize) to rehydrate. |
This comment was marked as abuse.
This comment was marked as abuse.
@nmilcoff I understand your solution, but in my experience it is not actually a solution for the most common scenario. Most phones have plenty of memory these days, so it's rare to see individual activities being killed unless "Don't keep activities" is turned on (which is purely artificial). What does appear to happen with our app regularly is that it is backgrounded for a long period of time and then recalled from the app drawer hours later, at which point the entire app has been cleared from memory and every single activity is restored from saved state. In this case the singleton cache is gone as well. @softlion In a nutshell: public class BaseViewModel<TParameter> : MvxViewModel<TParameter>
where TParameter: class, new()
{
public TParameter Parameter { get; protected set; }
protected override void SaveStateToBundle(IMvxBundle bundle)
{
base.SaveStateToBundle(bundle);
bundle.Data.Add("Parameter", ObjectSerializer.Serialize(Parameter));
}
protected override void ReloadFromBundle(IMvxBundle state)
{
base.ReloadFromBundle(state);
string data = null;
state.SafeGetData()?.TryGetValue("Parameter", out data);
if (data != null)
{
var parameter = ObjectSerializer.Deserialize<TParameter>(data);
Prepare(parameter);
}
}
public override void Prepare(TParameter parameter)
{
Parameter = parameter;
}
} |
This comment was marked as abuse.
This comment was marked as abuse.
@softlion I realize there are several ways in which activities can disappear - if you want to be able to deal with all of them (and especially the most common one) a singleton cache won't do. With regards to |
@ElteHupkes once again... I'm not saying we save only one ViewModel in the cache, but the entire stack (by default). So the scenario you mention would be covered. Also looking at your code, there's nothing special in it other than calling |
@nmilcoff Ok, I may be misunderstanding you, I don't mean to be rude or anything. The way I interpret your initial post, is the reason for storing all view models in the singleton cache is to all get them into state (1) where they essentially don't need to go through |
@ElteHupkes yes, that's the idea :). Basically, if they were kept alive, there's no need to rehydrate them. I'd propose to do this anyway:
private static async void RunInSandbox(Func<Task> myTask)
{
try
{
await Task.Yield();
await myTask.Invoke();
}
catch (Exception ex)
{
// log the exception and maybe rethrow
}
} What do you guys think? Would be nice to have your opinions here as well @MvvmCross/core. |
This comment was marked as abuse.
This comment was marked as abuse.
@nmilcoff Okay, but my point is the entire app is often cleared from memory - the singleton cache is gone too. Rehydration is required one way or another. I've had some additional beef with the singleton cache because the instance living inside it is never garbage collected, which forces you to be much more careful with any event handling / message subscriptions you may have going on in there. From Actually, in the old CIRS, any parameter data passed to With regards to the async issue, I guess the question is what else is going to on while the view model is awaiting the |
This comment was marked as abuse.
This comment was marked as abuse.
@ElteHupkes we're trying to phase out from reflection during navigation, so probably the best way to advance on this is just require users to save their parameters in SaveState. I think it makes sense to yield, because otherwise the method might run in a sync way and block the UI. @softlion AFAIK it's not possible to use ConfigureAwait(false) in a |
This comment was marked as abuse.
This comment was marked as abuse.
I was surprised by this as well. I started migrating the navigation code from 4.x to 5.x and wiped out all my Init, ReloadState and SaveState code... I thought that since I started using the I'm surprised this isn't mentioned in the documentation when you go over the steps to migrate from 4.x to 5.x or in the Lifecycle documentation. You do talk about SaveState* under the 4.x header during VM Lifecycle, but not under 5.x. https://www.mvvmcross.com/documentation/fundamentals/navigation |
In addition, I had some recovery code or recreation code that basically did a I get the new prepare and initialize, but I don't think the intent extra/bundle state stuff should get broken because of it. Is there a best practice to accommodate both the old and the new? Which ViewModel methods should I implement/override in order to be sure that the state is properly saved and available when needed, whether it's to recover from a crash or due to a navigation request? |
@Cybrosys I'm not sure which intent / bundle are you referring to, can you please point out a line of code? Presentation bundles and stuff wasn't removed. @ElteHupkes as far as I know, MvvmCross never did automagically restore the state of ViewModels. When recovering from tombstone, Init was normally called with parameters = null and you received the saved state in the bundle for |
@nmilcoff before the 5x migratiopn I used the following method: That method populated the activity intent with the passed in SavedState object, so I could, if needed, do a StartActivity(Intent) inside the new activity (if I wanted to recreate it). But now that I'm using: The Intent doesn't contain the SavedState object any longer. There's probably a good reason why it worked last time and why it doesn't work any longer. In either case, i've found a work-around for when I need to recreate activities. |
The solution posted by @ElteHupkes works for me, I just adapt it in my BaseViewModel to work in all my viewmodels: public abstract class BaseViewModel<TParameter> : BaseViewModel, IMvxViewModel<TParameter> where TParameter : class
{
private TParameter _parameter;
public TParameter Parameter
{
get { return _parameter; }
set
{
_parameter = value;
RaisePropertyChanged(() => Parameter);
}
}
public void Prepare(TParameter parameter = null)
{
if (parameter != null)
{
Parameter = parameter;
SpecificPrepare(parameter);
}
}
public virtual void SpecificPrepare(TParameter parameter)
{
//Implement in ViewModel to do specific code
}
protected override void SaveStateToBundle(IMvxBundle bundle)
{
base.SaveStateToBundle(bundle);
if (Parameter != null)
{
bundle.Data["ViewModelType"] = GetType().ToString();
bundle.Data["Parameter"] = JsonConvert.SerializeObject(Parameter);
}
}
protected override void ReloadFromBundle(IMvxBundle state)
{
base.ReloadFromBundle(state);
try
{
if (state != null && state.Data != null && state.Data.Any())
{
string viewModelType = "";
string parameter = "";
state.SafeGetData()?.TryGetValue("ViewModelType", out viewModelType);
state.SafeGetData()?.TryGetValue("Parameter", out parameter);
if (!string.IsNullOrWhiteSpace(viewModelType) && viewModelType == GetType().ToString() && !string.IsNullOrWhiteSpace(parameter))
{
Prepare(JsonConvert.DeserializeObject<TParameter>(parameter));
RunAsyncTaskInVoid(Initialize);
}
}
}
catch (Exception)
{
//Navigate to your FirstViewModel
}
}
protected async void RunAsyncTaskInVoid(Func<Task> myTask)
{
try
{
await Task.Yield();
await myTask.Invoke();
}
catch (Exception)
{
}
}
} But I was thinking, in some simple cases, there is no reason to restore the viewmodel/view. @nmilcoff What I need to do to "restart" the app from the SplashScreen again? |
I'm working on a fix for this, I'll make a PR later today. @rrispoli I guess you need to create an Intent for it and call StartActivity(). Probably a bit offtopic for this issue. |
@nmilcoff Ok, thanks! |
@nmilcoff Actually, when calling In the end, the view model will be created in the same fashion whether it be from |
You're right @ElteHupkes. But for fragments I think it didn't work like that. When recovering from tombstone, |
Aah okay, I wasn't working with fragments that had state before tbh, so I didn't know that, thanks for the clarification. With regards to your PR, over the past days I've come to the conclusion more and more that VM lifecycle really needs to be handled by the ViewModelLoader, so definitely good thinking there. There's an ugly case if you use fragments as in the examples, by opening them with |
This is a concern for every platform, but maybe Android is the most affected one because if we don't do something, apps will crash.
Basically the SaveState/ReloadState mechanism is activated when the device goes out of memory, so what we do internally is to provide a way for users to save the state of their ViewModels and then a way to reload it. This is how it goes inside the framework:
If there is only one ViewModel going into tombstone (of if many, then the last one): This ViewModel will not be destroyed, it will be saved in a "fast" cache named
IMvxSingleViewModelCache
instead. Then when the View is coming back, the ViewModel is just retrieved from the cache.For the rest of the ViewModels (these are probably part of a backstack): When the Android View is destroyed, each ViewModel gets a chance to save its state by populating a
Bundle
(which contains aDictionary<string,string>
) before being disposed. After this process - when the View comes back - the ViewModel is loaded from the ground up, andReloadState
is supposed to be called with aBundle
that contains the previously saved data.I have done some testing for this and so far it seems to be working for Activities + new AndroidViewPresenter + MvxNavigationService I didn't test it for fragments yet, as we didn't finish the caching functionality yet.
BUT there is a problem with lifecycle events for ViewModels: As ReloadState is part of the “old” CIRS (Constructor - Init - Reload State - Start), it is called by
IMvxViewModelLocator.RunViewModelLifecycle
, so currently this is what the chain looks like when a ViewModel is coming back to life:Init -> ReloadState -> Start.
Just that.
Prepare
andInitialize
are not getting called.This makes sense - in a way - but if these methods are not getting called then the developer can't really recreate the ViewModel.
That being said, I think it will be really difficult to make a call to
Initialize
when reloading the state, because we are working in a sync context (View lifecycle events).I have two alternatives in mind right now:
Store the entire stack of ViewModels in a cache singleton (instead of the last one opened). This will probably require the developer to be responsible for configuring which ViewModels should be stored in that cache.
Leave the process as it is right now. This means that once we finally deprecate CIRS, the process or saving/reloading will only involve
SaveState
->ReloadState
. This leaves the responsibility of running the ViewModel initialization to the user.The text was updated successfully, but these errors were encountered: