Android Developer

เขียนแอพ Android ดึง JSON มาเล่นกับ RecyclerView และ Picasso

การพัฒนาแอพพลิเคชัน Android ที่ถูกพัฒนาให้ง่ายขึ้นด้วย Layout ตัวใหม่อย่าง RecyclerView ให้สามารถดึง JSON Webservices ได้ง่ายพร้อม Image Load ผ่าน Picasso

เราเคยประสบปัญหาแรกๆ สมัยเริ่มพัฒนาแอพพลิเคชัน Android มาใหม่ๆ พอถึงการดึง Web Services อย่า JSON เช่นบทความ

เขียนแอพ Android เรียก JSON Web service แสดงบน ListView

เราจะเจอปัญหาแรกคือการกระตุกเมื่อมีการโหลดรูปภาพเข้าไปผ่าน ListView ซึ่ง View เราต้องไปเขียนฟังก์ชันให้มัน Clear View เพิ่มเมื่อพ้น Layout ไป

หลังจากนั้นเราก็ใช้ Third Party อย่าง Volley มาช่วยโหลด Cache Image ให้เลื่อน ListView เร็วขึ้น ปัญหาคือตัวอย่างที่ปรากฏในบทความนี้:

เขียนแอพ Android ดึงข้อมูล JSON เก็บ Cache Image ด้วย Valley

มีคนบ่นไป “Code เยอะไปนะ”

จนกระทั่งตอนนี้มี Layout ตัวใหม่คือ RecyclerView และ Third Party ที่เป็น Dependencies สำหรับโหลดรูปภาพที่ใช้งานง่ายมากๆ อย่าง Picasso การทำ JSON parse หรือดึงข้อมูล JSON เข้าแอพพลิเคชันของเราก็ง่ายขึ้น Code สั้นลงอีกต่างหาก

RecyclerView

Layout อย่างเจ้า RecyclerView นี่มีความพิเศษอย่างหนึ่ง คือมันถูกออกแบบมาเพื่อจัดการกับข้อมูลจำนวนมากที่ต้องดึงมาแสดงทีละหลายๆ ข้อมูลใน JSON เพื่อเป็นการลด Memory แทนที่จะใช้ ScrollView และ ListView ที่เปลือง Memory มากเหมือนเมื่อก่อน, ส่วนตัวคิดว่า RecyclerView นั้นคือ ListView รูปแบบใหม่ ที่เพิ่มประสิทธิภาพขึ้นมานั่นคือ:

  • มันสร้าง ListView Layout ได้ทั้ง Vertical แนวดิ่ง และ Horizontal แนวระนาบ ได้อย่างสะดวก
  • ใช้การควบคุมรูปแบบผ่าน LayoutManager
  • ลด Memory ด้วยการ Reuse โดยตรงผ่าน ViewHolder
  • จัดการข้อมูลผ่าน Adapter เหมือน ListView เดิม

Picasso

เป็น Third Party Library สำหรับการจัดการแคชบนรูปภาพ ImageView Cache มีการดาวน์โหลดรูปภาพภายหลัง Activity เริ่มทำงานโดยเก็บ cache ให้ทันที สามารถใช้งานได้ง่ายทั้งแบบ jar ไว้ใน Project หรือ Build ผ่าน Gradle ตรงๆ (ข้อมูลเพิ่มเติม: Picasso)

ลองมาพัฒนาแอพพลิเคชันเล่นๆ กันหน่อยดีกว่า:

ตัวอย่างนี้ผมจะใช้ Pattern จาก Plugin JSON API ของ WordPress เข้ากับเว็บไซต์ http://www.lovedesigner.net เว็บรีวิวหนังของผม หากติดตั้งเสร็จมันจะสามารถดู JSON Webservice ได้แบบนี้ครับ:

http://www.lovedesigner.net/api/get_posts?include=id,title,thumbnail

หรือดึงเลือกบาง fields ก็ต่อ param ไป, อีกทั้งเลือกเฉพาะหมวด category ได้ด้วยดังนี้

http://www.lovedesigner.net/api/get_category_posts/?slug=action&include=id,title,thumbnail

เวลาแสดงผลก็จะเป็นแบบนี้:

ลง Plugin ให้เรียบร้อย
ลง Plugin ให้เรียบร้อย

ตัว Arrays Key ที่เราจะทำงานหลักๆ คือ Key ที่ชื่อ posts ที่ประกอบไปด้วย post ย่อย หลายๆ ตัวเราจะดึงแค่ title และ thumbnail มาทำงานกับแอพพลิเคชันของเรา

ทำการสร้าง แอพฯ Android ของเราเป็นแบบ Empty Activity ผ่าน Android Studio ให้เรียบร้อย หลังจากนั้นเปิดไฟล์ build.gradle (Module)

