iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0
Mobile Development

【Kotlin Notes And JetPack】Build an App系列 第 23

Day 23.【Architecture】ViewModel 的介紹與應用

  • 分享至 

  • xImage
  •  

前幾篇多多少少都有提到 ViewModel,今天終於要來講講 ViewModel 的故事,以下如有解釋不清或是描述錯誤的地方還請大家多多指教:

什麼?

用來處理給介面呈現的資料邏輯,ViewModel 本身也有自己的生命周期,在前幾篇的 lifecycle 中也有小提到,透過 LiveData 傳遞給介面所需資訊, ViewModel 不可持有 View 的 context,不然 View 在銷毀時無法完全的死亡,property 有可能無法被 GC 而造成 memory leak,而 Architecture Components 提供了 ViewModel class 來協助我們實作 ViewModel。

| Lifecycle

ViewModel 會持有資料的 memory 直到持有他 instance 的頁面生命週期結束,以頁面來說:

  • 他會在 Activity finished 後進到 onCleared()
  • 他會在 Fragment detached 後進到 onCleared()

| Create a ViewModel

我們透過 delegate 的方式來建立我們的 ViewModel,這邊分成兩種方式:

// 一個 instance
private val viewModel by activityViewModels<MyViewModelName>()

// 當個 fragment 建立一個新的 instance
private val viewModel by viewModels<MyViewModelName>()

這兩個有什麼差別呢? activityViewModels 只會在 activity 建立出一個 ViewModel,所有透過 activityViewModels 產出的 fragment 都會吃到從這個 viewModel 送出來的值,例如:

class MainActivity: AppCompatActivity() {
    private val viewModel by viewModels<MyViewModelName>()
    ...

    fun setupView() {
        viewModel.getCity()
    }

    fun setupViewModel() {
        viewModel.cityList.observe(viewLifecycleOwner) {
            // catch data
        }
    }
}


class HomeFragment : Fragment() {
    private val viewModel by activityViewModels<MyViewModelName>()
    private val listAdapter by lazy { MyAdapterName() }
    ...

    fun setupViewModel() {
        viewModel.cityList.observe(viewLifecycleOwner) {
            // catch data
        }
    }
}

class DetailFragment : Fragment() {
    private val viewModel by activityViewModels<MyViewModelName>()
    private val listAdapter by lazy { MyAdapterName() }
    ...

    fun setupViewModel() {
        viewModel.cityList.observe(viewLifecycleOwner) {
            // catch data
        }
    }
}

如果每個 fragment 都是透過 by viewModels 來產,那每個 view 透過 viewModel 執行的事情都不會干擾到另一個 view,也就是說每個 view 都建立一個新的 viewModel:

class MainActivity: AppCompatActivity() {
    private val viewModel by viewModels<MyViewModelName>()
    ...

    fun setupView() {
        viewModel.getCity()
    }

    fun setupViewModel() {
        viewModel.cityList.observe(viewLifecycleOwner) {
            // catch data
        }
    }
}


class HomeFragment : Fragment() {
    private val viewModel by viewModels<MyViewModelName>()
    private val listAdapter by lazy { MyAdapterName() }
    ...

    fun setupViewModel() {
        viewModel.cityList.observe(viewLifecycleOwner) {
            // won't catch data
        }
    }
}

class DetailFragment : Fragment() {
    private val viewModel by viewModels<MyViewModelName>()
    private val listAdapter by lazy { MyAdapterName() }
    ...

    fun setupViewModel() {
        viewModel.cityList.observe(viewLifecycleOwner) {
            // won't catch data
        }
    }
}

如何?

我們在 lifecycle 那篇已經有加過 ViewModel 的 dependency 了,而 ViewModel 透過 delegate by 來建立 是 androidx.fragment:fragment-ktx 所提供方法:

| 建立 ViewModel

建立一個 MainViewModel 並繼承 ViewModel()

class MainViewModel: ViewModel() {

}

在 main 使用 viewModels delegate 來幫我們取得要使用的 ViewModel:

private val viewModel by viewModels<MainViewModel>()

在 home 使用 activityViewModels delegate 來幫我們取得 activity 使用的 ViewModel:

private val viewModel by activityViewModels<MainViewModel>()

| 新增 function

將先前寫在 main 的 API 邏輯搬移到 ViewModel 去,並使用 viewModelScope 去呼叫 API 及 DB ,viewModelScope 會在 ViewModel 在 onCleared 時取消所有已啟動的工作:

class MainViewModel: ViewModel() {
    fun getForecast(country: String) {
        val service = WeathbyRetrofit.makeRetrofitService()
        viewModelScope.launch {
            runCatching {
                service.getForecast(query = country)
            }.onSuccess {
                Log.i("success", "onCreate: $it")
            }.onFailure {
                Log.i("fail", "onCreate: $it")
            }
        }
    }
}

// MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    viewModel.getForecast()
}

點擊 viewModelScope 可以看到 job 是設定 Main,以及 close 會 cancel 所有工作:

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"

/**
 * [CoroutineScope] tied to this [ViewModel].
 * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

Reference

Android official


上一篇
Day 22.【Architecture】Room 的介紹與應用
下一篇
Day 24.【Architecture】LiveData 的介紹與應用
系列文
【Kotlin Notes And JetPack】Build an App30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言