HelloCW
HelloCW

Reputation: 2295

Must a ViewModel class inherit from ViewModel() in Android Jetpack?

I'm learning Android Jetpack, the following Code is from a sample project at https://github.com/android/sunflower .

In my mind, a ViewModel classs must inherit from ViewModel() in Code B, just like Code E.

1: Why doesn't the class PlantAndGardenPlantingsViewModel inherit from ViewModel() in Code B?

2: And more, it seems that the app need launch executePendingBindings() opeartion (Code D, see source code) to update list_item_garden_planting.xml UI when it use Code B, right?

BTW, the other ViewModel class in the project is inherit from ViewModel(), such as Code E, you can see source code.

Code A

list_item_garden_planting.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="clickListener"
            type="android.view.View.OnClickListener"/>
        <variable
            name="viewModel"
            type="com.google.samples.apps.sunflower.viewmodels.PlantAndGardenPlantingsViewModel"/>
    </data>
     ...

Code B

PlantAndGardenPlantingsViewModel.kt

class PlantAndGardenPlantingsViewModel(plantings: PlantAndGardenPlantings) {
    private val plant = checkNotNull(plantings.plant)
    private val gardenPlanting = plantings.gardenPlantings[0]
...

Code D

 ...
  fun bind(plantings: PlantAndGardenPlantings) {
        with(binding) {
            viewModel = PlantAndGardenPlantingsViewModel(plantings)
            executePendingBindings()
        }
    }
  ...

Code E

class PlantDetailViewModel(
    plantRepository: PlantRepository,
    private val gardenPlantingRepository: GardenPlantingRepository,
    private val plantId: String
) : ViewModel() {

    val isPlanted = gardenPlantingRepository.isPlanted(plantId)
    val plant = plantRepository.getPlant(plantId)

    fun addPlantToGarden() {
        viewModelScope.launch {
            gardenPlantingRepository.createGardenPlanting(plantId)
        }
    }
}

Upvotes: 1

Views: 967

Answers (2)

Thracian
Thracian

Reputation: 67189

Actually there is not anything magical with ViewModel class. It's creation, and retention which makes it useful.

ViewModel source code simplified

public abstract class ViewModel {
    // Can't use ConcurrentHashMap, because it can lose values on old apis (see b/37042460)
    @Nullable
    private final Map<String, Object> mBagOfTags = new HashMap<>();
    private volatile boolean mCleared = false;

    /**
     * This method will be called when this ViewModel is no longer used and will be destroyed.
     * <p>
     * It is useful when ViewModel observes some data and you need to clear this subscription to
     * prevent a leak of this ViewModel.
     */
    @SuppressWarnings("WeakerAccess")
    protected void onCleared() {
    }

    @MainThread
    final void clear() {
        mCleared = true;
        // Since clear() is final, this method is still called on mock objects
        // and in those cases, mBagOfTags is null. It'll always be empty though
        // because setTagIfAbsent and getTag are not final so we can skip
        // clearing it
        if (mBagOfTags != null) {
            synchronized (mBagOfTags) {
                for (Object value : mBagOfTags.values()) {
                    // see comment for the similar call in setTagIfAbsent
                    closeWithRuntimeException(value);
                }
            }
        }
        onCleared();
    }
}

Which is a container where you call onCleared() when you are done with it. 2 things that make a ViewModel class is useful are

1- You get the same ViewModel if you use the same Activity(key of hashMap) 2- You get the same ViewModel after device rotation

If you check out the super classes of AppCompatActivity, you can see that ComponentActivity class implements theViewModelStoreOwner interface.

This interface has a method getViewModelStore() which creates or returns a ViewModelStore item.

public ViewModelStore getViewModelStore() {
            if (getApplication() == null) {
                throw new IllegalStateException("Your activity is not yet attached to the "
                        + "Application instance. You can't request ViewModel before onCreate call.");
            }
            if (mViewModelStore == null) {
                NonConfigurationInstances nc =
                        (NonConfigurationInstances) getLastNonConfigurationInstance();
                if (nc != null) {
                    // Restore the ViewModelStore from NonConfigurationInstances
                    mViewModelStore = nc.viewModelStore;
                }
                if (mViewModelStore == null) {
                    mViewModelStore = new ViewModelStore();
                }
            }
            return mViewModelStore;
        }

and ViewModelStore is container for a HashMap that holds keys as class names.

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}

If you use the same objects to retrieve a ViewModel, if key has equals and hashcode, you get the same ViewModel.

That's how you get same ViewModel in fragments if you use Activity as key

mvViewModel = ViewModelProvider(activity!!, viewModelFactory).get(MyViewModel::class.java)

2- You get the same ViewModel after device rotation since ComponentActivity class has a method

  /**
     * Retain all appropriate non-config state.  You can NOT
     * override this yourself!  Use a {@link androidx.lifecycle.ViewModel} if you want to
     * retain your own non config state.
     */
    @Override
    @Nullable
    public final Object onRetainNonConfigurationInstance() {
        Object custom = onRetainCustomNonConfigurationInstance();

        ViewModelStore viewModelStore = mViewModelStore;
        if (viewModelStore == null) {
            // No one called getViewModelStore(), so see if there was an existing
            // ViewModelStore from our last NonConfigurationInstance
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                viewModelStore = nc.viewModelStore;
            }
        }

        if (viewModelStore == null && custom == null) {
            return null;
        }

        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = viewModelStore;
        return nci;
    }

If you wish to benefit from these two features you can use Jetpack ViewModel or you can build same structure using a HashMap and factory pattern for creation of a custom ViewModel class implementation.

Upvotes: 3

turbofan
turbofan

Reputation: 318

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations. So,in some cases,you shouldn't inherit from ViewModel() when Activity/Fragment is killed and re-built the view-model will be reused, the same instance of view-model. For example, local(country or language) of the device has been changed, the Activity might be destroyed and recreated, the view-model will be still the one like before-destroy. Besides,you can see more detail in this link.https://github.com/android/sunflower/issues/132

Upvotes: 0

Related Questions