How to generate composed view models with temporary sub views?
Asked Answered
T

2

2

Scenario

I have a quizz generator, which generates a sequence of quizzes of different classes. The sequence is of unlimited length.

There is a view model for the quizz generator. There is a view model for each type of a quizz. The quizz generator view model should create the view models of the quizzes depending on their classes.

Issue

A view model must not hold a reference to the lifecycle, but I need the lifecycle to create view models.

ViewModelProviders.of(lifecycle).get(classForQuizzType);

Questions

Where do I create the sub view models of the quizzes?

The one solution I can think of is to inject the sub view model from the activity each time. This is a detour, especially if nested views are involved.

The other solution is to create the view model in the nested view, which seems ugly either, as it is not common practice to access the lifecycle from inside a view.

If there is no clean solution, what's wrong with my approach of architecture? Should I use fragments for this kind of scenario?

Twittery answered 18/4, 2018 at 8:16 Comment(6)
cannot you create a new ViewModelProvider?Brittneybrittni
View models have to be bound to the lifecycle.Twittery
but you want your new quizz ViewModel to be bound to your "master" ViewModel, right?Brittneybrittni
Not necessarily but I get your point. Only a single handle is required to bind a composition to the lifecycle. Components don't even need to be a ViewModel.Twittery
the title says: How to generate view models from inside a view model? so i dont get what you really needBrittneybrittni
I need to reconsider the title now.Twittery
T
0

If there is no clean solution, what's wrong with my approach of architecture? Should I use fragments for this kind of scenario?

Yes, fragments are the right choice

Summary:

  1. There is no real alternative to binding Views by LiveData.
  2. If using LiveData a LifeCycle is required.
  3. If the lifecycle of a sub views in a sequence should be shorter than the lifecycle of the activity, then fragments are the way to go.

Details

There is no real alternative to binding Views by LiveData.

View models should not hold unterminated references to views, else the views exists as long as the view model exists causing memory leaks. There are three observer patterns to discuss how views could observe view models.

a.) MutableLiveData

They require a lifecycle. The references are cleaned up automatically, when the lifecycle ends. This is the recommended solution.

b.) WeakReferences

In theory this should work. The weak reference should be cleaned up by the garbage collector, when the last hard reference to the view is gone. In practice the solution is unstable and references sometime go away prematurely.

c.) Handmade observer

A handmade observer must call a remove method. Unfortunately there is no defined destruction hook, when a view goes away. There is no place to call the remove method in a view.

As a result a.) is the only possible solution according to my experience.

As a lifecycle is required for LiveData fragments are the way to go

The sub views mentioned here are created in a sequence. If we would bind them to the activity, they would pile up until the activity goes away although they are only needed for a small interval of time in sequence.

Fragments can exist for a subpart of the time of the activity. They are the right solution to bind the sub views of the sequence to them.

Example code

The quizzes are called challenges here. The FragmentManger is always that of the activity, while the LifecycleOwner is either the activity or a fragment.

# A view model acceptor interface for views

public interface ViewModelAcceptor<T extends  ViewModel> {
    void plugViewModel(
        T viewModel,
        LifecycleOwner lifecycleOwner,
        FragmentManager fragmentManager
    );
}

# In the parent view class of the challenges new challenges are created
# in sequence

ChallengeFragment challengeFragment = new ChallengeFragment();
challengeFragment.setChallengeViewModel(challengeViewModel);
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(this.getId(), challengeFragment);
fragmentTransaction.commit();

# ChallengeFragment

public class ChallengeFragment extends Fragment {

    private ChallengeViewModel challengeViewModel;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        return new ChallengeView(getActivity(), null);
    }

    public void setChallengeViewModel(ChallengeViewModel challengeViewModel) {
        this.challengeViewModel = challengeViewModel;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ChallengeView challengeView = (ChallengeView) getView();
        Objects.requireNonNull(challengeView)
                .plugViewModel(challengeViewModel, this, getFragmentManager());
    }

}
# Challenge views are the child views of the sequence

public class ChallengeView extends ConstraintLayout implements ViewModelAcceptor<ChallengeViewModel> {
  [...]
}
Twittery answered 26/4, 2018 at 19:3 Comment(0)
T
2

I give a first answer by myself inspired by pskink. Maybe I will update the answer after some experiences with the suggested approach.

