Gọi onActivityResult trong Nested Fragment

Update latest 11/04/2016 Hiện tại Google đã update thư viện support v4, cho phép nested fragment có thể nhận trực tiếp callback từ onRequestPermissionResult() và onActivityResult, vì thế bài viết dưới đây không còn ý nghĩa nữa, nhưng có thể nó cũng giúp bạn hiểu 1 phần cơ chế Thông tin chi tiết các bạn có thể tham khảo dưới đây If you’re using Support v4 Fragments, nested fragments will now receive callbacks to onRequestPermissionsResult() (in addition to callbacks to onActivityResult(), fixed in 23.2.1).

----Deprecated-------- Đối với các lập trình viên Android, thì phương thức onActivityResult có lẽ đã quá quen thuộc. Từ activity A muốn triệu gọi một activity B để làm một việc x nào đó, sau khi thực hiện xong thì trả kết quả về cho A. Tại A, kết quả trả về từ B sẽ được xử lý trong hàm onActivityResult, rất đơn giản phải không. Vâng, quả thực rất đơn giản nếu như chúng ta gọi hàm startActivityForResult từ activity A.

Bài viết này tôi muốn chia sẻ với các bạn vấn đề tôi gặp phải trong quá trình làm thực tế, mà nó đã làm tôi mất rất nhiều công để fix, hi vọng các bạn sẽ giải quyết nó êm xuôi khi gặp phải tình huống này.

Tình huống tôi gặp phải như sau:

  • Có 1 activity, đặt tên là ActivityMain
  • Activity chứa 1 fragment đặt tên là FragmentLevelOne
  • FragmentLevelOne lại chứa trong nó 1 (hoặc n) fragment khác, đặt tên là FragmentLevelTwo. Trong FragmentLevelTwo có thể chứa fragment con và cứ như thế. Với dân android thì gọi đó là fragment lồng fragment hay chính xác hơn là Nested Fragment
  • Trong các nested fragment này, tôi gọi lệnh startActivityForResult để triệu gọi activity khác để thực hiện công việc nào đó, tạm gọi là ActivityWorker.

Đọc đến đây, chắc có lẽ các bạn sẽ nghĩ ngay rằng: “xời, thì sau khi ActivityWorker nó thực hiện xong nó sẽ trả về kết quả cho ActivityMain thôi, trong FragmentLevelTwo cài đè phương thức onActivityResult là sẽ xử lý được kết quả thôi mà”.

Vâng, đây chính là mấu chốt vấn đề. Quả thực, ở FragmentLevelTwo tôi đã thử sử dụng cả 2 kiểu gọi ActivityWorker: gọi trực tiếp bằng phương thức startActivityForResult và getActivity().startActivityForResult. Tuy nhiên, kết quả trả về từ ActivityWorker nó không trả về cho onActivityResult ở FragmentLevelTwo. Có lẽ các bạn sẽ cho điều này là hoang đường, không thể như thế được. Nhưng tình huống tôi gặp thì đúng là như vậy. Các bạn có biết tại sao không? Sau rất nhiều câu trả lời trên mạng, cuối cùng tôi đã tìm được câu trả lời và fix được vấn đề này.

Lý do tại sao? Đó là bởi vì fragment được thiết kế không dành cho mục đích lồng (nested). Do đó, một khi chúng ta mở rộng khả năng của nó, kiến trúc sử dụng fragment không thể kiểm soát được tất cả các tình huống, và dĩ nhiên, chúng ta phải tự kiểm soát. Tôi đã tìm ra được giải pháp ở trên mạng có thể giải quyết vấn đề này.

Trước hết, chúng ta tìm hiểu về việc gọi phương thức startActivityForResult và tại sao lại không có kết quả trả về onActivityResult trong nested fragment. Mặc dù chúng ta gọi phương thức startActivityForResult trực tiếp từ fragment nhưng thực tế, mọi thứ được kiểm soát bởi Activity. Mỗi khi chúng ta gọi phương thức startActivityForResult từ một fragment, requestCode sẽ bị thay đổi theo hướng gắn định danh của fragment vào code. Đây chính là mấu chốt của vấn đề. Việc thay đổi requestCode giúp cho Activity có thể đánh dấu xem fragment nào đã gửi request này mỗi khi nhận được kết quả trả về. Mỗi khi Activity nhận kết quả trả về, kết quả sẽ được gửi đến phương thức onActivityResult với requestCode đã chỉnh sửa và requestCode này sẽ được giải mã thành requestCode ban đầu và định danh của fragment. Sau đó, Activity sẽ gửi kết quả này đến fragment thông qua phương thức onActivityResult. Xong.

Vấn đề ở đây là: activity chỉ có thể gửi kết quả đến fragment nào đã attach trực tiếp vào activity mà thôi, còn với nested fragment thì không. Đó là lý do tại sao mà phương thức onActivityResult của nested fragment không bao giờ được gọi.

Giải pháp là gì?

