Data Binding with view model Part 3

Suriya Wongkasum
3 min readAug 11, 2020

--

หลังจากตอนที่แล้วได้ทำ data binding หน้า add note สำหรับเพิ่มข้อมูลโน็ตไป เพื่อให้แน่ใจว่าข้อมูลที่เราบันทึกลง room database ถูกบันทึกจริงหรือไม่ ต่อมาเราจะต้องเพิ่มการแสดงผลข้อมูลโน็ตของเราที่หน้า home fragment โดยใช้ RecyclerView ที่ทำ data binding ที่ ListAdapter

เริ่มจากการสร้าง note_item_layout.xml สำหรับแสดง item view และทำ data binding โดยการใส่ data class เข้าไปดังนี้

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

<data>

<variable
name="note"
type="my.learing.com.recyclerviewbinding.database.NoteItem" />

</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{note.title}"
android:textSize="18sp"
android:textStyle="bold"
tools:text="title" />
</LinearLayout>
</layout>

การแสดงผลข้อมูลใน RecyclerView ปกติ adapter class เดิมปกติเรามักจะ RecyclerView.Adapter กัน ที่ต้องจัดการ data ที่ใช้แสดงผลเอง ไม่ว่าจะเป็น item size หรือมีข้อมูลเปลี่ยนแปลงเราต้องมาเราต้องมา manage เอง ถ้าเกิดมีการเปลี่ยนข้อมูลเยอะๆ แน่นอนว่าต้องเกิดอาการหน่วงๆ หรือว่าแอพค้างไป ทาง google เองจึงได้สร้าง adapter แบบใหม่ที่จะมาช่วยแก้ปัญหาเหล่านี้ ชึ่งก็คือ..ListAdapter

โดยโครงสร้างก็จะคล้ายๆ กันกับ adapter ของ RecyclerView.Adapter เลยคือประกอบด้วย ViewHolder class และมี DiffUtil class เพิ่มเข้ามา มีข้อดีกว่า RecyclerView.Adapter ก็คือเราไม่ต้องไป manage data เอง ทุกครั้งที่มีการเปลี่ยนแปลงข้อมูล adapter จัดจะการ data ให้หมด โดยใน data class เราจะต้องมี unique id ไว้ระบุความแตกต่างของข้อมูล ให้ DiffUtil class เป็นตัวจัดการเอง เราแค่โยนข้อมูลเข้าไป submitList ก็จบ

เรามาสร้าง NoteAdapter class กันเลยครับ

class NoteAdapter : ListAdapter<NoteItem, NoteAdapters.NoteHolder>(NoteDiffCallback()) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = NoteItemLayoutBinding.inflate(inflater, parent, false)
return NoteHolder(binding)
}

override fun onBindViewHolder(holder: NoteHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}

class NoteHolder private constructor(binding: NoteItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {

}

class NoteDiffCallback: DiffUtil.ItemCallback<NoteItem>() {
override fun areItemsTheSame(oldItem: NoteItem, newItem: NoteItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: NoteItem, newItem: NoteItem): Boolean {
return oldItem == newItem
}
}
}

ไปที่ NoteHolder class ในส่วนของ constructor เดิม holder class เรา type จะเป็น view แล้วเจ้า NoteItemLayoutBinding มาจากไหนกัน?

NoteItemLayoutBinding มาการ generate ให้อัตโนมัติหลังจากที่เรา convert data binding layout หรือ เพิ่มแท็ก layout ครอบ layout เดิม เพื่ออ้างอิงไป layout ที่จะทำ data binding โดยเมื่อเราคลิกที่ NoteItemLayoutBinding จะเห็นว่ามันจะเปิดไฟล์ note_item_layout.xml ทันที

เพิ่ม function bind ที่รับค่า parameter type เป็น NoteItem ใน class NoteHolder() เพื่อทำการผูก data เข้ากับ UI โดยไม่ต้อง set view ที่ adapter

fun bind(item: NoteItem) {
noteItemLayoutBinding.note = item
noteItemLayoutBinding.executePendingBindings()
}

ถ้าอยากทำ action เมื่อกดปุ่มละ ต้องใช้ setOnClickListener มั้ย? คำตอบคือไม่ เราสามารถใส่ action เมื่อคลิก view ของ recycler view ได้ท่าเดิมคือเพิ่ม OnClickItemListener class สำหรับดักการคลิก โดยรับ parameter ด้วย ramda

