Android RecycleView with MVVM and databing (Kotlin)

Source code : gitbub

1.先使用Android Studio 創建一個Empty Activity

2.在build.gradle 加入以下code再重新sync

android {
    buildFeatures{
        dataBinding = true
        viewBinding = true
    }
}

dependencies {
    ...
    implementation "androidx.fragment:fragment-ktx:1.5.5"
    implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
}

3. 新增兩個檔案

MainFragment.kt 在ui資料夾下,

MainViewModel在 viewModel資料夾下.

MainFragment.kt 內容如下:

package com.sqtek.recyclerviewmvvm.ui

import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import com.sqtek.recyclerviewmvvm.R
import com.sqtek.recyclerviewmvvm.databinding.FragmentMainBinding
import com.sqtek.recyclerviewmvvm.viewModel.MainViewModel

class MainFragment : Fragment() {

    companion object {
        fun newInstance() = MainFragment()
    }

    private lateinit var viewModel: MainViewModel
    private lateinit var viewDataBinding: FragmentMainBinding

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        viewDataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_main ,container, false)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        viewDataBinding.pViewModel = viewModel //pViewModel 要與對應的xxx.xml中data<>定義的name一致
        return viewDataBinding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel.updateTitle()
    }

}

MainViewModel.kt 內容如下:

package com.sqtek.recyclerviewmvvm.viewModel

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel() {
    var title: MutableLiveData<String> = MutableLiveData()

    fun updateTitle() {
        title.value = "SQTek for Ethan"
    }
}

fragment_main.xml 內容如下: 記得要改成layout

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".ui.MainFragment">

    <data>
        <variable
            name = "pViewModel"
            type = "com.sqtek.recyclerviewmvvm.viewModel.MainViewModel"
            />
    </data>


    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:text="@{pViewModel.title}" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

4. 在MainActivity.kt 加入以下code去呼叫MainFragment顯示畫面

package com.sqtek.recycleview

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.sqtek.recycleview.ui.MainFragment

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        loadFragment()
    }

    private fun loadFragment() {
        val fragmentManager = this.supportFragmentManager
        val transaction =  fragmentManager.beginTransaction()
        transaction.replace(R.id.container, MainFragment.newInstance())
        transaction.commit()
    }
}

5. 在activity_main.xml加入FrameLayout

<?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=".MainActivity">

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

6.完成基本MVVM架構之後, 只要在MainViewModel修改updateTitle()就會在UI上看見效果

7.增加一個Course data class並存於model/資料夾下,用來存放顯示資料欄位

package com.sqtek.recyclerviewmvvm.model

data class Course(var name: String, val imageUrl: Int, val courseUrl: String, val category: String, val desc: String)

8.修改fragment_main.xml加入RecyclerView元件, 移除原本textview與data

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".ui.MainFragment">

    <data>
        <variable
            name = "pViewModel"
            type = "com.sqtek.recyclerviewmvvm.viewModel.MainViewModel"
            />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black">

       <TextView
            android:id="@+id/txTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@{pViewModel.title}"
            android:textColor="@color/white"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:itemCount="8"
            tools:listitem="@layout/card_view_layout"
            app:layout_constraintEnd_toEndOf="parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintStart_toStartOf="parent"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

9. 增加一個card_view_layout.xml顯示每個item的內容

<?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 = "pViewModel"
            type = "com.sqtek.recyclerviewmvvm.model.Course"/>
    </data>

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        app:cardCornerRadius="5dp">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={pViewModel.name}"//有=代表雙向binding
            android:textSize="16sp"/>

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/imageview"
            android:layout_width="match_parent"
            android:layout_height="160dp"
            android:scaleType="centerCrop"
            app:imageResource="@{pViewModel.imageUrl}"
            android:layout_marginTop="50dp" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:gravity="center_vertical"
            android:text="@{pViewModel.name}"
            android:layout_marginTop="160dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{pViewModel.desc}"
            android:layout_marginTop="200dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"/>
    </androidx.cardview.widget.CardView>
