本篇延續 Day9 MVVM專案-1a
今天會用Data Binding, ViewModel, LiveData 構築一個Fragment
裡面會包含一個 lottie 動畫 (resources by Eddy Gann)
並使用到 BindingAdapter綁定 onAnimationEnd 屬性
以及示範如何針對單個Fragment進行 instrumented test
先上成品畫面
點擊後隨機產生哭哭臉或笑臉動畫
首先先去下載哭臉跟笑臉的動畫json檔(或是直接從solution複製過去也可)
https://lottiefiles.com/562-emoji-reaction
https://lottiefiles.com/767-crying-emoji-reaction
build.gradle(module:app)
dependencies {
implementation 'com.airbnb.android:lottie:2.8.0'
}
string.xml
<string name="stage02_score_format">score : %d</string>
Stage02ViewModelFactory.kt
class Stage02ViewModelFactory : ViewModelProvider.Factory {
private fun randomBooleanGenerator(): () -> Boolean {
val r = Random()
return { r.nextBoolean() }
}
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = when (modelClass) {
Stage02ViewModel::class.java -> Stage02ViewModel(randomBooleanGenerator())
else -> throw IllegalArgumentException("$modelClass is not registered ViewModel")
} as T
}
randomBooleanGenerator()每次運行時會產生隨機的true or false
透過Stage02ViewModel(randomBooleanGenerator())
傳遞給viewModel
S02Bindings.kt
@BindingAdapter("onAnimationEnd")
fun bind_onAnimationEnd(v: LottieAnimationView, listener: LottieOnAnimationEnd) {
Log.d("bind_onAnimationEnd", "binding")
v.addAnimatorListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) = Unit
override fun onAnimationEnd(animation: Animator?) {
listener.onAnimationEnd(v)
}
override fun onAnimationCancel(animation: Animator?) = Unit
override fun onAnimationStart(animation: Animator?) = Unit
})
}
@BindingAdapter("animation_speed")
fun bind_animSpeed(v: LottieAnimationView, speed: Float) {
v.speed = speed
}
動畫結束或取消時的行為
結束時會call onAnimationEnd
行為與viewModel.applyScore()綁定(寫在xml中)
Stage02ViewModel.kt
class Stage02ViewModel(private val nextBoolean: () -> Boolean) : ViewModel() {
//結果
enum class TryResult { FAILED, SUCCESS }
val score = MutableLiveData<Int>().apply { value = 0 }
val result = MutableLiveData<TryResult>()
val speedOfAnim = MutableLiveData<Float>().apply { value = 1.0f }
val clearAction = MutableLiveData<Unit>()
fun tryResult() {
result.value = if (nextBoolean()) TryResult.SUCCESS else TryResult.FAILED
}
fun applyScore() {
val amount = when (result.value) {
TryResult.FAILED -> -1
TryResult.SUCCESS -> 1
else -> throw IllegalStateException("result.value must be one of TryResult. Call tryResult() first.")
}
score.value = score.value!! + amount
}
fun clear() {
score.value = 0
clearAction.value = Unit
}
}
tryResult與TRY按鍵綁定
其中nextBoolean()就是Stage02ViewModelFactory中的randomBooleanGenerator()
Fragment
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, 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)
}
}
}
}
最後是xml
stage02_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.s02.ui.stage02.Stage02ViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_result"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_marginTop="8dp"
app:animation_speed="@{viewModel.speedOfAnim}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:lottie_autoPlay="false"
app:lottie_loop="false"
app:lottie_rawRes="@raw/s02_failed"
app:onAnimationEnd="@{_ -> viewModel.applyScore()}" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{@string/stage02_score_format(viewModel.score)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lav_result"
tools:text="@string/stage02_score_format" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/bt_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:onClick="@{_ -> viewModel.clear()}"
android:text="clear"
android:textSize="20sp"
app:layout_constraintEnd_toStartOf="@+id/bt_try"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_score" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/bt_try"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:onClick="@{_ -> viewModel.tryResult()}"
android:text="try"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/bt_clear"
app:layout_constraintTop_toBottomOf="@+id/tv_score" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
請確認onAnimationEnd、animation_speed 與其他bind資料是否正確無誤
至此畫面就建置完成了
明天再來撰寫instrumented test
solution
https://github.com/mars1120/jetpackMvvmDemo/tree/mvvm-02-fragment