Android MVVM框架搭建(一)ViewModel + LiveData + DataBinding
- 前言
- 正文
- 一、創建專案
- 二、ViewModel使用
- ① 系結Activity
- ② 頁面布局繪制
- ③ 實作登錄
- 二、LiveData使用
- ① 可修改資料
- ② 資料觀察
- 三、DataBinding使用
- ① 單向系結
- ② 雙向系結
- 四、原始碼
前言
??MVVM框架出來已經有一段時間了,現在也有很多的專案運用了MVVM框架,因此也不算是很新的東西,但是從個人的角度來說我希望寫出來,因為每年都會有新的Android開發工程師進入,一些框架的使用都是封裝好的,或者寫的很高級,剛開始不容易看懂,因此我的想法是寫一個簡單易懂的MVVM框架,并且在這個上面去加入Jetpack的組件,當然了,我技術比較菜,大佬要是看見了高抬貴手,
正文
??MVVM框架是有由來的,這個其實說來話長了,還得從最開始的Android 視圖、UI來說起,最開始的時候Android撰寫頁面,里面的業務邏輯和UI處理都在Activity中,很符合這樣一個圖,

一、創建專案
而最開始的解耦框架是MVC,Model + View + Controller,
??Model (模型層) 保存資料的狀態,比如資料存盤,網路請求,同時還與View 存在一定的耦合,可以通過觀察者模式通知 View 狀態的改變來讓view 更新,
??View (視圖層) 同時回應用戶的互動行為并觸發 Controller 的邏輯,View 還有可能修改Model 的狀態 以使其與 Model 同步,View 還會在model 中注冊 model 事件的改變,以此來重繪自己并展示給用戶,
??Control (控制層)控制器由View 根據用戶行為觸發并回應來自view 的用戶互動,然后根據view 的事件邏輯來修改對應的Model, Control 并不關心 View 如何展示 相關資料或狀態,而是通過修改 Model 來實作view 的資料的重繪,
而中間的框架是MVP,Model + View + Presenter,相當于對MVC框架的進一步升級,解耦,不過也有缺點,額外增加了大量的介面、類,不方便進行管理,所以關于MVP的話就還有一個Contract要去處理,
Contract 如其名,是一個契約,將Model、View、Presenter 進行約束管理,方便后期類的查找、維護,
presenter - 邏輯處理層對UI的各種業務事件進行相應處理,不與View產生直接關系,
最后是我們當前最流行的框架MVVM,Model + View + ViewModel,解耦更徹底,如果說之前是藕斷絲連的話,現在就是一刀兩斷,
ViewModel:關聯層,將Model和View進行系結,只做和業務邏輯相關的作業,不涉及任何和UI相關的操作,不持有控制元件參考,不更新UI,
View只做和UI相關的作業,不涉及任何業務邏輯,不涉及操作資料,不處理資料,UI和資料嚴格的分開,
好了,說了這么多理論的東西,下面進入實操環節,先說明開發環境,我使用的Android Studio是4.2.1,API版本30,gradle 版本6.7.1,JDK8,電腦Win10,
首先創建一個專案,命名為MVVM-Demo,

本文的主要目標是ViewModel 和 DataBinding,
??從Google的官方說明來看,ViewModel 類旨在以注重生命周期的方式存盤和管理界面相關的資料,ViewModel 類讓資料可在發生螢屏旋轉等配置更改后繼續留存,DataBinding資料系結庫是一種支持庫,借助該庫,您可以使用宣告性格式(而非程式化地)將布局中的界面組件系結到應用中的資料源,
看懂了之后首先在專案中,啟用DataBinding,找到app模塊下的build.gradle,在android{}閉包下添加如下代碼:
//啟用DataBinding
buildFeatures {
dataBinding true
}
然后點擊AS右上角的Sync Now進行工程配置同步,而ViewModel不需要做什么就可以使用了,
二、ViewModel使用
??ViewModel的優勢在于生命周期和資料持久化,那么它就適用于Activity和Fragment,其次就是異步回呼,不會造成記憶體泄漏,再次就是對View層和Model層進行隔離,是兩者不存在耦合性,因此你可以知道ViewModel在整個MVVM框架中的重要性了,
① 系結Activity
在MVVM的框架中,每一個Activity都應該對應一個ViewModel,而現在我們有一個MainActivity,因此可以新建一個viewmodels包,包下新建一個MainViewModel類,表示與MainActivity進行系結,
public class MainViewModel extends ViewModel {
}
注意這里要繼承ViewModel,雖然現在里面什么都沒有的,但后面使用的時候會進行增加,下面先將我們的MainActivity與MainViewModel進行系結,
回到MainActivity中,修改代碼如下圖所示;

