iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
0
Mobile Development

Android × CI/CD 如何用基本的MVVM專案實現 CI/CD 系列 第 15

Day15 MVVM專案-2 Simple Fragment -3 DI

  • 分享至 

  • xImage
  •  

本篇延續 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


上一篇
Day14 MVVM專案-2 Simple Fragment -2
下一篇
Day16 MVVM專案-3 RecyclerView
系列文
Android × CI/CD 如何用基本的MVVM專案實現 CI/CD 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言