Data binding provides two-way binding function, so many frameworks directly inject ViewModel into xml. I personally dislike this practice. It may be that getting up early and Databinding is immature often leads to inexplicable compilation errors XXX Br not found and can't find variable. When using Databinding, I use it more to get the control and complete the assignment in the code. In addition, the code in xml lacks a compile time check mechanism. For example, when you assign an int type to a String type in xml, it will not report an error during compilation. In addition, the business logic that can be completed in xml is limited. A three-dimensional operator?: The code width limit has been exceeded. Many times you have to put part of your business in Java code and part of your code in xml. When a problem occurs, it increases the difficulty of troubleshooting. I remember that there was a problem that the UI was not refreshed in time when Databinding was used in the project. Due to the age, I can't remember the specific reasons. Finally, using Databinding may slow down the compilation of the project. If the project is small, it may not be a problem, but it is undoubtedly worse for a large project with more than 100 modules.
As far as a framework is concerned, what Android vmlib does on Databinding is enabling. We provide you with the ability to apply data binding, and we also provide you with a scheme to completely exclude data binding. Taking Activity as an example, we provide two abstract classes: BaseActivity and CommonActivity. If you want to use Databinding in the project, copy the following classes to pass in the Databinding class of the layout and ViewModel, and then obtain and use the control through binding:
class MainActivity : CommonActivity<MainViewModel, ActivityMainBinding>() { override fun getLayoutResId(): Int = R.layout.activity_main override fun doCreateView(savedInstanceState: Bundle?) { // Get control through binding setSupportActionBar(binding.toolbar) } }
If you don't want to use Databinding in the project, you can inherit BaseActivity like the following class, and then get the control through the traditional findViewById and use it:
class ContainerActivity : BaseActivity<EmptyViewModel> { override fun getLayoutResId(): Int = R.layout.vmlib_activity_container override fun doCreateView(savedInstanceState: Bundle?) { // Get the control through findViewById // Alternatively, after introducing kotlin Android extensions, you can use the control directly through id } }
As you can see, I use data binding more as ButterKinfe. I specifically provide the ability not to include data binding, which is another consideration - after using kotlin Android extensions, you can use it directly in the code through the id of the control. If you just want to get the control through Databinding, there is no need to use Databinding. For students who really like the data binding function of data binding, they can personalize the encapsulation layer on Android vmlib. Of course, I'm not rejecting data binding. Data binding is a good design concept, but I still hold a wait-and-see attitude towards its wide application to the project.
2.3 unified data interaction format
Students with back-end development experience may know that in the back-end code, we usually divide the code into DAO, Service and controller layers according to the level. When data interaction is carried out between each layer, it is necessary to uniformly encapsulate the data interaction format. The data format should also be encapsulated during the interaction between the back-end and the front-end. We extend it to MVVM. Obviously, a layer of data packaging should also be carried out when interacting between ViewModel layer and View layer. Here is a piece of code I saw,
final private SingleLiveEvent<String> toast; final private SingleLiveEvent<Boolean> loading; public ApartmentProjectViewModel() { toast = new SingleLiveEvent<>(); loading = new SingleLiveEvent<>(); } public SingleLiveEvent<String> getToast() { return toast; } public SingleLiveEvent<Boolean> getLoading() { return loading; } public void requestData() { loading.setValue(true); ApartmentProjectRepository.getInstance().requestDetail(projectId, new Business.ResultListener<ProjectDetailBean>() { @Override public void onFailure(BusinessResponse businessResponse, ProjectDetailBean projectDetailBean, String s) { toast.setValue(s); loading.setValue(false); } @Override public void onSuccess(BusinessResponse businessResponse, ProjectDetailBean projectDetailBean, String s) { data.postValue(dealProjectBean(projectDetailBean)); loading.setValue(false); } }); }
Here, in order to notify the View layer of the loading status of data, a Boolean type LiveData is defined for interaction. In this way, you need to maintain one more variable, which makes the code not concise enough. In fact, through the specification of data interaction format, we can accomplish this task more gracefully.
In Android vmlib, we use custom enumeration to represent the state of data,
public enum Status { // success SUCCESS(0), // fail FAILED(1), // Loading LOADING(2); public final int id; Status(int id) { this.id = id; } }
Then, the error information, data results, data status and reserved fields are packaged into a Resource object as a fixed data interaction format,
public final class Resources<T> { // state public final Status status; // data public final T data; // Status, success or error code and message public final String code; public final String message; // Reserved field public final Long udf1; public final Double udf2; public final Boolean udf3; public final String udf4; public final Object udf5; // ... }
Explain the function of the reserved fields here: they are mainly used as supplementary data descriptions. For example, when paging, if the View layer wants to get not only the real data, but also the current page number, you can insert the page number information into the udf1 field. Above, I only provide a general choice for different types of basic data types. For example, integer only provides Long type, and floating-point only provides Double type. In addition, we also provide an unconstrained type udf5
In addition to the encapsulation of data interaction format, Android vmlib also provides shortcut methods for interactive format. As shown in the figure below,
[external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-196i9s6f-1630811386566)( https://user-gold-cdn.xitu.io/2020/5/23/1723f99e54a89a43?imageView2/0/w/1280/h/960/ignore -error/1)]
So, what will the code look like after using Resource?
// View layer code class MainActivity : CommonActivity<MainViewModel, ActivityMainBinding>() { override fun getLayoutResId(): Int = R.layout.activity_main override fun doCreateView(savedInstanceState: Bundle?) { addSubscriptions() vm.startLoad() } private fun addSubscriptions() { vm.getObservable(String::class.java).observe(this, Observer { when(it!!.status) { Status.SUCCESS -> { ToastUtils.showShort(it.data) } Status.FAILED -> { ToastUtils.showShort(it.message) } Status.LOADING -> {/* temp do nothing */ } else -> {/* temp do nothing */ } } }) } } // ViewModel layer code class MainViewModel(application: Application) : BaseViewModel(application) { fun startLoad() { getObservable(String::class.java).value = Resources.loading() ARouter.getInstance().navigation(MainDataService::class.java) ?.loadData(object : OnGetMainDataListener{ override fun onGetData() { getObservable(String::class.java).value = Resources.loading() } }) } }
After encapsulating the data interaction format, is the code much simpler? As for making your code more concise, Android vmlib also provides you with other methods. Please continue to read.
2.4 further simplify the code and optimize the ubiquitous LiveData
Previously, when using ViewModel+LiveData, in order to interact with data, I needed to define a LiveData for each variable, so the code became like this. I even saw in some students that 10 + LiveData are defined in a ViewModel This makes the code very ugly,
public class ApartmentProjectViewModel extends ViewModel { final private MutableLiveData<ProjectDetailBean> data; final private SingleLiveEvent<String> toast; final private SingleLiveEvent<Boolean> submit; final private SingleLiveEvent<Boolean> loading; public ApartmentProjectViewModel() { data = new MutableLiveData<>(); toast = new SingleLiveEvent<>(); submit = new SingleLiveEvent<>(); loading = new SingleLiveEvent<>(); } // ... }
Later, one of my colleagues suggested that I consider how to organize LiveData. After continuous promotion and evolution, this solution has been relatively perfect - that is, the LiveData of a single instance is managed uniformly through HashMap. Later, in order to further simplify the code of the ViewModel layer, I entrusted this part of the work to a Holder. So the following solutions are basically formed,
public class BaseViewModel extends AndroidViewModel { private LiveDataHolder holder = new LiveDataHolder(); // Get a LiveData object by the data type to be passed public <T> MutableLiveData<Resources<T>> getObservable(Class<T> dataType) { return holder.getLiveData(dataType, false); } }
The Holder here is implemented as follows,
public class LiveDataHolder<T> { private Map<Class, SingleLiveEvent> map = new HashMap<>(); public MutableLiveData<Resources<T>> getLiveData(Class<T> dataType, boolean single) { SingleLiveEvent<Resources<T>> liveData = map.get(dataType); if (liveData == null) { liveData = new SingleLiveEvent<>(single); map.put(dataType, liveData); } return liveData; } }
The principle is simple. After using this scheme, your code will become as concise and elegant as below,
// ViewModel layer class EyepetizerViewModel(application: Application) : BaseViewModel(application) { private var eyepetizerService: EyepetizerService = ARouter.getInstance().navigation(EyepetizerService::class.java) private var nextPageUrl: String? = null fun requestFirstPage() { getObservable(HomeBean::class.java).value = Resources.loading() eyepetizerService.getFirstHomePage(null, object : OnGetHomeBeansListener { override fun onError(errorCode: String, errorMsg: String) { getObservable(HomeBean::class.java).value = Resources.failed(errorCode, errorMsg) } override fun onGetHomeBean(homeBean: HomeBean) { nextPageUrl = homeBean.nextPageUrl getObservable(HomeBean::class.java).value = Resources.success(homeBean) // Request another page requestNextPage() } }) } fun requestNextPage() { eyepetizerService.getMoreHomePage(nextPageUrl, object : OnGetHomeBeansListener { override fun onError(errorCode: String, errorMsg: String) { getObservable(HomeBean::class.java).value = Resources.failed(errorCode, errorMsg) } override fun onGetHomeBean(homeBean: HomeBean) { nextPageUrl = homeBean.nextPageUrl getObservable(HomeBean::class.java).value = Resources.success(homeBean) } }) } } // View layer class EyepetizerActivity : CommonActivity<EyepetizerViewModel, ActivityEyepetizerBinding>() { private lateinit var adapter: HomeAdapter private var loading : Boolean = false override fun getLayoutResId() = R.layout.activity_eyepetizer override fun doCreateView(savedInstanceState: Bundle?) { addSubscriptions() vm.requestFirstPage() } private fun addSubscriptions() { vm.getObservable(HomeBean::class.java).observe(this, Observer { resources -> loading = false when (resources!!.status) { Status.SUCCESS -> { L.d(resources.data) } private fun addSubscriptions() { vm.getObservable(HomeBean::class.java).observe(this, Observer { resources -> loading = false when (resources!!.status) { Status.SUCCESS -> { L.d(resources.data)