</layout>

10. 增加一個MainAdapter.kt將資料塞到recyclerview

package com.sqtek.recyclerviewmvvm.ui

import android.annotation.SuppressLint
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.NonNull
import androidx.recyclerview.widget.RecyclerView
import com.sqtek.recyclerviewmvvm.databinding.CardViewLayoutBinding
import com.sqtek.recyclerviewmvvm.model.Course

class MainAdapter: RecyclerView.Adapter<MainAdapter.ViewHolder>() {
    var courses = mutableListOf<Course>()
    private val TAG = "MainAdapter"

    @SuppressLint("NotifyDataSetChanged")
    fun setCourseList(course: List<Course>) {
        this.courses = course.toMutableList()
        Log.d(TAG, "setCourseList: $courses")
        notifyDataSetChanged()
    }

    @NonNull
    override fun onCreateViewHolder(@NonNull parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.from(parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.apply {
            bind(courses[position])
        }
    }

    override fun getItemCount(): Int {
        Log.d(TAG, "getItemCount: ${courses.size}")
        return courses.size
    }

    companion object {
        @JvmStatic
        @BindingAdapter("loadImage")
        fun loadImage(thumbs: ImageView, url: String) {
            Glide.with(thumbs)
                .load(url)
                .circleCrop(
                .placehol)der(R.drawable.ic_launcher_foreground)
                .error(R.drawable.ic_launcher_foreground)
                .fallback(R.drawable.ic_launcher_foreground)
                .into(thumbs)
        }
    }

    class ViewHolder(val binding: CardViewLayoutBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(course: Course) {
            binding.pViewModel= course
        }

        companion object {
            fun from(parent: ViewGroup): ViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = CardViewLayoutBinding.inflate(layoutInflater, parent, false)

                return ViewHolder(binding)
            }
        }
    }
}

11.修改MainViewModel加入一些假的資料

package com.sqtek.recyclerviewmvvm.viewModel

import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.sqtek.recyclerviewmvvm.R
import com.sqtek.recyclerviewmvvm.model.Course

class MainViewModel : ViewModel() {
    var title: MutableLiveData<String> = MutableLiveData()
    val courseList = MutableLiveData<List<Course>>()


    fun updateTitle() {
        title.value = "SQTek for Ethan"
    }

    fun getAllCourse() {
        val title = arrayOf<String>(
            "【Highlands, Islands, Cities: The Magic of Scotland】",
            "【Study Finds Why Gardening Is Good for You】",
            "【French Bulldog Becomes Most Popular US Dog Breed】",
            "【Three-Year Cruise Visits 135 Countries for \$85 a Day】",
            "【'I'm Terribly Sorry': How to Apologize in English】",
            "【Japanese Scientists Create Mice with Cells from Two Males】",
            "【Google's AI chatbot Bard takes on Microsoft's ChatGPT】",
            "【Archaeologists Find Earliest Evidence of Horse Riding】")

        val imageId = arrayOf<Int>(
            R.drawable.course1, R.drawable.course2,         R.drawable.course3,
            R.drawable.course4, R.drawable.course5, R.drawable.course6,
            R.drawable.course7, R.drawable.course8
        ))
        val articleUrl = arrayOf<String>(
            "https://engoo.com.tw/app/daily-news/article/highlands-islands-cities-the-magic-of-scotland/RuaCPMUKEe2Pj58I1puG_w",
            "https://engoo.com.tw/app/daily-news/article/study-finds-why-gardening-is-good-for-you/UhFf6LkLEe221f9tVn_GlQ",
            "https://engoo.com.tw/app/daily-news/article/french-bulldog-becomes-most-popular-us-dog-breed/GyuvJMc0Ee2GvBOw-NIEew",
            "https://engoo.com.tw/app/daily-news/article/three-year-cruise-visits-135-countries-for-85-a-day/1f2UHMK6Ee2lN59yxslasw",
            "https://engoo.com.tw/app/daily-news/article/im-terribly-sorry-how-to-apologize-in-english/QY1m7sc0Ee2GLHdMNyUqPg",
            "https://engoo.com.tw/app/daily-news/article/japanese-scientists-create-mice-with-cells-from-two-males/-FCkIsS5Ee279ouyuxjQkA",
            "https://engoo.com.tw/app/daily-news/article/googles-ai-chatbot-bard-takes-on-microsofts-chatgpt/zzeQ_MjMEe2Z5Qcx9XxsIw",
            "https://engoo.com.tw/app/daily-news/article/archaeologists-find-earliest-evidence-of-horse-riding/PLMOHLxdEe2OOytrSUBaAA")

