Android Developer

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

การเขียนแอพพลิเคชันบนแพลตฟอร์ม Android ด้วย Android Studio ในการดึงข้อมูล JSON Webservice และโหลดรูปภาพ Image จาก Serve ผ่าน Cache เพิ่มความเร็ว ด้วย Volley

แรกเริ่มเดิมทีเราเลยเขียนแอพพลิเคชันในการดึง ข้อมูลจาก JSON Web Service มาบ้างแล้วกัยบทเรียนเหล่านี้

หรือ ทบทวนทั้งหมดก่อนที่นี่

https://www.daydev.com/category/developer/android-developer

จะเห็นว่าตัวอย่างที่เคยทำมาจะมีปัญหาในเรื่องของการแสดงผลรูปภาพของ ListView นั่นคือ อาการกระตุก ไม่ Smooth เมื่อมีการ Scrolling หน้าจอของ ListView หลักๆ เป็นเพราะเราต้องเสียเวลาในการโหลด Image จาก URL ที่อยู่ใน JSON ทุกทั้งที่มีการแสดงผล ซึ่งถ้าจะให้เร็ว และ Smooth นั้นมีทางเดียวคือต้องจัดการเก็บ Cache ของมันสำหรับโหลดภาพ หรือ Image ทีหลัง ซึ่งทางนักพัฒนาก็มี ส่วนเสริมมากมายหลากหลายให้เราเลือกใช้ ตัวเลือกหนึ่งที่คิดว่าไม่น่าจะลำบากมากน่าจะเป็น Volley ครับ

เริ่มต้นพัฒนากันดีกว่า เปิด Android Studio ขึ้นมาครับ (ตัวอย่างใช้ SDK 23.0.1)

Screen Shot 2558-09-26 at 10.44.52 AM

เลือกรูปแบบเป็น Blank Activity ปรกติครับ เสร็จแล้วดาวน์โหลด ไฟล์ volley.jar มาใช้กับ Project ของเรา

https://drive.google.com/file/d/0B08PZSOd4UmOTFBLbW1sQUlYVHc/view?usp=sharing

วิธีการวางไฟล์ .jar ไว้ใช้กับ Project ของเราให้ทำดังนี้ครับ

Screen Shot 2558-10-01 at 10.53.56 PM

คลิกที่ “Android” ตำแหน่งดังภาพข้างบนเลือกเป็น “Project”  แล้วลากไฟล์ volley.jar ที่ดาวน์โหลดมาไปวางในโฟลเดอร์ “app/libs” ของ Project ของเราให้เรียบร้อย แล้ว คลิกเปลี่ยน “Project” เป็น “Android” เช่นเดิมครับ

Screen Shot 2558-10-01 at 10.54.03 PM

 

Screen Shot 2558-10-01 at 10.55.50 PM

เมื่อเสร็จแล้วเปิด Gradle (build) ออกมาทำการเขียนคำสั่ง Compile Gradle เข้าไปครับ

dependencies {
   compile fileTree(dir: 'libs', include: ['*.jar'])
   compile 'com.android.support:appcompat-v7:23.0.1'
   compile files('libs/volley.jar')
}

นั่นคือการเพิ่ม

 compile files('libs/volley.jar')

ไปแค่บรรทัดเดียว ทำการ Sync ให้เรียบร้อย และ Clean Project ครับต่อมาให้เราไปที่ Folder “res/values” สร้างไฟล์ colors.xml ขึ้นมา เพื่อกำหนด value ของชุดสีที่ต้องการจะใช้

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <color name="author">#32a546</color>
   <color name="cate">#32a546</color>
   <color name="list_divider">#d9d9d9</color>
   <color name="list_row_start_color">#ffffff</color>
   <color name="list_row_end_color">#ffffff</color>
   <color name="list_row_hover_start_color">#ebeef0</color>
   <color name="list_row_hover_end_color">#ebeef0</color>
</resources>

แก้ไขไฟล์ dimens.xml ให้เป็นดังนี้

