Open manas-chaudhari opened 8 years ago
That's a good catch. I am thinking along the lines of making ViewModels implement Parcelable
. MvvmActivity
can then serialize and deserialize them.
But how will dependencies be injected back to the ViewModel
like Navigator
? Also, shouldn't we have an MvvmFragment
as well?
Support for fragments should be added. I have created https://github.com/manas-chaudhari/android-mvvm/issues/43 for this.
Right, perhaps we can have ViewModels return an instance of Parcelable
. ViewModel can build an internal state class instance, when asked to save state. Similarly while restoring, it will populate its fields from the same Parcelable
instance.
This approach also seems crude. Let's think of something better.
Would using Parcelable create an Android SDK dependency in the ViewModels?
I'm not sure of the best way to do it, but perhaps a singleton ViewModelManager that retains them as the activity gets recreated.
Yes, Parcelable would create a dependency on Android SDK. It is not desirable.
The core issue here as @szantogab pointed is injecting the dependencies. Retaining the ViewModel would retain the dependencies. Other objects in ViewModel do not depend on Context, and hence can be retained.
Another approach that I have in mind, is to replace dependencies with a bus. For example, when ViewModel wants to invoke navigation, it would pass an event into a PublishSubject
. View
would subscribe and unsubscribe to this based on its lifecycle. This way, context reaches the ViewModel only when View subscribes and gets cleared on unsubscription, making the ViewModel safe to be retained over config changes.
While this looks promising, I have this concern:
Subscribing to the ViewModel's outputs, would vary from view to view based on its required functionality. This would result in boilerplate.
My thoughts on how this can be tackled: In the (current) dependencies approach, we have Dagger to simplify things. We can use similar way to provide the PublishSubject
s as dependencies. Thus, the dependencies become subjects instead of interfaces.
With this idea, it is even possible to implement this while retaining the interfaces. The buses can be moved inside the implementation of Navigator
, etc. Thus, these dependency objects would retain and release the Context, based on lifecycle. We could go with subscriptions to achieve this OR use a weak reference.
@manas-chaudhari This sounds promising. Can you show a brief implementation here ?
Sure. It would be something like this:
public abstract class BaseActivity extends MvvmActivity {
protected final Navigator navigator = new NavigatorImpl();
private Disposable subscription;
// Also, the viewModel object created in MvvmActivity needs to be retained. This can be done using Fragment.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
subscription = navigator.connect(this);
}
@Override
protected void onDestroy() {
subscription.dispose();
super.onDestroy();
}
static class NavigatorImpl implements Navigator {
private Context context;
public Disposable connect(final Context context) {
this.context = context;
return Disposables.fromAction(new Action() {
@Override
public void run() throws Exception {
NavigatorImpl.this.context = null;
}
});
}
@Override
public void openDetailsPage(Item item) {
navigate(ItemDetailsActivity.class);
}
@Override
public void navigateToFunctionalDemo() {
navigate(DataLoadingActivity.class);
}
private void navigate(Class<?> destination) {
if (context != null) {
Intent intent = new Intent(context, destination);
context.startActivity(intent);
}
}
}
}
To reduce boilerplate, we can create an interface containing connect(Context)
, which all lifecycle-based dependencies would implement. Then, the code can be moved to MvvmActivity
OR a separate class as @bigmikehoncho had created in #54 .
I am getting a good feeling about this approach. Lets give this more thought to ensure we aren't missing anything.
So with Dagger, we would inject the Navigator
into the ViewModel
, and our Navigator
does NOT depend on Context
at first. Instead, the Context will be connected and disconnected to/from the Navigator
.
Because of the ViewModel instance is retained, it holds a reference to the Navigator, which is also retained. The Context will be disconnected on orientation change from the Navigator, so no memory leak will occur.
This sounds good to me.
Now let's see another use case: what if we want to search for NFC tags in the foreground? To do that, we need to enable NFC foreground dispatch, so we need a foreground Activity
to be able to do that. Let's create an NfcService
interface for that, which will have a single property:
interface NfcService {
val cardIds: Observable<String>
}
class ActivityNfcService(nfcAdater: NfcAdapter) : NfcService, Connectable {
override val cardIds = PublishSubject.create<String>()
override connect(activity: Activity): Disposable {
// Init NFC related stuff
nfcAdater?.enableForegroundDispatch(activity, .....)
return Disposable.fromAction {
nfcAdapter?.disableForegroundDispatch(activity)
}
}
}
With this, we are not holding a reference to the Activity, so we are not leaking memory. The only to watch out for is that we need to dispose the disposable from onPause().
What do you think about this solution? If we solve this use case (which is really closely related to the Activity lifecycle), then I can't think of any other use case which we couldn't solve with this.
PS: Sorry for Kotlin :)
Yes, your explanation is accurate.
Regarding NFC, I think that we would be mixing two different things by invoking connect
from onResume.
connect
means the resource (context in this case) is available.I think the responsibility of Activity should be to notify when its available and when its not, i.e. in onCreate and onDestroy.
The second part can be handled inside the implementation of ActivityNfcService
.
When activity invokes connect
, NfcService
would subscribe to the activity's lifecycle. When it receives Resume, it would enable the nfc and disable when it gets Pause.
Also, you might not want to enableForegroundDispatch
inside connect
, but instead when cardIds
is subscribed. I am sure this is doable using Rx.
With NFC, I am assuming that its the restriction of Android to ensure that its invoked only in Resume/Pause lifecycle. In some cases, resume/pause lifecycle dependency may come based on functionality requirement instead of platform restrictions. For example, lets say we want to refresh data in onResume. Handling for such cases should be done inside the ViewModel by passing the lifecycle to it.
When activity invokes connect, NfcService would subscribe to the activity's lifecycle. When it receives Resume, it would enable the nfc and disable when it gets Pause.
How do you achieve this? With this library: https://github.com/trello/navi ?
Yes
Let's see a simple login example from the ViewModel's perspective:
class LoginViewModel @Inject constructor(userService: UserService, navigator: Navigator, messageHelper: MessageHelper) : ViewModel {
// Input
val userName = ObservableField<String>("")
val password = ObservableField<String>("")
val loginButtonTap = PublishSubject.create<Any>()
// Output
private val loginResult = loginButtonTap.switchMap {
userService.login(userName.get(), password.get())
}.share()
val loginSuccessText = loginResult.asSuccess()
.doOnNext { navigator.navigateToMain() }
.map { messageHelper.getString(R.string.login_success) }
val loginErrorText = loginResult.asError()
.map { error -> error.format() }
Is this a valid example? The whole thing of Rx is that we should avoid creating side-effects as much as possible. But here, loginSuccessText
creates a side-effect, because it calls navigator.navigateToMain()
Is this how it is supposed to be done? What do you think?
Interesting example. I haven't figured out yet how to avoid such side-effects.
Normally, I implement an Action
which subscribes to the api call. But even that leads to imperative code.
Perhaps it is possible to avoid side-effects if we structure dependencies to use Observables.
class LoginViewModel @Inject constructor(userService: UserService, navigator: Navigator, messageHelper: MessageHelper) : ViewModel {
// Input
val userName = ObservableField<String>("")
val password = ObservableField<String>("")
val loginButtonTap = PublishSubject.create<Any>()
// Output
private val loginResult = loginButtonTap.switchMap {
userService.login(userName.get(), password.get())
}.share()
val loginSuccess = loginResult.asSuccess()
navigator.navigateToMainOn(loginSuccess)
// This would internally subscribe to loginSuccess based on lifecycle of the navigator (its connect).
// However, that means the viewmodel would stay in memory until Activity is destroyed, irrespective of whether its used OR not.
val loginSuccessText = loginResult
.map { messageHelper.getString(R.string.login_success) }
val loginErrorText = loginResult.asError()
.map { error -> error.format() }
Even this approach has a drawback as mentioned.
Also, I am thinking about this example wrt Configuration Changes. What happens when configuration changes during the api call?
Action
, subscription doesn't get unsubscribed until api completes, thus, even after config changes, api would be in progress. Here, api call's lifecycle matches with the application process. There's no stop to it.Observable
implementation above, subscription gets unsubscribed on activity's onDestroy. Thus, api call's lifecycle matches with lifecycle of the view.I feel that change of view orientation shouldn't affect viewModel's state. For example, changing orientation shouldn't modify whether loading is visible or not. Action implementation aligns with this. It is unsafe though, as the viewmodel stays alive even after activity gets destroyed. This needs to be tackled. @szantogab What do you think?
It seems this reply comes up one and a half years later.:)
For handling configuration change, could it be possible to replace your ViewModel with the ViewModel in AAC ?
That will take care of retaining the ViewModel instance. But invoking actions which require context, in a reusable manner, is the main hindrance. So the questions raised in the above discussion are still open.
On Fri 23 Nov, 2018, 11:11 AM rayworks <notifications@github.com wrote:
It seems this reply comes up one and a half years later.:)
For handling configuration change, could it be possible to replace your ViewModel with the ViewModel https://developer.android.com/topic/libraries/architecture/viewmodel in AAC ?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/manas-chaudhari/android-mvvm/issues/37#issuecomment-441159767, or mute the thread https://github.com/notifications/unsubscribe-auth/AEJL3J1gQXnc-v4xDk-i2xHfNtokW-J3ks5ux4qSgaJpZM4KA44A .
@manas-chaudhari What if we saved the ViewModel's instance state with IcePick? The only problem is that if a Navigator or any object is injected into the ViewModel, then obviously those dependencies should be somehow injected again on config changes. Which is not something IcePick will do :(