        val name = arrayOf<String>(
            "Highlands, Islands, Cities: The Magic of Scotland",
            "Study Finds Why Gardening Is Good for You",
            "French Bulldog Becomes Most Popular US Dog Breed",
            "Three-Year Cruise Visits 135 Countries for \$85 a Day",
            "'I'm Terribly Sorry': How to Apologize in English",
            "Japanese Scientists Create Mice with Cells from Two Males",
            "Google's AI chatbot Bard takes on Microsoft's ChatGPT",
            "Archaeologists Find Earliest Evidence of Horse Riding")

        val desc = arrayOf<String>(
            "Harry Potter author JK Rowling often wrote her books in Scotland — and if it's magic you're looking for, this might just be a good place to find it.",
            "Most gardeners will probably say gardening is good for you. It gets you out in the fresh air, getting lots of sunshine and exercise while watching things grow.",
            "For the first time in over 30 years, the US has a new favorite dog breed, according to the American Kennel Club.",
            "Would you like to take a cruise around the world with enough time on land to visit everything from the Great Wall of China to the pyramids of Egypt, the bars of Barcelona and the ice-covered rock of Antarctica?",
            "It's said that being polite costs nothing. But when we want to apologize — to say we're sorry — it can sometimes be hard to choose the best words.",
            "Japanese scientists have created baby mice with two males for the first time by turning their stem cells into female cells in a laboratory.",
            "Google has announced it will allow more people to interact with \"Bard,\" its artificial intelligence (AI) chatbot.",
            "Archaeologists believe they have found the earliest direct evidence of horseback riding in 5,000-year-old human skeletons in central Europe.")

        val list = mutableListOf<Course>()
        (0..7).forEach {
            list.add(Course(title[it], imageId[it], articleUrl[it], name[it], desc[it]))
        }
        Log.d("Ethan", "getAllCourse: $list")
        courseList.postValue(list)
    }
}

12. 修改MainFragment

package com.sqtek.recyclerviewmvvm.ui

import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import com.sqtek.recyclerviewmvvm.R
import com.sqtek.recyclerviewmvvm.databinding.FragmentMainBinding
import com.sqtek.recyclerviewmvvm.viewModel.MainViewModel

class MainFragment : Fragment() {

    companion object {
        fun newInstance() = MainFragment()
    }
    private val TAG = "MainFragment"
    private lateinit var viewModel: MainViewModel
    private lateinit var viewDataBinding: FragmentMainBinding
    private lateinit var mainAdapter: MainAdapter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        viewDataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_main ,container, false)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        viewDataBinding.lifecycleOwner = requireActivity()
        mainAdapter = MainAdapter()
        mainAdapter.setHasStableIds(true)
        viewDataBinding.recyclerview.adapter = mainAdapter
        viewModel.getAllCourse()
        return viewDataBinding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        Log.d(TAG, "onActivityCreated()")
        viewModel.updateTitle()
        viewModel.movieList.observe(viewLifecycleOwner, Observer {
            Log.d(TAG, "onActivityCreated()::observe: $it")
            mainAdapter.setCourseList(it)
        })
    }

}

