12/08/2018, 11:57

Gọi Web API không đồng bộ & Cài đặt Callback trong Android

Nguồn http://www.justinmccandless.com/blog/Now+in+Android%3A+Asynchronous+Web+API+Calls http://www.justinmccandless.com/blog/Setting+Up+a+Callback+Function+in+Android Lý do dịch 2 bài này Một ngày chủ nhật đẹp trời, bỗng dưng trong đầu mình muốn hiểu về Callback trong Android hoạt động ...

Nguồn

  • http://www.justinmccandless.com/blog/Now+in+Android%3A+Asynchronous+Web+API+Calls
  • http://www.justinmccandless.com/blog/Setting+Up+a+Callback+Function+in+Android

Lý do dịch 2 bài này

  • Một ngày chủ nhật đẹp trời, bỗng dưng trong đầu mình muốn hiểu về Callback trong Android hoạt động như thế nào, và đã tìm thấy 2 bài trên. Vừa dịch muốn vừa ngẫm nghĩ từng câu tác giả viết.
  • Sức ép nghìn cân đến từ hệ thống monthly report (huhu)

Gọi không đồng bộ Web API

Class được viết dưới đây sử dụng AsyncTask của Android để thực hiện gọi HTTP trên một thread riêng biệt từ thread GUI chính. Thực tế thì trên các phiên bản mới hơn của Android, sẽ xuất hiện exception android.os.networkonmainthreadexception nếu bạn thực thực việc gọi trên thread chính (cũng hợp lý thôi nếu không GUI của bạn sẽ treo trong suốt quá trình của cuộc gọi thông qua internet!).

public class ApiCall extends AsyncTask {
	private String result;
	protected Long doInBackground(URL... urls) {
		int count = urls.length;
        long totalSize = 0;
        StringBuilder resultBuilder = new StringBuilder();
        for (int i = 0; i < count; i++) {
        	try {
            	// Đọc toàn bộ text trả về từ Server
                InputStreamReader reader = new InputStreamReader(urls[i].openStream());
                BufferedReader in = new BufferedReader(reader);
                String resultPiece;
                while ((resultPiece = in.readLine()) != null) {
                	resultBuilder.append(resultPiece);
                }
                in.close();
            } catch (MalformedURLException e) {
            	e.printStackTrace();
            } catch (IOException e) {
            	e.printStackTrace();
            }
            // Nếu cancel() được gọi, rời khỏi vòng loop sớm
            if (isCancelled()) {
            	break;
            }
        }
        // Lưu kết quả
        this.result = resultBuilder.toString();
        return totalSize;
    }
    protected void onProgressUpdate(Integer... progress) {
        // cập nhật progress tại đây
    }
	// Được gọi sau khi doInBackground kết thúc
    protected void onPostExecute(Long result) {
    	Log.v("result, yay!", this.result);
    }
}

Bạn có thể sử dụng class trên để thực hiện mới việc gọi API như sau:

URL url = null;
try {
    url = new URL("http://search.twitter.com/search.json?q=@justinjmcc");
} catch (MalformedURLException e) {
    e.printStackTrace();
}
new ApiCall().execute(url);

Việc gọi trên sẽ trả về những tweet gần đây nhất của tôi dưới dạng JSON. Trước khi bạn implement, hãy chú ý một vài điều sau:

  • Đầu tiên hãy nhớ cho quyền để app của bạn có thể truy cập internet, nếu không sẽ bắt gặp lỗi như sau: socket failed :EACCES (Permission denied). Hãy mở manifest file AndroidManifest.xml và thêm dòng sau:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
  • Tiếp theo, tôi gặp phải một vấn đề nhỏ khi cố gắng kết nối tới local server bằng phương pháp trên. URl vẫn có thể parse khi đưa vào localhost tuy nhiên sẽ dẫn tới lỗi ECONNREFUSED. Có một phương pháp đơn giản là sử dụng địa chỉ local IP của máy ban, hoặc sử dụng Android để lấy địa chỉ đó cho bạn sẽ giúp code đẹp hơn. InetAddress.getByName có thể thực hiện được việc đó, mặc dù khi bạn đang truy cập local server đồng nghĩa chắc đó chưa phải phiên bản production của app.
  • Vấn đề cuối chính là tôi đã sử dụng String và StringBuilder có vẻ hơi thừa cho kết quả của việc gọi. Nếu bạn cố gắng điền String resultPiece trực tiếp vào điều kiện của while loop, nhưng không làm gì trong phần thân của while loop, sau đó while loop sẽ không thực hiện và phần string sẽ không được điền tiếp.
  • Còn một vấn đề nữa với class này đó chính là thiếu một cách đơn giản để implement callback. Trừ phi bạn luôn muốn làm điều y hệt sau khi việc gọi trả về trong onPostExecute, nếu không bạn chắc sẽ muốn định nghĩa một callback phương thức động.