<resources>
   <!-- Default screen margins, per the Android Design guidelines. -->
   <dimen name="activity_horizontal_margin">16dp</dimen>
   <dimen name="activity_vertical_margin">16dp</dimen>
   <dimen name="title">17dp</dimen>
   <dimen name="author">15dip</dimen>
   <dimen name="genre">13dip</dimen>
   <dimen name="year">12dip</dimen>
</resources>

เมื่อเรียบร้อยแล้วให้เราสร้าง Values ใน โฟลเดอร์ “res/drawables” โดยสร้างไฟล์ขึ้นมา 3 ไฟล์ตามนี้ คือ list_row_bg.xml, list_row_bg_hover.xml และ list_row_selector.xml

Screen Shot 2558-10-03 at 12.19.17 AM

แก้ไขแต่ละไฟล์ครับ เป็นการวางค่า Layout บางอย่างเวลามีการกระทำที่ Row ของ ListView

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
   android:shape="rectangle">
   <gradient
       android:startColor="@color/list_row_start_color"
       android:endColor="@color/list_row_end_color"
       android:angle="270" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
   android:shape="rectangle" >
   <gradient
       android:angle="270"
       android:endColor="@color/list_row_hover_end_color"
       android:startColor="@color/list_row_hover_start_color" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:drawable="@drawable/list_row_bg" android:state_pressed="false" android:state_selected="false"/>
   <item android:drawable="@drawable/list_row_bg_hover" android:state_pressed="true"/>
   <item android:drawable="@drawable/list_row_bg_hover" android:state_pressed="false" android:state_selected="true"/>
</selector>

ต่อมาให้เรานำ ListView ไปใส่ใน Layout ของ MainActivity นั่นคือไฟล์ activity_main.xml

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

   <ListView
       android:id="@+id/list"
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:divider="@color/list_divider"
       android:dividerHeight="1dp"
       android:listSelector="@drawable/list_row_selector" />

</RelativeLayout>

โดย ListView ที่แทรกเข้าไปชื่ “list” นั้นจะมีการแสดงผล แถวแต่ละแถวตามตัวอย่างก่อนหน้านี้ คือมีภาพประกอบ และ ชื่อหัวข้อ เราต้องสร้าง Layout ของแถว ชื่อ “list_row.xml” ขึ้นมา

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
   android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
   android:paddingRight="@dimen/activity_horizontal_margin"
   android:paddingTop="@dimen/activity_vertical_margin"
   android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

   <com.android.volley.toolbox.NetworkImageView
       android:id="@+id/thumbnail"
       android:layout_width="match_parent"
       android:layout_height="172dp"
       android:layout_alignParentTop="true"
       android:layout_centerHorizontal="true"
       android:maxHeight="172dp" />

   <TextView
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:textAppearance="?android:attr/textAppearanceLarge"
       android:text="Large Text"
       android:id="@+id/title"
       android:layout_below="@+id/thumbnail"
       android:layout_alignStart="@+id/thumbnail"
       android:width="300dp" />
<TextView
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:textAppearance="?android:attr/textAppearanceSmall"
       android:text="Small Text"
       android:id="@+id/author"
       android:textColor="@color/author"
       android:layout_below="@+id/title"
       android:layout_alignEnd="@+id/title"
       android:width="300dp" />
   <TextView
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:textAppearance="?android:attr/textAppearanceSmall"
       android:text="Small Text"
       android:id="@+id/categories"
       android:textColor="@color/cate"
       android:layout_below="@+id/author"
       android:layout_alignEnd="@+id/author"
       android:width="300dp" />
</RelativeLayout>

สังเกตจะเห็นว่าส่วนของรูปภาพเราจะไม่ใช้ ImageView เหมือนตัวอย่างก่อนๆ แต่จะเป็น

<com.android.volley.toolbox.NetworkImageView
       android:id="@+id/thumbnail"
       android:layout_width="match_parent"
       android:layout_height="172dp"
       android:layout_alignParentTop="true"
       android:layout_centerHorizontal="true"
       android:maxHeight="172dp" />

