Android Kotlin

เขียนแอพฯ Android ด้วย Kotlin ทำ E Book ร่วมกับ Firebase

บทเรียนสำหรับการพัฒนาแอพพลิเคชันแพลตฟอร์ม Android ด้วยภาษา Kotlin ทำ E Book โดยใช้ RecyclerView ร่วมกับ Firebase

ตัวอย่างนี้จะเหมือนตัวอย่างของ Java ส่วนของบทความ:

ดังนั้นเราจะเห็นว่าเราจะต้องสร้างตัวอย่างของ 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 นะครับ พยายามทำตาม เพื่อความเข้าใจดีกว่า 🙂

Asst. Prof. Banyapon Poolsawas

อาจารย์ประจำสาขาวิชาการออกแบบเชิงโต้ตอบ และการพัฒนาเกม วิทยาลัยครีเอทีฟดีไซน์ & เอ็นเตอร์เทนเมนต์เทคโนโลยี มหาวิทยาลัยธุรกิจบัณฑิตย์ ผู้ก่อตั้ง บริษัท Daydev Co., Ltd, (เดย์เดฟ จำกัด)

Related Articles

Back to top button

Adblock Detected

เราตรวจพบว่าคุณใช้ Adblock บนบราวเซอร์ของคุณ,กรุณาปิดระบบ Adblock ก่อนเข้าอ่าน Content ของเรานะครับ, ถือว่าช่วยเหลือกัน