Reputation: 3211
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
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
Reputation: 93
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()
}
}
}
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
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
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
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
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 WeakReference
s 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