ตัวอย่างการทำ ListView อ่านข้อมูล JSON ด้วย GSon

Published on
Android
2014/05/android-custom-listview-with-gson-tutorial
Discord

บทความนี้จะมาพูดถึง การเขียนแอพพลิเคชันแอนดรอยส์เพื่อดึงข้อมูล JSON โดยมาแสดงเป็น ListView ในส่วน ListView ก็จะใช้เป็น Custom ListView ครับ บทความนี้จะครอบคลุมเนื้อหาเรื่องใหญ่ๆ 3 เรื่องเลย ได้แก่

  • การอ่านข้อมูล JSON
  • การทำ Custom ListView
  • การใช้ AsyncTask

ตัวอย่างในบทความนี้ ผมจะใช้ JSON API ของทาง Teamtreehouse ครับ จากลิงค์นี้ http://blog.teamtreehouse.com/api/get_recent_summary/ ซึ่งเป็น API ที่ไว้แสดงบทความในบล็อก หน้าตาของ JSON จะมีประมาณนี้

{
status: "ok",
count: 10,
count_total: 1732,
pages: 174,
    posts: [
        {
            id: 23479,
            url: "http://blog.teamtreehouse.com/voodoo-responsive-tables-javascript-graphs-treehouse-show-episode-90",
            title: "Voodoo, Responsive Tables, JavaScript Graphs | The Treehouse Show Episode 90",
            date: "2014-05-13 18:02:54",
            author: "Jason Seifer",
            thumbnail: null
        },
        {
            id: 23432,
            url: "http://blog.teamtreehouse.com/working-home-3-tips-staying-task",
            title: "Working from Home? 3 Tips to Staying on Task",
            date: "2014-05-13 08:00:05",
            author: "Theresa Cramer",
            thumbnail: "http://blog.teamtreehouse.com/wp-content/uploads/2014/05/Computer-Desk-David-Martyn-Hunt-Flick-150x150.jpg"
        },
            ....
    ]
}

จากรายชื่อใน JSON ก็จะมี JSONObject ชื่อ posts นั้นมีรายชื่อบทความแต่ละบทความอยู่ ภายในแต่ละบทความ ก็จะมี id, url ของบทความ, ชื่อบทความ, วันที่โพส, ชื่อผู้โพส และ Path ของรูป thumbail เราจะใช้ข้อมูลพวกนี้แหละครับ มาแสดงในแอพของเรา

ในส่วน JSON รายละเอียดเชิงลึก จะไม่ขอพูดถึงนะครับ ใครอยากรู้อะไรเพิ่มเติม Google ครับ

หัวข้อในบทความ

Step 1: เริ่มต้นสร้างโปรเจ็ค

ทำการสร้างโปรเจ็คเลยครับ ขอข้ามในส่วนการสร้างโปรเจ็คไปเลยละกัน ใครทำไม่เป็น อ่านที่นี่ครับ การสร้างโปรเจ็คด้วย Android Studio ถ้าเป็น Eclipse ลองหา Google ครับ มีคนเขียนไว้เยอะมาก

เริ่มแรก ทำการสร้าง Layout สำหรับหน้าแรก ตั้งชื่อว่า activity_main.xml เพื่อแสดงรายชื่อบทความ ก็ไม่มีอะไรมาก ใช้แค่ ListView ธรรมดาได้เลย

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/listView" />

</RelativeLayout>

ต่อมาในส่วนคลาส MainActivity.java ผมทำการประกาศ static String ชื่อว่า URL ดังนี้

public static final String URL =
        "http://blog.teamtreehouse.com/api/get_recent_summary/";

ต่อมาก็ใช้ AsyncTask เพื่อทำการอ่านข้อมูล JSON จากเว็บไซต์ สำหรับบทความ AsyncTask อ่านเพิ่มเติมได้ที่นี่ครับ

private class SimpleTask extends AsyncTask<String, Void, String> {

