LunaVulpo
LunaVulpo

Reputation: 3211

How to finish activity from ViewModel using Android's architecture components?

I am trying to figure out the best way to finish an Activity from a ViewModel. I found one way to do this using a LiveData object and emitting a "signal".

I have a doubt that this solution has a overhead. Is it the right solution or I should use more accurate?

Here's an activity which demonstrates this solution:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val model = ViewModelProviders.of(this).get(MainViewModel::class.java)

        model.shouldCloseLiveData.observe(this, Observer { finish() })
    }
}

Here is the ViewModel:

class MainViewModel(app: Application) : AndroidViewModel(app) {

    val shouldCloseLiveData = MutableLiveData<Void>()

    fun someAction(){
        shouldCloseLiveData.postValue(null)
    }
}

Upvotes: 27

Views: 12007

Answers (6)

SMBiggs
SMBiggs

Reputation: 11688

Yes of course there are lots of times where a viewmodel detects a condition that needs a full-scale shut down. While it's still ugly, we can use a kotlin flow to do this.

First set up the flow variable in the MyViewmodel class:

private val _finishNow = MutableStateFlow(false)
/** when true, the Activity should call finish() */
var finishNow = _finishNow.asStateFlow()

Somewhere within MyViewmodel, a function will need the Activity to exit:

if (someBadThing) {
    Log.e("myviewmodel", "this is really bad, terminate now!")
    _finishNow.value = true
}

The Activity needs to receive the flow data and respond to it. Best place is inside your Activity's onCreate() method:

// startup the viewmodel
val viewmodel = MyViewModel()

setContent {
    MyAppTheme {
        Scaffold( .... ) {

            val finishNow by viewmodel.finishNow.collectAsStateWithLifeCycle()

            // Has there been a signal in the viewmodel to exit?
            if (finishNow) {
                finish()
            }
        ...
        ...

Note that I'm just typing code directly here--there may be errors that the IDE would easily catch. But this is the general gist of how I do it.

Upvotes: 0

jpvillegas
jpvillegas

Reputation: 93

Edit:

A better way of doing this is using Kotlin Channels. I'll use LiveData for this example but you can also consume the Flow directly.

ViewModel:

class MyViewModel : ViewModel() {

    private val _event = Channel<MyEvent>()
    
    val event = _event.receiveAsFlow().asLiveData(Dispatchers.Main)

    fun someAction() {
        _event.send(MyEvent.FINISH)
    }
}

enum class MyEvent {
    FINISH
}

And on the activity side:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val model = ViewModelProvider(this).get(MyViewModel::class.java)

        model.event.observe(this) {
            if (it == MyEvent.FINISH) {
                finish()
            }
        }

        myButton.setOnClickListener {
            model.someAction()
        }
    }
}

Previous answer (not a good one):

I agree that there doesn't seem to be a good solution for this, and your suggestion does work pretty well. But I would suggest the following.

Since you are using Kotlin you could pass a function from your activity to the viewmodel like this:

ViewModel:

class MainViewModel(app: Application) : AndroidViewModel(app) {
    
    fun someAction(block: () -> Unit) {
        // do stuff
        block()
    }
}

Activity: here the button (and clicklistener) is used as an example but this can be anywhere in the activity's code.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val model = ViewModelProviders.of(this).get(MainViewModel::class.java)

        myButton.setOnClickListener {
            model.someAction() {
                finish()
            }
        }
    }
}

the block function will essentially act as a callback.

Upvotes: 1

Bruno Martins
Bruno Martins

Reputation: 1436

You can use a Single Event, as you can see the implementation here: https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#file-event-kt

So, you don't have change the implementation of your activity

 class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val model = ViewModelProviders.of(this).get(MainViewModel::class.java)

        model.shouldCloseLiveData.observe(this, Observer { finish() })

    }
 }

The View Model will be like

class MainViewModel : ViewModel() {

  private val _shouldCloseLiveData = MutableLiveData<Event<Boolean>>()
  val shouldCloseLiveData: LiveData<Event<Boolean>> = _shouldCloseLiveData
  
  fun someAction(){
      _shouldCloseLiveData.postValue(Event(true))
  }

}

Upvotes: 2

Yessy
Yessy

Reputation: 1352

