Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Working with Fragments and Keeping Your Sanity ...

Working with Fragments and Keeping Your Sanity - DroidCon Zagreb 2016

At a first glance fragments may seem simple enough to use, however diving deeper reveals that fragments have a complex lifecycle (~ 14 lifecycle methods), and a lot of edge cases and pitfalls that need to be given special attention.

Avatar for Ido Feins

Ido Feins

April 29, 2016

Other Decks in Programming

Transcript

  1. Why Fragments? • Multi-pane UI. • Greater flexibility: • As

    many methods as you like. • Pass non Parcelable/Serializable objects. • What about the bad stuff?
  2. Fragment state class AmountFragment extends Fragment { onCreate(Bundle state) {

    if (state != null) { mSelectedCurrency = state.getState("state_currency"); } } }
  3. Fragment state class AmountFragment extends Fragment { onCreate(Bundle state) {

    if (state != null) { mSelectedCurrency = state.getState("state_currency"); class AmountFragment extends Fragment { } else { Bundle args = getArguments(); mSelectedCurrency = args.getString("arg_default_curr"); } } }
  4. Fragment state class AmountFragment extends Fragment { onCreate(Bundle state) {...}

    onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("state_currency", mSelectedCurrency); } } class AmountFragment extends Fragment { onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("state_currency", mSelectedCurrency); } }
  5. View state class AmountFragment extends Fragment { onCreate(Bundle state) {...}

    onSaveInstanceState(Bundle outState) {...} onCreateView(LayoutInflater inflater, ...) { View view = inflater.inflate(...); mMoneyView = (MoneyView) view.findViewById(...); mMoneyView.setCurrency(mSelectedCurrency); return view; } } class AmountFragment extends Fragment { onCreateView(LayoutInflater inflater, ...) { View view = inflater.inflate(...); mMoneyView = (MoneyView) view.findViewById(...); mMoneyView.setCurrency(mSelectedCurrency); return view; } }
  6. View state class AmountFragment extends Fragment { onCreate(Bundle savedInstanceState) {...}

    onSaveInstanceState(Bundle outState) {...} onCreateView(LayoutInflater inflater, ...) {...} onDestroyView() { mMoneyView = null; super.onDestroyView(); } } onDestroyView() { mMoneyView = null; super.onDestroyView(); }
  7. What happens when we get it wrong? class AmountFragment extends

    Fragment { onCreateView(...) { View view = inflater.inflate(...); mMoneyView = (MoneyView) view.findViewById(...); if (state != null) { // can be overridden with null! mSelectedCurrency = state.getState("state_currency"); } mMoneyView.setCurrency(mSelectedCurrency); } }
  8. What happens when we get it wrong? class AmountFragment extends

    Fragment { onCreateView(...) { View view = inflater.inflate(...); mMoneyView = (MoneyView) view.findViewById(...); if (state != null) { // can be overridden with null! mSelectedCurrency = state.getState("state_currency"); } mMoneyView.setCurrency(mSelectedCurrency); } } class AmountFragment extends Fragment { if (state != null) { // can be overridden with null! mSelectedCurrency = state.getState("state_currency"); } } }
  9. class AmountFragment extends Fragment { onCreate(SavedInstanceState state) { if (state

    != null) { mAmount = state.getState("state_amount"); } else { mAmount = 0; } } onCreateView(...) { mNumPadView.setDelEnabled(mAmount > 0); } }
  10. class NumPadView extends GridLayout { setDelEnabled(boolean enabled) { mDeleteButton.setEnabled(enabled); mDelEnabled

    = enabled; } Parcelable onSaveInstanceState() { ... SavedState savedState = new SavedState(superState); savedState.mDelEnabled = mDelEnabled; return savedState; } onRestoreInstanceState(Parcelable state) { ... setDelEnabled(savedState.mDelEnabled); } }
  11. Don’t mix fragment and view state Fragment’s state - onCreate,

    onSaveInstanceState View’s state - onCreateView, SavedState, onDestroyView
  12. FragmentTransaction private void moveToAmountFragment() { AmountFragment amountFragment = new AmountFragment();

    FragmentTransaction transaction = beginTransaction(); transaction.addToBackStack(null); transaction.replace(R.id.container, amountFragment); transaction.commit(); }
  13. Add or replace? private void moveToAmountFragment() { AmountFragment amountFragment =

    new AmountFragment(); FragmentTransaction transaction = beginTransaction(); transaction.addToBackStack(null); transaction.commit(); } private void moveToAmountFragment() { transaction.replace(R.id.container, amountFragment); }
  14. To add or not to add (to backstack)? private void

    moveToAmountFragment() { AmountFragment amountFragment = new AmountFragment(); FragmentTransaction transaction = beginTransaction(); transaction.replace(R.id.container, amountFragment); transaction.commit(); } private void moveToAmountFragment() { transaction.addToBackStack(null); }
  15. Committing transactions private void moveToAmountFragment() { AmountFragment amountFragment = new

    AmountFragment(); FragmentTransaction transaction = beginTransaction(); transaction.addToBackStack(null); transaction.replace(R.id.container, amountFragment); transaction.commit(); } transaction.commit();
  16. IllegalStateException: Can not perform this action after onSaveInstanceState at android.support.v4.app.FragmentManagerImpl.checkStateLoss

    at android.support.v4.app.FragmentManagerImpl.enqueueAction at android.support.v4.app.BackStackRecord.commitInternal at android.support.v4.app.BackStackRecord.commit
  17. onActivityResult onResumeFragments() { super.onResumeFragments(); if (mCardLinked) { mCardLinked = false;

    SpinnerFragment spinner = new SpinnerFragment(); FragmentTransaction transaction = beginTransaction(); transaction.replace(R.id.container, spinner); transaction.commit(); // re-run send money } }
  18. Async callbacks public void onSendMoneyResult(SendMoneyResult result) { if (mResumedFragments) {

    moveToSuccessPage(result); // commits transaction } else { mRetainedFragment.setResult(result); } }
  19. Async callbacks protected void onResumeFragments() { super.onResumeFragments(); mResumedFragments = true;

    SendMoneyResult result = mRetainedFragment.getResult(); if (result != null && <not already in success page>) { moveToSuccessPage(result); } }
  20. Use FragmentTransaction.replace (90% of the cases) Don’t add intermediate pages

    to backstack Safe commits: onCreate onResumeFragments Unsafe commits: onActivityResult / onRequestPermissionResult Async callbacks
  21. • Avoiding stale objects • Avoiding duplicate “work” • State

    restoration (Bundle) • Commit transactions in safe points • Avoiding stale objects • Avoiding duplicate “work”
  22. class ContactFragment extends Fragment { displayContacts(List<Contact> contacts) { ... }

    private class FetchContactsTask extends AsyncTask { List<Contact> doInBackground(...) { // fetch contacts from the server } onPostExecute(List<Contact> contacts) { // = ContactFragment.this.displayContacts(contacts) displayContacts(contacts); } } }
  23. class ContactFragment extends Fragment { displayContacts(List<Contact> contacts) { ... }

    private class FetchContactsTask extends AsyncTask { List<Contact> doInBackground(...) { // fetch contacts from the server } onPostExecute(List<Contact> contacts) { // = ContactFragment.this.displayContacts(contacts) displayContacts(contacts); } } } onPostExecute(List<Contact> contacts) { // = ContactFragment.this.displayContacts(contacts) displayContacts(contacts); }
  24. Use Loaders They automatically reconnect to the last loader's cursor

    when being recreated after a configuration change.
  25. class ContactFragment implements LoaderCallbacks<Long> { displayContacts(List<Contact> contacts) { ... }

    onResume() { getLoaderManager().initLoader(LOADER_ID, null, this); } Loader<Long> onCreateLoader(...) { return new ContactsLoader(getContext()); } onLoadFinished(..., List<Contact> contacts) { displayContacts(contacts); } }
  26. class ContactFragment implements LoaderCallbacks<Long> { displayContacts(List<Contact> contacts) { ... }

    onResume() { getLoaderManager().initLoader(LOADER_ID, null, this); } Loader<Long> onCreateLoader(...) { return new ContactsLoader(getContext()); } onLoadFinished(..., List<Contact> contacts) { displayContacts(contacts); } } class ContactFragment implements LoaderCallbacks<Long> { Loader<Long> onCreateLoader(...) { return new ContactsLoader(getContext()); } onLoadFinished(..., List<Contact> contacts) { displayContacts(contacts); } }
  27. onResume() { getLoaderManager().initLoader(LOADER_ID, null, this); } class ContactFragment implements LoaderCallbacks<Long>

    { displayContacts(List<Contact> contacts) { ... } onResume() { getLoaderManager().initLoader(LOADER_ID, null, this); } Loader<Long> onCreateLoader(...) { return new ContactsLoader(getContext()); } onLoadFinished(..., List<Contact> contacts) { displayContacts(contacts); } }
  28. Use Loaders They automatically reconnect to the last loader's cursor

    when being recreated after a configuration change. Thus, they don't need to re-query their data.
  29. class OperationFragment extends Fragment { public interface Listener { void

    onSuccess(Result result); } onCreate(Bundle state) { super.onCreate(state); setRetainInstance(true); } sendMoney(Contact contact, MoneyValue amount) { execute(new OperationListener() { onSuccess(result) { ((Listener) getActivity()).onSuccess(result); } }); } }
  30. class OperationFragment extends Fragment { public interface Listener { void

    onSuccess(Result result); } onCreate(Bundle state) { super.onCreate(state); setRetainInstance(true); } sendMoney(Contact contact, MoneyValue amount) { execute(new OperationListener() { onSuccess(result) { ((Listener) getActivity()).onSuccess(result); } }); } } onCreate(Bundle state) { super.onCreate(state); setRetainInstance(true); }
  31. class OperationFragment extends Fragment { public interface Listener { void

    onSuccess(Result result); } onCreate(Bundle state) { super.onCreate(state); setRetainInstance(true); } sendMoney(Contact contact, MoneyValue amount) { execute(new OperationListener() { onSuccess(result) { ((Listener) getActivity()).onSuccess(result); } }); } } public interface Listener { void onSuccess(Result result); }
  32. class OperationFragment extends Fragment { public interface Listener { void

    onSuccess(Result result); } onCreate(Bundle state) { super.onCreate(state); setRetainInstance(true); } sendMoney(Contact contact, MoneyValue amount) { execute(new OperationListener() { onSuccess(result) { ((Listener) getActivity()).onSuccess(result); } }); } } sendMoney(Contact contact, MoneyValue amount) { execute(new OperationListener() { onSuccess(result) { ((Listener) getActivity()).onSuccess(result); } }); }
  33. class SendMoneyFlowActivity { onCreate(...) { mOpFragment = findFragmentByTag(OP_FRAGMENT_TAG); if (mOpFragment

    == null) { mOpFragment = new OperationFragment(); FragmentTransaction transaction = beginTransaction(); transaction.add(mOpFragment, OP_FRAGMENT_TAG); transaction.commit(); } } onAmountSelected(MoneyValue amount) { mOpFragment.sendMoney(mContact, amount); } }
  34. class SendMoneyFlowActivity { onCreate(...) { mOpFragment = findFragmentByTag(OP_FRAGMENT_TAG); if (mOpFragment

    == null) { mOpFragment = new OperationFragment(); FragmentTransaction transaction = beginTransaction(); transaction.add(mOpFragment, OP_FRAGMENT_TAG); transaction.commit(); } } onAmountSelected(MoneyValue amount) { mOpFragment.sendMoney(mContact, amount); } } class SendMoneyFlowActivity { onCreate(...) { mOpFragment = findFragmentByTag(OP_FRAGMENT_TAG); } }
  35. class SendMoneyFlowActivity { onCreate(...) { mOpFragment = findFragmentByTag(OP_FRAGMENT_TAG); if (mOpFragment

    == null) { mOpFragment = new OperationFragment(); FragmentTransaction transaction = beginTransaction(); transaction.add(mOpFragment, OP_FRAGMENT_TAG); transaction.commit(); } } onAmountSelected(MoneyValue amount) { mOpFragment.sendMoney(mContact, amount); } } if (mOpFragment == null) { mOpFragment = new OperationFragment(); FragmentTransaction transaction = beginTransaction(); transaction.add(mOpFragment, OP_FRAGMENT_TAG); transaction.commit(); }
  36. class SendMoneyFlowActivity { onCreate(...) { mOpFragment = findFragmentByTag(OP_FRAGMENT_TAG); if (mOpFragment

    == null) { mOpFragment = new OperationFragment(); FragmentTransaction transaction = beginTransaction(); transaction.add(mOpFragment, OP_FRAGMENT_TAG); transaction.commit(); } } onAmountSelected(MoneyValue amount) { mOpFragment.sendMoney(mContact, amount); } } onAmountSelected(MoneyValue amount) { mOpFragment.sendMoney(mContact, amount); }