    @Override
    protected void onPreExecute() {
        // Create Show ProgressBar
    }

    protected String doInBackground(String... urls)   {
        String result = "";
        try {

            HttpGet httpGet = new HttpGet(urls[0]);
            HttpClient client = new DefaultHttpClient();

            HttpResponse response = client.execute(httpGet);

            int statusCode = response.getStatusLine().getStatusCode();

            if (statusCode == 200) {
                InputStream inputStream = response.getEntity().getContent();
                BufferedReader reader = new BufferedReader
                        (new InputStreamReader(inputStream));
                String line;
                while ((line = reader.readLine()) != null) {
                    result += line;
                }
            }

        } catch (ClientProtocolException e) {

        } catch (IOException e) {

        }
        return result;
    }

    protected void onPostExecute(String jsonString)  {
        // Dismiss ProgressBar
        showData(jsonString);
    }
}

ด้านบน ผมทำการสร้างคลาส SimpleTask โดยทำการ extends AsyncTask เพื่อทำการอ่านข้อมูลจาก URL ที่กำหนด ในส่วน onPostExecute() จะเห็นว่าเรียกเมธอด showData(String jsonString) ซึ่งยังไม่ได้สร้างเมธอดนี้นะครับ

ทำการสั่ง execute ที่เมธอด onCreate()

new SimpleTask().execute(URL);

จากนั้นเมธอด showData() ก็ประกาศไว้แล้วใช้ Toast แสดงดูว่าอ่านค่าได้หรือไม่

private void showData(String jsonString) {
    Toast.makeText(this, jsonString, Toast.LENGTH_LONG).show();
}

สุดท้ายเพิ่ม Permission Internet ที่ไฟล์ AndroidManifest.xml ด้วยครับ

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

ไฟล์ MainActivity.java ตอนนี้ก็จะได้ประมาณนี้

package com.devahoy.sample.ahoygson;

import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.widget.Toast;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class MainActivity extends ActionBarActivity {

    public static final String URL =
            "http://blog.teamtreehouse.com/api/get_recent_summary/";

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

        new SimpleTask().execute(URL);
    }

    private class SimpleTask extends AsyncTask<String, Void, String> {

        @Override
        protected void onPreExecute() {
            // Create Show ProgressBar
        }

        protected String doInBackground(String... urls)   {
            String result = "";
            try {

                HttpGet httpGet = new HttpGet(urls[0]);
                HttpClient client = new DefaultHttpClient();

                HttpResponse response = client.execute(httpGet);

                int statusCode = response.getStatusLine().getStatusCode();

                if (statusCode == 200) {
                    InputStream inputStream = response.getEntity().getContent();
                    BufferedReader reader = new BufferedReader
                            (new InputStreamReader(inputStream));
                    String line;
                    while ((line = reader.readLine()) != null) {
                        result += line;
                    }
                }

            } catch (ClientProtocolException e) {

            } catch (IOException e) {

            }
            return result;
        }

        protected void onPostExecute(String jsonString)  {
            // Dismiss ProgressBar
            showData(jsonString);
        }
    }

    private void showData(String jsonString) {
        Toast.makeText(this, jsonString, Toast.LENGTH_LONG).show();
    }

Toast JSON

หาก Toast แสดงข้อมูล JSON ได้ แสดงว่าคุณได้ไปต่อครับ

หากใครไม่ขึ้นแสดงว่าคุณทำผิดแล้วละครับ ลองอ่านและทำดูใหม่ครับ :D

Step 2: เพิ่ม Library Gson

จะเห็นว่าเราสามารถแสดงข้อมูลที่ได้จากเว็บได้แล้ว ที่เป็น JSON ทีนี้ เราจะอ่านมันยังไงละ เผอิญว่า Java นั้นมี Library สำหรับ Json โดยเฉพาะครับ เด่นๆ ก็จะมีพวก Gson, Jackson สำหรับบทความนี้ผมจะใช้ Gson นะครับ ใครไม่รู้จักก็อ่านเพิ่มเติมที่ เว็บไซต์ของ Gson เลยครับ