Cài đặt callback trong Android

Callback là gì

Ý tưởng của callback đó chính là thông báo đồng bộ hoặc không đồng bộ với một class nếu công việc trong một class khác đã được hoàn thành. Ví dụ:

class A implements ICallback {
     MyObject o;
     B b = new B(this, someParameter);

     @Override
     public void callback(MyObject o){
           this.o = o;
     }
}

class B {
     ICallback ic;
     B(ICallback ic, someParameter){
         this.ic = ic;
     }

    new Thread(new Runnable(){
         public void run(){
             // some calculation
             ic.callback(myObject)
         }
    }).start();
}

interface ICallback(){
    public void callback(MyObject o);
}

Class A gọi Class B để hoàn thành một vài việc trong một Thread. Nếu Thread này hoàn thành xong công viêc, nó sẽ thông báo Class A thông qua callback và cung cấp kết quả. Vì vậy không cần thiết phải polling hoặc làm gì đó. Bạn sẽ nhận được kết quả ngay sau khi nó có.

Trong Android, Callback được sử dụng ví dụ như giữa Activities và Fragments. Do Fragments nên là modular, bạn cần định nghĩa callback trong Fragment để gọi method trong Activity.

Áp dụng vào ApiCall

Đầu tiên chúng ta cần viết thêm code vào class nơi ApiCall được gọi, nó sẽ đại diện cho việc tham chiếu tới callback function mà có thể gọi từ một ApiCall.

public interface OnTaskCompleted{
    void onTaskCompleted(JSONObject result);
}
public class Callback implements OnTaskCompleted{
    @Override
    public void onTaskCompleted(JSONObject result) {
        // do something with result here!
    }
}

Bây giờ hãy điều chỉnh ApiCall để chấp nhận callback như một parameter.

public class ApiCall extends AsyncTask {
    private OnTaskCompleted listener;
    private String result;
    public ApiCall(OnTaskCompleted listener){
        this.listener=listener;
    }
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        StringBuilder resultBuilder = new StringBuilder();
        for (int i = 0; i < count; i++) {
            try {
                // Read all the text returned by the server
                InputStreamReader reader = new InputStreamReader(urls[i].openStream());
                BufferedReader in = new BufferedReader(reader);
                String resultPiece;
                while ((resultPiece = in.readLine()) != null) {
                    resultBuilder.append(resultPiece);
                }
                in.close();
             } catch (MalformedURLException e) {
                 e.printStackTrace();
             } catch (IOException e) {
                 e.printStackTrace();
             }
             // if cancel() is called, leave the loop early
             if (isCancelled()) {
                 break;
             }
         }
         // save the result
         this.result = resultBuilder.toString();
         return totalSize;
     }
    protected void onProgressUpdate(Integer... progress) {
        // update progress here
    }
    // called after doInBackground finishes
    protected void onPostExecute(Long result) {
        Log.v("result, yay!", this.result);
        // put result into a json object
        try {
            JSONObject jsonObject = new JSONObject(this.result);
            // call callback
            listener.onTaskCompleted(jsonObject);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}

Chỉ có một chút thay đổi nhỏ với class gốc không có callback. Ở phía đầu chúng ta đã cho thêm một private variable của dạng OnTaskCompleted mà chúng ta đã định nghĩa. Object nãy sẽ lắng nghe để gửi cho nó signal từ chính class này, rồi sau đó thực hiện các tác vụ cần thiết

Chúng ta cũng định nghĩa một constructor ngay phía dưới chấp nhận một OnTaskCompleted object như paramter. Vì vậy từ bây giờ bạn có thể truyền vào callback khi tạo một ApiCall mới. Ở phía gần cuối, sau khi nhận được response và đặt các kết quả vào trong một JSON object, chúng ta có thể gọi callback này bằng listener.onTaskCompleted(jsonObject);.

Cuối cùng, dưới đây là cách để bạn gọi ApiCall phiên bản mới từ một class khác nơi Callback được định nghĩa:

URL url = null;
try {
    url = new URL("http://search.twitter.com/search.json?q=@justinjmcc");
} catch (MalformedURLException e) {
    e.printStackTrace();
}
new ApiCall(new Callback()).execute(url);

Và đó là tất cả điều bạn cần để implement callback trong việc gọi Web API không đồng bộ này. Bằng việc định nghĩa và truyền một callback khác khi tạo một ApiCall object, bạn có thể thực hiện gọi ApiCall hiệu quả bằng bất kỳ function mong muốn sau khi nhận kết quả việc gọi.

0