Working with FragmentPagerAdapter and FragmentStatePagerAdapter

Giới thiệu

Là một Android Developer thì chắc hẳn phải biết đến ViewPager, một thành phần thường hay được sử dụng nằm trong gói support design. FragmentPagerAdapter và FragmentStatePagerAdapter là những thành phần để cấu hình cho ViewPager nhưng không phải ai cũng biết sự khác nhau giữa chúng. Và việc gọi notifyDatasetChanged() đôi lúc cũng không đi theo hướng chúng ta mong muốn, đôi khi có thể gây bực bội. Và memory leak có thể xảy ra dễ dàng trong khi sử dụng chúng,... Bạn đã biết rằng Fragment trong FragmentPagerAdapter chỉ được release ra khỏi bộ nhớ khi và chỉ khi bạn finish Activity ?

Sự khác nhau cơ bản

FragmentPagerAdapter

Sẽ rất tốt nếu bạn sử dụng ViewPager với số lượng cố định và ít Fragment ( ví dụ khi làm app có vài tab chính cố định ). Tại sao lại thế ? Vì Fragment sẽ không bao giờ được release từ FragmentManager một khi nó đã được tạo ra ( trừ khi finish activity chứa nó ). Nó chỉ detach view khi một fragment không còn hiển thị. onDestroyView() sẽ được gọi khi đó và onCreateView() sẽ được gọi khi bạn quay lại Fragment này.

FragmentStatePagerAdapter

Nó thông minh hơn trong việc quản lý bộ nhớ vì nó sẽ hoàn toàn release Fragment từ FragmentManager một khi nó không còn hiển thị ( ví dụ khi làm màn hình gallery ảnh ). State của những Fragment bị xoá sẽ được lưu trữ bên trong FragmentStatePagerAdapter. Trường hợp Fragment được khởi tạo lại khi bạn quay lại vị trí đó và state tại vị trí đó sẽ được restore . Nó thích hợp cho việc làm màn hình mà có số lượng Fragment không xác định hoặc số lượng đó được thay đổi nhiều lần.

Những vấn đề thường xảy ra

FragmentPagerAdapter - memory leak

Fragment trong FragmentPagerAdapter chỉ detach view khi nó không còn hiển thị chứ không bao giờ được gỡ bỏ khỏi FragmentManager (trừ khi finish activity). Vậy nên khi sử dụng FragmentPagerAdapter, bạn phải chắc chắn xóa bất kỳ tham chiếu nào tới View hoặc Context ở onDestroyView() . Nếu không, Garbage Collector không thể giải phóng bộ nhớ cho View và Context đó. Hãy tưởng tượng bạn có 10 item trong FragmentPagerAdapter và khi lướt qua tất cả chúng sẽ giữ cả 10 view trong bộ nhớ thay vì chỉ 3 cái (tùy thuộc vào setOffScreenPageLimit() mà bạn cài đặt), khi bạn xoay màn hinh thì sẽ làm cho nó trở nên tồi tệ hơn (7 trong 10 sẽ vẫn giữ một tham chiếu đến một activity đã bị huỷ sau khi xoay màn hình).

Troubles with notifyDatasetChanged()

Tôi đoán tất cả chúng ta đã gặp phải vấn đề khi gọi notifyDatasetChanged() cho một trong những adapter này. Làm như vậy sẽ không refresh được Fragment đang hiển thị mà bạn cần phải swipe qua lại thì mới thấy được sự thay đổi. Cả hai PagerAdapter này đều đang lưu trữ và sử dụng lại Fragment, dĩ nhiên điều đó là tốt bởi vì nếu không thì việc gọi notifyDataSetChanged sẽ tạo lại Fragment đó (điều này là không cần thiết). Cũng cần lưu ý rằng notifyDataSetChanged() dành cho tình huống khi list dữ liệu trong adapter được thay đổi - có nghĩa là số lượng item được thêm vào hoặc xoá bớt đi . Phương thức notifyDatasetChanged() không được dùng để làm mới Fragment đang được hiển thị. Bạn nên add thêm listener/callback đến Fragment của bạn nếu như bạn muốn refresh lại gì đó trên màn hình khi phướng thức đó được gọi.

FragmentPagerAdapter & notifyDatasetChanged()

Bạn cần phải override lại 2 method này trong FragmentPagerAdapter của bạn để hỗ trợ việc thay đổi bộ dữ liệu.

int getItemPosition(Object object)

Được gọi khi vị trí của item bị thay đổi. Sẽ return về POSITION_UNCHANGED nếu không thay đổi vị trí của item đó hoặc POSITION_NONE nếu item đó không còn trong adapter. Mặc định là item sẽ không bao giờ thay đổi vị trí và luôn luôn trả về POSITION_UNCHANGED. Bạn cần phải thực hiện getItemPosition(Object object) với một số trường hợp cần xác định vị trí của Fragment hiện tại đang hiển thị. Luôn luôn trả về POSITION_NONE thì hiệu suất làm việc và bộ nhớ sẽ không có hiệu quả - nó sẽ luôn luôn detach Fragment đang hiển thị và create lại chúng ngay cả khi vị trí của chúng trong list dữ liệu không thay đổi. Và những fragment đó sẽ được lưu trong memory cho đến khi bạn finish activity.

long getItemId(int position)

Sẽ trả về id cho item tại vị trí được cho trước, mặc định là như vậy. Khi override lại thì có thể trả về id của vị trí của item có thể bị thay đổi. Phương pháp này được sử dụng bên trong instantiateItem() để tìm kiếm instance của Fragment đang tồn tại trong FragmentManager. Gọi notifyDataSetChanged() mà không override method này sẽ chỉ trả về Fragment tại vị trí đó.

FragmentStatePagerAdapter - bug of state bundle

Bạn chỉ cần override getItemPosition() để hỗ trợ việc thay đổi list item trong FragmentStatePagerAdapter. Áp dụng tương tự như phần mô tả ở trên với FragmentPagerAdapter (đừng luôn luôn trả về POSITION_NONE). Có một vấn đề ở đây là FragmentStatePagerAdapter đang giữ một ArrayList các state bundle. instantiateItem() sẽ tạo mới Fragment nếu nó không tồn tại - nhưng sau đó nó sẽ dựa vào ArrayList state bundle để lấy ra state đúng của Fragment đó dựa trên vị trí của item đó. Đây có thể là một Bundle thuộc về instance Fragment trước đó, điều này là không đúng.

IllegalStateException: Fragment {} is not currently in the FragmentManager.

Vấn đề được mô tả chi tiết ở đây Cách giải quyết đơn giản là trả lại POSITION_NONE khi override getItemPosition() lại (một số trường hợp hiện tại tôi cũng đang làm như vậy :v). Nhưng điều này gây ra hiệu suất không tốt vì nó luôn tạo lại Fragment ngay cả khi list dữ liệu của bạn không thay đổi.

Bài viết được dịch hiểu từ https://medium.com/inloop/adventures-with-fragmentstatepageradapter-4f56a643f8e0