ทำการ ดาวน์โหลด Library ได้ที่นี่ ปัจจุบัน เวอร์ชั่น gson-2.2.4 นะครับ จากนั้นก็ทำการ Add Library / Add Build Path ครับ

สำหรับ Android Studio หรือ Eclipse ที่ใช้ Gradle ก็ไม่จำเป็นต้องโหลดก็ได้ครับ ตั้งค่า Gradle แล้วให้มัน Auto Load ให้เองครับ เปิด build.gradle เพิ่มนี้ลงไป และกด Sync Now

apply plugin: 'android'

android {
    ...
}

dependencies {
    ...
    compile 'com.google.code.gson:gson:2.2.4'
}

Note: compile 'com.google.code.gson:gson:2.2.4' คือ GroupId:ArtifactId:version

ทดลองดูว่าเพิ่ม Library ได้ถูกต้องหรือไม่ ลองทำการ import class Gson ดูที่ MainActivity

import com.google.gson.Gson;

ถ้า import มีปัญหา แสดงว่าเพิ่ม Library ผิด ลองกลับไปทำดูใหม่ครับ สำหรับ Eclipse อย่าลืม Add to Buld Path ด้วยนะครับ ส่วนใครที่ import ได้ไม่มีปัญหา แสดงว่าเพิ่ม Library ถูกแล้ว ไปขั้นตอนต่อไปได้เลยครับ

Step 3: เพิ่มคลาส Model

ขั้นตอนนี้ ผมจะทำการสร้างคลาสขึ้้นมาใหม่ครับ เนื่องจากการใช้ Gson นั้นจำเป็นจำเป็นจะต้อง Mapping คลาสเรา กับ JSON ครับ ในตอนแรก คิดว่าอาจจะงงๆเล็กน้อย หรือยังมองภาพรวมไม่ออก ก็พยายามลองทำก่อนครับ :D

เมื่อเปิดหน้า API จะเห็นว่า JSON ของทาง Teamtreehouse มีโครงสร้าง ประมาณนี้นะครับ

{
	status: "ok",
	count: 10,
	count_total: 1900,
	pages: 190,
	posts: [
		{
			id: 24453,
			url: "http://blog.teamtreehouse.com/havent-started-programming-yet",
			title: "The Real Reason Why Everyone Should Learn to Code",
			date: "2014-11-20 08:30:48",
			author: "Dave McFarland",
			thumbnail: "http://blog.teamtreehouse.com/wp-content/uploads/2014/11/Screen-Shot-2014-11-18-at-11.15.00-AM-150x150.png"
		},
		{
			id: 24462,
			url: "http://blog.teamtreehouse.com/css-shapes",
			title: "CSS Shapes",
			date: "2014-11-19 12:13:20",
			author: "Jason Seifer",
			thumbnail: "http://blog.teamtreehouse.com/wp-content/uploads/2014/11/Getting-Started-with-CSS-Shapes-Wrapping-content-around-custom-paths-HTML5-Rocks-2014-11-19-12-11-51-150x150.jpg"
		},
		...
	]
}

โดยในส่วนแต่ละโพส ก็จะมีรายละเอียดต่างๆเช่น id, title อื่นๆ ที่ได้พูดไปก่อนหน้านี้แล้ว ฉะนั้น ก็เลยสร้างคลาส Model เพื่อทำการ Mapping กับ JSON ครับ ทำการสร้างคลาสใหม่ขึ้นมา ตั้งชื่อคลาสว่า Post.java

public class Post {
    int id;
    String url;
    String title;
    String date;
    String author;
    String thumbnail;

// Getter and Setter
}