13.運行結果

14. 處理按下謀個課程時,跳轉到課程詳細頁面. (記得要將viewmodel 加入adapter中, 不然按下item會沒作用), 先在AndroidManifeat.xml加入網路權限

<uses-permission android:name="android.permission.INTERNET" />

15. 修改card_view_layout.xml 增加viewmode以及按下item後要處理的fun

<?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 = "pViewModel"
            type = "com.sqtek.recyclerviewmvvm.model.Course"/>
        <variable
            name = "ViewModel"
            type = "com.sqtek.recyclerviewmvvm.viewModel.MainViewModel"/>
    </data>

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        app:cardCornerRadius="5dp"
        android:onClick="@{() -> ViewModel.openItem(pViewModel)}">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={pViewModel.name}"
            android:textSize="18sp"/>

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/imageview"
            android:layout_width="match_parent"
            android:layout_height="160dp"
            android:scaleType="centerCrop"
            app:imageResource="@{pViewModel.imageUrl}"
            android:layout_marginTop="60dp"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:text="@{pViewModel.name}"
            android:layout_marginTop="230dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="@tools:sample/full_names"
            android:background="#E1303F9F"
            android:paddingStart="8dp"
            android:textColor="@color/white"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{pViewModel.desc}"
            android:layout_marginTop="290dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="@tools:sample/full_names"
            android:paddingStart="8dp"
            android:textSize="14sp"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"/>
    </androidx.cardview.widget.CardView>
</layout>

16. 修改MainViewModel.kt 新增一個fun openItem(course: Course) 處理按下的動作以及新增一個openItemEvent通知MainFragment有item被按下

package com.sqtek.recyclerviewmvvm.viewModel

import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.sqtek.recyclerviewmvvm.R
import com.sqtek.recyclerviewmvvm.model.Course
import com.sqtek.recyclerviewmvvm.ui.Event

class MainViewModel : ViewModel() {
    private val TAG = "MainFragment"
    //var title: MutableLiveData<String> = MutableLiveData()
    val courseList = MutableLiveData<List<Course>>()
    val openItemEvent: MutableLiveData<Event<Course>> = MutableLiveData()

    //fun updateTitle() {
    //    title.value = "SQTek for Ethan"
    //}