現在我們的MainActivity和MainViewModel就系結起來了,ViewModel是資料持久化的,因為對于一些變數就可以直接放在ViewModel當中,而不再放在Activity中,可以根據一個實際的需求來進行,
② 頁面布局繪制
比如我現在有一個登錄的功能要去實作,要怎么去對資料進行處理呢?
在ViewModel中定義兩個變數
public String account;
public String pwd;
賬號和密碼這當然是最基本的兩個資料了,下面我們修改一下activity_main.xml中的布局,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp"
tools:context=".MainActivity">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:hint="賬號" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:hint="密碼"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_margin="24dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="登 錄"
app:cornerRadius="12dp" />
</LinearLayout>
③ 實作登錄
下面回到MainActivity中,增加代碼如下圖所示:

乍一看好像沒啥不同的,無非就是給mainViewModel中的兩個變數賦了值,不過這里有一個資料持久化的內容在里面,怎么證明呢?看一下下面這個GIF圖

??這個圖可能有一些黑屏的地方,因為我在給自己的手機做橫豎屏切換的時候,手機錄屏好像有一點問題,不過沒事,因為這個結果是對的,那就是資料持久化,因為我們知道手機在切換螢屏的時候Activity是會重新創建的,因此如果我們的資料是放在Activity中,那么切換螢屏之后就會重置,輸入框也不會有值,但是通過ViewModel去保存輸入框的值就不同了,雖然你的Activity在切換螢屏的時候銷毀并且重新創建了,但是我的MainModel依然穩定,所以我才能在橫屏的時候也登陸,這樣不會造成資料丟失,
二、LiveData使用
??LiveData是用來做什么的?資料變化感知,也就是說如果我一個頁面中對一個TextView進行多次賦值的話,可以通過LiveData來操作,只需要在值改變的時候進行設定就好了,可以簡化頁面上的代碼,下面舉一個實際的例子來說明,依然是之前那個登錄頁面,不過需要修改一下MainViewModel中的變數,如下:
① 可修改資料
public MutableLiveData<String> account = new MutableLiveData<>();
public MutableLiveData<String> pwd = new MutableLiveData<>();
??請注意這里使用的是MutableLiveData,表示值的內容開變動,而LiveData是不可變的,<>中的是泛型,你可以直接將一個物件放進去,當物件的內容有改動時,通知改變就可以了,現在這么寫是為了方便理解,下面進入MainActivity中,首先我們改變一下布局activity_main.xml在按鈕的下面再加如下代碼
<TextView
android:id="@+id/tv_account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_pwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
② 資料觀察
然后回到MainActivity中,修改代碼如下圖所示:

??上面的圖中從上往下有四處標注,我們從下面的兩處標注來看,首先在給MainViewModel中的account賦值時,采用了MutableLiveData的setValue()的方式,還有一種方式是postValue(),這里要注意一點setValue()只能在主執行緒中呼叫,postValue()可以在任何執行緒中呼叫,pwd也是一樣的,然后在最后一處標注的地方,對MainViewModel中的account和pwd進行資料觀察,當這兩個值有改變時通知頁面最新的值,這里用了lambda運算式進行了一次簡化,實際的代碼是這樣的,

下面我們運行一下:

三、DataBinding使用
??Android的DataBinding在已經內置了,因此只需要在app模塊的build.gradle中開啟就可以使用了,DataBinding,顧名思義就是資料系結,可以看到現在的三個組件都與資料有關系,ViewModel資料持有,LiveData資料觀察、DataBinding資料系結,
① 單向系結
??而DataBinding的系結有兩種方式:單向資料系結和雙向資料系結,舉個例子:比如我手機上收到一個通知,我需要顯示通知的文字內容在頁面上,這就是單向系結,而我頁面上的文字內容改變也重新發一個通知出去,這就是雙向系結,可以理解為A和B進行互動,A發訊息,B要做出反應,B發訊息,A也要相應改變,最常用的就是當我Model中的資料改變時,改變頁面上的值,這個是單向系結,下面我們可以新建一個用戶User物件,代碼如下:
public class User extends BaseObservable {
private String account;
private String pwd;
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
notifyChange();//通知改變 所有引數改變
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public User(String account, String pwd) {
this.account = account;
this.pwd = pwd;
}
}
這里我繼承了BaseObservable,注意它在androidx.databinding包下,然后我們的資料是需要顯示在頁面上的,而之前是通過Activity獲取xml中的控制元件,然后顯示資料在控制元件上,而現在有了DataBinding,可以直接和xml的中資料進行系結,這看起來和JS比較像,下面我們對activity_main.xml進行改變,改變后代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<!--系結資料-->
<data>
<variable
name="user"
type="com.llw.mvvm.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp"
tools:context=".MainActivity">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:hint="賬號" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:hint="密碼"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_margin="24dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="登 錄"
app:cornerRadius="12dp" />
<TextView
android:id="@+id/tv_account"
android:text="@{user.account}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_pwd"
android:text="@{user.pwd}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
這里我在最外層加了一個layout標簽,然后將原來的布局放在layout里面,再增加一個資料源,也就是user物件,然后再底部的兩個tv_account和tv_pwd兩個TextView中的text屬性中系結了user物件中的屬性值,當然這樣還沒有完成,最后一步是在MainActivity中去進行系結的,
進入MainActivity,在onCreate方法中,先將其他的代碼注釋掉,

