Android Jetpack 之 ViewModel

zhich發表於2018-11-27

前言

在 Android 中,ViewModel 的作用就是在 UI 控制器( 如 Activity、Fragment)的生命週期中儲存和管理 UI 相關的資料。ViewModel 儲存的資料在配置更改(如螢幕旋轉)後會依然存在,不會丟失。

在螢幕旋轉的時候,Activity 會重建,為了不讓資料丟失,我們通常的做法是在 onSaveInstanceState() 方法中通過 bundle 儲存資料,然後在 onCreate()onRestoreInstanceState() 方法中取出 bundle 來恢復資料。然而,這種方式有一定的侷限性,它只適用於可序列化然後反序列化的少量資料,對於 Bitmap 等比較大的資料就不適用了。

另一方面,UI 控制器通常需要做一些耗時的非同步呼叫操作,並且需要去管理這些呼叫。UI 控制器需要確保系統在銷燬後去清理掉這些非同步呼叫,以避免潛在的記憶體洩漏,這種管理方式需要大量的維護工作。而且,在配置更改後重建物件是很浪費資源的,因為該物件可能必須重新發出之前已經發出過的呼叫。

UI 控制器一般只負責顯示和處理使用者操作,載入資料庫資料或網路資料的工作應該委託給其它類,這樣會讓測試工作更加容易地進行。因此,將檢視資料相關操作從 UI 控制器邏輯中分離出來是很有必要。

ViewModel 使用

比如,一個 ViewModelActivity 需要展示一個 User 的列表資料,那麼可以定義一個 UserViewModel 來獲取資料,然後在 ViewModelActivity 中建立一個 UserViewModel 物件來獲取到 User 的列表資料。

class UserViewModel : ViewModel() { 
private lateinit var users: MutableLiveData<
List<
User>
>
fun getUsers(): LiveData<
List<
User>
>
{
if (!::users.isInitialized) {
users = MutableLiveData() loadUsers()
} return users
} private fun loadUsers() {
// Do an asynchronous operation to fetch users . Thread(Runnable {
Thread.sleep(3000) // 在子執行緒傳送值用 postValue , 否則用 setValue . users.postValue(listOf(User("1", "AA"), User("2", "BB")))
}).start()
}
}複製程式碼
class ViewModelActivity : AppCompatActivity() { 
private val TAG = "ViewModelActivity" override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) setContentView(R.layout.activity_view_model) // 就算配置更改(如螢幕旋轉)了,獲取到的 userViewModel 物件還會是上一次的 UserViewModel 物件 val userViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java) // 這裡的 this 需要用實現了 LifecycleOwner 的類的 this . 如 AppCompatActivity、FragmentActivity userViewModel.getUsers().observe(this, Observer {
Log.e(TAG, it.toString()) // 列印結果:[User(id=1, name=AA), User(id=2, name=BB)]
})
}
}複製程式碼

檢視原始碼可知,ViewModelProviders.of(this) 獲取了一個全新的 ViewModelProvider 物件,

public static ViewModelProvider of(@NonNull FragmentActivity activity,            @Nullable Factory factory) { 
Application application = checkApplication(activity);
if (factory == null) {
factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);

} return new ViewModelProvider(ViewModelStores.of(activity), factory);

}複製程式碼

ViewModelProvider 物件呼叫 get() 方法獲取到我們需要的 ViewModel 物件。追蹤一下 get() 方法可以知道,ViewModel 物件是儲存在一個 ViewModelStore 類的物件中的,該類裡面使用 HashMap 來儲存和獲取 ViewModel .

ViewModel viewModel = mViewModelStore.get(key);
複製程式碼

獲取 ViewModel 使用的 key 相對具體的 ViewModel 類是不會變化的,因此從 ViewModelStore 中取出的 ViewModel 物件也不會變。包括在配置更改後也可以獲取到之前的 ViewModel .

