
บทเรียนสำหรับการพัฒนาแอพพลิเคชันแพลตฟอร์ม Android ด้วยภาษา Kotlin ทำ E Book โดยใช้ RecyclerView ร่วมกับ Firebase
ตัวอย่างนี้จะเหมือนตัวอย่างของ Java ส่วนของบทความ:
- Android Studio กับการทำระบบ Bookshelf ร่วมกับ Firebase เบื้องต้น ตอนที่ 1
- Android Studio กับการทำระบบ Bookshelf ร่วมกับ Firebase เบื้องต้น ตอนที่ 3
ดังนั้นเราจะเห็นว่าเราจะต้องสร้างตัวอย่างของ Realtime Database บน Firebase มาก่อนโดยมีโครงสร้างเหมือนตัวอย่างของบทความก่อนหน้าก็จะขอละไว้ไม่อธิบายนะครับ:
หน้าแรกเราจะมี Key หลักที่ประกอบด้วย title คือชื่อการ์ตูน, desc คำโปรย, photos คือหน้าปกเป็นลิงค์ url ภาพ และ สุดท้ายคือ pages ที่จะเป็นการบอกว่าในการ์ตูนตอนนั้นมีหน้าย่อยที่เก็บ หน้าต่างๆ กี่หน้า:
โครงสร้าง Firebase หน้าแรกจะเป็น

URL แบบ JSON: https://enet5-7f9f6.firebaseio.com/bookshelf.json
ส่วนหน้าที่มี pages จะมี photo เป็น Path ของรูปภาพ โดยแบ่งตาม Node ที่เป็น Unique ID แต่ละหน้าตามตัวอย่าง:

URL แบบ JSON ไว้ดูเล่น https://enet5-7f9f6.firebaseio.com/bookshelf/data/key00004.json
ตรวจสอบการเปิด Permission ของ Database ให้ดี ไปที่ Firebase ตั้งค่า Rule ดังนี้:
{
"rules": {
".read": true,
".write": true
}
}
ต่อมาให้ทำการสร้างแอพพลิเคชันขึ้นมาใหม่เป็น Empty Activity ทำการเชื่อมต่อ Firebase โดยไปที่ เมนู: Tools-> Firebase

เปิด res->values->colours.xml ขึ้นมาทำชุดสีใหม่เล็กน้อย (ข้ามได้ถ้าไม่สำคัญ)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#4CAF50</color>
<color name="colorPrimaryDark">#4CAF50</color>
<color name="colorAccent">#FFC107</color>
<color name="colorDarkBG">#333</color>
<color name="colorWhite">#FFFFFF</color>
</resources>
ทำการดาวน์โหลด RecyclerView มาใช้งานหลังจากนั้นให้นำ Widget ไปวาง ทำการเชื่อม ConstraintLayout ทุกมุม ตั้งชื่อ ID ของ RecyclerView ว่า recyclerView

ไฟล์ activity_main.xml จะเป็นดังนี้:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorDarkBG"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/recyclerView"/>
</android.support.constraint.ConstraintLayout>
ทำการไปที่ MainActivity.kt ประกาศตัวแปรที่ Global Variable ใต้ Class ด้วยคำสั่ง:
lateinit var recyclerView : RecyclerView
ไปที่ onCreate() ทำการ ประกาศตัวแปรรับ findViewById ที่ acitivy_main.xml
recyclerView = findViewById(R.id.recyclerView)
ไปที่ เมนู Tools -> Firebase เพื่อเปิดใช้งาน Firebase Assistance

เปิดเลือก Realtime Database กดที่ Save and retrieve data

เลือก Choose an Existing Firebase or Google Project ของเราที่เคยสร้างไว้
ทำตามขั้นตอนที่ 1 และ ขั้นตอนที่ 2 โดยมันจะมีการบังคับให้เราเข้าระบบ Google บัญชีที่เราใช้ในการทำ Firebase อยู่อีกทั้งขั้นตอนนี้จะมีการ Sync ตัว Gradle ให้ด้วยเรียบร้อย:

เมื่อเสร็จแล้วซ่อนหน้าต่าง Assistance นี้ออกไป แก้ไข Module App ของ Gradle ใหม่เล็กน้อย:

implementation 'com.google.firebase:firebase-database:16.0.1' implementation 'com.google.firebase:firebase-core:16.0.1' implementation 'com.squareup.picasso:picasso:2.5.2'
แก้ไขส่วนของ Project Gradle เล็กน้อย แก้ไข Class Path dependencie ดังนี้:

แก้ google-services ที่เป็น 4.1.0 ให้อยู่ใน เวอร์ชันที่เสถียรคือ 4.0.0
classpath 'com.google.gms:google-services:4.1.0'
แก้ไขเป็น
classpath 'com.google.gms:google-services:4.0.0'
คลิกขวาที่ Package ของเราเลือก New -> Kotlin File/Class

ตั้งประเภทเป็น Class ตั้งชื่อว่า DataModel, DataAdapter ตรวจสอบตำแหน่ง Class

ไปที่ DataModel.kt ประกาศตัวแปรใน Class ของ DataModel ดังนี้ (สังเกตุมันคือ Key ของ Firebase ของเรา)
class DataModel {
var title: String?=null
var desc: String?=null
var photos: String?=null
var files: String?=null
var key:String?=null
var photo:String?=null
}
เราจะทำ Contructor Class ของตัวแปรทั้งหมดใน Class DataModel จะเป็นดังนี้:
package com.daydev.iapplication
class DataModel {
var title: String?=null
var desc: String?=null
var photos: String?=null
var files: String?=null
var key:String?=null
var photo:String?=null
constructor()
constructor(title: String?, desc: String?, photos: String?, key: String?, files: String?, photo: String?) {
this.title = title
this.desc = desc
this.photos = photos
this.key = key
this.files = files
this.photo = photo
}
fun toMap(): Map<String, Any> {
val result = HashMap<String, Any>()
result.put("title", title!!)
result.put("desc", desc!!)
result.put("thumbnail", photos!!)
result.put("key", key!!)
result.put("files", files!!)
result.put("photo", photo!!)
return result
}
}
มีการเรียกใช้ Map และ HashMap ในการทำ Dictionary ระหว่าง Key, Value จาก Firebase มาเก็บใน Model ของเรา และสร้าง None Argument Constructor ที่
constructor()
Constructor คือ สิ่งที่มีไว้สำหรับกำหนดค่าเริ่มต้นให้กับตัวแปร Class ของ object Constructor จะทำงานอัตโนมัติเมื่อมีการ new object ขึ้นมา
ต่อมาเราจะสร้าง res->layout ขึ้นมาใหม่ชื่อว่า model.xml

ออกแบบ model.xml ด้วย text mode ดังนี้:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="300dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="240dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="center"
android:textColor="@color/colorWhite"
android:textSize="16sp"/>
</LinearLayout>
</LinearLayout>
ปที่ DataAdapter เราจะประกาศฟังก์ชัน ในรอบนี้จะทำงานเรียก DataModel มาปรากฏ และใช้งาน ViewHolder มาจัดการเช่นเดิม และรอบนี้เราจะเรียก List ของ Array ทั้งก้อนมาใส่ โดยใช้ Pattern จาก Class DataModel
ให้ทำการ alt + enter ที่ class ก่อนเพื่อ Implement Memebers ของ Class นี้ให้มีเมธอดหลักของการจัดการ DataAdapter:

