上一篇連結Day18 MVVM專案-4
本篇會使用Day15 MVVM專案-2 Simple Fragment -3 DI建置好的fragment
以及創一個新的fragment來做互動
先新增一個activity
S05Activity.kt
override fun onCreate(savedInstanceState: Bundle?) {
val s02Fragment = Stage02Fragment.newInstance()
val s05Fragment = S05Fragment.newInstance()
supportFragmentManager.beginTransaction()
.replace(R.id.fl_top, s02Fragment)
.replace(R.id.fl_bottom, s05Fragment)
.commit()
}
目前階段畫面
然後目前直接沿用DI的專案會有BUG 我還在修正中 所以先把S02的專案還原回之前的寫法
Stage02Fragment.kt
class Stage02Fragment : Fragment() {
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.activity!!, createVMFactory()).get(Stage02ViewModel::class.java)
}
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 {
@JvmStatic
fun newInstance(createVMFactory: () -> ViewModelProvider.Factory = ::Stage02ViewModelFactory) =
Stage02Fragment().apply {
arguments = Bundle().apply {
putSerializable("createVMFactory", createVMFactory as Serializable)
}
}
}
}
注意 除了還原外
ViewModelProviders.of(this, createVMFactory()).get(Stage02ViewModel::class.java)
要改為
ViewModelProviders.of(this.activity!!, createVMFactory()).get(Stage02ViewModel::class.java)
否則S5Fragment呼叫S02的viewModel時資料不會連動
單元測試也一樣記得改回去
S02FragmentTest.kt
@RunWith(AndroidJUnit4::class)
class S02FragmentTest {
@get:Rule
val rule = object : ActivityTestRule<FragmentTestActivity>(FragmentTestActivity::class.java) {}
private fun justTrue() = true
@Before
fun setFragment() {
// given
val createVMFactory = {
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
Stage02ViewModel(::justTrue).apply {
speedOfAnim.value = 10.0f
} as T
}
}
rule.activity.replaceFragment(Stage02Fragment.newInstance(createVMFactory))
}
@Test
fun score_should_increase_when_success() {
// when
Espresso.onView(withId(R.id.bt_try)).perform(ViewActions.click())
Espresso.onView(isRoot())
.perform(ViewAssertionsEx.waiting(1000)) // waiting to end animation
// then
val expected =
String.format(rule.activity.resources.getString(R.string.stage02_score_format), 1)
Espresso.onView(withId(R.id.tv_score)).check(matches(withText(expected)))
}
object ViewAssertionsEx {
@JvmStatic
fun waiting(milliSec: Long) = object : ViewAction {
override fun getDescription(): String = "waiting $milliSec milli seconds"
override fun getConstraints(): Matcher<View> = isDisplayed()
override fun perform(uiController: UiController, view: View) =
uiController.loopMainThreadForAtLeast(milliSec)
}
}
}
更新viewModel
S05ViewModel.kt
class S05ViewModel : ViewModel() {
val logList: MutableLiveData<MutableList<String>> =
MutableLiveData<MutableList<String>>().apply {
value = mutableListOf()
}
val progress = MutableLiveData<Int>().apply { value = 0 }
val animSpeed: LiveData<Int> = Transformations.map(progress) { it + 1 }
val sdf = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.getDefault())
fun add(line: String) {
logList.value!!.add("${sdf.format(Date())} $line")
}
fun clear() {
logList.value = mutableListOf("clearAction")
}
}
新增一個adapter
S05LogListAdapter.kt
class S05LogListAdapter : ListAdapter<String, S05LogListAdapter.VH>(getDiffCallback()) {
class VH(v: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(v)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val v = LayoutInflater.from(parent.context).inflate(R.layout.listitem_s05, parent, false)
return VH(v)
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.itemView.tv_line.text = getItem(position)
}
companion object {
@JvmStatic
fun getDiffCallback() = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean =
oldItem === newItem
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
}
}
}
更新fragment
S05Fragment.kt
class S05Fragment : Fragment() {
private val createVMFactory: () -> ViewModelProvider.Factory by lazy {
@Suppress("UNCHECKED_CAST")
arguments?.getSerializable("createVMFactory") as? () -> ViewModelProvider.Factory
?: throw IllegalArgumentException("no createVMFactory for ${this::class.java.simpleName}")
}
private val viewModel: S05ViewModel by lazy {
ViewModelProviders.of(this).get(S05ViewModel::class.java)
}
private val adapter = S05LogListAdapter()
val e02ViewModel: Stage02ViewModel by lazy {
ViewModelProviders.of(this.activity!!, createVMFactory()).get(Stage02ViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.logList.observe(this, Observer {
adapter.submitList(it)
})
e02ViewModel.clearAction.observe(this, Observer {
viewModel.clear()
})
fun <T : Any> createObserverForAddingLine(prefix: String): Observer<T> = Observer {
viewModel.add("$prefix${it.toString()}")
adapter.notifyItemInserted(adapter.itemCount - 1)
rcv_log.scrollToPosition(adapter.itemCount - 1)
}
e02ViewModel.result.observe(this, createObserverForAddingLine("try result : "))
e02ViewModel.score.observe(this, createObserverForAddingLine("score : "))
val setSpeedObserver = createObserverForAddingLine<Int>("set speed ✕ ")
viewModel.animSpeed.observe(this, Observer {
e02ViewModel.speedOfAnim.value = it!!.toFloat()
setSpeedObserver.onChanged(it)
})
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? = DataBindingUtil.inflate<S05FragmentBinding>(
inflater,
R.layout.s05_fragment,
container,
false
).also {
it.setLifecycleOwner(this)
it.viewModel = viewModel
}.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rcv_log.adapter = adapter
rcv_log.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
}
companion object {
@JvmStatic
fun newInstance(createVMFactory: () -> ViewModelProvider.Factory = ::Stage02ViewModelFactory) =
S05Fragment().apply {
arguments = Bundle().apply {
putSerializable("createVMFactory", createVMFactory as Serializable)
}
}
}
}
新增對應的xml
listitem_s05.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tv_line"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/darker_gray"
android:textSize="12sp"
tools:text="[00.00.00] line" />
s05_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.ithome11.jetpackmvvmdemo.main.s05.S05ViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".main.s05.S05Activity">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_anim_speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@{@string/stage05_animation_speed(viewModel.animSpeed)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/sb_speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:max="9"
android:progress="@={viewModel.progress}"
app:layout_constraintTop_toBottomOf="@id/tv_anim_speed" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rcv_log"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:background="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sb_speed"
tools:listitem="@layout/listitem_s05" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
更新字串
strings.xml
<resources>
...
<string name="stage05_animation_speed">animation speed : ✕ %d</string>
</resources>
完成後畫面
最後新增整合測試
S05FragmentTest.kt
@RunWith(AndroidJUnit4::class)
class S05FragmentTest {
@get:Rule
val rule = ActivityTestRule<FragmentTestActivity>(FragmentTestActivity::class.java)
lateinit var fragment: S05Fragment
private fun justTrue() = true
@Before
fun setFragment() {
// given
val createVMFactory = {
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = Stage02ViewModel(::justTrue).apply {
speedOfAnim.value = 10.0f
} as T
}
}
UiThreadStatement.runOnUiThread {
// viewModel = E02ViewModel(::justTrue)
fragment = S05Fragment.newInstance(createVMFactory)
rule.activity.replaceFragment(fragment)
}
}
@Test
fun tryResult_should_add_list() {
// when
UiThreadStatement.runOnUiThread {
fragment.e02ViewModel.tryResult()
fragment.e02ViewModel.applyScore()
}
// then
onView(withId(R.id.rcv_log)).check(ViewAssertionsEx.hasItemCountOfRecyclerView(4))
}
@Test
fun clear_should_clear_list() {
// when
UiThreadStatement.runOnUiThread {
fragment.e02ViewModel.tryResult()
fragment.e02ViewModel.applyScore()
fragment.e02ViewModel.clear()
}
// then
onView(withId(R.id.rcv_log)).check(ViewAssertionsEx.hasItemCountOfRecyclerView(1))
}
@Test
fun observing_animSpeed_works() {
UiThreadStatement.runOnUiThread {
fragment.e02ViewModel.speedOfAnim.observe(fragment, Observer {})
}
// given
val givenProgress = 2
// when
onView(withId(R.id.sb_speed)).perform(ViewActionsEx.setProgress(givenProgress))
// then
Assert.assertEquals(3.0f, fragment.e02ViewModel.speedOfAnim.value)
}
}
object ViewAssertionsEx {
@JvmStatic
fun isInvisible() = ViewAssertion { view, noViewFoundException ->
if (noViewFoundException != null ) throw noViewFoundException
Assert.assertEquals(View.INVISIBLE, view.visibility)
}
@JvmStatic
fun hasItemCountOfRecyclerView(count: Int) = ViewAssertion { view, noViewFoundException ->
if (noViewFoundException != null ) throw noViewFoundException
val v = view as androidx.recyclerview.widget.RecyclerView
Assert.assertEquals(count, v.adapter!!.itemCount)
}
}
單元測試
E05ViewModelTest.kt
class E05ViewModelTest {
@Rule
@JvmField
val rule = InstantTaskExecutorRule()
@Test
fun addAndClear() {
val viewModel = E05ViewModel()
// given
val givenLine = "first"
// when
viewModel.add(givenLine)
// then
Assert.assertEquals(1, viewModel.logList.value!!.size)
Assert.assertTrue(viewModel.logList.value!![0].contains(givenLine))
// when
viewModel.add(givenLine)
viewModel.add(givenLine)
viewModel.clear()
// then
Assert.assertEquals(1, viewModel.logList.value!!.size)
}
@Test
fun animSpeed() {
val viewModel = E05ViewModel()
val lifecycle = LifecycleRegistry(mockk()).apply {
handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
viewModel.animSpeed.observe({ lifecycle }) {}
// given
val givenProgress = 0
// when
viewModel.progress.value = givenProgress
// then
Assert.assertEquals(1, viewModel.animSpeed.value)
}
}
solution
https://github.com/mars1120/jetpackMvvmDemo/tree/mvvm-05-Fragments