ส่วนของ NetworkImageView ที่มากับเข้า Valley นั่นเอง

สร้าง Package สำหรับ Project ของเราเพิ่มขึ้นมาคือ app, model, adapter และ util

Screen Shot 2558-10-03 at 12.09.13 AM

ไปที่ package “util” ทำการสร้าง Class ใหม่ขึ้นมาชื่อว่า LruBitmapCache.java” ซึ่งจะเป็นคำสั่งของ Volley ในการโหลดรูปภาพมาเก็บใน Cache ทีหลังจากการโหลดข้อมูล และเมื่อมาดูใหม่อีกครั้งจะไม่เกิดการโหลดรูปภาพจาก URL อีกเพราะอยู่ใน Cache เป็นที่เรียบร้อยแล้ว

package util;
import com.android.volley.toolbox.ImageLoader.ImageCache;
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;

public class LruBitmapCache extends LruCache<String, Bitmap> implements ImageCache
 {
   public static int getDefaultLruCacheSize() {
       final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
       final int cacheSize = maxMemory / 8;
       return cacheSize;
   }
   public LruBitmapCache() {
       this(getDefaultLruCacheSize());
   }
   public LruBitmapCache(int sizeInKiloBytes) {
       super(sizeInKiloBytes);
   }
   @Override
   protected int sizeOf(String key, Bitmap value) {
       return value.getRowBytes() * value.getHeight() / 1024;
   }
   @Override
   public Bitmap getBitmap(String url) {
       return get(url);
   }
   @Override
   public void putBitmap(String url, Bitmap bitmap) {
       put(url, bitmap);
   }
}

ไปที่ Package “app” สร้าง Class ใหม่ชื่อว่า “AppController.java” เพื่อทำการเรียก util ที่เราสร้างไว้เมื่อมีการโหลดข้อมูล และเป็นการควบคุมการเรียกโหลดข้อมูลว่าสิ่งไหนควรโหลดหลัง หรือโหลดก่อนหน้า

package app;
import android.app.Application;
import android.text.TextUtils;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.Volley;

import util.LruBitmapCache;

public class AppController extends Application {

   public static final String TAG = AppController.class.getSimpleName();
   private RequestQueue mRequestQueue;
   private ImageLoader mImageLoader;
   private static AppController mInstance;

   @Override
   public void onCreate() {
       super.onCreate();
       mInstance = this;
   }
   public static synchronized AppController getInstance() {
       return mInstance;
   }
   public RequestQueue getRequestQueue() {
       if (mRequestQueue == null) {
           mRequestQueue = Volley.newRequestQueue(getApplicationContext());
       }

       return mRequestQueue;
   }
public ImageLoader getImageLoader() {
       getRequestQueue();
       if (mImageLoader == null) {
           mImageLoader = new ImageLoader(this.mRequestQueue,
                   new LruBitmapCache());
       }
       return this.mImageLoader;
   }

   public <T> void addToRequestQueue(Request<T> req, String tag) {
       // set the default tag if tag is empty
       req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
       getRequestQueue().add(req);
   }

   public <T> void addToRequestQueue(Request<T> req) {
       req.setTag(TAG);
       getRequestQueue().add(req);
   }

   public void cancelPendingRequests(Object tag) {
       if (mRequestQueue != null) {
           mRequestQueue.cancelAll(tag);
       }
   }
}

เปิด Permission ของ AndroidManifest.xml ในส่วนของการ INTERNET และใช้ android:name ให้ควบคุมโดย AppController

<uses-sdk
       android:minSdkVersion="11"
       android:targetSdkVersion="18" />
   <uses-permission android:name="android.permission.INTERNET" />

และ

 <application
       ...
       android:name="app.AppController">

ไฟล์ AndroidManifest.xml จะเป็นดังนี้

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.daydev.webserviceadvance" >
   <uses-sdk
       android:minSdkVersion="11"
       android:targetSdkVersion="18" />
   <uses-permission android:name="android.permission.INTERNET" />
   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:theme="@style/AppTheme"
       android:name="app.AppController">
       <activity
           android:name=".MainActivity"
           android:label="@string/app_name" >
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

