Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android ViewModel call Activity methods

I'm using android AAC library and Android databinding library in my project. I have AuthActivity and AuthViewModel extends android's ViewModel class. In some cases i need to ask for Activity to call some methods for ViewModel. For example when user click on Google Auth or Facebook Auth button, which initialized in Activity class (because to initialize GoogleApiClient i need Activity context which i can not pass to ViewModel, view model can not store Activity fields). All logic with Google Api and Facebook API implemented in Activity class:

//google api initialization
googleApiClient = new GoogleApiClient.Builder(this)
                .enableAutoManage(this, this)
                .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
                .build();

//facebook login button
loginButton.setReadPermissions(Arrays.asList("email", "public_profile"));
loginButton.registerCallback(callbackManager,

Also i need to call sign in intent which requires Activity context too:

Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient);
startActivityForResult(signInIntent, GOOGLE_AUTH);

I can not request facebook login and google login, or startActivity intent from view model class, so i created class interface AuthActivityListener:

public interface AuthActivityListener {
    void requestSignedIn();

    void requestGoogleAuth();

    void requestFacebookAuth();

    void requestShowDialogFragment(int type);
}

Implement listener in activity class:

AuthActivityRequester authRequestListener = new AuthActivityRequester() {
        @Override
        public void requestSignedIn() {
            Intent intent = new Intent(AuthActivity.this, ScanActivity.class);
            startActivity(intent);
            AuthActivity.this.finish();
        }

        @Override
        public void requestGoogleAuth() {
            Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient);
            startActivityForResult(signInIntent, GOOGLE_AUTH);
        }
        ...

And assign this listener in view model class to call activity methods:

// in constructor
this.authRequester = listener;

// call activity method
public void onClickedAuthGoogle() {
        authRequester.requestGoogleAuth();
}

After google or facebook authentication passed i call view model method from activity:

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        callbackManager.onActivityResult(requestCode, resultCode, data);
        if (requestCode == GOOGLE_AUTH) {
            GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
            if (result.isSuccess()) {
                GoogleSignInAccount acct = result.getSignInAccount();
                if (acct != null) {
                    viewModel.onGoogleUserLoaded(acct.getEmail(), acct.getId());
                } else {
                    viewModel.onGoogleUserLoaded("", "");
                }
            }
        }
    }

Can anyone explain me is this approach of communication between view model and activity is right, or i need to find another way to call activity methods from view model ?

like image 902
Vlad Morzhanov Avatar asked Sep 26 '17 14:09

Vlad Morzhanov


People also ask

Do we have references of activity in ViewModel?

Caution: A ViewModel must never reference a view, Lifecycle , or any class that may hold a reference to the activity context. ViewModel objects are designed to outlive specific instantiations of views or LifecycleOwners .

How does a ViewModel work internally?

ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way.It is the main component in the MVVM architecture. ViewModel can be created with activity context or fragment context. When a ViewModel object is created, it is stored inside Activity OR FragmentManager.

Can two activity share same ViewModel?

You can't share a ViewModel across Activities. That's specifically one of the downsides of using multiple activities as per the Single Activity talk.


1 Answers

There are several different approaches on how to do this. Here I want to share my approach with you. Which, in my opinion, is the most suitable for MVVM pattern ideology.

As was mentioned - "View Model must know nothing about the View and reference it". This leaves not many options on how a View Model will call an Activity method. First, what comes to mind is a Listener approach. But this approach has several drawbacks in my opinion:

  • The View should take care of subscribing/unsubscribing to/from ViewModel, as it's lifetime most likely shorter than ViewModel's
  • The first drawback also leads to a situation where something happened and the ViewModel should call View's method but the View is in between subscribing/unsubscribing; ViewModel also should aware of empty listener situation as it can be null
  • When adding new methods of ViewModel-Activity communication you will have to make changes in ViewModel, Activity and Listener interface.

So the Listener approach doesn't suite quite well. And it looks more like an MVP approach. To eliminate the above-mentioned drawbacks (or at least some of them), I've created, what I call, ViewModel Events approach. In this approach, ViewModel "emits" (or generates) it's events and lets the View to observe them. Let me show what I'm talking about.

At first, we will need some representation of the ViewModel event.

abstract class ViewModelEvent {
    var handled: Boolean = false
        private set

    open fun handle(activity: BaseActivity) {
        handled = true
    }
}

As you already can see, the handle() method will do the magic. When the Activity will handle received event it will pass its instance to handle() method as a parameter. Inside this method, we can call any Activity methods (or safe cast it to some specific Activity). The handled property is aimed to not let the Activity to handle this ViewModelEvent twice.

Further, we need to create some mechanism for the ViewModel to emit its events. LiveData suits the most for these needs. It will cancel an observer subscription on lifecycle events and it will store last emitted event (that is why the ViewModelEvent should have the above-mentioned handled property).

abstract class BaseViewModel: ViewModel() {
    private val observableEvents = MutableLiveData<ViewModelEvent>()

    fun observeViewModelEvents(): LiveData<ViewModelEvent> = observableEvents

    protected fun postViewModelEvent(event: ViewModelEvent) {
        observableEvents.postValue(event)
    }
}

Nothing complex here. Just a MutableLiveData (exposed as LiveData) and a method to emit events. By the way, inside the postViewModelEvent we can check the thread this method was called from and use MutableLiveData.postValue or MutableLiveData.setValue.

And finally, the Activity itself.

abstract class BaseActivity: Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        viewModel.observeViewModelEvents().observe(this, Observer {
            val event = it.takeUnless { it == null || it.handled } ?: return@Observer
            handleViewModelAction(event)
        })
    }

    protected open fun handleViewModelAction(event: ViewModelEvent) {
        event.handle(this)
    }
}

As you can see, general events can be handled in the BaseActivity, while some specific events can be handled by overriding the handleViewModelAction method.

This approach can be changed for specific needs. For example, ViewModelEvent doesn't have to work with Activity instance and can be used as a "marker" event or it can pass some specific parameters for the required action, etc.

The ViewModel Events approach makes ViewModel-Activity communication robust and seamless. Activity will have to subscribe once and it will not miss the latest ViewModel's event.

like image 64
Demigod Avatar answered Sep 19 '22 14:09

Demigod