📜 ⬆️ ⬇️

Arguments against the use of fragments in Android

I recently spoke at a Droidcon conference in Paris with a report (original in French) in which I looked at the problems that we had in Square when working with fragments and the possibility of completely abandoning fragments.

In 2011, we decided to use fragments for the following reasons:


Since 2011, a lot of water has flowed under the bridge, and we have found better options.

What your parents never told you about the fragments


Life cycle


Context Android context is a divine object , and the Activity is a Context with an additional life cycle. Life cycle deity? Ironically. Fragments of the divine pantheon are not included, but they are more than compensate for this deficiency in a very complex life cycle.
')
Steve Pomeroy (Steve Pomeroy) made a diagram of all the transitions in the life cycle of the fragment, and it does not inspire much optimism:


Made by Steve Pommer, slightly modified to remove the Activity life cycle and posted under the CC BY-SA 4.0 license .

The life cycle poses many interesting questions. What can and cannot be done in each of the above callback functions? Are they called synchronously, or in turn? If by turns, in what order?

Debugging is difficult


When an error creeps into your program, you take a debugger and execute the code instruction by instruction to understand what is happening. And everything is fine, until you get to the FragmentManagerImpl class. Careful, Mina!

This code is quite difficult to understand, which makes it difficult to find errors in your application:

 switch (f.mState) { case Fragment.INITIALIZING: if (f.mSavedFragmentState != null) { f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray( FragmentManagerImpl.VIEW_STATE_TAG); f.mTarget = getFragment(f.mSavedFragmentState, FragmentManagerImpl.TARGET_STATE_TAG); if (f.mTarget != null) { f.mTargetRequestCode = f.mSavedFragmentState.getInt( FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0); } f.mUserVisibleHint = f.mSavedFragmentState.getBoolean( FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true); if (!f.mUserVisibleHint) { f.mDeferStart = true; if (newState > Fragment.STOPPED) { newState = Fragment.STOPPED; } } } // ... } 

If you ever discovered that you have a fragment created after the screen rotation and not attached to the Activity fragment, then you understand what I am saying (and for the sake of all that is holy, do not tempt fate and don’t mention about nested fragments with me).

The law obliges me (at least I read about this on Coding Horror) to attach the following image to the post, so don’t blame me:


After several years of deep analysis, I came to the conclusion that the number of WTFs per minute while debugging an Android application is 2 fragment_count .

Fragment creation magic


The fragment can be created by you or by the FragmentManager class. Take a look at the following code, everything is simple and clear, right?

 DialogFragment dialogFragment = new DialogFragment() { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ... } }; dialogFragment.show(fragmentManager, tag); 

However, when the Activity state is restored, the FragmentManager may attempt to re-create the fragment through reflection. Since we created an anonymous class at the top, there is a hidden argument in its constructor that refers to the external class. Bams:

 android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.squareup.MyActivity$1: make sure class name exists, is public, and has an empty constructor that is public 

What we understood after working with fragments


Despite all the problems of the fragments, invaluable lessons can be learned from working with them, which we will use to create our applications:


Adaptive interface: fragments against views


Fragments


Let's look at a simple example with fragments: an interface consisting of a list and a detailed view of each element of the list.

HeadlinesFragment is a list of elements:

 public class HeadlinesFragment extends ListFragment { OnHeadlineSelectedListener mCallback; public interface OnHeadlineSelectedListener { void onArticleSelected(int position); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setListAdapter( new ArrayAdapter<String>(getActivity(), R.layout.fragment_list, Ipsum.Headlines)); } @Override public void onAttach(Activity activity) { super.onAttach(activity); mCallback = (OnHeadlineSelectedListener) activity; } @Override public void onListItemClick(ListView l, View v, int position, long id) { mCallback.onArticleSelected(position); getListView().setItemChecked(position, true); } } 

Moving on to ListFragmentActivity more interesting: ListFragmentActivity should deal with whether to show the details on the same screen as the list or not:

 public class ListFragmentActivity extends Activity implements HeadlinesFragment.OnHeadlineSelectedListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.news_articles); if (findViewById(R.id.fragment_container) != null) { if (savedInstanceState != null) { return; } HeadlinesFragment firstFragment = new HeadlinesFragment(); firstFragment.setArguments(getIntent().getExtras()); getFragmentManager() .beginTransaction() .add(R.id.fragment_container, firstFragment) .commit(); } } public void onArticleSelected(int position) { ArticleFragment articleFrag = (ArticleFragment) getFragmentManager() .findFragmentById(R.id.article_fragment); if (articleFrag != null) { articleFrag.updateArticleView(position); } else { ArticleFragment newFragment = new ArticleFragment(); Bundle args = new Bundle(); args.putInt(ArticleFragment.ARG_POSITION, position); newFragment.setArguments(args); getFragmentManager() .beginTransaction() .replace(R.id.fragment_container, newFragment) .addToBackStack(null) .commit(); } } } 

Representation


Let's rewrite this code using self-written representations. First, we introduce the concept of a container ( Container ), which can show an element of the list, and also handles clicks back:

 public interface Container { void showItem(String item); boolean onBackPressed(); } 

