Khi làm việc với Fragment thì có lẽ không ai là chưa từng bị lỗi Fragment IllegalStateException. Bạn có thể lục tung cả StackOverFlow để tìm giải pháp khắc phục. Nhưng có lẽ đáp án chính xác và triệt để nhất thì không hẳn là dễ tìm.
Nguyên nhân của lỗi này có thể khác nhau, tùy vào cách các bạn sử dụng Fragment trong dự án. Nhựng tựu chung lại thì nguyên nhân gốc rễ là: sử dụng Fragment ở sai trạng thái của Acitivity.
Bài viết này mình sẽ hướng dẫn các bạn cách khắc phục triệt để lỗi này. Đồng thời mình cũng giải thích chi tiết những điều gì xảy ra khi lỗi này xảy ra.
Nội dung chính của bài viết
Một trường hợp sử dụng Fragment phổ biến và xảy ra lỗi
Bạn thực hiện gửi một network request từ một Activity và chuyển màn hình bằng cách replace một fragment thông qua hàm FragmentTransaction.commit()
mỗi khi request network kia hoàn thành.
Giả sử người dùng nhấn phím HOME trước khi network request hoàn thành. Lúc này onSaveInstanceState()
của Activity được gọi, nếu cố tình gọi commit(
) thì lỗi Fragment IllegalStateException xảy ra.
Đi tìm nguyên nhân lỗi Fragment IllegalStateException
Như mình nói ở trên, nguyên nhân gốc rẽ của lỗi là do fragment transactions không được commit một fragment sau khi onSaveInstanceState()
của Activity được gọi.
Nhưng làm thế nào để tránh commit fragment khi không được phép? Trước khi tìm giải pháp, chúng ta cần biết khi nào thì hàm onSaveInstanceState()
được gọi.
Khi nào OnSaveInstanceState() được gọi?
Ai cũng biết rằng các thiết bị di động dù có cấu hình mạnh tới đâu thì vẫn bị coi là có tài nguyên hạn chế. Các nhà phát triển không thể phung phí tài nguyên vì ít nhất là dung lượng pin là có hạn.
Chính vì điều này mà hệ điều hành Android có thể kill bất kỳ Activity không thực sự tương tác với người dùng khi thiếu tài nguyên.
Do đó, khi người dùng mở lại Activity đó thì hệ thống sẽ tạo lại Activity. Trong trường hợp có tạo lại Activity thì bạn cũng cần phải khôi phục lại trạng thái dữ liệu như lúc trước khi bị kill.
Và để hỗ trợ điều này, Android cung cấp hàm onSaveInstanceState()
và onRestoreInstanceState()
. Những hàm này sẽ được gọi ngay trước khi Activity bị kill và tạo lại. Bạn sẽ tiến hành sao lưu dữ liệu trong hàm onSaveInstanceState()
.
Thế bạn sẽ thắc mắc là tại sao không thực hiện lưu dữ liệu trong onPause()
hay onStop()?
Sự khác nhau cơ bản là: onStop()
được gọi mỗi khi Activity trở thành inactive, không cần phải bị kill. Còn OnSaveInstanceState() chỉ được gọi khi Activity bị destroy/kill.
Để hiểu rõ hơn, bạn có thể tham khảo bảng bên dưới:
Khi bạn commit một Fragment – sử dụng fragmentTransaction.commit()
. Transaction không thực hiện commit ngay lập tức, mà nó sẽ thực hiện bất kỳ lúc nào mà nó “rảnh”.
Bạn sẽ không phải lỗi Fragment IllegalStateException nếu bạn commit trong onCreate(). Nói một cách đơn giản là fragment chỉ nên được commit khi Activity đang active, còn không thì sẽ gặp lỗi crash trên.
>> Đọc thêm nhé: Android Fragment Lifecycle – Những điều chưa kể
Giải pháp
Qua phần lý thuyết ở trên, hi vọng bạn đã hiểu phần nào nguyên nhân gốc rễ của vấn đề.
Bản thân Android không đưa ra một giải pháp cụ thể để khắc phục. Họ chỉ khuyên rằng bạn nên cẩn thận mỗi khi commit fragment bên ngoài vòng đời của Activity.
Tuy nhiên, Android cũng rất có “tâm” khi cung cấp một vài API gọi là work aroud để khắc phục vấn đề này.
- commitAllowStateLoss(): Hàm này về cơ bản giống như hàm commit(). Chỉ khác là nó không bắn một exception nếu bị mất state. Thực chất cách làm này là giấu bug thôi. Nhưng ít nhất thì nó không làm ứng dụng bị crash là được rồi.
- Custom Solution: Thay vì giấu bug, chúng ta đã hiểu rõ nguyên nhân tại sao lại xảy ra bug crash này. Chúng ta sẽ cố gắng không commit một fragment khi Activity state bị mất. Với phương pháp này, chúng ta sẽ trì hoãn việc commit cho đến khi Activity được khôi phục. Và bài viết này mình sẽ trình bày cách implement này.
Example code
Để giải quyết lỗi này, trước tiên chúng ta cần phải dummy một đoạn code để có thể tái hiện lỗi Fragment IllegalStateException thường xuyên. Chứ nếu đợi Android thực hiện kill Activity thì nó may rủi lắm.
Đây là đoạn code gây lỗi:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void onSaveInstanceState(Bundle bundle) { super.onSaveInstanceState(bundle); commitFragment(); } private void commitFragment() { MyFragment myFragment = new MyFragment(); FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.add(R.id.frame, myFragment); fragmentTransaction.commit(); } }
Giờ bạn chạy đoạn code trên xem. Mỗi khi bạn nhấn phím HOME, ứng dụng đều bị crash.
Ok, giờ chúng ta sẽ từng bước sửa lỗi.
Bạn định nghĩa hai biến như bên dưới:
public class MainActivity extends AppCompatActivity { //Boolean variable to mark if the transaction is safe private boolean isTransactionSafe; //Boolean variable to mark if there is any transaction pending private boolean isTransactionPending;
Trong hai hàm onPostResume()
và onPause()
, chúng ta tiến hành cài đặt và reset cờ isTransactionSafe. Ý tưởng ở đây là chúng ta đánh dấu trasnsaction chỉ an toàn khi Activity đang ở trạng thái foreground.
/* onPostResume is called only when the activity's state is completely restored. In this we will set our boolean variable to true. Indicating that transaction is safe now */ public void onPostResume() { super.onPostResume(); isTransactionSafe = true; } /* onPause is called just before the activity moves to background and also before onSaveInstanceState. In this we will mark the transaction as unsafe */ public void onPause() { super.onPause(); isTransactionSafe = false; }
Giờ mỗi khi commit fragment, bạn đều cần phải kiểm tra cờ trên.
private void commitFragment() { if (isTransactionSafe) { MyFragment myFragment = new MyFragment(); FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.add(R.id.frame, myFragment); fragmentTransaction.commit(); } }
Những đoạn code mới chỉ giải quyết được một nửa vấn đề. Tức là tránh cho lỗi crash xảy ra. Nhưng chúng ra sẽ bị mất transaction sau khi Activity active trở lại từ background. Đó là lý do chúng ta cần thêm cờ isTransactionPending
private void commitFragment() { if (isTransactionSafe) { MyFragment myFragment = new MyFragment(); FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.add(R.id.frame, myFragment); fragmentTransaction.commit(); isTransactionPending = false; } else { /* If any transaction is not done because the activity is in background. We set the isTransactionPending variable to true so that we can pick this up when we come back to foreground */ isTransactionPending = true; } }
Cuối cùng thì chúng ta cần kiểm tra isTransactionPending
mỗi khi Activity trở lại foreground
public void onPostResume() { super.onPostResume(); isTransactionSafe = true; /* Here after the activity is restored we check if there is any transaction pending from the last restoration */ if (isTransactionPending) { commitFragment(); } }
Vậy là xong rồi đấy!
Giải pháp này hoạt động với trường hợp có nhiều fragment trên cùng một Activity. Bạn chỉ cần thêm một unique ID cho mỗi Fragment và một biến số nguyên được khởi tạo cho mỗi fragment được commit.
Bạn nghĩ sao về giải pháp này? Hãy để lại bình luận bên dưới nhé.
Bình luận. Cùng nhau thảo luận nhé!