ทำการเพิ่ม dependencies เข้าไปใหม่โดยเราจะใช้งาน RecyclerView ร่วมกับ CardView พร้อมทั้งดึง Library ของ Picasso มาใช้

compile 'com.android.support:cardview-v7:26.0.+'
compile 'com.android.support:recyclerview-v7:26.0.+'
compile 'com.squareup.picasso:picasso:2.5.2'

กด Sync Gradle ให้เรียบร้อยหลังจากนั้นเราจะมาเริ่มออกแบบ Layout และ Color กัน เปิดไฟล์ colors.xml ขึ้นมาแก้ไขชุดสีให้สวยหน่อยตามนี้:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#ef6136</color>
    <color name="colorPrimaryDark">#141415</color>
    <color name="colorAccent">#27d0e2</color>
    <color name="MyWhite">#ffffff</color>
</resources>

หลังจากนั้นไปหาภาพ Placeholder สำหรับแสดงผลรอระหว่างโหลดภาพลง  ImageView ซึ่งเราต้องเอาไปวางที่ Folder ชื่อ drawable ตามนี้

ตัวอย่าง Placeholder
ตัวอย่าง Placeholder

ทำการสร้าง Class ใหม่ขึ้นมาชื่อว่า JSONData.java

package com.daydev.recyclerview;

/**
 * Created by daydev on 9/3/17.
 */

public class JSONData {
    private String title;
    private String thumbnail;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getThumbnail() {
        return thumbnail;
    }

    public void setThumbnail(String thumbnail) {
        this.thumbnail = thumbnail;
    }
}

ทำการสร้าง Model สำหรับ Bind Keys ที่เราต้องใช้ในตัวอย่างเราต้องการแค่ title กับ thumbnail ออกแบบ Layout ของเราใหม่เปิด activity_main.xml ขึ้นมา:

สร้าง RecyclerView มาเก็บ Container ข้อมูล โดยมี ProgessBar มาแสดงการโหลดขณะที่เราดึงข้อมูลในตัวอย่างมีการแก้ไข สี ของ Constrain Layout เป็นสี Background เทาดำ

<?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"
    android:background="@color/cardview_dark_background"
    tools:context="com.daydev.recyclerview.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_centerInParent="true"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginTop="8dp"
        android:background="@color/cardview_dark_background"
        app:layout_constraintHorizontal_bias="0.487"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


</android.support.constraint.ConstraintLayout>

ทำการสร้าง Layout เพิ่มมาอีกตัวชื่อว่า list_row.xml จัดแต่งด้วย CardView เพื่อความสวยงาม โดยมี TextView คือ title และ ImageView คือ thumbnail

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:cardview="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="5dp"
    android:background="@android:color/black"
    cardview:cardCornerRadius="2dp"
    cardview:cardElevation="3dp"
    cardview:cardUseCompatPadding="true">

    <LinearLayout
        android:layout_width="365dp"
        android:layout_height="match_parent"
        android:background="@color/cardview_dark_background"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/thumbnail"
            android:layout_width="90dp"
            android:layout_height="90dp"
            android:layout_weight="0.07"
            android:scaleType="centerCrop"
            cardview:srcCompat="@mipmap/ic_launcher" />

        <TextView
            android:id="@+id/title"
            android:layout_width="158dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="30dp"
            android:layout_weight="1"
            android:text="TextView"
            android:textColor="@color/MyWhite"
            android:textSize="16sp" />

    </LinearLayout>

</android.support.v7.widget.CardView>

กลับมาเขียน Code สร้าง Class Java ใหม่ขึ้นมาเป็น Adapter เก็บข้อมูล JSON ชื่อว่า “MyRecyclerViewAdapter.java”

package com.daydev.recyclerview;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.text.TextUtils;
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 java.util.List;

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.CustomViewHolder> {
    private List<JSONData> feedItemList;
    private Context mContext;

    public MyRecyclerViewAdapter(Context context, List<JSONData> feedItemList) {
        this.feedItemList = feedItemList;
        this.mContext = context;
    }

    @Override
    public CustomViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.list_row, null);
        CustomViewHolder viewHolder = new CustomViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(CustomViewHolder customViewHolder, int i) {
        final JSONData jsonData = feedItemList.get(i);

        //Render image using Picasso library
        if (!TextUtils.isEmpty(jsonData.getThumbnail())) {
            Picasso.with(mContext).load(jsonData.getThumbnail())
                    .error(R.drawable.placeholder)
                    .placeholder(R.drawable.placeholder)
                    .into(customViewHolder.imageView);
        }

        //Setting text view title
        customViewHolder.textView.setText(Html.fromHtml(jsonData.getTitle()));
    }

    @Override
    public int getItemCount() {
        return (null != feedItemList ? feedItemList.size() : 0);
    }

    class CustomViewHolder extends RecyclerView.ViewHolder {
        protected ImageView imageView;
        protected TextView textView;

        public CustomViewHolder(View view) {
            super(view);
            this.imageView = (ImageView) view.findViewById(R.id.thumbnail);
            this.textView = (TextView) view.findViewById(R.id.title);
        }
    }
}