If a view model shall generate child objects aka components, the components don't need to be bound to the lifecycle themselves as long a the master object is bound to the lifecycle and the components are referenced from the master.

For the given example this means a good place to create the master object is in the top level position of the activity, where the lifecycle is directly available. The quizz objects are referenced from the master objects. They don't need direct access to the lifecycle and may be created anywhere, for example inside the master object. This enables to create them on demand.

The components may or may not be a subclass of ViewModel. I think it a good practice to extend ViewModel. This parent class brings in the onCleared method. This is the place to remove observers from the underlying model. Without doing this, you likely create memory leaks.

You have to take care to call onCleared at the right moments, at least from the onCleared method of the master object. In this special case each previous quizz has to be cleared just before a new quizz is generated, to remove the references from the underlying quizz models.

The view models of the components can simply be created using the new keyword. There is no need to use factory or a provider.

Twittery answered 18/4, 2018 at 10:52 Comment(4)
exactly, your components are most likely the containers for LiveData objects and they dont have to be ViewModels (unless there is some requirement for this)Brittneybrittni
Components as containers for LiveData objects need access to the lifecycle for the LiveData objects. Where to get the lifecycle from on demand, if the master is not allowed to hold a reference to it?Twittery
liveData.observe(lifecycle, observer); Similar to the original question the lifecycle is only available on the top level, but neither inside views nor inside view models. So I can't create components in those places, that are containers for LiveData. I need to go without LiveData.Twittery
If the feature of LiveData is auto unregistering of observers, I need to manually unregister observers instead, I guess.Twittery
T
0

If there is no clean solution, what's wrong with my approach of architecture? Should I use fragments for this kind of scenario?

Yes, fragments are the right choice

Summary:

  1. There is no real alternative to binding Views by LiveData.
  2. If using LiveData a LifeCycle is required.
  3. If the lifecycle of a sub views in a sequence should be shorter than the lifecycle of the activity, then fragments are the way to go.

Details

There is no real alternative to binding Views by LiveData.

View models should not hold unterminated references to views, else the views exists as long as the view model exists causing memory leaks. There are three observer patterns to discuss how views could observe view models.

a.) MutableLiveData

They require a lifecycle. The references are cleaned up automatically, when the lifecycle ends. This is the recommended solution.

b.) WeakReferences

In theory this should work. The weak reference should be cleaned up by the garbage collector, when the last hard reference to the view is gone. In practice the solution is unstable and references sometime go away prematurely.

c.) Handmade observer

A handmade observer must call a remove method. Unfortunately there is no defined destruction hook, when a view goes away. There is no place to call the remove method in a view.

As a result a.) is the only possible solution according to my experience.

As a lifecycle is required for LiveData fragments are the way to go

The sub views mentioned here are created in a sequence. If we would bind them to the activity, they would pile up until the activity goes away although they are only needed for a small interval of time in sequence.

Fragments can exist for a subpart of the time of the activity. They are the right solution to bind the sub views of the sequence to them.

Example code

The quizzes are called challenges here. The FragmentManger is always that of the activity, while the LifecycleOwner is either the activity or a fragment.

# A view model acceptor interface for views

public interface ViewModelAcceptor<T extends  ViewModel> {
    void plugViewModel(
        T viewModel,
        LifecycleOwner lifecycleOwner,
        FragmentManager fragmentManager
    );
}

# In the parent view class of the challenges new challenges are created
# in sequence

ChallengeFragment challengeFragment = new ChallengeFragment();
challengeFragment.setChallengeViewModel(challengeViewModel);
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(this.getId(), challengeFragment);
fragmentTransaction.commit();

# ChallengeFragment

public class ChallengeFragment extends Fragment {

    private ChallengeViewModel challengeViewModel;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        return new ChallengeView(getActivity(), null);
    }

    public void setChallengeViewModel(ChallengeViewModel challengeViewModel) {
        this.challengeViewModel = challengeViewModel;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ChallengeView challengeView = (ChallengeView) getView();
        Objects.requireNonNull(challengeView)
                .plugViewModel(challengeViewModel, this, getFragmentManager());
    }

}
# Challenge views are the child views of the sequence

public class ChallengeView extends ConstraintLayout implements ViewModelAcceptor<ChallengeViewModel> {
  [...]
}
Twittery answered 26/4, 2018 at 19:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.