เราจะได้ DataAdapter ดังนี้ โดยมีการเรียก ViewHolder เป็น inner Class (inner Function) ส่วนของ ViewHolder นั้นมีการประกาศ widget ของ ImageView และ TextView แล้วผ่านตัวแปร thumbnail, textTitle:
class DataAdapter(val dataModelList: List<DataModel>) : RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): ViewHolder {
}
override fun onBindViewHolder(p0: ViewHolder, p1: Int) {
}
override fun getItemCount(): Int {
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var textTitle: TextView = itemView.findViewById(R.id.title)
var imageView: ImageView
init {
imageView = itemView.findViewById(R.id.thumbnail)
}
}
ในเมธอด getItemCount() ให้ใส่ return dataModelList.size เพื่อรับจำนวน Arrays
override fun getItemCount(): Int {
return dataModelList.size
}
ไปที่ เมธอด onCreateViewHolder() ให้ลบข้อมูล แล้วใส่ Code ต่อไปนี้ เพื่อทำการสร้าง ViewHolder รายการแต่ละแถวผ่านการแสดงผลด้วย ไฟล์ layout ชื่อ model.xml
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(p0.context).inflate(R.layout.model,p0,false))
}
ในเมธอด onBindViewHolder() ให้เราสร้างการ เรียกใช้ DataModel ไปยัดข้อมูลง ViewHolder ทีละแถว ส่วนของรูปภาพเราจัดการ Cache ด้วย Picasso (3rd Party การจัดการ แคช ของรูปภาพที่มาจาก Web Service เพื่อลดการโหลดกระตุกของแอพฯ สำหรับการจัดการแคชบนรูปภาพ ImageView Cache มีการดาวน์โหลดรูปภาพภายหลัง Activity เริ่มทำงานโดยเก็บ cache ให้ทันที สามารถใช้งานได้ง่ายทั้งแบบ jar ไว้ใน Project หรือ Build ผ่าน Gradle ตรงๆ ข้อมูลเพิ่มเติม:http://square.github.io/picasso/ )
override fun onBindViewHolder(p0: ViewHolder, p1: Int) {
val dataModel = dataModelList[p1]
p0.textTitle.text = dataModel.title
Picasso.with(p0.itemView.context).load(dataModel.photos)
.error(R.mipmap.ic_launcher)
.placeholder(R.mipmap.ic_launcher)
.into(p0.imageView)
}
กลับไปที่ MainActivity.kt เพิ่ม Global var ดังนี้:
private val TAG = "Comic" private lateinit var response_data: MutableList<DataModel> private var dataAdapter: DataAdapter? = null private lateinit var recyclerView: RecyclerView private lateinit var firebaseDatabase: FirebaseDatabase private lateinit var databaseReference: DatabaseReference
ทำการสร้าง LayoutGridManager เป็น 2 ประกาศตัวแปร response_data เป็น mutableList() เป็น ListArray รูปแบบหนึ่ง และองค์ประกอบอื่นๆ ใน onCreate()
recyclerView = findViewById(R.id.recyclerView)
recyclerView!!.layoutManager = LinearLayoutManager(this)
recyclerView!!.setLayoutManager(GridLayoutManager(this, 2))
firebaseDatabase = FirebaseDatabase.getInstance()
databaseReference = firebaseDatabase!!.getReference("bookshelf/data")
response_data = mutableListOf()
dataAdapter = DataAdapter(response_data as ArrayList<DataModel>)
recyclerView!!.setAdapter(dataAdapter)
bindingData()
ทำการ Create method ใหม่ โดยการกด Alt + Enter ที่ bindingData()

จะเห็นว่ามีการเรียกไปยัง refernce Database ส่วนของ Node ที่ชื่อว่า bookshelf/data ให้ตรงกับส่วนที่เราเก็บข้อมูลไว้ใน Firebase RealtimeDatabase เมื่อเสร็จขั้นตอนการ setAdapter ของ recyclersView แล้วเราจะต้องสร้าง Method ใหม่ชื่อว่า bindingData();
private fun bindingData() {
databaseReference!!.addChildEventListener(object : ChildEventListener {
override fun onCancelled(p0: DatabaseError) {
}
override fun onChildMoved(p0: DataSnapshot, p1: String?) {
}
override fun onChildChanged(p0: DataSnapshot, p1: String?) {
}
override fun onChildAdded(p0: DataSnapshot, p1: String?) {
response_data!!.add(p0.getValue(DataModel::class.java)!!)
dataAdapter!!.notifyDataSetChanged()
}
override fun onChildRemoved(p0: DataSnapshot) {
}
})
}
ทดสอบการทำงานโดย run ตัว Android Studio ขึ้นมาถ้าไม่มีข้อผิดพลาดจะเป็นดังนี้:

ต่อมาเราจะทำหน้า เนื้อหาข้างใน ไปคลิกขวาที่ apps ให้ทำการเพิ่ม Empty Activity ใหม่ขึ้นมาว่า InformationActivity โดยเราจะแก้ไข activity_information.xml เป็นดังนี้:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".InformationActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycleComic"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="1dp"
android:layout_marginEnd="1dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:layout_marginStart="1dp"
android:layout_marginTop="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
ทำการประกาศตัวแปรต่อไปนี้ ใน InformationActivity.kt
private val TAG = "Comic" private val response_data: List<DataModel>? = null private val dataAdapter: ComicDataAdapter? = null private val mRecyclerView: RecyclerView? = null private val get_key: String? = null
ระบบจะทำการบังคับให้เราสร้าง Class ใหม่ชื่อ ComicDataAdapter ขึ้นมา(กด Alt+Enter) ทำตาม Step ภาพด้านล่าง:

Class ชื่อ ComicDataAdapter นั้นให้แก้ไขไฟล์ดังนี้:
class ComicDataAdapter (val dataModelList: List<DataModel>) : RecyclerView.Adapter<DataViewHolder>(){
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): DataViewHolder {
return DataViewHolder(LayoutInflater.from(p0.context).inflate(R.layout.pages,p0,false))
}
override fun onBindViewHolder(p0: DataViewHolder, p1: Int) {
val dataModel = dataModelList[p1]
Picasso.with(p0.itemView.context).load(dataModel.photo)
.error(R.mipmap.ic_launcher)
.placeholder(R.mipmap.ic_launcher)
.into(p0.imageView)
}
override fun getItemCount(): Int {
return dataModelList.size
}
}
class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var imageView: ImageView
init {
imageView = itemView.findViewById(R.id.imageView)
}
}
มีการทำงานคล้ายกับ DataAdapter แค่ส่วนของ ViewHolder นั้นเราจะแยกใช้อีกตัวคนละชื่อ ดังนั้นใน ComicDataAdapter จะเรียก inner Class ส่วนนี้ว่า DataViewHolder
สร้าง Layout ใหม่ชื่อ pages.xml เป็นหน้าอ่านหนังสือ:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
tools:srcCompat="@tools:sample/avatars[12]"
android:id="@+id/imageView"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>

