之前幾小章我們講了DataBinding,其中將一個普通類化身為ViewModel,但是以我的觀點來看,他僅僅只是一個普通類,一個將各種可觀察屬性封裝起來的普通類,而這個普通類我們還在裡面定義了各種相應按鈕點選事件等方法,其實這些都違背了官方的建議的,我只是想讓大家知道可以這樣做而已。所以我們要介紹Android Jetpack中正統的ViewModel類,以及一些它的最佳實踐指南。
Android MVVM探索系列
Android MVVM探索(一) - DataBiding初解
Android MVVM探索(二) - DataBiding常用註解
Android MVVM探索(三) - ViewModel,DataBinding,LiveData混合三打
Android Jetpack是谷歌為了幫助開發者們更快更高效地開發安卓應用而推出來的一套元件。Android Jetpack包含了開發庫,工具以及最佳實踐指南。而我們今天要講的ViewModel類是屬於Android Jetpack庫中的lifecycle庫。說到這,順帶解釋以下。lifecycle,中文意思為生命週期。所以這個庫的存在就跟它的中文含義一樣,它可以有效避免記憶體洩漏和解決Android常見的生命週期難題。(如果各位看官不知道記憶體洩露的可以去好好補補課)lifecycle最近釋出了2.0版本,在這個版本中,可以結合DataBinding進行使用,那可以說是方便太多了。
一,ViewModel的定義。
來自官方的解釋:ViewModel類是用來儲存UI資料的類,它會在配置變更(即 Configuration Change,例如手機螢幕的旋轉)之後繼續存在。
二,它的一些特點。
我們都知道,當手機螢幕發生旋轉的時候,Activity會被重新建立,也就是說生命週期又將從onCreate開始,如果你此時不及時儲存,那麼一些UI資料將會丟失,這樣肯定是會出問題的。但是,ViewModel並不會受此影響,即便手機螢幕發生旋轉,ViewModel依然存在,這樣的話Activity的UI資料便可以儲存下來。
三,最佳實踐(官方推薦做法)。
-
所有Activity的UI相關資料應該儲存在ViewModel中,而不是儲存在Activity中。這樣做的好處是,在配置變更的時候,你應用的UI資料仍然存在。即使ViewModel這麼強大,但它也不應該不承擔過多責任,當有UI資料處理等相關事件建議建立Presenter類,或者建立一個更成熟的架構。
-
Activity負責展示UI資料,並接收互動(一般來說是與使用者的互動)。但是Activity不應當處理這些互動。
-
在應用需要載入資料或者儲存資料的時候,建議建立一個Repository的儲存區類,裡面放置儲存與載入應用資料的API。
-
ViewModel不應持有Context,就像之前說的:
它會在配置變更(即 Configuration Change,例如手機螢幕的旋轉)之後繼續存在。
所以,ViewModel生命週期遠比Activity,Fragment等生命週期更長,具體如下圖所示。如果你這樣做了,加入在螢幕旋轉情況下,原Activity將會銷燬,新的Activity將會被建立。而ViewModel會一直持有原Activity,這樣便會造成記憶體洩漏。如果你的ViewModel確實需要Context,那麼你的ViewModel可以繼承AndroidViewModel,這樣你的ViewModel中會有Application的引用。
-
ViewModel不應當取代onSaveInstanceState方法。儘管ViewModel很出色了,但是它和onSaveInstanceState依然是相輔相成的作用。因為,當程式被關閉時,ViewModel將會被銷燬,但是onSaveInstanceState不會受到影響。(個人猜想:比如在後臺記憶體緊張情況下,你的應用處於後臺被系統釋放了,ViewModel會被銷燬,但是你通過onSaveInstanceState儲存下來的資料在你的應用重新回到前臺時仍然可以被恢復)
-
ViewModel與Activity生命週期對比圖:
四,ViewModel的用法
-
引入lifecycle庫:
implementation "android.arch.lifecycle:extensions:1.1.0" annotationProcessor "android.arch.lifecycle:compiler:1.1.0" 複製程式碼
-
首先建立出我們的ViewModel類。我們需要新建一個普通類,(雖然類名是隨意的,但是作為一名合格的程式設計師,我們取得每一個名字要具有規範性,要讓程式碼閱讀者一看名字就知道這個類或者這個變數是幹嘛的)讓它繼承ViewModel類,並且在其中存放UI相關資料:
// 假設我們要存放的UI資料就是User物件 // 先新建一個實體物件 data class User(val name: String, val age: Int, val sex: Int) // 新建ViewModel類 // 新建ViewModel類 class UserViewModel : ViewModel() { val user = User("張三", 21, 1) } 複製程式碼
-
新建Activity,並且加入例項化ViewModel的程式碼。這裡我們例項化ViewModel不再是new一下:
class ViewModelActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_view_model) val userViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java) } } 複製程式碼
至此,我們一個ViewModel就建立好了。
-
感覺單單一個ViewModel的存在沒有很大的價值,但是如果搭配上LiveData和DataBinding你就能體會到什麼是飛一樣的感覺。用上這些,你就能夠建立反應式介面(最基本的只要LiveData和ViewModel就可以建立反應式介面。或者單單DataBinding就可以完成,但是不具備生命週期感知能力,我們需要手動處理生命週期問題)。也就是說,當你底層資料發生變動時(這裡暫時只是ViewModel中資料發生變動),UI會自動重新整理。
-
ViewModel只提供一個預設的無參建構函式,如果你需要一個有參建構函式,那麼就需要使用ViewModelFactory這個類,具體使用方法如下所示(摘自官方sunflower Demo程式碼):
// 新建一個Factory類,用來提供帶有有參建構函式的ViewModel例項,必須繼承ViewModelProvider.NewInstanceFactory,然後重寫create方法 class MessageViewModelFactory(private val message: Message) : ViewModelProvider.NewInstanceFactory() { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel?> create(modelClass: Class<T>): T { return MessageViewModel(message) as T } } // 新建一個帶有有參建構函式的ViewModel class MessageViewModel(val message: Message) : ViewModel() // Activity中初始化ViewModel的程式碼也要進行改動 // 建立Factory物件 val factory = MessageViewModelFactory(Message("我是通過有參建構函式直接初始化的資訊內容", "我是通過有參建構函式直接構造出來的資訊傳送人")) // 通過Factory物件初始化帶參建構函式的ViewModel val messageViewModel = ViewModelProviders.of(this, factory).get(MessageViewModel::class.java) // 將MessageViewModel例項賦值給xml中的msg binding.msg = messageViewModel 複製程式碼
這樣便可以完成帶有有參建構函式的ViewModel的初始化。
五,搭配上LiveData
-
LiveData簡介:LiveData是一種具有生命週期感知能力的可觀察資料持有類。它同屬於Android Jectpack中的lifecycle庫。LiveData物件通常儲存在我們上面講的ViewModel中。
-
結合ViewModel的使用。我們說過,使用LiveData加ViewModel可以建立一個反應式介面,那麼我們應該怎麼做呢?我們需要改寫上面的UserViewModel,讓其中的user具有可觀察性:
class UserViewModel : ViewModel() { val user = MutableLiveData<User>() } 複製程式碼
-
接著,我們在ViewModelActivity中監聽UserViewModel中的user屬性的變化,並且在它變化後進行相應的操作:
<!-- 為了演示效果,我們在佈局檔案activity_view_model中新增了一些控制元件 --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".viewmodel.ViewModelActivity"> <TextView android:id="@+id/tv_vm" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:id="@+id/bt_vm" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="設定姓名"/> </LinearLayout> // 在Activity中設定資料變化監聽,並且進行相應處理 class ViewModelActivity : AppCompatActivity() { private lateinit var tv: TextView private lateinit var bt: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_view_model) tv = findViewById(R.id.tv_vm) bt = findViewById(R.id.bt_vm) val userViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java) // 監聽ViewModel中user的變化,當它變化時,將TextView重新設定文字 userViewModel.user.observe(this, Observer { tv.text = it?.name }) // 為按鈕設定點選事件,點選後設定user的值 bt.setOnClickListener{ val user = User("張三", 21, 1) userViewModel.user.value = user // Java程式碼 // userViewModel.user.setValue(user) } } } 複製程式碼
寫完以上程式碼,當我們點選按鈕的時候TextView就會顯示“張三”二字了。
-
上面程式碼中除了setValue,還有一個方法叫postValue。區別在於setValue只可以在主執行緒執行(即UI執行緒),postValue只可以在後臺執行緒執行。
-
LiveData能夠感知生命週期的好處:1,當Activity不在螢幕上時(不可見),LiveData不會出發沒必要的介面更新;2,當Activity被銷燬時,LiveData將自動清空與Observer的連線;
六,搭配上DataBinding
我們看到,上面的程式碼還是有些繁瑣,我們還要自己寫程式碼監聽資料變化,並且自己手動去更新UI。之前我們DataBinding可不是這樣的。是的,我們還可以更簡單。別忘了,lifecycle2.0是支援了DataBinding資料繫結的。我們可以通過以下步驟來結合DataBinding使用:
-
像DataBinding中那樣寫佈局,所以將activity_view_model改成如下所示:
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="viewModel" type="top.cyixlq.test.viewmodel.UserViewModel"/> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".viewmodel.ViewModelActivity"> <TextView android:id="@+id/tv_vm" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{viewModel.user.name}"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{String.valueOf(viewModel.user.age)}"/> <Button android:id="@+id/bt_vm" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="設定姓名"/> </LinearLayout> </layout> 複製程式碼
-
註釋掉ViewModelActivity中的之前的程式碼,並重新寫,所以有用的程式碼就是下面這樣:
class ViewModelActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_view_model) val userViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java) val binding = DataBindingUtil.setContentView<ActivityViewModelBinding>(this, R.layout.activity_view_model) // Java程式碼 // ActivityViewModelBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_view_model) binding.viewModel = userViewModel // java程式碼 // binding.setViewModel(userViewModel) // 讓xml內繫結的LiveData和Observer建立連線,也正是因為這段程式碼,讓LiveData能感知Activity的生命週期 binding.setLifecycleOwner(this) } } 複製程式碼
-
為了驗證效果,我們設定一下按鈕的的點選事件,當按鈕點選後TextView顯示姓名年齡的資訊:
bt_vm.setOnClickListener { userViewModel.user.value = User("李四", 22, 1) } // java程式碼 // findViewById(R.id.bt_vm).setOnClickListener(new View.OnClickListener() { // userViewModel.user.setValue(new User("李四", 22, 1)) // }) 複製程式碼
經過以上的步驟就可以結合DataBinding使用了