Not so long ago for Android developers, Google introduced a new library - Android Architecture Components. It helps to implement an MVx pattern-based architecture (MVP, MVVM etc.) in the application. In addition, another Google library, the Data Binding Library, has long been released. It allows you to link the mapping of UI controls directly to the markup with the values ​​contained in the objects. This is an important feature of the MVVM pattern - to associate the View layer with the ViewModel layer.
Both libraries are aimed at building the architecture of Android applications in MVVM style.
I'll tell you how you can use them together to create a project with an MVVM-based architecture.
The MVVM pattern assumes separation of the application architecture into 3 layers:
The main interest in the article will be chained to binding. These are the links for displaying specific View parameters (for example, “text” in a TextView) with specific fields of the ViewModel (for example, the “user name” field). They are set in the markup View (in layout), and not in the code. The ViewModel, in turn, should represent the data in such a way that it can be easily linked with View.
By itself, the MVVM pattern, like MVP and MVC, allows the code to be divided into independent layers. The main difference MVVM - in the banding. That is, in the possibility directly in the markup to link the display of what is visible to the user - the View layer, with the state of the application - the Model layer. In general, the advantage of MVVM is not to write extra code to associate a view with a display — bindings do it for you.
Google is moving toward supporting the MVVM pattern-based architecture. Libraries Android Architecture Components (hereinafter AAC) and Data Binding are a direct confirmation of this. In the future, most likely, this pattern will be used on most Android projects.
At the moment, the problem is that neither AAC nor Data Binding provides an opportunity to fully implement the MVVM pattern. AAC implements the ViewModel layer, but banding must be configured manually in code. Data Binding, in turn, provides the ability to write bindings in the markup and bind them to the code, but the ViewModel layer must be implemented manually in order to propagate the update of the application state through the binding to the View.
In the end, everything seems to be ready, but divided into two libraries, and in order to make it really look like MVVM, you just need to take and combine them.
In general, what should be done for this:
We will try to do this on the example of a simple user profile screen.
There will be three elements on the screen:
Login will be stored in SharedPreferences. A user is considered authorized if a login is entered into SharedPreferences.
For simplicity, third-party frameworks, network requests, and error mapping will not be used.
I will start with the View layer so that it is clear what the user will see on the screen. Immediately mark the bindings I need without reference to a specific ViewModel. How it will all work - it will become clear later.
Actually, layout:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="touchin.aacplusdbtest.R"/> <import type="android.view.View"/> <!-- ViewModel , --> <variable name="profileViewModel" type="touchin.aacplusdbtest.ProfileViewModel"/> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!-- . , userLogin. visibility isUserLoggedIn, . --> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{profileViewModel.userLogin}" android:visibility="@{profileViewModel.isUserLoggedIn ? View.VISIBLE : View.GONE}"/> <!-- . , inputLogin, , inputLogin ViewModel View, - inputLogin. visibility isUserLoggedIn, . --> <touchin.aacplusdbtest.views.SafeEditText android:layout_width="match_parent" android:layout_height="wrap_content" android:addTextChangedListener="@{profileViewModel.inputLogin}" android:text="@{profileViewModel.inputLogin}" android:visibility="@{profileViewModel.isUserLoggedIn ? View.GONE : View.VISIBLE}"/> <!-- /. isUserLoggedIn: "", , "" - . isUserLoggedIn: logout login. --> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{profileViewModel.isUserLoggedIn ? R.string.logout : R.string.login}" android:onClick="@{(v)-> profileViewModel.loginOrLogout()}"/> </LinearLayout> </layout>
Before implementing the Model layer, you need to deal with the LiveData class from AAC. We need it to notify the ViewModel layer about changes to the Model layer.
LiveData is a class whose objects deliver data and update it to subscribers. It is an implementation of the Observer pattern. You can subscribe to LiveData, and LiveData itself implements internally how it will calculate and update data for subscribers.
The feature of LiveData is that it can be tied to a lifecycle object and activated only when such an object is in the started state. This is convenient for updating the View layer: as long as the activation or fragment is in the started state, this means that they have initialized the entire UI and need the actual data. LiveData responds to this and is activated - it calculates the current value and notifies subscribers about the updated data.
From the Model layer, we need the following functionality: the login (String login), logout () methods and the ability to track the current login of an authorized user based on LiveData.
Add the ProfileRepository class, which will be responsible for the user authorization logic:
class ProfileRepository(context: Context) { private val loginKey = "login" private val preferences = context.getSharedPreferences("prefs", Context.MODE_PRIVATE) // LiveData, // private val innerLoggedInUser = LoggedInUserLiveData() val loggedInUser: LiveData<String?> get() = innerLoggedInUser fun login(login: String) { preferences.edit().putString(loginKey, login).apply() notifyAboutUpdate(login) } fun logout() { preferences.edit().putString(loginKey, null).apply() notifyAboutUpdate(null) } private fun notifyAboutUpdate(login: String?) { innerLoggedInUser.update(login) } private inner class LoggedInUserLiveData : LiveData<String?>() { // , // onActive. init { value = preferences.getString(loginKey, null) } // postValue UI- // , , // UI- setValue fun update(login: String?) { postValue(login) } } }
Place this object in Application to make it easier to access it, having Context:
class AacPlusDbTestApp : Application() { lateinit var profileRepository: ProfileRepository private set override fun onCreate() { super.onCreate() profileRepository = ProfileRepository(this) } }
Before implementing the ViewModel layer, you need to deal with the main class from AAC, which is used for this.
ViewModel is a class representing objects of the ViewModel layer. An object of this type can be created from anywhere in the application. In this class there should always be either a default constructor ( ViewModel class), or a constructor with a parameter of type Application ( AndroidViewModel class).
To request a ViewModel by type, call:
mvm = ViewModelProviders.of(fragmentOrActivity).get(MyViewModel::class.java)
Or by key:
mvm1 = ViewModelProviders.of(fragmentOrActivity).get("keyVM1", MyViewModel::class.java) mvm2 = ViewModelProviders.of(fragmentOrActivity).get("keyVM2", MyViewModel::class.java)
ViewModel is stored separately for each activation and for each fragment. The first request, they are created and placed for storage in the activation or fragment. Upon repeated request, the already created ViewModel is returned. The uniqueness of a specific ViewModel is its type or string key + where it is stored.
ViewModel and AndroidViewModel are created by default through reflection - the corresponding constructor is called. So, when adding your constructors, in the ViewModelProviders.of (...) method you need to explicitly specify the factory for creating such objects.
From ProfileViewModel we need the following:
Create a ProfileViewModel and associate it with the ProfileRepository:
// AndroidViewModel, ProfileRepository Application class ProfileViewModel(application: Application) : AndroidViewModel(application) { private val profileRepository: ProfileRepository = (application as AacPlusDbTestApp).profileRepository // Transformations — - // map, - String? boolean val isUserLoggedInLiveData = Transformations.map(profileRepository.loggedInUser) { login -> login != null } // LiveData val loggedInUserLiveData = profileRepository.loggedInUser // , // TextField - ObservableField, TextWatcher // , text addTextChangedListener, // . // EditText ViewModel, ViewModel — EditText. val inputLogin = TextField() fun loginOrLogout() { // - , isUserLoggedInLiveData.observeForever(object : Observer<Boolean> { override fun onChanged(loggedIn: Boolean?) { if (loggedIn!!) { profileRepository.logout() } else if (inputLogin.get() != null) { // - profileRepository.login(inputLogin.get()) } else { // , " " } // isUserLoggedInLiveData.removeObserver(this) } }) } }
Now, when calling the loginOrLogout method in ProfileRepository, LoginLiveData will be updated and these updates can be displayed on the View layer by subscribing to LiveData from ProfileViewModel.
But LiveData and ViewModel are not yet adapted for binding, so this code cannot be used yet.
With access to the ViewModel from the markup, there are no special problems. We declare it in the markup:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="profileViewModel" type="touchin.test.ProfileViewModel"/> </data> ... </layout>
And set in activit or fragment:
// LifecycleActivity, LiveData. // LiveData , started. class ProfileActivity : LifecycleActivity() { lateinit private var binding: ActivityProfileBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // binding = DataBindingUtil.setContentView<ActivityProfileBinding>(this, R.layout.activity_profile) // ViewModel binding.profileViewModel = ViewModelProviders.of(this).get(ProfileViewModel::class.java) } }
I decided to adapt LiveData based on the ObservableField class. It allows you to bind a changing value of an arbitrary type to a specific view property.
In my example, it will be necessary to add visibility of the view to whether the user is authorized or not. As well as the text property to the user login.
ObservableField has two methods - addOnPropertyChangedCallback and removeOnPropertyChangedCallback. These methods are called when binding is added and removed from the view.
In essence, these methods are those moments when you need to subscribe and unsubscribe from LiveData:
// ObservableField // Observer ( LiveData) LiveData ObservableField class LiveDataField<T>(val source: LiveData<T?>) : ObservableField<T>(), Observer<T?> { // ObservableField private var observersCount: AtomicInteger = AtomicInteger(0) override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) { super.addOnPropertyChangedCallback(callback) if (observersCount.incrementAndGet() == 1) { // LiveData, ObservableField view source.observeForever(this) } } override fun onChanged(value: T?) = set(value) override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) { super.removeOnPropertyChangedCallback(callback) if (observersCount.decrementAndGet() == 0) { // LiveData, view ObservableField source.removeObserver(this) } } }
To subscribe to LiveData, I used the observeForever method. It does not transmit a lifecycle object and activates LiveData regardless of the state in which the activity is or the fragment on which the view is located.
In principle, from the OnPropertyChangedCallback object, you can get a view, from view - context, context lead to LifecycleActivity and bind LiveData to this activity. Then it will be possible to use the observe (lifecycleObject, observer) method. Then LiveData will be activated only when the activation on which the view is located is in the started state.
This hack will look like this:
class LifecycleLiveDataField<T>(val source: LiveData<T?>) : ObservableField<T>(), Observer<T?> { ... override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) { super.addOnPropertyChangedCallback(callback) try { // , - val callbackListenerField = callback.javaClass.getDeclaredField("mListener") callbackListenerField.setAccessible(true) val callbackListener = callbackListenerField.get(callback) as WeakReference<ViewDataBinding> val activity = callbackListener.get()!!.root!!.context as LifecycleActivity if (observersCount.incrementAndGet() == 1) { source.observe(activity, this) } } catch (bindingThrowable: Throwable) { Log.e("BINDING", bindingThrowable.message) } } ... }
Now we change the ProfileViewModel so that it can be easily reached:
class ProfileViewModel(application: Application) : AndroidViewModel(application) { ... // null val userLogin = LifecycleLiveDataField(loggedInUserLiveData) // val isUserLoggedIn = LifecycleLiveDataField(isUserLoggedInLiveData) ... }
Important! During testing, there was one unpleasant flaw in the Data Binding library - the attached views do not call the removeOnPropertyChangedCallback method even when the activation dies. This causes the Model layer to hold references to objects on the View layer through the ViewModel layer. In general, a memory leak from LiveDataField objects.
To avoid this, you can use another hack and manually reset all banding on onDestroy activations:
class ProfileActivity : LifecycleActivity() { ... override fun onDestroy() { super.onDestroy() // profileViewModel binding.profileViewModel = null // , // onDestroy :( binding.executePendingBindings() } }
In addition, attentive readers might notice in the markup class SafeEditText. In general, it was needed, because of the bug in the Data Binding Library. The bottom line is that it adds text to the listener via addTextChangedListener even if this listener is null.
Since at the onDestroy stage I nullify the model, first the null-listener is added to EditText, and then the text is updated, which also becomes null. As a result, NPE crash occurred on onDestroy when trying to inform the null listener that the text had become null.
In general, when using Data Binding, be prepared for such bugs - there are quite a few of them.
In general, with some difficulties, hacks and some disappointments, but it turned out to link AAC and Data Binding. Most likely, in the near future (year 2?) Google will add some features to link them - the same analogue of my LiveDataField. So far, AAC is in alpha, so much there can still change.
The main problems at the moment, in my opinion, are related to the Data Binding library - it is not adapted to work with the ViewModel and there are unpleasant bugs in it. This is clearly seen from the hacks that had to be used in the article.
First, when bundling, it is difficult to get an activation or fragment to get the LifecycleObject required for LiveData. This problem can be solved: either we take it out through reflection, or we simply observeforever, which will keep the subscription to LiveData, until we manually reset the baydings to onDestroy.
Secondly, Data Binding assumes that ObservableField and other Observable objects live in the same life cycle as the view. In fact, these objects are part of the ViewModel layer, which has a different life cycle. For example, in AAC, this layer is going through activations coups, and Data Binding does not update the banding after a coup of activations - all views for it have died, which means that all Observable objects also died and there is no point in updating anything. This problem can be solved by zeroing manually on the onDestroy. But this requires unnecessary code and the need to ensure that all bandings are reset to zero.
Thirdly, there is a problem with the objects of the View layer without an explicit life cycle, for example, the ViewHolder adapter for RecyclerView. They do not have a clear call onDestroy, as they are reused. At what point it is hard to say unambiguously zeroing the binding in the ViewHolder.
I would not say that at the moment a bunch of these libraries look good, although you can use it. Should you use this approach in view of the shortcomings described above - you decide.
An example from the article can be found on the Touch Instinct githab .
Source: https://habr.com/ru/post/330830/
All Articles