ViewModel的創建

ViewModel的創建

ViewModel本身只是ViewModel這個類的子類:

class MainViewModel: ViewModel() {
}

在屏幕旋轉UI重建的時候, 它是如何擁有保持數據的能力的呢? 它又是何時被清理的呢?
答案全跟它是如何創建, 保存的有關係.

本文回顧一下創建ViewModel的幾種常見寫法.
注: 本文中的圖並不是嚴格意義的時序圖(也不符合規範), 只是爲了簡略表示一下代碼中的調用關係.

原生手動創建ViewModel

當ViewModel沒有構造參數

當ViewModel沒有參數的時候很簡單:

class MainViewModel: ViewModel() {}

class MainActivity : ComponentActivity() {

    private lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
    }
}

當然不是隨便就new出來的啦, 得通過ViewModelProvider來get.

注意: 這句不能在Activity的onCreate()之前調用, 也意味着你不能聲明字段直接賦值.

否則你就會得到這個報錯:

Caused by: java.lang.IllegalStateException: Your activity is not yet attached to the Application instance. You can't request ViewModel before onCreate call.

此時的方法調用大概是這樣:

viewmodel

從這裏我們看到ViewModelProvider這個工具人要藉助其他兩個東西來提供ViewModel:

  • ViewModelStore: 負責存儲ViewModel.
  • Factory: 負責實例化具體的ViewModel類型.
    請記住這兩個知識點, 後面要考.

在上面這個最簡單的例子中:

  • Activity是ViewModelStoreOwner, 它可以getViewModelStore()
    ViewModelStoreOwner(比如Activity)因爲configuration changes重建, 新的owner仍然會get這個舊的ViewModelStore實例.
  • 我們沒有傳Factory, 所以最終用的是沒有參數的NewInstanceFactory.

當ViewModel有構造參數

但是通常, ViewModel會需要一些依賴, 我們就需要從構造傳入一些參數

class MainViewModel(
    private val repository: MainRepository,
) : ViewModel() 

此時我們的工廠就需要自己實現了:
我們需要把依賴對象傳給工廠, 好讓它構造ViewModel的時候能用上:

class MyViewModelFactory constructor(private val repository: MainRepository) :
    ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

接着把我們的工廠傳給ViewModelProvider:

class MainActivity : ComponentActivity() {

    private lateinit var viewModel: MainViewModel
    private val repository: MainRepository = MainRepository()
    private val viewModelFactory: MyViewModelFactory = MyViewModelFactory(repository)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel = ViewModelProvider(this, viewModelFactory).get(MainViewModel::class.java)
    }
}

此時的過程大概是這樣:
viewmodel-2

AndroidX來幫忙

感謝AndroidXactivity-ktx(fragment-ktx裏也有Fragment版本的)包裏的by viewModels()屬性代理,
我們上述的代碼可以簡化成這樣:

class MyViewModelFactory constructor(private val repository: MainRepository) :
    ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

class MainActivity : ComponentActivity() {

    private val viewModel: MainViewModel by viewModels {
        viewModelFactory
    }
    private val repository: MainRepository = MainRepository()
    private val viewModelFactory: MyViewModelFactory = MyViewModelFactory(repository)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // use viewModel
    }
}

工廠依然自己寫, 簡化了provider get的部分:

  • 擴展方法, 隱含activity對象.
  • lazy規避了生命週期的問題, 只要使用ViewModel的地方不在onCreate之前就行.

ViewModel沒有參數的時候更簡單:

private val viewModel: MainViewModel by viewModels()

by viewModels

Dagger時代

有了dagger, 我們可以在構造上標記@Inject告訴dagger幫我們創建repository和viewModelFactory:

class MainRepository @Inject constructor(){}

@Singleton
class MyViewModelFactory @Inject constructor(private val repository: MainRepository) :
    ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

但是我們卻不能對ViewModel這樣做, 因爲ViewModel應該被存儲在ViewModelStore裏, 而它是由activity提供的.

我們會注入viewModelFactory, 然後用它來創建ViewModel:

class MainActivity : ComponentActivity() {

    private val viewModel: MainViewModel by viewModels {
        viewModelFactory
    }
    @Inject
    lateinit var viewModelFactory: MyViewModelFactory
}

因爲你用的是dagger, 你會需要在onCreate()裏寫類似這樣的東西:

 (applicationContext as MyApplication).appComponent.inject(this)

這裏只是用DI框架簡化了依賴和工廠的構造.

Hilt時代

在ViewModel上標記: @HiltViewModel, 構造標記:@Inject:

@HiltViewModel
class MainViewModel @Inject constructor(
    private val repository: MainRepository,
) : ViewModel() {
}

依賴們也可以構造標記@Inject:

class MainRepository @Inject constructor(){}

在Activity加上註解@AndroidEntryPoint,
也用了by viewModels():

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // use viewModel
    }
}

請注意, 和dagger不同的是, 這裏並不需要注入工廠(也不需要寫工廠), Hilt用的是自己的工廠HiltViewModelFactory.

這個過程:
viewmodel-hilt

Compose的ViewModel()方法

在Compose中獲取一個ViewModel非常簡單, 可以只用一個viewModel()方法.
(這個方法在androidx.lifecycle:lifecycle-viewmodel-compose依賴包裏).

@Composable
fun Greeting(name: String) {
    val viewModel: MainViewModel = viewModel()
    // use viewModel
}

Compose viewModel()

Activity的onCreate()中setContent是Compose, 於是在這裏就設置好各種owners,
與ViewModel相關的就是這個LocalViewModelStoreOwner, 之後包在裏面的內容就可以隨時獲取owner, 得到ViewModelStore和ViewModel了.

Compose的hiltViewModel()方法

上面的viewModel()方法不管在哪裏獲取, ViewModel的scope都是和當前的Activity或Fragment綁定.
假如我們有多個composable的界面呢?

使用hiltViewModel()這個composable方法我們可以獲取到一個scope到某個導航目的地的ViewModel.
(這個方法在androidx.hilt:hilt-navigation-compose依賴包裏).

NavHost(navController = navController, startDestination = "friendslist") {
    composable("friendslist") {
        val viewModel = hiltViewModel<MainViewModel>()
        FriendsList(viewModel = viewModel, navHostController = navController)
    }
    ...
}

Compose hiltViewModel()

這種方式獲取的ViewModel, 它的生命週期是和這個導航目的地相關的, 當退出這個界面, ViewModel就被clear, 下次再進就又是新的對象.

此時的owner終於不再是Activity或Fragment, 而是NavBackStackEntry.
導航這個話題就先不展開這裏講了.

總結

ViewModel的創建很關鍵, 關係到它的生命週期.
手動創建比較麻煩, 很多樣板代碼.
本文總結了幾種常見的創建方式, 希望讀者看完後能有更清晰的理解, 每種方式都是怎麼回事, 那些方便的工具替我們做了什麼.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章