one-lined method

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>

        <import type="android.app.Activity" />

        <import type="androidx.databinding.DataBindingUtil" />

        <import type="androidx.databinding.ViewDataBinding" />
    </data>

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{(v)->((Activity)(((ViewDataBinding)DataBindingUtil.findBinding(v)).lifecycleOwner)).finish()}"
            android:text="Exit" />
    </LinearLayout>
</layout>

or simplify the layout expression, move code to a helper class

public class DataBindingHelper {
    public static Activity findActivity(View v) {
        final ViewDataBinding binding = DataBindingUtil.findBinding(v);
        return binding != null ? (Activity) binding.getLifecycleOwner() : null;
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>

        <import type="com.xxx.settingitemmoretest.DataBindingHelper" />

        <import type="android.app.Activity" />

        <variable
            name="viewModel"
            type="com.xxx.settingitemmoretest.MainActivity.ViewModel" />

    </data>

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">


        <ToggleButton
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:checked="@{viewModel.shouldCloseLiveData}"
            android:onCheckedChanged="@{(v, p)-> p ? DataBindingHelper.findActivity(v).finish(): void}"
            android:text="Stub Useless Button"
            android:visibility="gone" />

        <ToggleButton
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:checked="@{viewModel.shouldCloseLiveData}"
            android:onCheckedChanged="@{(v, p)-> p ? ((Activity)context).finish(): void}"
            android:text="Stub Useless Button"
            android:visibility="gone" />
    </LinearLayout>
</layout>

or use a Binding Adapter

public class ViewGroupBindingAdapter {

    @BindingAdapter({"android:shouldClose"})
    public static void setShouldClose(ViewGroup viewGroup, boolean shouldClose) {
        if(shouldClose){
            final ViewDataBinding binding = DataBindingUtil.getBinding(viewGroup);
            if (binding != null) {
                ((Activity) binding.getLifecycleOwner()).finish();
            }
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>

        <variable
            name="viewModel"
            type="com.xxx.settingitemmoretest.MainActivity.ViewModel" />

    </data>

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:shouldClose="@{viewModel.shouldCloseLiveData}"
        tools:context=".MainActivity">
     </LinearLayout>
</layout>

Upvotes: 0

xcesco
xcesco

Reputation: 4838

I had a similar problem: I had two activities (A and B) with its view models connected to an object (a table in a database): from an observable of a live data I had to navigate to another activity B (from A to B). The problem was, after invoking the new activity B, the observable in B change the value in the observed object.. activity A was still live, its live data call again the navigation code to B.. in an infinite loop.

After some research, I realized running a finish method does not mean the activity is really destroyed.

The solution is, in the observable code, remove from the live data the observables linked to specific activity.

liveData.removeObservers(activity);

I show it in the following snippet code. It's written in Java, but I think you have no problem to read it. With this, I solved my problem.

public class LockActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...    

        mViewModel = ViewModelProviders.of(this).get(ConfigurationViewModel.class);

        LiveData<Configurazione> liveData = mViewModel.getConfiguration();
        liveData.observe(this, config-> {
            // this code will be executed even another activity is in front of
            // screen
            boolean validToken = (config.getToken()!=null);

            if (!tokenValido) {
                intent = LoginActivity.createIntent(this);
            } else {
                intent = MainActivity.createIntent(this);
            }

            // this line remove the observable, so even activity will be destroied with calm, it is not a problem, the code is no more executed
            liveData.removeObservers(this);
        });
    }

    ...
}

I think you can easily adapt to this situation to your code. I hope it helps.

Upvotes: 1

Blcknx
Blcknx

Reputation: 2431

I share your feelings that this solution does not look tidy for two reasons. First using a MutableLiveData object to signal an event is a workaround. No data is changed. Second exposing LiveData to the outside of the view model violates the principle of encapsulation in general.

I am still surprised about this ugly concept of android. They should provide an option to observe the view model instead of it's internal LiveData objects.

I experimented with WeakReferences to implement the observer pattern. This was unstable. In an unpredictable manner the referent of the WeakReference was lost (null), in wich cases it was not possible to call finish(). This was surprising as I don't think the activity is garbage collected while running.

So this is an partly answer by exclusion. The observer pattern implemented as WeakReference seems to be no alternative to your suggestion.

A wonder if it is legitimate implement the observer pattern by hard references, if I remove the references during onStop(), or onDestroy(). I asked this question here.

Upvotes: 6

Related Questions