Android Kotlin JSON Parsing using Retrofit + RecyclerView (List && Grid)


บทความนี้เราจะมาเล่นกับ JSON บน Android กันครับ
จุดประสงค์หลักเราจะทำ Http request ไปที่ api และนำข้อมูลกลับมาใช้งานกันครับ
เริ่มจาก format ง่ายๆก่อน





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


Dependency

//RETROFIT ...  NETWORK LIBRARY
implementation 'com.squareup.retrofit2:retrofit:2.4.0'

// RETROFIT .. CONVERTER & ADAPTER
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'

// RX-ANDROID
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

// RECYCLERVIEW
implementation 'com.android.support:recyclerview-v7:25.3.1'



INetworkAPI.kt

import io.reactivex.Observable
import retrofit2.http.GET

interface INetworkAPI {

    @GET("posts/")
    fun getAllPosts(): Observable<List<Post>>
}

// Observable เป็นความสามารถของ RxAndroid 
    ถ้าใครใช้ Retrofit แบบไม่มี RxAndroid ตามที่เห็นส่วนมากจะใช้ Call
// List<Post> จะเป็นข้อมูลที่จะถูก return ไปที่ MainActivity.kt 
   เพื่อส่งเข้าไปใช้ใน Adapter ของ RecyclerView ตรง >>> PostItemAdapter(it, this)


Post.kt

import com.google.gson.annotations.SerializedName

data class Post(
      @SerializedName("userId") val userId: Int,
      @SerializedName("id") val id: Int,
      @SerializedName("title") val title: String,
      @SerializedName("body") val body: String
)

// SerializedName พวกนี้จะหมายถึง key ของข้อมูลที่ส่งกลับมาในรูปแบบ JSON
   เราแค่ตั้งมันให้ตรงกับ https://jsonplaceholder.typicode.com/posts/ ทำให้ง่ายมาก
   ต่อการ ดึงมันออกมาใช้งานใน onBindViewHolder ของ Recycler View 
    หรือจะ setให้ View ตรงๆเลยใน MainActivity กรณีที่ไม่ได้ใช้ RecyclerView


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        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=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_list_posts"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            tools:listitem="@layout/item_list"
            app:spanCount="2"
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" />

            // ถ้าจะใช้เป็น ListView ให้ใช้
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"

</LinearLayout>



item_list.xml

<?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="match_parent"
              android:layout_margin="16dp"
              android:orientation="vertical">

    <TextView
            android:id="@+id/txtPostTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black"
            android:textSize="16sp" />

    <TextView
            android:id="@+id/txtPostBody"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/colorPrimaryDark"
            android:textSize="22sp"
            android:textStyle="bold" />

</LinearLayout>



PostItemAdapter.kt



class PostItemAdapter(val postList: List<Post>, val context: Context) :
    RecyclerView.Adapter<PostItemAdapter.ViewHolder>() {

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

    override fun getItemCount(): Int {
        return 10
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {

        holder.itemView.txtPostTitle.text = postList[position].title
        holder.itemView.txtPostBody.text = postList[position].body

    }
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
}



MainActivity.kt

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

        // กรณีจะใช้เป็น GridView 2 column
        rv_list_posts.layoutManager = GridLayoutManager(this,2)

        // กรณีใช้เป็น ListView
        rv_list_posts.layoutManager = LinearLayoutManager(this)

        val retrofit = Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .baseUrl("https://jsonplaceholder.typicode.com/").build()

   
        val postsApi = retrofit.create(INetworkAPI::class.java)

        บรรทัดต่อจากนี้  ลองหลับตานึกถึงหน้า 1 หน้า
        ที่มี ListView แนวนอนหรือแนวตั้งอยุ่ในหน้าเดียวกันดูครับ บลาๆ
        val response = postsApi.getAllPosts()


       // observeOn ระบุว่าให้ส่งผลลัพธ์ไปบน foreground (mainThread) ด้วย
       "AndroidSchedulers.mainThread()" คือความสามารถของ RxAndroid 
        ส่วน subscribeOn ใช้ระบุว่า ให้รอการ load ใน background และส่งผลลัพธ์กลับมาในตัวแปร it 
        คือ Object ของข้อมูลที่จะถูกส่งกลับมาตามที่ระบุไว้ใน Interface ของ Retrofit 
        (ดูที่ INetworkAPI.kt )

        response.observeOn(AndroidSchedulers.mainThread()).subscribeOn(IoScheduler()).subscribe {
            rv_list_posts.adapter = PostItemAdapter(it, this)
        }
    }


ท้ายสุด
จบ Tutorial นี้แนะนำให้ไปดูกันต่อในเรื่อง getAllUsers , getUserWithID ,  postData กันต่อเลยครับ
โดยเฉพาะ getUserWithID เราจะได้เจอกับ JSON ที่มี child ซ้อนกันถึง 2 ชั้น
แบบนี้


จาก https://jsonplaceholder.typicode.com/users/1
แต่ยังไงถ้าทำตาม Tutorial นี้จบ ก็ไปต่อกันไม่ยากแล้วครับ ...  Goog Luck :D


เพิ่มเติมการใช้ notify แบบต่างๆ
เช่น เมื่อผู้ใช้เลื่อนถึง item ล่างสุดของ Recycler view จะทำการ Request ไปที่ API
เพื่อขอ Data มาเพิ่ม และสั่ง notify ให้เกิดการเปลี่ยนแปลง

คีย์เวิร์ด
notifyItemChanged(int pos)   // แจ้งเตือนรายการที่ตำแหน่งมีการเปลี่ยนแปลง 
notifyItemInserted(int pos)    // แจ้งว่ารายการที่สะท้อนอยู่ในตำแหน่งถูกแทรกใหม่ 
notifyItemRemoved(int pos)  //แจ้งเตือนว่ารายการที่เคยอยู่ที่ตำแหน่งก่อนหน้านี้ถูกลบออกจากชุดข้อมูล
notifyDataSetChanged()       // แจ้งว่าชุดข้อมูลนั้นมีการเปลี่ยนแปลง ใช้เป็นทางเลือกสุดท้ายเท่านั้น 


Ref :
http://developine.com/kotlin-android-json-parsing-tutorial-retrofit/
https://stackoverflow.com/questions/35679776/how-to-set-recyclerview-applayoutmanager-from-xml
http://www.akexorcist.com/2016/08/introduction-to-the-rxjava-and-rxandroid-part-2.html
https://blog.nextzy.me/%E0%B9%80%E0%B8%A3%E0%B8%B5%E0%B8%A2%E0%B8%81-rx-%E0%B8%A7%E0%B9%88%E0%B8%B2%E0%B8%A3%E0%B8%AD-rxandroid-54dda8de108