Android Jetpack 之 ViewModel

前言

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

在屏幕旋轉的時候,Activity 會重建,爲了避免讓數據丟失,咱們一般的作法是在 onSaveInstanceState() 方法中經過 bundle 保存數據,而後在 onCreate()onRestoreInstanceState() 方法中取出 bundle 來恢復數據。然而,這種方式有必定的侷限性,它只適用於可序列化而後反序列化的少許數據,對於 Bitmap 等比較大的數據就不適用了。android

另外一方面,UI 控制器一般須要作一些耗時的異步調用操做,而且須要去管理這些調用。UI 控制器須要確保系統在銷燬後去清理掉這些異步調用,以免潛在的內存泄漏,這種管理方式須要大量的維護工做。並且,在配置更改後重建對象是很浪費資源的,由於該對象可能必須從新發出以前已經發出過的調用。git

UI 控制器通常只負責顯示和處理用戶操做,加載數據庫數據或網絡數據的工做應該委託給其它類,這樣會讓測試工做更加容易地進行。所以,將視圖數據相關操做從 UI 控制器邏輯中分離出來是頗有必要。github

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 .app

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

獲取 ViewModel 使用的 key 相對具體的 ViewModel 類是不會變化的,所以從 ViewModelStore 中取出的 ViewModel 對象也不會變。包括在配置更改後也能夠獲取到以前的 ViewModel .異步

當宿主 Activity 調用了 finish() 方法,系統會調用 ViewModel 對象的 onCleared() 方法來讓它清理掉資源,到這裏以後 ViewModel 纔會被釋放掉。async

ViewModel 裏面不要引用 View、或者任何持有 Activity 類的 context , 不然會引起內存泄漏問題。ide

當 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 地址

相關文章
相關標籤/搜索