本篇延續 Day14 MVVM專案-2 Simple Fragment -2
今天會用Dependency Injection(依賴注入)來改寫代碼
這東西有點複雜 所以今天只會講怎麼使用而不會去講原理
如果是用java撰寫 那麼很多人是使用Dagger
而今天範例是用kotlin寫的 則會使用Kodein
先新增dependencies
build.gradle(module:app)
dependencies{
def kodein_version = "6.1.0"
implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version"
implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version"
}
接著新增一個Module
S02Module.kt
val S02ViewModelModule = Kodein.Module("Stage02ViewModel") {
bind<() -> Boolean>() with singleton {
val r = Random()
val nextBoolean: () -> Boolean = {
r.nextBoolean()
}
nextBoolean
}
}
val S02FragmentModule = Kodein.Module("Stage02Fragment") {
bind<ViewModelProvider.Factory>() with singleton {
Stage02ViewModelFactory()
}
bind<Stage02ViewModel>() with provider {
ViewModelProviders.of(context as Fragment, instance()).get(Stage02ViewModel::class.java)
}
}
調整後的ViewModelFactory
Stage02ViewModelFactory.kt
class Stage02ViewModelFactory : ViewModelProvider.Factory, KodeinAware {
override val kodein: Kodein = Kodein.lazy {
import(S02ViewModelModule)
}
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val vm by kodein.newInstance { Stage02ViewModel(instance()) }
return when (modelClass) {
Stage02ViewModel::class.java -> vm
else -> throw IllegalArgumentException("$modelClass is not registered ViewModel")
} as T
}
}
注意 原本的
Stage02ViewModel(randomBooleanGenerator())
改成Stage02ViewModel(instance())
然後函式從 import(S02ViewModelModule) 取得
也就是說其實這邊是綁定這段代碼
bind<() -> Boolean>() with singleton {
val r = Random()
val nextBoolean: () -> Boolean = {
r.nextBoolean()
}
nextBoolean
}
調整之後的Fragment
Stage02Fragment.kt
class Stage02Fragment : Fragment(), KodeinAware {
override val kodein: Kodein by lazy {
@Suppress("UNCHECKED_CAST")
val createKodein = arguments?.getSerializable("createKodein") as? () -> Kodein
?: throw IllegalArgumentException("no createKodein for ${this::class.java.simpleName}")
createKodein()
}
private val viewModel: Stage02ViewModel by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.result.observe(this, Observer {
lav_result.setAnimation(
when (it) {
Stage02ViewModel.TryResult.FAILED -> R.raw.s02_failed
Stage02ViewModel.TryResult.SUCCESS -> R.raw.s02_succes
else -> throw IllegalStateException()
}
)
lav_result.playAnimation()
})
viewModel.clearAction.observe(this, Observer {
lav_result.cancelAnimation()
lav_result.progress = 0.0f
})
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? =
DataBindingUtil.inflate<Stage02FragmentBinding>(
inflater,
R.layout.stage02_fragment,
container,
false
).also {
it.setLifecycleOwner(this)
it.viewModel = viewModel
}.root
companion object {
private val _createKodein: () -> Kodein by lazy {
{
Kodein.lazy { import(S02FragmentModule) }
}
}
@JvmStatic
fun newInstance(createKodein: () -> Kodein = _createKodein) =
Stage02Fragment().apply {
arguments = Bundle().apply {
putSerializable("createKodein", createKodein as Serializable)
}
}
}
}
以下是替換前的代碼
Stage02Fragment.kt
private val createVMFactory: () -> ViewModelProvider.Factory by lazy {
arguments?.getSerializable("createVMFactory") as? () -> ViewModelProvider.Factory
?: throw IllegalArgumentException("no createVMFactory for ${this::class.java.simpleName}")
}
val viewModel: Stage02ViewModel by lazy {
ViewModelProviders.of(this, createVMFactory()).get(Stage02ViewModel::class.java)
}
...
companion object {
private val _createKodein: () -> Kodein by lazy {
{
Kodein.lazy { import(S02FragmentModule) }
}
}
@JvmStatic
fun newInstance(createKodein: () -> Kodein = _createKodein) =
Stage02Fragment().apply {
arguments = Bundle().apply {
putSerializable("createKodein", createKodein as Serializable)
}
}
}
接著撰寫tests
先新增兩個class
Stage02TestViewModelFactory.kt
class Stage02TestViewModelFactory : ViewModelProvider.Factory, KodeinAware {
override val kodein: Kodein = Kodein.lazy {
import(S02TestViewModelModule)
}
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val vm by kodein.newInstance {
Stage02ViewModel(instance()).apply {
speedOfAnim.value = 10.0f
}
}
return when (modelClass) {
Stage02ViewModel::class.java -> vm
else -> throw IllegalArgumentException("$modelClass is not registered ViewModel")
} as T
}
}
Stage02TestViewModelFactory 跟Stage02ViewModelFactory
有點像 主要是把一些參數替換成測試用的
例如speedOfAnim在測試時會加快
boolen值也從亂數改為固定true
S2TestModules.kt
val S02TestViewModelModule = Kodein.Module("Stage02ViewModel") {
bind<() -> Boolean>() with singleton { { true } }
}
val S02TestFragmentModule = Kodein.Module("Stage02Fragment") {
bind<ViewModelProvider.Factory>() with singleton {
Stage02TestViewModelFactory()
}
bind<Stage02ViewModel>() with provider {
ViewModelProviders.of(context as Fragment, instance()).get(Stage02ViewModel::class.java)
}
}
這邊一樣是把部分項目替換
接著來撰寫一個新的tests
S02aViewModelTest.kt
class S02aViewModelTest : KodeinAware {
@Rule
@JvmField
val rule = InstantTaskExecutorRule()
override val kodein: Kodein = Kodein.lazy {
import(S02TestViewModelModule)
}
@Test
fun tryResult_and_applyScore() {
val viewModel by kodein.newInstance { Stage02ViewModel(instance()) }
viewModel.tryResult()
// then
Assert.assertEquals(Stage02ViewModel.TryResult.SUCCESS, viewModel.result.value)
// when
viewModel.applyScore()
// then
Assert.assertEquals(1, viewModel.score.value)
}
}
這裡就避開UI 單純測試數據是否正確
然後會發現修改後 昨天的測試會無法運行
所以要做點調整
S02FragmentTest.kt
...
@Before
fun setFragment() {
val createKodein: () -> Kodein = {
Kodein.lazy {
import(S02TestFragmentModule)
}
}
rule.activity.replaceFragment(Stage02Fragment.newInstance(createKodein))
}
然後可以比較一下今天跟昨天的測試案例差異
最後附上solution
https://github.com/mars1120/jetpackMvvmDemo/tree/mvvm-02-fragment-DI