1 the MVVM overview
This article covers many parts of the MVVM architecture in Android, mainly analyzing the technical architecture used by the author such as ViewModel+DataBinding+RxJava+LiveData+Lifecycle.
This article has a large number of words, the content is relatively complete and will be updated later. It will take a long time to read this article, so it is recommended that readers read it in sections.
All words are personal learning summary and understanding, only for reference, if there is a mistake also please point out, the author is very grateful.
1.1 Configuration Environment
- The author’s
Android Studio
Version =3.2
Jetpack
Least compatible toAndroid
=2.1
.API
=7
1.2 Why MVVM?
To answer this question, we should first introduce MVC and MVP modes. From MVC to MVVM, what we want is how to separate Model and View as much as possible (those who are familiar with the definitions of the three can skip this section).
1.2.1 MVC
MVC (Model-view-Controller) is the most commonly used pattern in traditional Android development:
- Usually use
Activity
/Fragment
As aController
Layer, - In order to
android.view.View
A subclass of toxml
Built by the build filelayout
As aView
layer - In order to
SQLite
Database, network request asModel
Layer.
However, because the function of the Activity/Fragment is too powerful and actually contains some View layer functions, the Activity/Fragment finally assumes the responsibility of both the View and the Controller. As a result, activities/fragments tend to pile up code on complex pages, resulting in controllers mixing the View layer with business logic (you know, 3,000 lines of an Activity).
In MVC, there is almost no separation between the View layer and the Model layer. The View layer can operate the Model layer directly, and the Model layer may assign values to the View directly in its callback. The concept of Controller was weakened, and finally only MV was left without C.
This also leads to the problem that when you want to update an element on the interface, it will involve a lot of code related to the Model layer. This problem also occurs when you change the Model layer, which is caused by not properly layering the logic.
1.2.2 the MVP
MVP (Model-View-Presenter) architecture design is the most popular development mode at present, which is mainly dominated by TodoMVP launched by Google. MVP is not a framework, but actually more similar to a layered idea, an interface convention, which is embodied in the following:
- define
IView
Interface, and is specified in the interfaceView
Layer of various operations usedandroid.view.View
A subclass of toxml
Built by the build filelayout
andActivity
/Fragment
As a layout controller, implementIView
thisView
Layer interface,View
The actual implementation class of the layer remains oneIPresenter
Interface instance. - define
IPresenter
Interface, and is specified in the interfacePresenter
Layer of various operations. Can be used withView
Irrelevant classes implement it, generallyXxxPresenterImpl
.UsuallyPresenter
Layer containsModel
Layer reference and oneIView
A reference to an interface, but not directly or indirectlyView
layerandroid.view.View
Subclasses of, and even operation parameters are best not includedandroid.view.View
Because it should only handle business logic and data processing through a unified interfaceIView
Passed to theView
Layer. - Don’t need to
Model
Layer defines aIModel
The interface of this layer is the least modified. Pretty much the same way we came here before. But nowPresenter
And itView
Separated,Presenter
It can be reused as a separate piece of logic.
The MVP model solves the problem of layering in MVC, and the Presenter layer is highlighted, which is actually an implemented MVC
However, there are still some problems in THE MVP. As the business logic becomes more complex, the number of operations on the IPresenter and IView layer can explode in pairs. Adding one business logic may require adding several communication interfaces on both sides, which feels silly.
Also, we need to know that a Presenter needs to have an IView. When a Presenter needs to be reused, the View needs to perform all of these operations, but often some of these operations are not required, leaving a lot of TODO, which is ugly.
1.2.3 MVVM
MVVM (Model-view-ViewModel) is an evolution of the MVP mode. It consists of View layer,DataBinding layer,ViewModel layer,Model layer. MVVM is an updated version of MVP and is supported by the Framework provided by Google Jetpack toolkit:
View
The layer contains the layout, as well as the layout lifecycle controller (Activity
/Fragment
)DataBinding
Used to implementView
Layer andViewModel
Two-way binding of data (but actually inAndroid Jetpack
In theDataBinding
Exists only between the layout and the layout lifecycle controller and is forwarded to when data changes are bound to the layout lifecycle controllerViewModel
The layout controller can be heldDataBinding
butViewModel
Should not holdDataBinding
)ViewModel
withPresenter
Pretty much the same, processing data and implementing business logic, butViewModel
Layers should not be held directly or indirectlyView
Layer any references because of aViewModel
You should not direct yourself to which oneView
Interactive.ViewModel
The main job is to makeModel
The data provided is translated directly intoView
Layers are able to directly use the data and expose those data at the same timeViewModel
Can also publish events forView
Layer of subscription.Model
Layer andMVP
In the agreement.
The core idea of MVVM is the observer pattern, which decoupled the View layer from the ViewModel layer through events and transfers of View layer data holding rights.
In MVVM, the View is not the actual holder of the data, it is only responsible for how the data is presented and the transmission of click events, do not do the data processing work, and the data handler and holder become the ViewModel, it changes its state by receiving the time transmitted from the View layer, issue events or change the data it holds to trigger the View The update.
MVVM solves some of the problems in MVP, such as it doesn’t need to define an interface, the ViewModel is completely independent of the View layer and is more reusable, and has Google’s Android Jetpack as a powerful back-up.
However, MVVM also has its own disadvantages, that is to use MVVM in the case of ViewModel and View layer communication becomes more difficult, so please use in some extremely simple pages, otherwise there will be a kind of pants fart feeling, in the use of MVP this truth also applies.
2 DataBinding
2.1 the pit
If you want to use a frame you have to talk about its pit points. It is not recommended to use the Apply plugin: ‘kotlin-kapt’ with modules using DataBinding.
When using KAPT, utF-8 errors will be reported if Chinese characters are contained in XML in DataBinding format under Windows.
JVM startup parameter: -dfile. encoding= utF-8; gradle.properties: -dfile. encoding= utF-8
It’s fine if you use both Kotlin and DataBinding in modules, but please don’t use Kapt until JB fixes these weird issues.
This means that all of your Kotlin code cannot rely on an annotation handler to provide additional functionality to your code, but you can replace it with an equivalent Java implementation and it will work just fine.
2.2 DataBinding compatibility
To start with,DataBinding style XML has “weird” things that creep into Android’s native XML format, which LayoutInfalter doesn’t understand, but it doesn’t give an error when you use LayoutInfalter#inflate against these weird XML, and The layout also loads properly. Why?
This is because Gradle translates all of your DataBinding style XML through APT at package time so LayoutInfalter can read it. This compatible implementation allows you to switch between using and not using DataBinding.
2.3 DataBinding style XML
To use DataBinding, add it in the build.gradle module
Android {// omit... dataBinding { enabled =true}}Copy the code
To enable DataBinding support.
DataBinding does not require additional library support; it is attached to your Android plugin and has the same version number as your Android plugin.
classpath 'com. Android. Tools. Build: gradle: 3.3.2 rainfall distribution on 10-12'
Copy the code
In DataBinding style XML, the outermost layer must be a layout tag, and the merge tag is not supported, so write XML as follows
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="text"
type="String"/>
<variable
name="action"
type="android.view.View.OnClickListener"/>
</data>
<TextView
android:onClick="@{action}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<layout/>
Copy the code
2.3.1 Variable domain
The data tag wraps around the variable domain, where you can define the type of variable bound to the layout using variable, name to specify the variable name, and type to specify its type.
If some types are long and need to be used frequently, you can import them using import just like Java (java.lang.* is imported by default), and then you don’t have to write fully qualified names, like this
<import
type="android.view.View"
alias="Action"/>
<variable
name="action"
type="Action"/>
Copy the code
If necessary (such as name conflicts), you can also specify an alias for a type using Action so that you can use the alias in the following article.
2.3.2 Escape Characters
Those of you who are familiar with XML probably know that < and > are illegal characters in XML, so to use generics, we need to use the escape characters in XML. And & gt; To escape
//↓ Error × <variable name="list"
type="java.util.List<String>"/> //↓ Yes, you can compile √ <variable name="list"
type="java.util.List< String>"/>
Copy the code
After the data tag ends, the original layout is written in much the same place as before, with the addition of the DataBinding expression
<data>
//......
<data/>
<TextView
android:onClick="@{action}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Copy the code
2.3.3 DataBinding expression
The locations wrapped in @{} are called DataBinding expressions. DataBinding expressions support almost all of Java’s operators and add some additional operations. This allows us to have some Java programming experience in XML, which those of you who have learned Java Web might find very similar to JSP:
- Don’t need
xml
Escaped binary operation+
.-
./
.*
.%
.||
.|
.^
.= =
- Need to be
xml
Escaped binary operation&&
.>>
>>>
.<<
.>
.<
.> =
.< =
, the same operator as generics> =
.>
.<
.< =
And so on, also need to escape,&
Need to use&
Escape. This is a little lame, but this isxml
We can’t avoid the limitations, so inDataBinding
In the style ofxml
These symbols should be used as little as possible. lambda
expression@{()->persenter.doSomething()}
- The ternary operation
? :
null
Merge operator??
If the left side is not empty, select the left side; otherwise, select the right side
android:text="@{nullableString?? `This a string`}"
Copy the code
- self-imported
context
Variables you can use inxml
Is used with any expression incontext
This variable, theContext
From the root of the layoutView
thegetContext
Get if you set your owncontext
Variable, then it will be overwritten - If the expression contains string text
xml
Require special treatment
Use single quotes around the periphery and double quotes around the expression Android :text='@{"This a string"}'Or use the 'enclosing string, yes, on the key below Esc symbol android:text="@{`This a string`}"
Copy the code
- Determine type
instanceof
- parentheses
(a)
- A null value
null
- Method calls, field access, and
Getter
andSetter
Short for, e.gUser#getName
andUser#setName
Now I can just write it@{user.name}
, this expression is also the simplest expression, belongs to the direct assignment expression - The default value
default
In thexml
In the
`android:text="@{file.name, default=`no name`}"`
Copy the code
- The subscript
[]
, not just arrays,List
.SparseArray
.Map
This operator is now available - use
@
Read the resource file as follows, but reading is not supportedmipmap
The files under the
android:text="@{@string/text}"// Or use it as part of the expression android:padding="@{large? @dimen/large : @dimen/small}"
Copy the code
There are some resources that need to be referenced
type | normal | DataBinding expression reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
ColorStateList | @animator | @stateListAnimator |
StateListAnimator | @color | @colorStateList |
There are also some operations that are not in the DataBinding expression and we cannot use them:
- There is no
this
- There is no
super
- Cannot create object
new
- Display calls to generic methods cannot be used
Collections.<String>emptyList()
Write a simple DataBinding expression like the following
<data>
<improt type="android.view.View"/>
<variable
name="isShow"
type="Boolean"/>
<data/>
<TextView
android:visibility="@{isShow? View.VISIBLE:View.GONE}"
android:text="@{@string/text}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Copy the code
Complex DataBinding expressions should be avoided and all direct assignment expressions should be preferred. The processing of the data should be left to the layout controller or ViewModel, and the layout should only render the data.
2.3.4 Using ViewDataBinding generated in Java
With DataBinding, Android Studio generates a subtype for each XML layout that inherits from ViewDataBinding to help map the binding relationships defined in the XML files to Java.
For example, if you have an r.layout. fragment_main layout file, it will generate a ViewDataBinding for you under the current package,FragmentMainBinding.
Implementing DataBinding style XML layouts in Java is different from the traditional way.
- in
Actvity
In the
private ActivityHostBinding mBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_host);
}
Copy the code
- In the custom
View
andFragment
In the
private FragmentMainBinding mBinding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
mBinding = DataBindingUtil.inflate(inflater,
R.layout.fragment_main,
container,
false);
return mBinding.getRoot();
}
Copy the code
- In already used ordinary
LayoutInfalter
instantiatedView
(xml
It must beDataBinding
Style, ordinaryLayoutInflater
No binding mechanism is triggered when the layout is instantiated,DataBindingUtil#bind
Before binding occurs.)
View view = LayoutInflater.from(context).inflate(R.layout.item_view,null,false);
ItemViewBinding binding = DataBindingUtil.bind(view);
Copy the code
The variables that you set in XML it’s going to generate getters and setters for you in this class. You can call them to assign values to the interface, such as the action we defined earlier.
// This code is Java8's lambda mbinding.setAction (v->{//TODO})Copy the code
2.3.5 Using BR Files
It will also generate an R-like BR file for you that contains references to all variable names you defined in DataBinding style XML (sometimes Rebuild is required because APT builds are used) For example, our previous action, which generates br.action for us, we can use it this way
mBinding.setVariable(BR.action,new View.OnClickListener(){
@Override
void onClick(View v){
//TODO
}
})
Copy the code
2.3.6 Passing complex Objects
While we used to assign values to variables in XML using simple objects like strings, we can also define complex objects and pass them into the XML layout at once
//java
public class File
{
public File(String name,
String size,
String path)
{
this.name = name;
this.size = size;
this.path = path;
}
public final String name;
public final String size;
public final String path;
}
//xml
<data>
<variable
name="file"
type="org.kexie.android.sample.bean.File"/>
<data/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:text="@{file.name}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.size}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.path}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout/>
Copy the code
I personally believe that data bound to XML should be immutable, so I used final in the above fields, but it’s not necessary to customize it to your own needs
2.3.7 Binding does not occur immediately
The important thing to note here is that the assignment you make to ViewDataBinding does not take effect immediately, but rather after the current method is executed back into the event loop and is guaranteed to be executed before the next frame is rendered. If it needs to be executed immediately, call ViewDataBinding#executePendingBi ndings
2.3.8 using android: id
If you use Android: ID, the View can also be used as a variable in the DataBinding expression below, just like writing Java. It will also bind your views to View Databinding, so you can use them that way
//xml
<TextView
android:id="@+id/my_text"
android:layout_width="match_parent"
android:layout_height="wrap_context"/>
<TextView
android:id="@+id/my_text2"
android:text="@{my_text.getText()}"
android:layout_width="match_parent"
android:layout_height="wrap_context"Mbinding.mytext.settext () {my_text ();"This is a new text");
Copy the code
Those of you who have used ButterKnife probably know that ButterKnife has had an incident where it was incompatible with gradle versions, but DataBinding is shipped packaged with Gradle, and this problem usually doesn’t occur if you don’t want to use ButterKnife but don’t want to use Data If the Binding style of writing is too intrusive for your XML, just using Android: ID would be a good choice.
2.4 Forward Binding
Some third-party views are definitely not compatible with DataBinding. The industry has always said that MVVM is good, but MVP development is still the mainstream, although we can use Android: ID, and then in the Activity/Fragment solution, but sometimes we want to directly in XML Configuration to eliminate some boilerplate code, where you need to customize the forward binding.
2.4.1 Customizing the Forward Binding Adapter
We can use @bindingAdapter to customize View attributes that can be used in XML. Namespaces are not required and will give you a warning.
@target (elementtype.method) public @interface BindingAdapter {/** * attributes associated with this BindingAdapter. */ String[] value(); /** * Whether binding expressions must be assigned to each attribute, or whether certain attributes can be unassigned. * if forfalse, BindingaAapter is called when at least one of the associated properties has a binding expression. */ boolean requireAll() defaulttrue; } // @bindingAdapter requires a static method whose first argument is the View type compatible with the adapter // Starting with the second argument, the values passed in by your custom properties // If requireAll =false@bindingAdapter (value = {) @bindingAdapter (value = {)"load_async"."error_handler"},requireAll = true) public static void loadImage(ImageView view, String url, String error) { Glide.with(view) .load(url) .error(Glide.with(view).load(error)) .into(view); } // Use it in XML (neither of the following urls actually exists) <ImageView load_async="@{`http://android.kexie.org/image.png`}"
error_handler="@{`http://android.kexie.org/error.png`}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Copy the code
2.4.2 Third-party View Adaptation
DataBinding style XML can also be adapted to some extent to third-party views
// If your custom View has such a Setter↓ public class RoundCornerImageView extends AppCompatImageView{//...... public voidsetRadiusDp(floatDp) {/ / TODO}} / / so you can use it in XML using radiusDp < org. Kexie. Android. Ftper. Widget. RoundCornerImageView radiusDp ="@ {100}"
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"
android:src="@drawable/progress"/> // It will find the name for yousetRadiusDp and can accept 100 as a parameter method.Copy the code
2.4.3 Attribute redirection in XML
Use @bindingMethod to redirect XML attributes:
@target (elementtype.annotation_type) public @interface BindingMethod {// The View type Class to be redirectedtype(a); // Name of the attribute to be redirected String attribute(); // The name of the method to redirect to String method(); } / / this is DataBinding source, the TextView system at DataBinding for writing an adapter. / / this is androidx DataBinding. Adapters. TextViewBindingAdapter source @BindingMethods({ @BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
@BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
@BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"), / /... }) public class TextViewBindingAdapter { //...... } // This will establish the relationship between the attributes in the XML and the setters in the ViewCopy the code
2.4.4 Adding a Conversion layer
Use @bindingConversion to add a conversion layer
@BindingConversion
public static ColorDrawable toDrawable(int color) {
returnnew ColorDrawable(color); } // You can convert color int to android: SRC acceptable ColorDrawable type // but conversion only applies to direct assignments // if you write complex expressions, such as? The ternary operator // will not take care of youCopy the code
2.5 Reverse Binding
If there is a forward binding, there must be a reverse binding. The forward binding and reverse binding together form a bidirectional binding.
In DataBinding expressions like android:text we assign a String directly to a TextView. This is the forward binding. The value we assign to the View is reflected directly in the View, and the reverse binding is the sum of the View values Reaction to us.
2.5.1 Using bidirectional binding
All prior to the use of @{} wrapping is forward binding, while two-way binding is @={}, and supports only direct writing of variables, fields, setters (such as user #setName, @={user.name}) and does not support complex expressions
2.5.2 Compatible with LiveData and ObservableField
In fact, Android: Text does not only accept strings. When using bidirectional binding, it can also accept MutableLiveData
and ObservableField
as assignment objects, which will assign the Android :t of the TextView Ext changes are bound to Either LiveData(actually MutableLiveData) or ObservableField so that we can better observe their changes at the Activity/Fragment level of the View.
Except ObservableField in androidx. Databinding ObservableInt don’t packing, are still under the package ObservableFloat and so on.
But to support LiveData we must enable version 2 of DataBinding APT.
Add it to your gradle.properties
android.databinding.enableV2=true
Copy the code
Now we can bind android:text changes to Activity/Fragment via LiveData(actually MutableLiveData)
//xml
<data>
<variable
name="liveText"
type="MutableLiveData< String>">
<data/>
<TextView
android:text="@={text}"
android:layout_width="match_parent"
android:layout_height="wrap_context"/> // Then MutableLiveData<String> liveText = new MutableLiveData<String>(); mBinding.setLiveText(liveText); Livetext. observe(this,text->{//TODO observe View layer change});Copy the code
2.5.3 Customizing reverse Binding adapters
. Here we go back to androidx databinding. Adapters. TextViewBindingAdapter source, continue to analyze the reverse binding custom adapter.
@inversebindingAdapter (attribute =) {InverseBindingAdapter(attribute =)"android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
returnview.getText().toString(); } @bindingAdapter (value = {InverseBindingListener (value = {);} @bindingAdapter (value = {);"android:textAttrChanged"})
public static void setTextWatcher(TextView view , InverseBindingListener textAttrChanged){
view.addTextChangedListener(new TextWatcher() {/ /... @Override public void onTextChanged(CharSequence s, int start, int before, int count) { textAttrChanged.onChange(); }}); } // When you use @={}, you are actually using the Android :textAttrChanged property to set the TextWatcher to TextView When onChange is called in the InverseBindingListener, the @bindingAdapter method is called to fetch the data and write it back to the variable.Copy the code
2.6 Cooperate with DataBinding to create universal RecyclerView.Adapter
Let’s do a little real work, where we can build wheels on the shoulders of giants.
// Import universal adapter as base class, can greatly enrich the functionality of our universal adapter implementation'com. Making. CymChad: BaseRecyclerViewAdapterHelper: 2.9.46'
Copy the code
There isn’t much code because the base class is powerful:
Public class GenericQuickAdapter<X> extends BaseQuickAdapter<X, GenericQuickAdapter. GenericViewHolder > {/ / BR in the variable name protected final int mName; //layoutResId is DataBinding style XML public GenericQuickAdapter(int layoutResId, int name) {super(layoutResId); mName = name; openLoadAnimation(); } @Override protected void convert(GenericViewHolder helper, X item) {// Trigger DataBinding helper.getBinding().setvariable (mName, item); } public static class GenericViewHolder extends BaseViewHolder { private ViewDataBinding mBinding; public GenericViewHolder(View view) { super(view); // Bind ViewDataBinding mBinding = databindingutil.bind (View); } @SuppressWarnings("unchecked")
public <T extends ViewDataBinding> T getBinding() {
return(T) mBinding; GenericQuickAdapter<File> adapter = new GenericQuickAdapter<>(r.layout.item_file, br.file); // GenericQuickAdapter<File> Adapter = new GenericQuickAdapter<>(r.layout.item_file, br.file); <layout> <data> <variable name="file"
type="org.kexie.android.sample.bean.File"/>
<data/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:text="@{file.name}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.size}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.path}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout/>
<layout/>
Copy the code
3 Lifecycle
Lifecycle management has always been a problem in Android, and this has been addressed since Google released the Android Jetpack component, which has since become the core of Android Jetpack.
3.1 the import
Using AndroidX as an example Lifecycle component can be used by adding a dependency to the build.gradle file of the module:
api 'androidx. Lifecycle: lifecycle - extensions: 2.1.0 - alpha02'
Copy the code
Because Lifecycle components are made up of multiple packages, apis can be used to import all of their dependent packages into the module including Common, LiveData, Process, Runtime, ViewModel, Service, etc.
If annotations are to be used in Lifecycle you will also need to add the following annotation handlers so that they can be processed at compile time.
annotationProcessor 'androidx. Lifecycle: lifecycle - compiler: 2.0.0'
Copy the code
Lifecycle is not intrusive for an App because it is naturally integrated into Google’s AppCompat library and almost every application today needs AppCompat. It can be said that integrating Lifecycle simply enables functionality that hasn’t been available before.
3.2 LifecycleOwner
LifecycleOwner is an interface in the Lifecycle component package that must be implemented by all types that need to manage the Lifecycle.
public interface LifecycleOwner
{
/**
* Returns the Lifecycle of the provider.
*
* @return The lifecycle of the provider.
*/
@NonNull
Lifecycle getLifecycle();
}
Copy the code
But a lot of times we don’t even need to care about LifecycleOwner. In Android, fragments, activities, and services are all components with a life cycle, but Google has made them implement LifecycleOwner. Respectively is androdx fragments. App. Fragments, AppCompatActivity, androidx. Lifecycle. LifecycleService.
Lifecycle instances can be easily obtained by LifecycleOwner#getLifecycle() in a project as long as these types are inherited. This is a decoupled implementation,LifecycleOwner does not contain any Lifecycle management logic, the actual logic is in Lifecycle instances and we can prevent memory leaks by passing Lifecycle instances instead of LifecycleOwner.
Lifecycle only has these three methods:
@MainThread
public abstract void removeObserver(@NonNull LifecycleObserver observer);
@MainThread
@NonNull
public abstract State getCurrentState();
@MainThread
public abstract void addObserver(@NonNull LifecycleObserver observer);
Copy the code
GetCurrentState () returns the current Lifecycle state of the LifecycleOwner, which is associated with certain callback events on the LifecycleOwner. Only the following states occur, which are abstracted in Java as an enumerated class and defined in the Lifecycle class.
public enum State
{
DESTROYED,
INITIALIZED,
CREATED,
STARTED,
RESUMED;
}
Copy the code
-
DESTROYED, which changes to this state before the component’s onDestroy call. No state changes will occur and no lifecycle events will be sent
-
INITIALIZED, this state is the initial state after the constructor is executed but onCreate is not executed
-
CREATED becomes this state after the onCreate call and before the onStop call
-
STARTED, after an onStart call, and before an onPause call
-
RESUMED, onResume is called to get to this state
AddObserver, which adds an observer to LifecycleOwner to receive callback events on the LifecycleOwner. The callback event is also an enumeration defined in the Lifecycle class: Lifecycle
public enum Event
{
/**
* Constant for onCreate event of the {@link LifecycleOwner}.
*/
ON_CREATE,
/**
* Constant for onStart event of the {@link LifecycleOwner}.
*/
ON_START,
/**
* Constant for onResume event of the {@link LifecycleOwner}.
*/
ON_RESUME,
/**
* Constant for onPause event of the {@link LifecycleOwner}.
*/
ON_PAUSE,
/**
* Constant for onStop event of the {@link LifecycleOwner}.
*/
ON_STOP,
/**
* Constant for onDestroy event of the {@link LifecycleOwner}.
*/
ON_DESTROY,
/**
* An {@link Event Event} constant that can be used to match all events.
*/
ON_ANY
}
Copy the code
Each event corresponds to an event in the Fragment/Activity.
3.3 LifecycleObserver
LifecycleObserver is the LifecycleObserver and is probably the most commonly used interface in this package.
Looking at the source code, it is an empty interface that does not contain any implementation, but we still have to inherit the interface if we want to use it.
public interface LifecycleObserver { }
Copy the code
Inherit LifecycleObserver and use the @onlifecycleEvent annotation (where the previously declared annotation handler comes in handy) and set the lifecycle callback events to listen for.
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void test()
{
///TODO...
}
Copy the code
Then in the Activity/Fragment:
getLifecycle().addObserver(yourLifecycleObserver);
Copy the code
You can receive the corresponding callback event at runtime, but note that the @onlifecycleEvent annotation should be in package access or public, otherwise you may get an error at compile time, or the callback will not be received.
To remove LifecycleObserver at runtime, there is also a Lifecycle#removeObserver method.
4 LiveData
LiveData is a sticky event that is aware of the life cycle of Android components, that is, when LiveData holds data, you subscribe to it to receive the last data it received. In practice, the LiveData we can use is generally its two subclasses MutableLiveData and MediatorLiveData.
4.1 Basic Use of LiveData
We can use LiveData# Observe to observe changes in the values it holds, or use LiveData#getValue to directly retrieve internally stored values (non-thread-safe).
Public class MyViewModel extends ViewModel{private MutableLiveData<Boolean> mIsLoading = new MutableLiveData<>(); LiveData<Boolean>isLoading() {returnmIsLoading; }} //Activity/Fragment ViewModel mviewmodel.isloading ().observe(this, isLoading -> {//TODO happened on the main thread, trigger the related processing logic}); //LiveData will only notify LifecycleOwner in the activated state (STARTED and RESUMED) You can also re-receive the data saved by LiveData when the Activity/Fragment is rebuilt. // LiveData removes it from the observer list when the component DESTROYED the viewer. // You can also unassociate the LifecycleOwner and keep the subscription alive. // observeForever mviewModel.isloading ().observeforever (isLoading -> {//TODO}); // This subscription will never be cancelled unless you display calls to LiveData#removeObserver
Copy the code
4.2 MutableLiveData
As the name implies,MutableLiveData. The base class, LiveData, is immutable by default. MutableLiveData opens an interface that can change the data it holds inside.
public class MutableLiveData<T> extends LiveData<T> {
/**
* Creates a MutableLiveData initialized with the given {@code value}.
*
* @param value initial value
*/
public MutableLiveData(T value) {
super(value);
}
/**
* Creates a MutableLiveData with no value assigned to it.
*/
public MutableLiveData() {
super();
}
@Override
public void postValue(T value) {
super.postValue(value);
}
@Override
public void setValue(T value) { super.setValue(value); }}Copy the code
Were postValue and setValue setValue internal check is given priority to the thread, the thread is not allowed to use in the child thread, used as an error. PostValue value through the main thread Handler will be forwarded to the main thread.
LiveData can have an initial value or none, and if subscribed without an initial value, the subscriber will not receive any value.
4.3 MediatorLiveData
MediatorLiveData is derived from MutableLiveData and is mainly used to merge multiple LiveData sources.
public class MediatorLiveData<T> extends MutableLiveData<T> { private SafeIterableMap<LiveData<? >, Source<? >> mSources = new SafeIterableMap<>(); @MainThread public <S> void addSource(@NonNull LiveData<S>source, @NonNull Observer<? super S> onChanged) {
Source<S> e = new Source<>(source, onChanged); Source<? > existing = mSources.putIfAbsent(source, e);
if(existing ! = null && existing.mObserver ! = onChanged) { throw new IllegalArgumentException("This source was already added with the different observer");
}
if(existing ! = null) {return;
}
if(hasActiveObservers()) { e.plug(); } } @MainThread public <S> void removeSource(@NonNull LiveData<S> toRemote) { Source<? >source = mSources.remove(toRemote);
if (source! = null) { source.unplug(); } } @CallSuper @Override protected voidonActive() {
for(Map.Entry<LiveData<? >, Source<? >>source : mSources) {
source.getValue().plug();
}
}
@CallSuper
@Override
protected void onInactive() {
for(Map.Entry<LiveData<? >, Source<? >>source : mSources) {
source.getValue().unplug();
}
}
private static class Source<V> implements Observer<V> {
final LiveData<V> mLiveData;
final Observer<? super V> mObserver;
int mVersion = START_VERSION;
Source(LiveData<V> liveData, final Observer<? super V> observer) {
mLiveData = liveData;
mObserver = observer;
}
void plug() {
mLiveData.observeForever(this);
}
void unplug() {
mLiveData.removeObserver(this);
}
@Override
public void onChanged(@Nullable V v) {
if(mVersion ! = mLiveData.getVersion()) { mVersion = mLiveData.getVersion(); mObserver.onChanged(v); }}}}Copy the code
It has two more methods than MutableLiveData, addSource and removeSource, through which we can merge other LiveData into this LiveData, and this LiveData can be notified when other LiveData changes.
@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged)
@MainThread
public <S> void removeSource(@NonNull LiveData<S> toRemote)
Copy the code
By looking at the source code, we can see that LiveData#onActive is called back when there is an observer,MediatorLiveData iterates internally, subscribs to all merged LiveData with observeForever, and receives all changes to LiveData LiveData#onInactive will be called back if an observer is present, in which case the removeObserver action is performed.
4.4 transform
Use androidx. Lifecycle. Transformations of this utility class can hold one type of LiveData into another LiveData. It has a similar approach to RxJava.
LiveData<Boolean> boolLiveData = getBoolLiveData();
LiveData<String> stringLiveData = Transformations.map(boolLiveData,bool->Boolean.toString(bool));
Copy the code
The above is just a demonstration, but more complex logic can actually be performed, and the transition is lazy and does not occur without an active state observer.
5 ViewModel
5.1 Customizing the ViewModel
There’s really nothing to say about the ViewModel, and that’s the main part of the source code, right
public abstract class ViewModel {
protected void onCleared() {}}Copy the code
As a matter of fact, we can use LiveData as fields on the ViewModel to hold data and write business logic (data processing logic). Like this
public class MyViewModel extends ViewModel
{
public MutableLiveData<String> username = new MutableLiveData<>();
public MutableLiveData<String> password = new MutableLiveData<>();
public MutableLiveData<String> text = new MutableLiveData<>();
public void action1(){
//TODO
}
public void initName(){
username.setValue("Luke Luo"); } / /... @Override protected voidonCleared() {//TODO cleanup resources}}Copy the code
The onCleared method is called back when the component is destroyed, and we can override this method to add some custom cleanup logic during ViewModel destruction.
There’s also a subclass of ViewModel, AndroidViewModel, which is pretty straightforward, but it just holds the Application instance.
public class AndroidViewModel extends ViewModel {
@SuppressLint("StaticFieldLeak")
private Application mApplication;
public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}
/**
* Return the application.
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
@NonNull
public <T extends Application> T getApplication() {
//noinspection unchecked
return(T) mApplication; }}Copy the code
5.2 Customizing the ViewModel Construction
We can get the ViewModel via ViewModelProviders, so that the retrieved ViewModel is bound to the component’s lifecycle (i.e., onCleared is called automatically on destruction).
mViewModel = ViewModelProviders.of(this).get(CustomViewModel.class);
Copy the code
In Android’s Lifecycle implementation the framework adds a ReportFragment that inherits the system Fragment to the Activity to report the Lifecycle of the component. If you are using appCompat’s Fragment, it will not be visible to you, so be sure Avoid using system fragments (deprecated in API28).
The ViewModel manages its release through Lifecycle, and its onCleared() is called when the component’s ON_DESTROY event arrives.
If you want to have a custom constructor parameters ViewModel that you have to inherit ViewModelProvider. AndroidViewModelFactory
Public class NaviViewModel extends AndroidViewModel {private AMapNavi mNavi; public NaviViewModel(AMapNavi navi,Application application) { super(application); mNavi = navi; } / /... } / / inheritance and rewrite the create public final class NaviViewModelFactory extends ViewModelProvider. AndroidViewModelFactory {private final AMapNavi navi; private final Application application; public NaviViewModelFactory(@NonNull Context context, AMapNavi navi) { super((Application) context.getApplicationContext()); this.application = (Application) context.getApplicationContext(); this.navi = navi; } @NonNull @Override public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { try { Constructor<T> constructor = modelClass .getConstructor(Application.class, AMapNavi.class);return constructor.newInstance(application, navi);
} catch (Exception e)
{
returnsuper.create(modelClass); }} // Use NaviViewModelFactory factory = new NaviViewModelFactory(context, navi); mViewModel = ViewModelProviders.of(this, factory).get(NaviViewModel.class);Copy the code
To put it bluntly, the reflection call constructor is created.
6 RxJava
This article focuses on the use of reactive programming in MVVM architecture and does not discuss RxJava in depth, but there will be a later article devoted to RxJava.
RxJava is primarily used in MVVM to publish events, so here are some points to note.
6.1 use AutoDispose
RxJava is an implementation of the idea of responsive programming on the JVM, so it wasn’t originally optimized for the Android platform.
As mentioned above,Android components have a defined life cycle, and if RxJava still has background threads running after the component is destroyed and your Observer references your Activity, you can cause a memory leak.
RxJava does provide a release mechanism called Disposeable, but the logic to implement it requires manual hard-coding in Activity#onDestroy, which leads to a lot of boilerplate code.
In order to solve this situation, RxLifecycle was developed before Android Jetpack was born, but the framework required mandatory inheritance of base classes, which was not friendly to some existing projects and personally did not solve the problem.
With Android Jetpack, AutoDispose gave us another way out. It uses the AS operator in RxJava2 to convert a subscriber into a subscriber object that can be released automatically.
Add dependencies to your build.gradle:
implementation 'the IO. Reactivex. Rxjava2: rxjava: 2.2.6'
implementation 'the IO. Reactivex. Rxjava2: rxandroid: 2.1.0'
implementation 'com. Uber. Autodispose: autodispose: 1.1.0'
implementation 'com. Uber. Autodispose: autodispose - android - archcomponents: 1.1.0'
Copy the code
A simple example:
Observable.just(new Object()) // Use AutoDispose#autoDisposable/ / and use AndroidLifecycleScopeProvider#form/ / specified LifecycleOwner and an event where you need to destroy key left is this line down / / as (AutoDispose. AutoDisposable (AndroidLifecycleScopeProvider. The from (activity, Lifecycle.Event.ON_DESTROY))) .subscribe();Copy the code
The time subscription for the above code will be released when the component’s Lifecycle.event.ON_DESTROY Event arrives, or you can specify other events.
6.2 Preventing multiple clicks
First of all, you can use JW’s RxBinding to implement this requirement, but we won’t discuss RxBinding today, because there are so many articles about RxBinding on the web that any one of them is already excellent.
Today we implement a simple, lightweight, Java-based dynamic proxy mechanism modeled after RxBinding that is compatible with the Listener interface defined by all third party views to prevent multiple clicks.
Without further ado, the code:
import androidx.collection.ArrayMap; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import com.uber.autodispose.AutoDispose; import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider; import io.reactivex.subjects.PublishSubject; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Map; import java.util.concurrent.TimeUnit; import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread; Public final class RxOnClick<X> {private static final int MINI_TIME = 200; private final Class<X> mInterface; private X mInner; private LifecycleOwner mOwner; private int mTime; private Lifecycle.Event mEvent; private RxOnClick(Class<X>type)
{
mInterface = type; } public static <X> RxOnClick<X> create(Class<X>type)
{
return new RxOnClick<>(type); } public RxOnClick<X> inner(X inner) {mInner = inner;returnthis; } public RxOnClick<X> owner(LifecycleOwner owner) {mOwner = owner;returnthis; } public RxOnClick<X> throttleFirst(int time) {mTime = time;returnthis; Public RxOnClick<X> releaseOn(Lifecycle.Event Event) {mEvent = Event;returnthis; } // Create a proxy class instance @suppressWarnings ("unchecked")
public X build() {// Check parametersif(mInterface == null || ! mInterface.isInterface()) { throw new IllegalArgumentException(); }if (mTime < MINI_TIME)
{
mTime = MINI_TIME;
}
if (mEvent == null)
{
mEvent = Lifecycle.Event.ON_DESTROY;
}
if(mOwner == null || mInner == null) { throw new IllegalStateException(); } // Get all methods Map<Method, PublishSubject<Object[]>> subjectMap = new ArrayMap<>();for(Method method : mInterface.getDeclaredMethods()) { PublishSubject<Object[]> subject = PublishSubject.create(); subject.throttleFirst(mTime, TimeUnit.MILLISECONDS) .observeOn(mainThread()) .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(mOwner, mEvent))) .subscribe(args -> method.invoke(mInner, args)); subjectMap.put(method, subject); } // Use the dynamic proxy proxy to proxy this interface and use the PublishSubject for forwardingreturn(X) Proxy.newProxyInstance(mInterface.getClassLoader(), new Class[]{mInterface}, (proxy, method, Args) -> {//Object class method directly calledif (Object.class.equals(method.getDeclaringClass()))
{
returnmethod.invoke(proxy, args); } PublishSubject<Object[]> subject = subjectMap.get(method);if(subject ! = null) { subject.onNext(args); }returnnull; }); }}Copy the code
The above class is designed with the Builder pattern, so it is actually a Builder.
The core principle is to use Java’s dynamic proxy mechanism to create a proxy class for the Listener. The proxy class does not process events, but instead transforms events into RxJava event streams through a PublishSubject(events received after releasing the subscription) and pushes them to the Listener that actually processes the event.
This allows us to tamper with events on the event stream, and it also allows for third-party custom views that are not compatible with RxBinding.
For example, XXX is added to the first click in milliseconds and the binding component life cycle, when used like the following, still very concise and very useful:
View.OnClickListener listener = RxOnClick
.create(View.OnClickListener.class)
.owner(this)
.inner(v -> {
//TODO
})
.build();
Copy the code
7 Use MVVM to transform the existing Android system
The author classifies the various class libraries and frameworks under the existing Android system as follows based on the experience obtained through his own practice. His views are for reference only. In practice, the author should carry out appropriate transformation according to the characteristics of the project.
7.1 the View layer
Contents under the existing system:
Activity/Fragment
(Layout lifecycle and logical Controller)android.view.View
And its subclasses
Design principles:
View
The layer should not be responsible for processing the data, it should only be responsible for how the data is displayed.- It should not be held directly
Model
Any references to the layer should also not be held directlyModel
Layer data. View
The normal behavior of layers should be to observe somethingViewModel
To obtain this indirectlyViewModel
fromModel
Layer fetching and processing can be done inView
Layer directly displayed data, data byViewModel
Save, so that you can be sure inActivity
Data related to the page will not be lost and will not be caused when rebuildingView
Layer andModel
Layer coupling.
7.2 DataBinding
Contents under the existing system:
Jetpack DataBinding
Function libraryView
theAdapter
- .
Design principles:
- Ideally,
DataBinding
withView
Build relationships that are data-driven, as long as the data doesn’t changeView
Layer implementation changes do not result in logic rewrites (e.gTextView
toEditText
You don’t need to change a single line of code. - although
DataBinding
The library is mostly completeDataBinding
Should be done, but don’t exclude use for data-driven reasonsandroid:id
In order to getView
And theView
Direct assignment, while not data-driven enough, can be used appropriately, after allAndroid
theView
There is currently no way for tier to be fully data-driven (mainly due to third-party library compatibility issues). Adapter
Should belong toDataBinding
A kind of, andDataBinding
Generated in the function libraryDataBinding
Again, it uses data to triggerView
Layer changes. So try not to write itViewModel
Yes, but it’s not necessary. Do it rightList
If the operation requirements are high, it can be writtenViewModel
In, but to ensure a principle —ViewModel
You should only be responsible for providing data, not knowing what that data is related toView
Interact.
7.3 Event Transmission
Contents under the existing system:
EventBus
Event busRxJava
Flow of events
Design principles:
Jetpack
In the implementation ofLiveData
It works well as a data holder and is lifecycle aware, but there are times when we need to respond toView
Layer sends some single data at this pointLiveData
Doesn’t work very well.Rxjava
andEventBus
Is a better choice.
7.4 the ViewModel layer
Contents under the existing system:
Jetpack ViewModel
Jetpack LiveData
- Is used to
Model
Data conversion toView
Utility classes that display data directly - .
Design principles:
ViewModel
You should usually useLiveData
holdView
Actual control of layer dataViewModel
Can contain operations, butViewModel
It should not be quoted directly or indirectlyView
, even parameters in a method are best left out becauseViewModel
You shouldn’t know which one you’re withView
Interact.ViewModel
withModel
The relationship should be — willModel
Layer generated datatranslation
intoView
Layer can directly digest the absorbed data.ViewModel
You can ask theView
Layer sends the event, and thenView
You can subscribe to these events to receive themViewModel
Layer notification.
7.5 Model layer
Contents under the existing system:
- Part with
Activity
Irrelevant system services Room
(SQLite
Database)Retrofit
(Network data)SharedPreferences
- .
Design principles:
- involving
Activity
Please do not include, such asWindowManager
, they belong toView
Layer. Model
Layers are primarily the source of raw data due toStorage format/transfer format
withDisplay format
The vast differences that exist,View
Layers often do not digest this data directly, so one is neededA middleman
As atranslation
That’s abstracted outViewModel
.
Eight practical
I wrote a simple FTP client as the MVVM blog Demo, the project simply practice QMUI+MVVM+DataBinding+RxJava+LiveData+Room technology stack and kotlin and Java mixed writing, support breakpoint continuation, code quality is relatively general Love yourself.
9 References and extended reading
- DataBinding source code parsing
- Google official hack technology – DataBinding
10 epilogue
I haven’t updated this article for a few days, but something happened recently that made me calm down from the boundless enthusiasm and start to work patiently.
This article has more than 10,000 words. Thank you for taking time out of your busy schedule to read it. All contents are personal learning summary and understanding, for reference only.
Please give me a thumbs up if you like my article, it really means a lot to me.