Tìm kiếm trên mạng sẽ có rất nhiều giải pháp, các đại cao thủ trên stackoverflow sẽ cung cấp muôn vàn giải pháp. Tuy nhiên, tôi không tìm được giải pháp nào giải quyết triệt để vấn đề này. May thay, cuối cùng tôi đã thấy (Ơ rê ka) 😄 Như đã mô tả ở trên, vấn đề ở đây là request có thể được gửi từ nested fragment nhưng không thể nhận được kết quả như bình thường. Do đó, không cần làm gì trong Fragment. Mọi thứ cần làm ở mức Activity. Vì thế, từ giờ chúng ta sẽ gọi getActivity().startActivityForResult thay vì startActivityForResult() như thế này

Intent intent = new Intent(getActivity(),ActivityWorker.class);
getActivity().startActivityForResult(intent,REQUEST_CODE);

Do đó, tất cả kết quả nhận được sẽ được xử lý ở một nơi duy nhất: phương thức onActivityResult của Activity mà fragment được đính vào

Câu hỏi đặt ra lúc này là làm thế nào để gửi activity result đến fragment đã gửi request? Thực tế, chúng ta không thể giao tiếp với tất cả các nested fragment theo cách thông thường, hoặc ít nhất là theo cách dễ nhất. Mặt khác, mọi fragment đều biết nó phải xử lý mã requestCode nào. Vì thế chúng ta sẽ chọn cách “thông báo (broadcast) đến từng fragment đang active tại thời điểm nhận kết quả, và các fragment sẽ kiểm tra mã requestCode và thực hiện công việc chúng cần làm”

Về vấn đề thông báo (broadcasting), gói LocalBroadcastManager co thể thực hiện công việc này nhưng phương thức thực hiện đã quá cũ. Chúng ta sẽ chọn phương thức mới hơn, đó là EventBus và thư viện để thực hiện việc này là Otto của square bởi vì nó có hiệu năng khá tốt Việc đầu tiên, chúng ta cần add thư viện này vào file build.gradle của projec

dependencies{
       compile 'com.squareup:otto:1.3.6'
}

Theo cách của Otto, chúng ta sẽ tạo một Bus Event giống như 1 gói để chứa những giá trị của activity result ActivityResultEvent.java

private int requestCode;
private int resultCode;
private Intent data;
public ActivityResultEvent(int requestCode,int resultCode,Intent data){
    this.requestCode = requestCode;
    this.resultCode = resultCode;
    this.data = data;
}
public int getRequestCode() {
    return requestCode;
}
public void setRequestCode(int requestCode) {
    this.requestCode = requestCode;
}
public Intent getData() {
    return data;
}
public void setData(Intent data) {
    this.data = data;
}
public int getResultCode() {
    return resultCode;
}
public void setResultCode(int resultCode) {
    this.resultCode = resultCode;
}

Tất nhiên, chúng ta sẽ tạo một Event Bus Singleton để gửi package từ một activity đến tất cả các active fragment

ActivityResultBus.java

private static ActivityResultBus instance;
public static ActivityResultBus getInstance(){
    if(instance == null){
        instance = new ActivityResultBus();
    }
    return instance;
}
private Handler mHandler = new Handler(Looper.getMainLooper());
public void postQueue(final Object obj){
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            ActivityResultBus.getInstance().post(obj);
        }
    });
}

Ở đây có 1 điểm chú ý đó là ta đã tạo một phương thức tên là postQueue trong đối tượng bus. Nó được sử dụng để gửi một package vào bus. Và lý do tại sao chúng ta phải làm thế là vì chúng ta phải delay việc gửi một package một chút do tại thời điểm phương thức onActivityResult của activity được gọi thì Fragment vẫn chưa được active. Vì thế chúng ta cần dùng đến Handler để gửi những lệnh này vào hàng đời của MainThread với lệnh handler.post() như trên.

Sau đó, chúng ta sẽ cài đè phương thức onActivityResult và thêm các dòng code dưới đây để gửi package đến bus mỗi khi nhận được kết quả

super.onActivityResult(requestCode, resultCode, data);
ActivityResultBus.getInstance().postQueue(new ActivityResultEvent(requestCode, resultCode, data));

Trong Fragment, chúng ta cần lắng nghe package được gửi từ activity. Chúng ta có thể làm việc này dễ dàng với otto như sau:

public class FragmentLevelTwo extends Fragment{
protected Object mActivityResultSubscriber = new Object(){
    @Subscribe
    public void onActivityResultReceived(ActivityResultEvent event){
        int requestCode = event.getRequestCode();
        int resultCode = event.getResultCode();
        Intent data = event.getData();
        onActivityResult(requestCode,resultCode,data);
    }
};
@Override
public void onStart() {
    super.onStart();
    ActivityResultBus.getInstance().register(mActivityResultSubscriber);
}
@Override
public void onStop() {
    super.onStop();
    ActivityResultBus.getInstance().unregister(mActivityResultSubscriber);
}
}

That all! Đó là tất cả những gì bạn cần làm. Tuy nhiên, có 1 điểm cần chú ý, đó là bạn không sử dụng cùng requestCode ở các fragment khác nhau. Lý do là bởi vì tại thời điểm nhận activity result, các fragment sẽ được active. Nếu cùng requestCode có thể sẽ gửi sai địa chỉ. Chỉ có 1 chú ý nhỏ như thế thui.

Các bạn thử xem nhé. Đây là bài viết chi tiết mà mình đã tham khảo

How to make onActivityResult get called on Nested Fragment