Learned about the use of LiveData and Lifecycle architecture components:
ViewModel is lifecycle-aware and automatically stores and manages UI-related data, which persists even after device configuration changes. This means we no longer need to save data in onSaveInstanceState or restore it in onCreate; using ViewModel eliminates this part of the work, effectively separating the view from the logic.
- ViewModel Lifecycle
- Source Code Analysis of ViewModel
- What is ViewModelStore
- What is ViewModelStoreOwner
- How to Simplify Communication Between Fragments
ViewModel Lifecycle#
Once the ViewModel is obtained from OnCreate, it will exist until the View bound to that ViewModel is completely onDestroy.
Source Code Analysis of ViewModel#
This project was created by upgrading Android Studio to 3.2.1, so the dependencies in the project were directly replaced with the corresponding packages under androidx, with the main configuration as follows:
// Gradle plugin
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
// ViewModel and LiveData version
def lifecycle_version = "2.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
// gradle-wrapper.properties file
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
Creating ViewModel as follows:
/**
*
* If you need to use Context, you can choose to inherit AndroidViewModel
* Powered by jzman.
* Created on 2018/12/13 0013.
*/
public class MViewModel extends ViewModel {
private MutableLiveData<List<Article>> data;
public LiveData<List<Article>> getData(){
if (data == null){
data = new MutableLiveData<>();
data.postValue(DataUtil.getData());
}
return data;
}
}
If you need to use Context, you can choose to inherit AndroidViewModel; here, inheriting ViewModel is sufficient. Then, you can use it in the Activity as follows:
MViewModel mViewModel = ViewModelProviders.of(this).get(MViewModel.class);
mViewModel.getData().observe(this, new Observer<List<Article>>() {
@Override
public void onChanged(List<Article> articles) {
for (Article article : articles) {
Log.i(TAG, article.getDesc());
}
}
});
Let's look at the calling process, starting from ViewModelProviders. ViewModelProviders mainly provides four static methods to obtain the corresponding ViewModelProvider, as follows:
public static ViewModelProvider of(@NonNull Fragment fragment)
public static ViewModelProvider of(@NonNull FragmentActivity activity)
public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory)
public static ViewModelProvider of(@NonNull FragmentActivity activity, @Nullable Factory factory)
Taking the second method as an example, its implementation is as follows:
@NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
@Nullable Factory factory) {
Application application = checkApplication(activity);
if (factory == null) {
factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
}
return new ViewModelProvider(activity.getViewModelStore(), factory);
}
You can use the default AndroidViewModelFactory or customize a Factory; just call any of the above methods to create a ViewModelProvider:
Now let's look at ViewModelProvider, which has two key properties:
private final Factory mFactory;
private final ViewModelStore mViewModelStore;
When the ViewModelProvider is created, mFactory and mViewModelStore have already been initialized. Then comes the get() method, with the source code as follows:
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
// Get class name; when getting inner class names, it differs from getName
// getCanonicalName-->xx.TestClass.InnerClass
// getName-->xx.TestClass$InnerClass
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes cannot be ViewModels");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
Then call the get method with the key parameter as follows:
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
//noinspection unchecked
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
// Create ViewModel
viewModel = mFactory.create(modelClass);
// Retrieve the corresponding ViewModel from mViewModelStore based on the key
mViewModelStore.put(key, viewModel);
//noinspection unchecked
return (T) viewModel;
}
At this point, the ViewModel has been created. How is the ViewModel created? The specific implementation of mFactory here is the default AndroidViewModelFactory, which creates it through reflection to obtain the constructor method. The key code is as follows:
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
// Check if AndroidViewModel is a superclass or interface of modelClass
if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
//...
// Create ViewModel via reflection and return
return modelClass.getConstructor(Application.class).newInstance(mApplication);
}
return super.create(modelClass);
}
Once the specific ViewModel object is created, you can freely call the methods within the ViewModel. Earlier, while going through the source code, various wrapper classes like ViewModelStore, ViewModelStoreOwner, AndroidViewModelFactory, etc., will be introduced in the following text.
What is ViewModelStore#
ViewModelStore is mainly used to save the state of ViewModel when device configuration changes, such as when the current interface is recreated or destroyed. The new ViewModelStore should save all information corresponding to the ViewModel just like the old ViewModelStore. Only when the corresponding clear() method is called will it notify that this ViewModel is no longer in use, and its corresponding ViewModelStore will no longer store related information.
This class actually uses a HashMap to store the corresponding ViewModel, which is very simple:
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);
}
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.onCleared();
}
mMap.clear();
}
}
What is ViewModelStoreOwner#
This is an interface that defines a method getViewModelStore() to obtain the corresponding ViewModel's ViewModelStore. Similarly, if the clear() method of ViewModelStoreOwner is called, the corresponding ViewModelStore cannot be obtained. The source code is as follows:
public interface ViewModelStoreOwner {
/**
* Returns owned {@link ViewModelStore}
*
* @return a {@code ViewModelStore}
*/
@NonNull
ViewModelStore getViewModelStore();
}
Of course, the specific implementations are the classes that implement this interface. In fact, classes like FragmentActivity and Fragment indirectly or directly implement this interface, similar to LifecycleOwner. The source code reference is as follows:
- Activity indirectly implements:
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;
}
- Fragment directly implements:
@Override
public ViewModelStore getViewModelStore() {
if (mFragmentManager == null) {
throw new IllegalStateException("Can't access ViewModels from detached fragment");
}
return mFragmentManager.getViewModelStore(this);
}
The process of saving the ViewModelStore is completed in the upper layer implementation of Activity or Fragment. Understanding the ViewModelStoreOwner interface ends here.
How to Simplify Communication Between Fragments#
Communication between Fragments used to be implemented through interfaces forwarded by the host Activity. Now, the same ViewModel can be used to complete communication between two Fragments. Remember one point: when using ViewModel for communication between two Fragments, create the ViewModel using its host Activity. The implementation process is as follows. First, create a ViewModel as follows:
/**
* Powered by jzman.
* Created on 2018/12/14 0014.
*/
public class FViewModel extends ViewModel {
private MutableLiveData<String> mSelect = new MutableLiveData<>();
public void selectItem(String item) {
mSelect.postValue(item);
}
public LiveData<String> getSelect() {
return mSelect;
}
}
Then, create LeftFragment as follows:
public class LeftFragment extends Fragment {
private FViewModel mViewModel;
private FragmentTitleBinding titleBinding;
public LeftFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_title, container, false);
titleBinding = DataBindingUtil.bind(view);
mViewModel = ViewModelProviders.of(getActivity()).get(FViewModel.class);
RvAdapter adapter = new RvAdapter(getActivity(), new RvAdapter.OnRecycleItemClickListener() {
@Override
public void onRecycleItemClick(String info) {
mViewModel.selectItem(info);
}
});
titleBinding.rvData.setLayoutManager(new LinearLayoutManager(getActivity()));
titleBinding.rvData.setAdapter(adapter);
return view;
}
}
The layout file for LeftFragment contains just one RecyclerView, and its item layout file is as follows:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="itemData"
type="String"/>
<variable
name="onItemClick"
type="com.manu.archsamples.fragment.RvAdapter.OnRecycleItemClickListener"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:onClick="@{() -> onItemClick.onRecycleItemClick(itemData)}">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{itemData}"
android:padding="10dp"/>
</LinearLayout>
</layout>
The RecyclerView's Adapter is as follows:
public class RvAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context mContext;
private List<String> mData;
private OnRecycleItemClickListener mOnRecycleItemClickListener;
public RvAdapter(Context mContext, OnRecycleItemClickListener itemClickListener) {
this.mContext = mContext;
mData = DataUtil.getDataList();
mOnRecycleItemClickListener = itemClickListener;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.recycle_item, null);
view.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
return new MViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
MViewHolder mHolder = (MViewHolder) holder;
mHolder.bind(mData.get(position), mOnRecycleItemClickListener);
}
@Override
public int getItemCount() {
return mData.size();
}
private static class MViewHolder extends RecyclerView.ViewHolder {
RecycleItemBinding itemBinding;
MViewHolder(@NonNull View itemView) {
super(itemView);
itemBinding = DataBindingUtil.bind(itemView);
}
void bind(String info, OnRecycleItemClickListener itemClickListener) {
itemBinding.setItemData(info);
itemBinding.setOnItemClick(itemClickListener);
}
}
public interface OnRecycleItemClickListener {
void onRecycleItemClick(String info);
}
}
Next, create RightFragment as follows:
public class RightFragment extends Fragment {
private static final String TAG = RightFragment.class.getName();
private FragmentContentBinding contentBinding;
private FViewModel mViewModel;
public RightFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_content, container, false);
contentBinding = DataBindingUtil.bind(view);
mViewModel = ViewModelProviders.of(getActivity()).get(FViewModel.class);
mViewModel.getSelect().observe(this, new Observer<String>() {
@Override
public void onChanged(String s) {
// Receive the value of the LeftFragment Item click event
contentBinding.setData(s);
}
});
return view;
}
}
The layout file for RightFragment is as follows:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="data"
type="String"/>
</data>
<FrameLayout
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.LeftFragment">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginStart="12dp"
android:layout_marginTop="10dp"
android:text="@{data, default=def}"/>
</FrameLayout>
</layout>
The implementation method is quite simple, and there is not much to say. After using ViewModel, the host Activity becomes very clean, only responsible for switching Fragments. The testing effect is as follows:
The advantages of using ViewModel are as follows:
- The Activity no longer intervenes in communication between child Fragments, making responsibilities more singular.
- The Fragments, aside from using the same instance of ViewModel, are otherwise different, allowing any Fragment to work independently.
- Each Fragment has its own lifecycle, allowing for arbitrary replacement and removal without affecting the normal operation of another Fragment.