จัดการส่วนของ model ใน package “model” สร้าง Class “Movie.java” ขึ้นมาเพื่อดึง Key ของ Web Service ที่เราต้องการเรียกซึ่งผมจะใช้ตัวอย่างเดิมคือ

http://www.lovedesigner.net/feed/json

รูปแบบของ JSON เป็นดังนี้

Screen Shot 2558-10-04 at 1.34.46 PM

ดังนั้น Movie.java ต้องใช้คำสั่ง Mapping Key ตามนี้

package model;

import java.util.ArrayList;

public class Movie {
   private String title, thumbnailUrl;
   private String date;
   private String author;
   private ArrayList<String> categories;

   public Movie() {
   }
   public Movie(String name, String thumbnailUrl, 
               String date, String author,
                ArrayList<String> categories) {
       this.title = name;
       this.thumbnailUrl = thumbnailUrl;
       this.date = date;
       this.author = author;
       this.categories = categories;
   }
   public String getTitle() {
       return title;
   }
   public void setTitle(String name) {
       this.title = name;
   }
   public String getThumbnailUrl() {
       return thumbnailUrl;
   }
public void setThumbnailUrl(String thumbnailUrl) {
       this.thumbnailUrl = thumbnailUrl;
   }

   public String getYear() {
       return date;
   }

   public void setDate(String date) {
       this.date = date;
   }

   public String getAuthor() {
       return author;
   }

   public void setAuthor(String author) {
       this.author = author;
   }

   public ArrayList<String> getGenre() {
       return categories;
   }

   public void setCategories(ArrayList<String> categories) {
       this.categories = categories;
   }
}

ไปที่ package “adapter” สร้าง Class ตัวสุดท้ายขึ้นมาคือ Adapter ไว้จัดการข้อมูล “CustomListAdapter.java” ทำงานร่วมกับ Movie.java 

package adapter;

import java.util.List;
import app.AppController;
import model.Movie;

import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.NetworkImageView;
import com.daydev.webserviceadvance.R;
public class CustomListAdapter extends BaseAdapter {
   private Activity activity;
   private LayoutInflater inflater;
   private List<Movie> movieItems;
   ImageLoader imageLoader = AppController.getInstance().getImageLoader();
   public CustomListAdapter(Activity activity, List<Movie> movieItems) {
       this.activity = activity;
       this.movieItems = movieItems;
   }
   @Override
   public int getCount() {return movieItems.size();}
   @Override
   public Object getItem(int location) {return movieItems.get(location);}
   @Override
   public long getItemId(int position) {return position;}
@Override
   public View getView(int position, View convertView, ViewGroup parent) {
       if (inflater == null)
           inflater = (LayoutInflater) activity
                   .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
       if (convertView == null)
           convertView = inflater.inflate(R.layout.list_row, null);
       if (imageLoader == null)
           imageLoader = AppController.getInstance().getImageLoader();
       NetworkImageView thumbNail = (NetworkImageView) convertView
               .findViewById(R.id.thumbnail);
       TextView title = (TextView) convertView.findViewById(R.id.title);
       TextView author = (TextView) convertView.findViewById(R.id.author);
       TextView genre = (TextView) convertView.findViewById(R.id.categories);
       // getting movie data for the row
       Movie m = movieItems.get(position);
       // thumbnail image
       thumbNail.setImageUrl(m.getThumbnailUrl(), imageLoader);
       // title
       title.setText(m.getTitle());
       // rating
       author.setText("Rating: " + String.valueOf(m.getAuthor()));
       // genre
       String genreStr = "";
       for (String str : m.getGenre()) {
           genreStr += str + ", ";
       }
       genreStr = genreStr.length() > 0 ? genreStr.substring(0,
               genreStr.length() - 2) : genreStr;
       genre.setText(genreStr);
       return convertView;
   }
}