    fun getAllCourse() {
        val title = arrayOf<String>(
            "【Highlands, Islands, Cities: The Magic of Scotland】",
            "【Study Finds Why Gardening Is Good for You】",
            "【French Bulldog Becomes Most Popular US Dog Breed】",
            "【Three-Year Cruise Visits 135 Countries for \$85 a Day】",
            "【'I'm Terribly Sorry': How to Apologize in English】",
            "【Japanese Scientists Create Mice with Cells from Two Males】",
            "【Google's AI chatbot Bard takes on Microsoft's ChatGPT】",
            "【Archaeologists Find Earliest Evidence of Horse Riding】")
        val imageId = arrayOf<Int>(
            R.drawable.course1, R.drawable.course2, R.drawable.course3,
            R.drawable.course4, R.drawable.course5, R.drawable.course6,
            R.drawable.course7, R.drawable.course8
        )
        val articleUrl = arrayOf<String>(
            "https://engoo.com.tw/app/daily-news/article/highlands-islands-cities-the-magic-of-scotland/RuaCPMUKEe2Pj58I1puG_w",
            "https://engoo.com.tw/app/daily-news/article/study-finds-why-gardening-is-good-for-you/UhFf6LkLEe221f9tVn_GlQ",
            "https://engoo.com.tw/app/daily-news/article/french-bulldog-becomes-most-popular-us-dog-breed/GyuvJMc0Ee2GvBOw-NIEew",
            "https://engoo.com.tw/app/daily-news/article/three-year-cruise-visits-135-countries-for-85-a-day/1f2UHMK6Ee2lN59yxslasw",
            "https://engoo.com.tw/app/daily-news/article/im-terribly-sorry-how-to-apologize-in-english/QY1m7sc0Ee2GLHdMNyUqPg",
            "https://engoo.com.tw/app/daily-news/article/japanese-scientists-create-mice-with-cells-from-two-males/-FCkIsS5Ee279ouyuxjQkA",
            "https://engoo.com.tw/app/daily-news/article/googles-ai-chatbot-bard-takes-on-microsofts-chatgpt/zzeQ_MjMEe2Z5Qcx9XxsIw",
            "https://engoo.com.tw/app/daily-news/article/archaeologists-find-earliest-evidence-of-horse-riding/PLMOHLxdEe2OOytrSUBaAA")
        //val courseResponse = CourseResponse()

        val name = arrayOf<String>(
            "Highlands, Islands, Cities: The Magic of Scotland",
            "Study Finds Why Gardening Is Good for You",
            "French Bulldog Becomes Most Popular US Dog Breed",
            "Three-Year Cruise Visits 135 Countries for \$85 a Day",
            "'I'm Terribly Sorry': How to Apologize in English",
            "Japanese Scientists Create Mice with Cells from Two Males",
            "Google's AI chatbot Bard takes on Microsoft's ChatGPT",
            "Archaeologists Find Earliest Evidence of Horse Riding")

        val desc = arrayOf<String>(
            "Harry Potter author JK Rowling often wrote her books in Scotland — and if it's magic you're looking for, this might just be a good place to find it.",
            "Most gardeners will probably say gardening is good for you. It gets you out in the fresh air, getting lots of sunshine and exercise while watching things grow.",
            "For the first time in over 30 years, the US has a new favorite dog breed, according to the American Kennel Club.",
            "Would you like to take a cruise around the world with enough time on land to visit everything from the Great Wall of China to the pyramids of Egypt, the bars of Barcelona and the ice-covered rock of Antarctica?",
            "It's said that being polite costs nothing. But when we want to apologize — to say we're sorry — it can sometimes be hard to choose the best words.",
            "Japanese scientists have created baby mice with two males for the first time by turning their stem cells into female cells in a laboratory.",
            "Google has announced it will allow more people to interact with \"Bard,\" its artificial intelligence (AI) chatbot.",
            "Archaeologists believe they have found the earliest direct evidence of horseback riding in 5,000-year-old human skeletons in central Europe.")

        val list = mutableListOf<Course>()
        (0..7).forEach {
            list.add(Course(title[it], imageId[it], articleUrl[it], name[it], desc[it]))
        }
        Log.d(TAG, "getAllCourse: $list")
        courseList.postValue(list)
    }

    fun openItem(course: Course) {
        Log.d(TAG, "openItem: $course")
        openItemEvent.value = Event(course)

    }
}

17. 修改MainFragment, 將MainViewModel加入MainAdapter中才能處理再MainFragment按下item的動作, 在處理openItemEvent 呼叫detailfragment顯示課程詳細頁面

package com.sqtek.recyclerviewmvvm.ui

import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import com.sqtek.recyclerviewmvvm.R
import com.sqtek.recyclerviewmvvm.databinding.FragmentMainBinding
import com.sqtek.recyclerviewmvvm.model.Course
import com.sqtek.recyclerviewmvvm.viewModel.MainViewModel

class MainFragment : Fragment() {