จะเห็นว่า ที่ JSON มีค่าอะไรบ้าง ตรงส่วนคลาส Post ก็จะตั้งชื่อเหมือนกันเลย ในส่วน Getter, Setter ก็ Generate เอาเลยครับ

ต่อมาผมสร้างคลาสขึ้นมาใหม่อีกคลาสชื่อว่า Blog.java สำหรับเอาไว้ Mapping ทั้งบล็อกเลย คนละส่วนกับ Post.java นะครับ ซึ่ง Post.java นั้น Mapping เฉพาะโพสๆหนึ่งเท่านั้น แต่ Blog.java จะ map ทั้ง JSON เลย ฉะนั้น โค๊ดก็จะได้ประมาณนี้

public class Blog {
    String status;

    int count;

    @SerializedName("count_total")
    int totalCount;

    int pages;

    List<Post> posts;

    // Getter and Setter
}

จะเห็นได้ว่า ชื่อตัวแปรในคลาส Blog ก็ใช้ชื่อเดียวกับ field ใน JSON ส่วนตัวแปรไหนที่ชื่อไม่ตรงกัน เพราะว่าปกติ Java ไม่นิยมตั้งชื่อด้วย (_) underscore ก็จะใช้ @SerializeName("") แทนครับ

ทีนี้เมื่อสร้างคลาส Model 2 คลาสเสร็จแล้ว ทีนี้มาลองใช้งาน Gson กันเลยครับ กลับไปที่ MainActivity.java ตรงเมธอด showData() ที่่ตอนนี้แสดง Toast อย่างเดียวเลย ก็ใส่โค๊ดนี้ลงไปครับ

private void showData(String jsonString) {
    Gson gson = new Gson();
    Blog blog = gson.fromJson(jsonString, Blog.class);


    StringBuilder builder = new StringBuilder();
    builder.setLength(0);

    List<Post> posts = blog.getPosts();

    for (Post post : posts) {
        builder.append(post.getTitle());
        builder.append("\n\n");
    }

    Toast.makeText(this, builder.toString(), Toast.LENGTH_LONG).show();
}

ด้านบน ทำการสร้าง new Instance Gson ขึ้นม จากนั้นก็ใช้ gson.fromJson() โดย argument ตัวแรกคือ JSON และตัวสองเป็น คลาสที่เราต้องการจะ Map ครับ ในที่นี้ก็ใช้คลาส Blog ทีนี้เวลาจะเข้าถึงข้อมูลก็เพียงแค่เรียก getter ที่เราประกาศไว้ เช่น getTitle(), getAuthor() ครับ

ใน Blog ก็จะมีแต่ละ Post อยู่ ก็ทำการวนลูป แล้วแสดง Post ทั้งหมด โดยดึงเฉพาะ Title ด้วย Toast ดูครับ

Toast Title

ใครขึ้นแบบด้านบนแสดงว่า ทำถูกแล้ว

จริงๆเนื้อหาในส่วน Gson จริงๆมันมีแค่นี้เองครับ เมื่อเรียกข้อมูลได้แล้ว ที่เหลือก็เอาไปประยุกต์ได้หลากหลายครับ ในส่วนต่อไปของบทความนี้จะเป็นเรื่อง Custom View แล้วครับ หากใคร ทำ Custom View เป็นแล้ว ก็ข้ามส่วนนี้ไปได้เลยครับ

Step 4: วิธีการทำ Custom ListView

ทำไมผมถึงเอาเนื้อหา Custom ListView มาอยู่ตรงส่วนนี้?

ก็เพราะว่า โดยปกติแล้วผมมักจะเห็นการใช้งาน Custom ListView กับการดึงข้อมูลต่างๆมาจากเว็บไซต์ไม่ว่า FEED แบบ XML หรือว่า JSON ก็เลยเอามาประยุกต์รวมกับ Gson เลยเพื่อให้เห็นภาพ

ทีนี้ทำไมเราถึงต้องทำ Custom ListView ?