Activity knows that it always has a container in its hands, and simply delegates the necessary work to it:

 public class MainActivity extends Activity { private Container container; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); container = (Container) findViewById(R.id.container); } public Container getContainer() { return container; } @Override public void onBackPressed() { boolean handled = container.onBackPressed(); if (!handled) { finish(); } } } 

The implementation of the list is also quite trivial:

 public class ItemListView extends ListView { public ItemListView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); final MyListAdapter adapter = new MyListAdapter(); setAdapter(adapter); setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { String item = adapter.getItem(position); MainActivity activity = (MainActivity) getContext(); Container container = activity.getContainer(); container.showItem(item); } }); } } 

Let's move on to the interesting: loading different markup depending on resource classifiers:

res / layout / main_activity.xml
 <com.squareup.view.SinglePaneContainer xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/container" > <com.squareup.view.ItemListView android:layout_width="match_parent" android:layout_height="match_parent" /> </com.squareup.view.SinglePaneContainer> 

res / layout-land / main_activity.xml
 <com.squareup.view.DualPaneContainer xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:id="@+id/container" > <com.squareup.view.ItemListView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.2" /> <include layout="@layout/detail" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.8" /> </com.squareup.view.DualPaneContainer> 

The implementation of these containers:

 public class DualPaneContainer extends LinearLayout implements Container { private MyDetailView detailView; public DualPaneContainer(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); detailView = (MyDetailView) getChildAt(1); } public boolean onBackPressed() { return false; } @Override public void showItem(String item) { detailView.setItem(item); } } 

 public class SinglePaneContainer extends FrameLayout implements Container { private ItemListView listView; public SinglePaneContainer(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); listView = (ItemListView) getChildAt(0); } public boolean onBackPressed() { if (!listViewAttached()) { removeViewAt(0); addView(listView); return true; } return false; } @Override public void showItem(String item) { if (listViewAttached()) { removeViewAt(0); View.inflate(getContext(), R.layout.detail, this); } MyDetailView detailView = (MyDetailView) getChildAt(0); detailView.setItem(item); } private boolean listViewAttached() { return listView.getParent() != null; } } 

In the future, these containers will be able to abstract and build the entire application on similar principles. As a result, we will not only not need fragments, but the code will be more accessible for perception.

Presentations and Presenters


Samopisnye representations - it's good, but I want more: I want to highlight the business logic in separate controllers. We will call such controllers presenters ( Presenters ). Introduction of presenters will allow us to make the code more readable and simplify further testing:

 public class MyDetailView extends LinearLayout { TextView textView; DetailPresenter presenter; public MyDetailView(Context context, AttributeSet attrs) { super(context, attrs); presenter = new DetailPresenter(); } @Override protected void onFinishInflate() { super.onFinishInflate(); presenter.setView(this); textView = (TextView) findViewById(R.id.text); findViewById(R.id.button).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { presenter.buttonClicked(); } }); } public void setItem(String item) { textView.setText(item); } } 

Let's take a look at the code taken from the Square Register app discount editing screen.



The presenter performs high-level manipulation of the presentation:

 class EditDiscountPresenter { // ... public void saveDiscount() { EditDiscountView view = getView(); String name = view.getName(); if (isBlank(name)) { view.showNameRequiredWarning(); return; } if (isNewDiscount()) { createNewDiscountAsync(name, view.getAmount(), view.isPercentage()); } else { updateNewDiscountAsync(discountId, name, view.getAmount(), view.isPercentage()); } close(); } } 

Testing this presenter is very simple:

 @Test public void cannot_save_discount_with_empty_name() { startEditingLoadedPercentageDiscount(); when(view.getName()).thenReturn(""); presenter.saveDiscount(); verify(view).showNameRequiredWarning(); assertThat(isSavingInBackground()).isFalse(); } 

Backstack management


We wrote the Flow library to make it easier for us to work with backstack, and Ray Rayan wrote a very good article about it. Without going into particular details, I would say that the code turned out to be quite simple, since asynchronous transactions are no longer needed.

I'm deeply stuck in spaghetti fragments, what should I do?


Take out of the fragments all that is possible. The code related to the interface should go into your own views, and the business logic should be removed to presenters who know how to work with your views. After this, you will have an almost empty fragment that creates your own presentations (and they, in turn, know how and with what presenters to associate themselves):

 public class DetailFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.my_detail_view, container, false); } } 

Everything, the fragment can be deleted.

Migrating from fragments was not easy, but we went through it - thanks to the excellent work of Dimitris Koutsogiorgas and Ray Ryan .

And what are Dagger and Mortar for?


Both of these libraries are perpendicular to fragments: they can be used both with fragments and without them.

Dagger allows you to design your application as a graph of disconnected components. Dagger takes care of linking the components together, thus making it easy to extract dependencies and write classes with a single responsibility.

Mortar runs on top of Dagger, and it has two important advantages:


Conclusion


We intensively used fragments, but over time we changed our mind and got rid of them:

Source: https://habr.com/ru/post/277289/


All Articles