จัดการ Bind ข้อมูลผ่าน Layout ของ CardView ด้วย ViewHolder ส่วนนี้

View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.list_row, null);
CustomViewHolder viewHolder = new CustomViewHolder(view);
return viewHolder;

และ Bind ViewHolder ส่วนนี้

public void onBindViewHolder(CustomViewHolder customViewHolder, int i) {
        final JSONData jsonData = feedItemList.get(i);

        //Render image using Picasso library
        if (!TextUtils.isEmpty(jsonData.getThumbnail())) {
            Picasso.with(mContext).load(jsonData.getThumbnail())
                    .error(R.drawable.placeholder)
                    .placeholder(R.drawable.placeholder)
                    .into(customViewHolder.imageView);
        }

        //Setting text view title
        customViewHolder.textView.setText(Html.fromHtml(jsonData.getTitle()));
    }

เพื่อให้เกิดการรอข้อมูลรูปภาพเราจะไปโหลดไฟล์รูปภาพ placeholder มาเก็บไว้ ด้วย Library ของ Picasso เก็บค่าจำนวนของชุดข้อมูลผ่าน Method นี้:

@Override
    public int getItemCount() {
        return (null != feedItemList ? feedItemList.size() : 0);
    }

แล้วไล่ Render ข้อมูลลง Widget ใน Method นี้:

class CustomViewHolder extends RecyclerView.ViewHolder {
        protected ImageView imageView;
        protected TextView textView;

        public CustomViewHolder(View view) {
            super(view);
            this.imageView = (ImageView) view.findViewById(R.id.thumbnail);
            this.textView = (TextView) view.findViewById(R.id.title);
        }
    }

ขั้นตอนต่อไปคือการแสดงผลผ่าน หน้าแรกของแอพพลิเคชันของเรา MainActivity.java

private static final String TAG = "RecyclerViewJSON";
private List<JSONData> feedsList;
private RecyclerView mRecyclerView;
private MyRecyclerViewAdapter adapter;
private ProgressBar progressBar;

ประกาศตัวแปรข้างต้นใต้ Class เป็น Global หลังจากนั้น ทำการ Map ตัว Adapter เข้ากับ RecyclerView:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        progressBar = (ProgressBar) findViewById(R.id.progress_bar);

        //String url = "http://stacktips.com/?json=get_category_posts&slug=news&count=30";
        String url = "http://www.lovedesigner.net/api/get_posts/?count=20";
        new GetDataBinding().execute(url);
    }

ระบบจะถามเราว่าสร้าง Inner Class ของ GetDataBinding() ไหมให้สร้างขึ้นมาในตัวอย่างทำงานดังนี้ครับ:

private class GetDataBinding extends AsyncTask<String, Void, Integer> {
        @Override
        protected void onPreExecute() {
            progressBar.setVisibility(View.VISIBLE);
        }
        @Override
        protected Integer doInBackground(String... strings) {
            Integer result = 0;
            HttpURLConnection urlConnection;
            try {
                URL url = new URL(strings[0]);
                urlConnection = (HttpURLConnection) url.openConnection();
                int statusCode = urlConnection.getResponseCode();

                // 200 represents HTTP OK
                if (statusCode == 200) {
                    BufferedReader r = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = r.readLine()) != null) {
                        response.append(line);
                    }
                    parseResult(response.toString());

                    result = 1; // Successful
                } else {
                    result = 0; //"Failed to fetch data!";
                }
            } catch (Exception e) {
                Log.d(TAG, e.getLocalizedMessage());
            }
            return result; //"Failed to fetch data!";
        }

        @Override
        protected void onPostExecute(Integer result) {
            progressBar.setVisibility(View.GONE);

            if (result == 1) {
                adapter = new MyRecyclerViewAdapter(MainActivity.this, feedsList);
                mRecyclerView.setAdapter(adapter);


                adapter.setOnItemClickListener(new OnItemClickListener() {
                    @Override
                    public void onItemClick(JSONData item) {
                        Toast.makeText(MainActivity.this, item.getTitle(), Toast.LENGTH_LONG).show();

                    }
                });

            } else {
                Toast.makeText(MainActivity.this, "Failed to fetch JSON data!", Toast.LENGTH_SHORT).show();
            }
        }
    }