เนื่องจาก ListView ธรรมดา หรือแบบ ArrayAdapter นั้นมีข้อจำกัดอยู่ครับ คือจะแสดงได้เฉพาะ TextView แบบ แถวเดียว หรือสองแถวได้ แต่ถ้าเราอยากจะใส่ ImageView หรือ Button ด้วยก็จำเป็นต้องทำ Custom ListView ครับ

การทำ Custom ListView นั้นจำเป็นต้องมี 2 ส่วนครับ คือ หน้า Layout ไฟล์ ซึ่งเป็น xml และคลาส java 1 คลาส

เริ่มแรก ทำการสร้างเลเอาท์ที่ต้องการ โดยผมทำต้องการให้เป็นประมาณนี้

Layout Mockup

ทำการตั้งชื่อไฟล์ว่า post.xml เซฟไว้ที่ /res/layout

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">


    <ImageView
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginRight="8dp"
        android:id="@+id/post_thumbnail"
        android:src="@drawable/ic_launcher"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Title"
        android:textSize="16sp"
        android:textStyle="bold"
        android:layout_marginTop="8dp"
        android:id="@+id/post_title"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/post_thumbnail"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Author Name"
        android:id="@+id/post_author"
        android:layout_marginTop="8dp"
        android:layout_below="@+id/post_title"
        android:layout_alignLeft="@+id/post_title"
        android:layout_alignStart="@+id/post_title"/>
    <TextView android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Date"
              android:id="@+id/post_date"
              android:layout_marginRight="8dp"
              android:layout_alignTop="@+id/post_author"
              android:layout_alignParentRight="true"
              android:layout_alignParentEnd="true"/>
</RelativeLayout>

ต่อมาก็สร้างคลาสขึ้นมาหนึ่งคลาส ผมตั้งชื่อคลาสใหม่ว่า CustomAdapter.java แล้วทำการ extends BaseAdapter และ override เมธอด ทั้งหมดแบบนี้

package com.devahoy.sample.ahoygson;

import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class CustomAdapter extends BaseAdapter {
    @Override
    public int getCount() {
        return 0;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return null;
    }
}

ปกติเวลาเราทำ ListView แบบธรรมดา เราต้องมีข้อมูลที่เป็น List, Array ด้วยใช่มั้ยครับ Custom ListView ก็ต้องการเหมือนกัน ทีนี้เราจะส่งข้อมูลมาให้คลาส CustomAdapter นี้ได้ยังไง?

ก็ต้องสร้าง Constructor ให้มันครับ ผมสร้าง constructor แบบนี้

public class CustomAdapter extends BaseAdapter {
    private LayoutInflater mInflater;
    List<Post> mPosts;

    public CustomAdapter(Activity activity, List<Post> posts) {
        mInflater = (LayoutInflater) activity.getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
        mPosts = posts;
    }
    ...
}

โดย constructor รับ parameter 2 ค่าครับ คือ Activity และ List<Post> ทีนี้ส่วนเมธอด ที่ได้ทำการ override มาก็แก้ไขเป็นด้งนี้

@Override
public int getCount() {
    return mPosts.size();
}

@Override
public Object getItem(int position) {
    return mPosts.get(position);
}

@Override
public long getItemId(int position) {
    return position;
}

ส่วนในเมธอด getView() ส่วนนี้คือส่วนที่จะแสดงผล คล้ายๆ setContentView() ใน onCreate() ของ Activity ครับ ในส่วนนี้ก็จะทำการเชื่อม View ในเลเอาท์ครับ ก่อนเชื่อม ผมได้ใช้ ViewHolder Pattern เข้ามาช่วย เพื่อเพิ่ม Perfomance ของ ListView ครับ หากใครอยากรู้รายละเอียด ก็ลองไปศึกษาเรื่อง Android ViewHolder Pattern ดูเพิ่มเติมครับ