    companion object {
        fun newInstance() = MainFragment()
    }
    private val TAG = "MainFragment"
    private lateinit var viewModel: MainViewModel
    private lateinit var viewDataBinding: FragmentMainBinding
    private lateinit var mainAdapter: MainAdapter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        viewDataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_main ,container, false)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        viewDataBinding.lifecycleOwner = requireActivity()
        mainAdapter = MainAdapter(viewModel)
        //mainAdapter.setHasStableIds(true)
        //viewDataBinding.viewModel = viewModel
        viewDataBinding.recyclerview.adapter = mainAdapter

        viewModel.getAllCourse()
        return viewDataBinding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        Log.d(TAG, "onActivityCreated()")
        //viewModel.updateTitle()
        viewModel.courseList.observe(viewLifecycleOwner, Observer {
            Log.d(TAG, "onActivityCreated()::observe $it")
            mainAdapter.setCourseList(it)
        })

        viewModel.openItemEvent.observe(viewLifecycleOwner, Observer { event ->
            event.getContentIfNotHandled()?.let {
                val course: Course = it
                Log.d(TAG, "openItemEvent()")
                requireActivity().supportFragmentManager
                    .beginTransaction()
                    .replace(R.id.container, DetailFragment.newInstance(course))
                    .commit()
            }
        })
    }
}

18.修改MainAdapter的class新增帶入MainViewModel以及修改viewholder的bind fun

package com.sqtek.recyclerviewmvvm.ui

import android.annotation.SuppressLint
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.NonNull
import androidx.recyclerview.widget.RecyclerView
import com.sqtek.recyclerviewmvvm.databinding.CardViewLayoutBinding
import com.sqtek.recyclerviewmvvm.model.Course
import com.sqtek.recyclerviewmvvm.viewModel.MainViewModel

class MainAdapter(private val viewModel: MainViewModel): RecyclerView.Adapter<MainAdapter.ViewHolder>() {
    var courses = mutableListOf<Course>()
    private val TAG = "MainAdapter"

    @SuppressLint("NotifyDataSetChanged")
    fun setCourseList(course: List<Course>) {
        this.courses = course.toMutableList()
        Log.d(TAG, "setCourseList: $courses")
        notifyDataSetChanged()
    }

    @NonNull
    override fun onCreateViewHolder(@NonNull parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.from(parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.apply {
            bind(viewModel, courses[position])
        }
    }

    override fun getItemCount(): Int {
        Log.d(TAG, "getItemCount: ${courses.size}")
        return courses.size
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    class ViewHolder(val binding: CardViewLayoutBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(viewModel: MainViewModel,course: Course) {
            binding.pViewModel= course
            binding.viewModel =viewModel
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): ViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = CardViewLayoutBinding.inflate(layoutInflater, parent, false)

                return ViewHolder(binding)
            }
        }
    }
}

19. 新增Event.kt 處理event

package com.sqtek.recyclerviewmvvm.ui

open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

20. 新增DetailFragment.kt與fragment_course_detail.xml

DetailFragment.kt

package com.sqtek.recyclerviewmvvm.ui

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.OnBackPressedCallback
import androidx.databinding.DataBindingUtil
import com.sqtek.recyclerviewmvvm.R
import com.sqtek.recyclerviewmvvm.databinding.FragmentCourseDetailBinding
import com.sqtek.recyclerviewmvvm.model.Course

/**
 * A simple [Fragment] subclass.
 * Use the [DetailFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class DetailFragment(item: Course) : Fragment() {

    val item: Course = item

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val viewDataBinding: FragmentCourseDetailBinding = DataBindingUtil.inflate(
            inflater, R.layout.fragment_course_detail, container, false
        )
        val webView = viewDataBinding.root.findViewById(R.id.webView) as WebView
        webView.settings.javaScriptEnabled = true
        webView.webViewClient = WebViewClient()
        webView.loadUrl(item.courseUrl)
        webView.settings.setSupportZoom(true)
        return viewDataBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                requireActivity().supportFragmentManager
                    .beginTransaction()
                    .replace(R.id.container, MainFragment.newInstance(), null)
                    .commit()
            }
        })
    }
    companion object {
        @JvmStatic
        fun newInstance(item: Course) = DetailFragment(item)
    }
}

fragment_course_detail.xm

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.DetailFragment">

        <WebView
            android:id="@+id/webView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="50dp"
            android:layout_marginBottom="10dp"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp">
        </WebView>
    </FrameLayout>

</layout>

21. 完成

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

購物車