ต่อมาคือการแสดงผล ListView ที่เราตั้ง id ว่า “list” ให้แสดงผลของ CustomeList และเก็บ Cache Image ครับแก้ไขไฟล์ Class “MainActivity.java” ดังนี้

package com.daydev.webserviceadvance;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

import adapter.CustomListAdapter;
import app.AppController;
import model.Movie;

import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.app.ProgressDialog;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;

import android.widget.ListView;

import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.VolleyLog;
import com.android.volley.toolbox.JsonArrayRequest;

import android.util.Log;

public class MainActivity extends Activity {

   // Log tag
   private static final String TAG = MainActivity.class.getSimpleName();

   // Movies json url
   private static final String url = "http://lovedesigner.net/feed/json";
   private ProgressDialog pDialog;
   private List<Movie> movieList = new ArrayList<Movie>();
   private ListView listView;
   private CustomListAdapter adapter;

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

       listView = (ListView) findViewById(R.id.list);
       adapter = new CustomListAdapter(this, movieList);
       listView.setAdapter(adapter);

       pDialog = new ProgressDialog(this);
       // Showing progress dialog before making http request
       pDialog.setMessage("กำลังโหลดข้อมูล...");
       pDialog.show();
JsonArrayRequest movieReq = new JsonArrayRequest(url,
           new Response.Listener<JSONArray>() {
               @Override
               public void onResponse(JSONArray response) {
                   Log.d(TAG, response.toString()); hidePDialog();
                   for (int i = 0; i < response.length(); i++) {                    // Parsing json
                       try {
                           JSONObject obj = response.getJSONObject(i);
                           Movie movie = new Movie();
                           movie.setTitle(obj.getString("title"));
                           movie.setThumbnailUrl(obj.getString("thumbnail"));
                           movie.setAuthor(obj.getString("author"));
                           movie.setDate(obj.getString("date"));
                           // Genre is json array
                           JSONArray cateArry = obj.getJSONArray("categories");
                           ArrayList<String> getCate = new ArrayList<String>();
                           for (int j = 0; j < cateArry.length(); j++) {
                               getCate.add((String) cateArry.get(j));
                           }
                           movie.setCategories(getCate);
                           // adding movie to movies array
                           movieList.add(movie);

                       } catch (JSONException e) {e.printStackTrace(); }
                   }
                   adapter.notifyDataSetChanged();
               }
           }, new Response.ErrorListener() {
       @Override
       public void onErrorResponse(VolleyError error) {
           VolleyLog.d(TAG, "Error: " + error.getMessage());
           hidePDialog();
       }
   });
   AppController.getInstance().addToRequestQueue(movieReq);
}
@Override
   public void onDestroy() {
       super.onDestroy();
       hidePDialog();
   }

   private void hidePDialog() {
       if (pDialog != null) {
           pDialog.dismiss();
           pDialog = null;
       }
   }

   @Override
   public boolean onCreateOptionsMenu(Menu menu) {
       // Inflate the menu; this adds items to the action bar if it is present.
       getMenuInflater().inflate(R.menu.menu_main, menu);
       return true;
   }

   @Override
   public boolean onOptionsItemSelected(MenuItem item) {
       // Handle action bar item clicks here. The action bar will
       // automatically handle clicks on the Home/Up button, so long
       // as you specify a parent activity in AndroidManifest.xml.
       int id = item.getItemId();

       //noinspection SimplifiableIfStatement
       if (id == R.id.action_settings) {
           return true;
       }

       return super.onOptionsItemSelected(item);
   }
}

ทดสอบการโหลดข้อมูลผ่าน Emulator หรือเครื่องสมาร์ทโฟนของเราดู จะเห็นว่า การ Scrolling ส่วนของ ListView จะเร็ว และ Smooth ไม่เกิดอาการกระตุก

Screen Shot 2558-10-04 at 1.41.00 PM

แหล่งข้อมูลเพิ่มเติม stackoverflow.comhttps://github.com/mcxiaoke/android-volley

Asst. Prof. Banyapon Poolsawas

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

Related Articles

Back to top button

Adblock Detected

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