ทำการสร้าง inner class ข้างใน CustomAdapter.java ชื่อ ViewHolder ขึ้นมา ภายในก็มี View เหมือนกับที่สร้างไว้ใน Layout

private static class ViewHolder {
    ImageView thumbnail;
    TextView title;
    TextView author;
    TextView date;
}

จากนั้นเมธอด getView() ก็จะได้ประมาณนี้

@Override
public View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        convertView = mInflater.inflate(R.layout.post, parent, false);
        mViewHolder = new ViewHolder();
        mViewHolder.thumbnail = (ImageView) convertView.findViewById(R.id.post_thumbnail);
        mViewHolder.author = (TextView) convertView.findViewById(R.id.post_author);
        mViewHolder.title = (TextView) convertView.findViewById(R.id.post_title);
        mViewHolder.date = (TextView) convertView.findViewById(R.id.post_date);

        convertView.setTag(mViewHolder);

    } else {
        mViewHolder = (ViewHolder) convertView.getTag();
    }

    return convertView;
}

เช็คว่า convertView เป็น null หรือไม่ ถ้าเป็น ก็ทำการสร้าง View ขึ้นมาใหม่ จาก Layout ที่ประกาศ ถ้าไม่ใช่ ก็ใช้ View อันเดิม เป็นการ Reuse นำกลับมาใช้ใหม่ครับ

สุดท้าย ประกาศตัวแปร global ไว้ในคลาส CustomAdapter

private ViewHolder mViewHolder;

ทีนี้คลาส CustomAdapter ละไว้เท่านี้ก่อน กลับไปที่ MainActivity.java อีกครั้ง

Step 5: Implement CustomAdapter

มาถึงขั้นตอนสุดท้าย คือการนำ CustomAdapter ที่สร้างไว้มาใช้งานนั่นเอง ก็กลับมาที่คลาส MainActivity.java แล้ว ตรงส่วนเมธอด showData() ที่ตอนนี้ทำได้คือ แสดง Toast เป็นรายชื่อบทความ ต่อไป เราจะเปลี่ยนให้แสดง ListView เริ่มแรก ก็ต้องสร้างตัวแปร 2 ตัวคือ ListView และ CustomAdapter ประกาศเป็น global ไว้ภายในคลาส

public class MainActivity extends ActionBarActivity {

    public static final String URL =
            "http://blog.teamtreehouse.com/api/get_recent_summary/";

    private ListView mListView;
    private CustomAdapter mAdapter;
    ...

}

จากนั้นเชื่อม ListView กับ Layout ซะ ที่เมธอด onCreate()

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

    mListView = (ListView) findViewById(R.id.listView);

    new SimpleTask().execute(URL);
}

ต่อมาที่เมธอด showData() ก็ทำการ new CustomAdapter มาใหม่ พร้อมกับส่ง argument เป็น Activity และ List<Post> ไป และให้ setAdapter ให้กับ ListView สุดท้ายเมธอด showData() จะเหลือแค่นี้ (ลบ Toast ทิ้งแล้ว)

private void showData(String jsonString) {
    Gson gson = new Gson();
    Blog blog = gson.fromJson(jsonString, Blog.class);
    List<Post> posts = blog.getPosts();

    mAdapter = new CustomAdapter(this, posts);
    mListView.setAdapter(mAdapter);
}

ทดสอบรันโปรแกรม

CustomerAdapter

จะได้ดังนี้ แต่ว่าทำไมไม่มีข้อมูลเลย มีแต่ View ที่เป็น Default จากไฟล์ xml?

ก็เพราะว่าเรายังไม่ได้เซตค่าให้มันที่ CustomAdapter เลย ตรงที่ผมละไว้ก่อนหน้านี้ ฉะนั้นกลับไปที่ CustomAdapter ที่เมธอด getView() เพิ่มนี้ลงไป

Post post = mPosts.get(position);

mViewHolder.author.setText(post.getAuthor());
mViewHolder.title.setText(post.title);
mViewHolder.date.setText(post.date);

