24

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

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

So go to example: let's suppose that in an app is an activity MainActivity and view model like below:

 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() })

    }
 }

and as a companion to the MainActivity is a MainViewModel like below:

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

  val shouldCloseLiveData = MutableLiveData<Void>()

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

}
Blcknx
  • 1,921
  • 1
  • 12
  • 38
LunaVulpo
  • 3,043
  • 5
  • 30
  • 58
  • 3
    And i think this is the correct solution, as you should never have references to your activity inside the ViewModel. – woodii Mar 05 '18 at 14:13
  • 1
    @woodii : I understand that solution is consistent with MVVM but posting null to send close request to activity. I'm wondering if exist more elegant solution. – LunaVulpo Mar 06 '18 at 09:37
  • I have created a similar solution to this one, but I'm also not happy with it, have you found any other solution for this problem? – Pozzo Apps Apr 02 '18 at 13:46
  • @PozzoApps Nope. For some activities, in which I needed more actions, ViewModel sends a enum object. – LunaVulpo Apr 02 '18 at 19:20
  • I'm not a big expert with LiveData because I use RxJava but it should be the same. The only thing that I would change is sending a null. In RxJava2 they have even forbidden it: you can't emit nulls anymore. – kingston May 06 '18 at 19:29

5 Answers5

5

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.

Blcknx
  • 1,921
  • 1
  • 12
  • 38
2

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))
  }

}
Bruno Martins
  • 1,347
  • 2
  • 11
  • 32
1

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.

xcesco
  • 4,690
  • 4
  • 34
  • 65
1

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.

jpvillegas
  • 93
  • 1
  • 9
  • upvoting because of Kotlin – rodolfosrg Apr 11 '19 at 15:10
  • 6
    You are passing an implicit reference of activity to the ViewModel and it can cause memory leaks. it's clearly highlighted in the documentation: `Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.` https://developer.android.com/topic/libraries/architecture/viewmodel – Habib Kazemi Oct 27 '19 at 16:56
  • @HabibKazemi app is just passed to the AndroidViewModel, so not keeping the reference. Still this is not relevant to the question/answer, since this can be used with a ViewModel instead of an AndroidViewModel. Here is an example (in Java) from android where the application is received in the constructor and passed to super().: https://github.com/android/architecture-components-samples/blob/master/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java – jpvillegas Oct 28 '19 at 17:55
  • As per my understanding, this violates the rule. Here, though the finish() action will executed by the viewModel but the Activity took the decision of that action. Which is not a good practice for MVVM. – HBB20 Mar 13 '20 at 23:32
0

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>
Yessy
  • 1,172
  • 1
  • 8
  • 13