ระบบจะทำงาน Load Image เป็น BackGround Process ควบคู่ระหว่างที่แอพพลิเคชันกำลังทำงาน ผ่าน String ของเว็บไซต์ เข้าฟังก์ชันของ BufferedReader เรียกทำงานผ่าน parseResult(response.toString()); ซึ่งเราจะเขียนไว้แล้วว่า result =1 คือผ่าน, 0 คือโหลด JSON ไม่ได้ สร้าง Method ขึ้นมา

private void parseResult(String s) {
        try {
            JSONObject response = new JSONObject(s);
            JSONArray posts = response.optJSONArray("posts");
            feedsList = new ArrayList<>();
            for (int i = 0; i < posts.length(); i++) {
                JSONObject post = posts.optJSONObject(i);
                JSONData item = new JSONData();
                item.setTitle(post.optString("title"));
                item.setThumbnail(post.optString("thumbnail"));
                feedsList.add(item);
             }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

ขั้นตอนของ parseResult() คือฟังก์ชันปกติของพวก JSON Parser ลงตัวแปร ArrayList โดย Map ตัว Key ของ JSON คือ title,thumbnail (ส่วนนี้ใครจะเขียนแบบไหนก็แล้วแต่ Format JSON ของแต่ละคน)

เพิ่ม Interface Class สำหรับแตะ Touch ของ RecyclerView เพื่อให้แตะได้

package com.daydev.recyclerview;
/**
 * Created by daydev on 9/3/17.
 */
public interface OnItemClickListener {
    void onItemClick(JSONData item);
}

เปิดไฟล์ MyRecyclerViewAdapter.java เขียนเพิ่มใต้บรรทัด:

customViewHolder.textView.setText(Html.fromHtml(jsonData.getTitle()));

เพิ่มบรรทัดดังนี้:

View.OnClickListener listener = new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       onItemClickListener.onItemClick(jsonData);
   }
};
customViewHolder.imageView.setOnClickListener(listener);
customViewHolder.textView.setOnClickListener(listener);

เพิ่ม Method ใหม่เข้าไปดังนี้:

public OnItemClickListener getOnItemClickListener() {
   return onItemClickListener;
}

public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
   this.onItemClickListener = onItemClickListener;
}

อย่าลืมประกาศตัวแปรใต้ Class ของ MyRecyclerViewAdapter.java

private OnItemClickListener onItemClickListener;

เปิด AndroidManifest.xml เพิ่ม Permission ของ INTERNET เข้ามา

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

รันโปรแกรมของเราให้เรียบร้อย

ทดสอบโดยการแตะที่ RecyclerView และปรับ ได้ตามใจชอบในไฟล์ list_row.xml

ระบบของ RecyclerView จะเลื่อนและโหลด Cache ผ่าน Picasso ได้อย่าง Smooth ไม่มีกระตุกครับ ดาวน์โหลด Project ตัวอย่างได้ที่นี่นะครับ สำหรับสายขี้เกียจ

https://github.com/banyapondpu/RecycleView

แล้ว……..

ถ้าอยากเปลี่ยน RecyclerView ของเราจาก Vertical แบบ ListView เป็น Horizontal แบบ Banner เลื่อนซ้ายขวาล่ะ ให้ Backup Project เก่านะครับ แล้วแก้ไข Layout ของ list_row.xml เป็นแบบนี้ (หรือจะแต่งเองก็ได้นะ)

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:cardview="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="5dp"
    android:background="@android:color/black"
    cardview:cardCornerRadius="2dp"
    cardview:cardElevation="3dp"
    cardview:cardUseCompatPadding="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@android:color/black"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/thumbnail"
            android:layout_width="match_parent"
            android:layout_height="90dp"
            android:layout_weight="1"
            android:scaleType="centerCrop"
            cardview:srcCompat="@mipmap/ic_launcher" />

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="30dp"
            android:layout_marginTop="10dp"
            android:layout_weight="0.59"
            android:text="TextView"
            android:textColor="@color/MyWhite"
            android:textSize="12sp" />

    </LinearLayout>
</android.support.v7.widget.CardView>

ตามที่บอกไว้ข้างต้นว่า RecyclerView นั้นควบคุมการแสดงผลผ่าน LayoutManager ดังนั้นเปิด MainActivity.java ขึ้นมาแก้ไขส่วนของการแสดงผลจาก

mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

แก้ไขเป็น:

LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(layoutManager);

ทดสอบดูจะด้ RecyclerView เลื่อนแนวนอนสวยงาม

ทดลองเล่นกันนะครับง่ายดี 🙂

Asst. Prof. Banyapon Poolsawas

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

Related Articles

Back to top button

Adblock Detected

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