Android Jetpack 之 ViewModel
前言
在 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 的情況也類似。)
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 還是會繼續工作,沒有任何問題。