เปิดไฟล์ DataAdapter.kt ให้เพิ่ม Global Var เข้าไปคือ:
private val TAG = "Comic" val mylist = ArrayList<String>()
และทำการแก้ไขส่วนของ onBindViewHolder() ใน DataAdapter.kt ดังนี้:
var databaseReference = FirebaseDatabase.getInstance().reference
databaseReference.child("bookshelf/data").addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
Log.d(TAG, "Count= " + dataSnapshot.childrenCount)
for (childDataSnapshot in dataSnapshot.children) {
Log.d(TAG, "snapshot= " + childDataSnapshot.key!!)
mylist.add(childDataSnapshot.key!!)
}
}
override fun onCancelled(databaseError: DatabaseError) {
}
})
เป็นการดึง DataSnapshot ของ Firebase ทั้งหมดมาวน Foreach เพื่อเก็บค่า Key ของข้อมูลเราสำหรับนำไปอ้างเพื่อส่ง Intent โดยตัวอย่างของผมคือการเก็บลง ArrayList ชื่อ mylist
เพิ่มคำสั่ง setOnClickListener เข้าไปให้แตะที่ ViewHolder.itemView.thumbnail แล้วส่ง Intent ค่า Key ไปหน้า InformationActivity:
p0.imageView.setOnClickListener(View.OnClickListener { v ->
val filePath = dataModel.files
Log.d(TAG, "filePath=$filePath")
val readActivity = Intent(v.context, InformationActivity::class.java)
readActivity.putExtra("filePath", filePath)
readActivity.putExtra("keys", mylist[p1])
v.context.startActivity(readActivity)
})
ดังนั้นไฟล์ DataAdapter.kt จะเป็นดังนี้:
package com.daydev.iapplication
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.squareup.picasso.Picasso
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.FirebaseDatabase
import android.content.Intent
class DataAdapter(val dataModelList: List<DataModel>) : RecyclerView.Adapter<ViewHolder>() {
private val TAG = "Comic"
val mylist = ArrayList<String>()
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(p0.context).inflate(R.layout.model,p0,false))
}
override fun onBindViewHolder(p0: ViewHolder, p1: Int) {
val dataModel = dataModelList[p1]
p0.textTitle.text = dataModel.title
var databaseReference = FirebaseDatabase.getInstance().reference
databaseReference.child("bookshelf/data").addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
Log.d(TAG, "Count= " + dataSnapshot.childrenCount)
for (childDataSnapshot in dataSnapshot.children) {
Log.d(TAG, "snapshot= " + childDataSnapshot.key!!)
mylist.add(childDataSnapshot.key!!)
}
}
override fun onCancelled(databaseError: DatabaseError) {
}
})
Picasso.with(p0.itemView.context).load(dataModel.photos)
.error(R.mipmap.ic_launcher)
.placeholder(R.mipmap.ic_launcher)
.into(p0.imageView)
p0.imageView.setOnClickListener(View.OnClickListener { v ->
val filePath = dataModel.files
Log.d(TAG, "filePath=$filePath")
val readActivity = Intent(v.context, InformationActivity::class.java)
readActivity.putExtra("filePath", filePath)
readActivity.putExtra("keys", mylist[p1])
v.context.startActivity(readActivity)
})
}
override fun getItemCount(): Int {
return dataModelList.size
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var textTitle: TextView = itemView.findViewById(R.id.title)
var imageView: ImageView
init {
imageView = itemView.findViewById(R.id.thumbnail)
}
}
กลับไปยัง InformationActivity.kt ให้เพิ่มคำสั่งต่อไปนี้ใน onCreate()
val intent = intent
get_key = intent.getStringExtra("keys")
firebaseDatabase = FirebaseDatabase.getInstance()
databaseReference = firebaseDatabase!!.getReference("bookshelf/data/$get_key/pages")
Log.d(TAG, "bookshelf/data/$get_key/pages")
response_data = ArrayList()
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
mRecyclerView = findViewById(R.id.recycleComic) as RecyclerView
mRecyclerView!!.layoutManager = layoutManager
dataAdapter = ComicDataAdapter(response_data)
mRecyclerView!!.adapter = dataAdapter
comicBindingData()
ทำการสร้าง Method ใหม่ของ comicBindingData()

แก้ไขฟังก์ชัน comicBindingData() ดังนี้: (เหมือน bindingData() ใน MainActivity)
private fun comicBindingData() {
databaseReference!!.addChildEventListener(object : ChildEventListener {
override fun onCancelled(p0: DatabaseError) {
}
override fun onChildMoved(p0: DataSnapshot, p1: String?) {
}
override fun onChildChanged(p0: DataSnapshot, p1: String?) {
}
override fun onChildAdded(p0: DataSnapshot, p1: String?) {
response_data!!.add(p0.getValue(DataModel::class.java)!!)
dataAdapter!!.notifyDataSetChanged()
}
override fun onChildRemoved(p0: DataSnapshot) {
}
})
}
ทดสอบแอพพลิเคชันของเรา โดยการแตะเปลี่ยนหน้า:

กลายเป็นว่า Kotlin Android นั้นสามารถทำงานร่วมกับ Firebase ได้สบายๆ แค่นี้เราก็ได้แอพพลิเคชัน Ebook ที่มีระบบหลังบ้านเป็น Firebase ได้ง่ายแล้วครับ
ไม่ต้องถามหา Source Code นะครับ พยายามทำตาม เพื่อความเข้าใจดีกว่า 🙂