class OnClickItemListener(val onClickListener: (noteId: Long) -> Unit) {
fun onClick(noteItem: NoteItem) = onClickListener(noteItem.id)
}

เราสามารถนำคลาส OnClickItemListener ไปใช้ได้โดยการเพิ่ม แท็ค variable ที่ แท็ค data ใน note_item_layout.xml

<variable
name="listener"
type="my.learing.com.recyclerviewbinding.home.OnClickItemListener" />

แล้วเรียกใช้ function onClick ที่รับ parameter เป็น NoteItem ที่ layout หลักใน note_item_layout.xml

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()-> listener.onClick(note)}">

....
</LinearLayout>

และส่วนสุดท้ายคือเพิ่ม OnClickItemListener ใน parameter และ function ที่เกี่ยวข้อง

class NoteAdapter(private val onClickItemListener: OnClickItemListener): ...
override fun onBindViewHolder...
...
holder.bind(item, onClickItemListener = onClickItemListener)
}

class NoteHolder ...{

fun bind(item: NoteItem, onClickItemListener: OnClickItemListener) {
..
noteItemLayoutBinding.listener = onClickItemListener
..
}
}
..
}

เพื่อไม่ให้งงเมื่อเสร็จแล้ว NoteAdapter class ก็จะหน้าตาประมาณนี้

class NoteAdapter(private val onClickItemListener: OnClickItemListener) :
ListAdapter<NoteItem, NoteAdapter.NoteHolder>(NoteCallBack()) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = NoteItemLayoutBinding.inflate(inflater, parent, false)
return NoteHolder(binding)
}

override fun onBindViewHolder(holder: NoteHolder, position: Int) {
val item = getItem(position)
holder.bind(item, onClickItemListener = onClickItemListener)
}

class NoteHolder(private val noteItemLayoutBinding: NoteItemLayoutBinding) :
RecyclerView.ViewHolder(noteItemLayoutBinding.root) {

fun bind(item: NoteItem, onClickItemListener: OnClickItemListener) {
noteItemLayoutBinding.note = item
noteItemLayoutBinding.listener = onClickItemListener
noteItemLayoutBinding.executePendingBindings()
}
}

class NoteCallBack : DiffUtil.ItemCallback<NoteItem>() {
override fun areItemsTheSame(oldItem: NoteItem, newItem: NoteItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: NoteItem, newItem: NoteItem): Boolean {
return oldItem == newItem
}
}
}

class OnClickItemListener(val onClickListener: (noteId: Long) -> Unit) {
fun onClick(noteItem: NoteItem) = onClickListener(noteItem.id)
}

ส่วนสุดท้ายที่ขาดไปไม่ได้คือการใส่ adaper สำหรับแสดงข้อมูล note ให้กับ recycler view ที่ HomeFragment และใส่ action เมื่อคลิกที่ item view ให้ส่ง noteId ไปที่หน้า noteDetailFragment

override fun onCreateView(...){
...
val adapter = NoteAdapter(OnClickItemListener {
val bundle = bundleOf("noteId" to it)
this.findNavController()
.navigate(R.id.action_homeFragment_to_detailFragment, bundle)
})
binding.rcvNote.layoutManager =
LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
binding.rcvNote.adapter = adapter...

เมื่อเราสร้าง adapter list view และกำหนดค่าต่างๆ ที่ผูกให้เป็น data binding เรียบร้อยแล้ว สิ่งยังขาดก็คือข้อมูล note ที่อยู่ใน room database ที่จะเอามาใส่ใน adapter ยังง้ย

กลับไปที่ HomeViewModel จะมีตัวแปร _note ที่ดึงข้อมูลโน็ตทั้งหมดจาก room database เราสามารถดึงค่า _note ซึ่งเป็น private ได้ผ่านตัว mNote โดยทำการ subscribe mNote ที่ function onCreateView ของ HomeFragment แล้วเพิ่มข้อมูลโน็ตเข้าไปที่ adapter ผ่าน submitList()

override fun onCreateView(...){
...
homeViewModel.mNote.observe(viewLifecycleOwner, Observer { note ->
note?.let {
adapter.submitList(it)
}
}
)

รันทดสอบดูก็จะสามารถแสดงผลข้อมูลโน็ตที่เราเพิ่มไว้ได้ดังรูป

ตอนนี้ขอจบแค่นี้ก่อนนะครับ โปรดติดตามรับชมได้ในตอนต่อไป part 4

--

--