1. 程式人生 > >Android Jetpack 之 ViewModel

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 的情況也類似。)

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 還是會繼續工作,沒有任何問題。