iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 22
0
Mobile Development

大一之 Android Kotlin 自習心路歷程系列 第 22

[Day 22] Android in Kotlin: MVVM 架構分享 —— 簡單示範

透過上一節的文字說明,想在這節做一個簡單的示範。

我想完成從 MainActivity 輸入字串,按下按鈕後跳到 ResultActivity 並以 recycler view 顯示該字串和內容。

MainActivity:
edit text

ResultActivity:
recycler

因為有兩個畫面,所以會兩兩一起做介紹。

XML

Main

只有一個 edit text 跟兩個 button:一個功能是新增資料;另一個是切換至顯示資料的 ResultActivity

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.MainActivity">

    <EditText
        android:id="@+id/editMain"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.3"/>

    <Button
        android:id="@+id/btnMainSave"
        android:text="save"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/editMain"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.2"/>

    <Button
        android:id="@+id/btnMainResult"
        android:text="result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btnMainSave"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.2"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Result

一個 recycler view 顯示內容

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.result.ResultActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/listResult"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

還有它的 item

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <LinearLayout
        android:id="@+id/layoutItemResult"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/textItemResultId"
            android:text="0"
            android:layout_margin="30dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/textItemResultContent"
            android:text="content"
            android:layout_margin="30dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>


        <TextView
            android:id="@+id/textItemResultPrint"
            android:text="print"
            android:layout_margin="30dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="end"/>

    </LinearLayout>

    <View
        android:background="@android:color/darker_gray"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        app:layout_constraintTop_toBottomOf="@id/layoutItemResult"/>
</androidx.constraintlayout.widget.ConstraintLayout>

簡單的做完畫面,就可以開始了!
MVVM 有 三個部份,就先從 model 開始吧

Model

Data Class

這是上一篇講的 data class。我需要每次新增資料時會增加的 id,跟其內容,另外再加上一個方法給予我想要的字串

data class MainData(
    var _id: Int,
    var content: String,
) {
    fun print()= "$_id:$content"
}

Repository

在這個範例中,我把資料存在 repository 裡面,並用靜態的概念把它們放在 object 裡面

class MainRepository {
    companion object{
        private var data= ArrayList<MainData>()
    }

    fun saveText(text: String){
        data.add(MainData(data.size, text))
    }

    fun getAllText() = data
}

除此之外,還有兩個對於 data 操作的 function。

ViewModel

在 view model 裡面,會擁有它所需要的 repository 實體,再建立想要的方法並呼叫 repository 的方法。

因為我們有兩個畫面,所以也會需要有兩個 view model。

Main

MainActivity 裡面是新增資料的。

class MainViewModel : ViewModel(){
    private val repository= MainRepository()

    fun saveText(text: String){
        repository.saveText(text)
    }
}

Result

ResultActivity 裡面是顯示資料的。

class ResultViewModel: ViewModel(){
    private val repository= MainRepository()

    fun getAllText() = repository.getAllText()
}

Factory

除了 view model 自己本身,view model 還需要透過 factory 創造出實體,所以我在這裡,寫出 factory。

不過呢,其實是當 view model 有參數時,才要自製 view model factory,因為 如果採取一般的 ViewModelProvider 方式,view model 不能透過建構元傳入資料。但我習慣不管有沒有建構元,都會給他們 factory。

class  MyViewModelFactory(): ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return modelClass.newInstance()
    }
}

建立一個 class 實作 ViewModelProvider.Factory。待會在使用 view model factory 的時候會傳入 view model 的 class 名,所以就利用泛型,將傳入的 class 做出實體並回傳。

View

這裡底下就是 activity 的內容了

Main

class MainActivity : AppCompatActivity() {

    private lateinit var edit: EditText
    private lateinit var btnSave: Button
    private lateinit var btnResult: Button

    private val viewModel: MainViewModel by lazy {
        ViewModelProvider(this, MyViewModelFactory).get(MainViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        edit= findViewById(R.id.editMain)
        btnSave= findViewById(R.id.btnMainSave)
        btnResult= findViewById(R.id.btnMainResult)

        btnSave.setOnClickListener {
            viewModel.saveText(edit.text.toString())
            edit.text = null
        }

        btnResult.setOnClickListener {
            startActivity(Intent(this, ResultActivity::class.java))
        }
    }
}

view model 以剛剛的 factory class 建立實體,有了 view model 以後,就在需要的地方呼叫 view model 的方法就好了

至於為什麼要使用加上 by lazy 修飾詞,試試看就知道了
當我們把 lazy 拔掉後 run app 的時候就會出問題了,開不起來
看一下 logcat:
Caused by: java.lang.IllegalStateException: Your activity is not yet attached to the Application instance. You can't request ViewModel before onCreate call.

看來是不能在 onCreate() 呼叫前對 view model 操作。

這裡就要講一下除了 lateinit 的另一種延遲初始化的方法
lazy
他會在你要使用他的時候,才進行初始化,也就是整個跑完以後,等我按下 btnSave 按鈕才會開始初始化,藉此除掉錯誤。

就我的理解來說,lateinit 適用在 var;lazy 適用在 val

Result


class ResultActivity : AppCompatActivity() {

    private val viewModel: ResultViewModel by lazy {
        ViewModelProvider(this, MyViewModelFactory).get(ResultViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_result)

        val recyclerView: RecyclerView= findViewById(R.id.listResult)
        recyclerView.apply {
            adapter= ResultRecyclerAdapter(viewModel)
            layoutManager= LinearLayoutManager(context)
        }
    }
}

因為 recycler view adapter 需要資料,就把 view model 當參數傳入即可。

Result Recycler Adapter

要做出接收 view model 的建構元

class ResultRecyclerAdapter(
    private val viewModel: ResultViewModel
): RecyclerView.Adapter<ResultRecyclerAdapter.ResultViewHolder>() {
    class ResultViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
        val textId: TextView= itemView.findViewById(R.id.textItemResultId)
        val textContent: TextView= itemView.findViewById(R.id.textItemResultContent)
        val textPrint: TextView= itemView.findViewById(R.id.textItemResultPrint)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder {
        return ResultViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_result, parent, false))
    }

    override fun onBindViewHolder(holder: ResultViewHolder, position: Int) {
        Log.d(TAG, "onBindViewHolder: $position")
        holder.textId.text= viewModel.getAllText()[position]._id.toString()
        holder.textContent.text= viewModel.getAllText()[position].content
        holder.textPrint.text= viewModel.getAllText()[position].print()
    }

    override fun getItemCount(): Int = viewModel.getAllText().size
}

其中有需要資料的地方,就可以跟 view model 拿了。

成果就像開頭的那兩張圖片一樣。


上一篇
[Day 21] Android in Kotlin: MVVM 架構分享 —— 簡介
下一篇
[Day 23] Android in Kotlin: MVVM 架構分享 —— LiveData Observe 簡單示範
系列文
大一之 Android Kotlin 自習心路歷程30

尚未有邦友留言

立即登入留言