iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 13
0
Mobile Development

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

Day13 MVVM專案-2 Simple Fragment -1

  • 分享至 

  • xImage
  •  

本篇延續 Day9 MVVM專案-1a
今天會用Data Binding, ViewModel, LiveData 構築一個Fragment
裡面會包含一個 lottie 動畫 (resources by Eddy Gann)
並使用到 BindingAdapter綁定 onAnimationEnd 屬性
以及示範如何針對單個Fragment進行 instrumented test

先上成品畫面
https://i.imgur.com/lQ3bpin.gif
點擊後隨機產生哭哭臉或笑臉動畫

首先先去下載哭臉跟笑臉的動畫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資料是否正確無誤

至此畫面就建置完成了
https://i.imgur.com/lQ3bpin.gif

明天再來撰寫instrumented test

solution
https://github.com/mars1120/jetpackMvvmDemo/tree/mvvm-02-fragment


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

尚未有邦友留言

立即登入留言