然后修改待明日如下圖所示

??這里要注意一點,DataBindingUtil.setContentView回傳的是一個ViewDataBinding物件,這個物件的生成取決于你的Activity,例如MainActivity,就會生成ActivityMainBinding,然后再通過生成的ActivityMainBinding去設定要顯示在xml中控制元件的值,因此你會看到我完全沒有去findViewById,然后控制元件再去設定這個setText,還有一點就是當你使用了DataBinding之后就不需要去手動findViewById了,通過編譯時技識訓生成駝峰命名的物件,如上圖的btnLogin、etAccount、etPwd,上圖的代碼就是通過更改資料然后通知到xml做更改,初始化的修改時admin、123456,然后再通過輸入框去修改,我將會輸入study、666,然后點擊登錄按鈕,也會將輸入框的資料顯示在TextView上,這樣是否會省去很多不必要的繁瑣作業呢?下面運行一下:

② 雙向系結
??雙向系結是建立在單向系結的基礎上,實際的開發中用到雙向系結的地方并沒有單向系結多,雙向系結舉一個例子,在輸入框輸入資料時候直接將資料源中的資料進行改變,這里會用到ViewModel和LiveData,下面進行雙向系結的使用,修改一下MainViewModel,代碼如下:
public class MainViewModel extends ViewModel {
public MutableLiveData<User> user;
public MutableLiveData<User> getUser(){
if(user == null){
user = new MutableLiveData<>();
}
return user;
}
}
下面修改User類,這里面做了一些改變
public class User extends BaseObservable {
public String account;
public String pwd;
@Bindable
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
notifyPropertyChanged(BR.account);//只通知改變的引數
}
@Bindable
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
notifyPropertyChanged(BR.pwd);//只通知改變的引數
}
public User(String account, String pwd) {
this.account = account;
this.pwd = pwd;
}
}
不同于notifyChange()改變某一個引數,某一個物件都會通知,現在notifyPropertyChanged()就具有針對性,只通知對應屬性改變,之前在activity_main.xml中的data標簽中是使用的User,現在我們改成ViewModel,順便把布局調整一下,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<!--系結資料-->
<data>
<variable
name="viewModel"
type="com.llw.mvvm.viewmodels.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_account"
android:text="@{viewModel.user.account}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginBottom="24dp"
android:id="@+id/tv_pwd"
android:text="@{viewModel.user.pwd}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:text="@={viewModel.user.account}"
android:hint="賬號" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:text="@={viewModel.user.pwd}"
android:hint="密碼"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_margin="24dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="登 錄"
app:cornerRadius="12dp" />
</LinearLayout>
</layout>
這里要注意點的地方有幾個,第一個是資料源,這里系結的是ViewModel,那么相對應的ViewModel中的資料資料都可以拿到,

第二個就是回應的地方,通過這種方式去顯示ViewModel中物件的變數資料在控制元件上,這里我把這兩個TextView放到輸入框的上方

第三個地方,也是雙向系結的意義,就是UI改變資料源,我們都知道當輸入框輸入時,text屬性值會改變為輸入的資料,而@={viewModel.user.account}就是將輸入的資料直接賦值給資料源,這樣在Activity中我們將不需要去進行輸入框的處理,減少了耦合,

下面讓我們回到MainActivity中,修改代碼后如下:
private ActivityMainBinding dataBinding;
private MainViewModel mainViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//資料系結視圖
dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mainViewModel = new MainViewModel();
//Model → View
User user = new User("admin", "123456");
mainViewModel.getUser().setValue(user);
//獲取觀察物件
MutableLiveData<User> user1 = mainViewModel.getUser();
user1.observe(this, user2 -> dataBinding.setViewModel(mainViewModel));
dataBinding.btnLogin.setOnClickListener(v -> {
if (mainViewModel.user.getValue().getAccount().isEmpty()) {
Toast.makeText(MainActivity.this, "請輸入賬號", Toast.LENGTH_SHORT).show();
return;
}
if (mainViewModel.user.getValue().getPwd().isEmpty()) {
Toast.makeText(MainActivity.this, "請輸入密碼", Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(MainActivity.this, "登錄成功", Toast.LENGTH_SHORT).show();
});
}
下面運行一下:

我發現我的手機錄屏似乎是出了問題,當點擊第二個輸入框的時候,錄屏中會黑屏,所以這個GIF很短,具體的效果請下載原始碼去運行,
四、原始碼
GitHub:MVVM-Demo
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/341925.html
標籤:其他
上一篇:【Android 逆向】APK 檔案格式 ( Android 應用安裝 | Zip 檔案格式 | 使用 Python 代碼提取 APK 檔案 )