當宿主 Activity 呼叫了 finish() 方法,系統會呼叫 ViewModel 物件的 onCleared() 方法來讓它清理掉資源,到這裡之後 ViewModel 才會被釋放掉。

ViewModel 裡面不要引用 View、或者任何持有 Activity 類的 context , 否則會引發記憶體洩漏問題。

當 ViewModel 需要 Application 類的 context 來獲取資源、查詢系統服務等,可以繼承 AndroidViewModel 類。

class MyAndroidViewModel(application: Application) : AndroidViewModel(application) { 
private val app get() = getApplication<
Application>
() fun getStatus(code: Int): String {
return when (code) {
1 ->
app.resources.getString(R.string.be_late) // 遲到 2 ->
app.resources.getString(R.string.leave_early) // 早退 else ->
app.resources.getString(R.string.absenteeism) // 曠工
}
}
}複製程式碼
val myAndroidViewModel = ViewModelProviders.of(this).get(MyAndroidViewModel::class.java)Log.e(TAG, myAndroidViewModel.getStatus(2))// 列印結果:早退複製程式碼

ViewModel 的生命週期

ViewModel 會一直保留在記憶體中,直到 Activity / Fragment 在以下情況下才會銷燬:

  • 宿主 Activity 被 finish 後呼叫 onDestroy 方法。
  • 宿主 Fragment 被 detached 後呼叫 onDetach 方法。

下圖展示了一個 Activity 經歷了旋轉然後呼叫 finish 的各種生命週期狀態,同時展示了關聯了該 Activity 的 ViewModel 的生命週期。(UI 控制器是 Fragment 的情況也類似。)

Mou icon

Fragment 之間共享資料

假設我們有這樣的需求:在一個 MasterFragment 中有一個 User 列表,點選列表項後將點中的 User 物件傳遞給 DetailFragment 用於展示詳細的 User 資訊。

我們一般的做法是:在兩個 Fragment 中定義一些通訊介面,並且宿主 Activity 需要把它們繫結起來,這樣做相當繁瑣。並且兩個 Fragment 還需要處理另外的 Fragment 尚未建立或者可見的場景。

為了避免以上繁瑣的做法,我們可以通過兩個 Fragment 之間共享一個 ViewModel 的方式來實現資料通訊。

class SharedViewModel : ViewModel() { 
val selected = MutableLiveData<
User>
() fun select(user: User) {
selected.value = user
}
}複製程式碼
class MasterFragment : Fragment() { 
private val dataList = listOf(User("1", "張三"), User("2", "李四"), User("3", "王五")) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_master, container, false)
} override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) var model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity") lvMaster.adapter = ArrayAdapter<
User>
( activity, android.R.layout.simple_expandable_list_item_1, dataList) lvMaster.setOnItemClickListener {
_, _, position, _ ->
model.select(dataList[position])
}
}
}複製程式碼
class DetailFragment : Fragment() { 
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_detail, container, false)
} override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) var model: SharedViewModel = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity") model.selected.observe(this, Observer<
User>
{
item ->
tvDetail.setText("${item?.id
}
: ${item?.name
}
"
)
})
}
}複製程式碼

需要特別注意,兩個 Fragment 都需要使用它們的宿主 Activty 的 this 來獲取 ViewModelProviders , 這樣才確保它們獲取到的是同一個 ViewModel 物件。

這種資料通訊的方式有以下幾個好處:

  • 宿主 Activity 不需要做任何的事情,也完全不知道 Fragment 之間的通訊;
  • 一個 Fragment 不需要知道另一個 Fragment 中除了 ViewModel 契約之外的其它事情,哪怕另一個 Fragment 消失了,它也繼續保持正常工作;
  • 每個 Fragment 都有自己的生命週期,它們之間互不影響,哪怕某一個 Fragment 被其它 Fragment 替換了,UI 還是會繼續工作,沒有任何問題。

文中 Demo GitHub 地址

來源:https://juejin.im/post/5bfc9d40e51d450ad42ab3c8

相關文章