Tuesday, May 28, 2013

FragmentPagerAdapter và lỗi NullPointerException khi xoay màn hình

Hôm qua phát hiện ở phần mềm RC4W client của mình bị lỗi kì lạ: khi người sử dụng xoay màn hình, chương trình bị force close.

Nguyên nhân sau khi tìm hiểu thì do hàm getActivity dùng cho Fragment trả về giá trị null sau khi người sử dụng xoay màn hình, điều này phát sinh NullPointerException. Tìm hiểu rất nhiều nơi thì tìm được một câu trả lời hợp lý ở đây: http://stackoverflow.com/questions/11631408/android-fragment-getactivity-sometime-returns-null

Nguyên nhân lỗi

Ngắn gọn: mình đã làm "không theo sách".

Dài dòng hơn:
1) Lỗi thứ nhất: tạo fragment mới mỗi lần onCreate() được gọi.
myfragment = new MyFragment();

Lệnh này gọi trong mỗi lần onCreate() của Activity. Theo hướng dẫn (không biết nguồn chính thống ở đâu, nhưng có 1 ở  nguồn không chính thống ở đây ), KHÔNG nên tạo fragment mới mỗi lần onCreate() của Activity. Theo đó, người ta hướng dẫn là dùng kiểu như sau:
    CustomFragment fragment;
    if (savedInstanceState != null) {
        fragment = (CustomFragment) getSupportFragmentManager().findFragmentByTag("customtag");
    } else {
        fragment = new CustomFragment();
        getSupportFragmentManager().beginTransaction().add(R.id.container, fragment, "customtag").commit();
    }

Tức là mỗi lần onCreate() được gọi, kiểm tra xem fragment có đã được tạo ra và đang được FragmentManager quản lý hay không. Nếu có thì lấy ra (bằng tag), không thì tạo mới. Tag này là String do mình tùy chọn.

2) Nhưng vẫn chưa hiểu tạo sao fragment mới tạo ra mỗi lần onCreate() lại làm cho getActivity() của nó trả về null.
Nguyên nhân nằm ở FragmentPagerAdapter. FragmentPagerAdapter có cơ chế tự gán tag, từ tìm lại fragment đã tạo. Nhờ đó mình không phải quan tâm đến đoạn mã kiểm tra tag và gán tag như ở mục trên nữa. Xem mã nguồn của FragmentPagerAdapter để biết thêm chi tiết. Khi mình tạo mới một fragment khi onCreate(), FragmentPagerAdapter này nó lại không quan tâm, nó tìm xem fragment cũ có không, và nhiều khi nó còn tồn tại (do FragmentManager quản lý), thế là nó lấy fragment cũ ra sử dụng, fragment mới bị bỏ qua và không được gắn với Activity nào. Kết quả là hàm gọi getActivity() thực hiện với Fragment mới trả về null.
Nguyên nhân nữa nằm ở việc mình truy cập tới fragment thông qua tên biến fragment này. Tức là lúc nào cũng là Fragment mới.

Tóm lại, để có lỗi này cần làm những việc sau:
Khi coding:
+ Sử dụng Fragment, FragmentPagerAdapter
+ onCreate(): tạo new Fragment (có thể là CustomFragment của bạn)
+ trong mã lệnh của class CustomFragment có gọi getActivity()
+ truy cập đến 1 biến CustomFragment trực tiếp

Khi sử dụng:
+ Activity mở ra -> onCreate() -> new Fragment (1)
+ Khi cần hiển thị Fragment, FragmentPagerAdapter tìm xem có fragment này trong FragmentManager không. Nếu có thì lấy ra dùng luôn, nếu không sẽ gọi hàm getItem() và sẽ add fragment này vào FragmentManager với Tag nó tự tạo ra. Sau đó Attach fragment này vào Activity.
+ Xoay màn hình -> recreate lại Activity -> gọi onCreate() -> new fragment (2).
+ Khi cần hiển thị Fragment, FragmentPagerAdapter sẽ làm như trên và tìm lấy ra được fragment (1) và hiển thị bình thường
+ Ở đâu đó bạn truy cập vào fragment(2) một cách trực tiếp, trong đó lại gọi getActivity. ---> NullPointerException. Vì fragment(2) không được FragmentPagerAdapter sử dụng và attach cho Activity nào cả.

Cách khắc phục lỗi này: 

Lỗi này mình khắc phục bằng cách là mỗi lần onCreate(), không gọi tạo new fragment mới ngay mà kiểm tra findFragmentByTag trong FragmentManager trước. Đầu vào Tag được tạo từ hàm makeFragmentName() ở trong  mã nguồn của FragmentPagerAdapter

Dù cách này dùng được nhưng cho thấy một lỗi rất quái đản và sự thiếu logic trong SDK của android. Trước giờ mình đều nghĩ là ở onCreate() thì phải create (new) chứ sao lại phải tìm lại xem có fragment đã tạo trước rồi hay không. Hay mình đã sai từ đầu? Tất cả widget khi recreate đều có thể sử dụng lại, không cần tạo mới? Sẽ tìm hiểu thêm về vấn đề này.



No comments:

Post a Comment