ปกติเราส่งข้อมูลมาแบบ List แต่ว่าที่เมธอด getView จะมี position อยู่ซึ่งทำให้เรารู้ว่า List ที่ตำแหน่งนี้จะเป้นค่าอะไร เราก็ get จาก ตำแหน่ง Position เก็บค่าไว้ที่คลาส Post ส่วน View ต่างๆ ก็ setText จากค่าใน post ได้เลย

ส่วน ImageView ผมใช้วิธีนี้ เพิ่ม Bitmap และเปลียน Post เป็นตัวแปร global ภายในคลาส

public class CustomAdapter extends BaseAdapter {
    private LayoutInflater mInflater;
    private List<Post> mPosts;
    private ViewHolder mViewHolder;

    private Bitmap mBitmap;
    private Post mPost
    ....
    ....

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ...

        if (mPost.getThumbnail() != null) {
                new AsyncTask<Void, Void, Void>() {
                    @Override
                    protected Void doInBackground(Void... params) {
                        try {
                            URL url = new URL(mPost.getThumbnail());
                            mBitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());

                        } catch (MalformedURLException e) {

                        } catch (IOException e) {

                        }
                        return null;
                    }
                }.execute();


            mViewHolder.thumbnail.setImageBitmap(mBitmap);
            ...
            ...
    }
}

จริงๆ วิธีการแสดงข้อมูล ImageView จาก URL วิธีนี้ ไม่ค่อยดีเท่าไหร่นะครับ เพราะไม่มี Perfomance อะไรเลย ไม่มีการเก็บ cache ตัวอย่างผมแค่แสดง ImageView จาก URL ได้เท่านั้น การใช้งานจริงๆแนะนำ Library สำหรับจัดการเรื่อง ImageView ดีกว่าครับ เช่น Universal Image Loader, Picasso, ion หรือ Volley

สุดท้าย getView() ได้แบบนี้

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.post, parent, false);
            mViewHolder = new ViewHolder();
            mViewHolder.thumbnail = (ImageView) convertView.findViewById(R.id.post_thumbnail);
            mViewHolder.author = (TextView) convertView.findViewById(R.id.post_author);
            mViewHolder.title = (TextView) convertView.findViewById(R.id.post_title);
            mViewHolder.date = (TextView) convertView.findViewById(R.id.post_date);

            convertView.setTag(mViewHolder);

        } else {
            mViewHolder = (ViewHolder) convertView.getTag();
        }


        mPost = mPosts.get(position);

        if (mPost.getThumbnail() != null) {

            new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
                    try {
                        URL url = new URL(mPost.getThumbnail());
                        mBitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());

                    } catch (MalformedURLException e) {

                    } catch (IOException e) {

                    }
                    return null;
                }
            }.execute();


            mViewHolder.thumbnail.setImageBitmap(mBitmap);
        }

        mViewHolder.author.setText(mPost.getAuthor());
        mViewHolder.title.setText(mPost.title);
        mViewHolder.date.setText(mPost.date);

        return convertView;
    }

ทดสอบรัน ผลลัพธ์ก็จะได้แบบนี้

Customer Adapter

จะเห็นว่า มีปัญหาที่ ImageView ฉะนั้นใช้ Library มาช่วยจะดีมากเลยครับ ปกติผมใช้ Universal Image Loader กับ ion ครับ แนะนำเลยครับ :D

ส่วนโค๊ดทั้งหมด ก็ดูได้จาก gist นี้ครับ

ที่ไม่อัพเป็น .git เนื่องจากว่า โปรเจ็คมันไม่ใหญ่มาก มีไม่กี่ไฟล์ ค่อยๆทำ แล้วดูไปทีละไฟล์ น่าจะช่วยให้เข้าใจง่ายกว่าการ import แล้วไปเปิดเทสเลย

Buy Me A Coffee
Authors
Discord