Fragments, commonly known as fragments, have been introduced and widely used since Android 3.0. However, it is such a familiar thing, in the development we will still meet a variety of problems, emerge in endlessly. So, it’s time to wrap up the wave.

Introduction of fragments

As part of the Activity interface, a Fragment must be attached to an Activity and, like an Activity, have its own life cycle and process user interactions. The same Activity can have one or more fragments as interface content, and can dynamically add and delete fragments to flexibly control UI content, and can also be used to solve part of the screen adaptation problem.

In addition, the support V4 package also provides fragments, compatible with Android 3.0 before the system (of course, now 3.0 before the market is very rare, can not consider), use compatible package note two points:

  • Activities must inherit from FragmentActivity;

  • Use the getSupportFragmentManager () method to get FragmentManager object;

The life cycle

Most of the lifecycle functions that an Activity has as part of a host Activity also exist in the Fragment and are synchronized with the Activity. Also, as a special case, fragments have their own life-cycle functions, such as onAttach(), onCreateView(), etc.

As for the synchronization between activities and fragments lifecycle functions, GitHub’s XXV/Android-Lifecycle project illustrates this perfectly:

The meaning of each Fragment Lifecycle function is not described here. You can refer to the introduction of Fragment Lifecycle on the website.

Create an instance

Like a normal class, a Fragment has its own constructor, so you can create a Fragment instance in an Activity like this:

MainFragment mainFragment = new MainFragment();Copy the code

If you need to pass parameters when creating a Fragment instance for initialization, you can create a constructor with parameters and initialize Fragment member variables, etc. This may seem fine, but it can be problematic in some special cases.

We know that an Activity can destroy and recreate under special circumstances, such as when the screen is rotated or memory is tight; Similarly, a similar situation can occur with fragments that are attached to existing activities. Once re-created, the Fragment calls the default no-argument constructor, making it impossible to initialize the Fragment using the parameterized constructor.

Fortunately, Fragment provides an API to help us solve this problem. Using the bundle to pass data, reference code is as follows:

public static OneFragment newInstance(int args){
    OneFragment oneFragment = new OneFragment();

    Bundle bundle = new Bundle();
    bundle.putInt("someArgs", args);

    oneFragment.setArguments(bundle);
    return oneFragment;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Bundle bundle = getArguments();
    int args = bundle.getInt("someArgs");
}Copy the code

Embed mode

Activity embedded Fragment can be divided into two types: static embedded layout and dynamic embedded code. The former uses the tag to embed the Fragment in the Activity’s Layout, as in:




    

Copy the code

The latter replaces the corresponding container Layout in the Activity’s Java code with the replace() method provided by the FragmentManager and FragmentTransaction classes. For example:

FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fl_content, OneFragment.newInstance());
ft.commit();Copy the code

The Fragment life cycle corresponding to these two embedding methods is slightly different, as can be seen from the life cycle diagram. Dynamic code embedding is more commonly used than static layout embedding, because the latter can flexibly control multiple fragments and dynamically change the content in an Activity.

getChildFragmentManager()

As above, when an Activity is embedded with a Fragment, you need to use the FragmentManager, which you can get from the getFragmentManager() method provided by the Activity. Manage all level 1 Fragments embedded in your Activity.

Sometimes, however, we will continue to nest secondary or even tertiary fragments within a Fragment, that is, activities that nest multiple levels of fragments. In this case, you need to use the FragmentManager to manage sub-fragments in the Fragment. But be sure to use the getChildFragmentManager() method to get the FragmentManager object!

You can also see the difference between the FragmentManager objects obtained by these two methods from the official documentation comments:

Activity: getFragmentManager ()

Return the FragmentManager for interacting with fragments associated with this activity.

Fragments: getChildFragmentManager ()

Return a private FragmentManager for placing and managing Fragments inside of this Fragment.

FragmentTransaction

The FragmentTransaction class is used to dynamically add and remove fragments, such as the replace() operation mentioned above. FragmentTransaction Provides a variety of methods for developers to manipulate fragments in an Activity. For details, see the FragmentTransaction Public Methods website.

  • Add () series: Add fragments to the Activity screen;

  • Remove () : removes the specified Fragment from the Activity;

  • Replace () : Modify the Fragment with internal calls to remove() and add();

  • Hide () and show() : Hide and display fragments in an Activity;

  • AddToBackStack () : Adds the current transaction to the fallback stack, that is, when the back key is pressed, the interface returns to the current state of things;

  • Commit () : Commit the transaction. All changes to the Fragment must be made by calling commit().

Note: Dynamically switching between multiple fragments in an Activity can be done using replace(), hide(), and show() methods. In fact, we prefer to use the latter because the replace() method does not preserve the Fragment state, meaning that user operations such as EditText content input disappear when removed (). Of course, if you don’t want to keep user actions, you can choose the former, as the case may be.

BackStack

AddToBackStack () saves the current transaction, and when the user presses the return key, if the previous transaction is saved in the rollback stack, the transaction rollback is performed instead of finishing the current Activity.

For example, there is a new user registration function in the App, including the process of setting user name, password, mobile phone number and so on. In the UI design, the designer designs each process into a separate interface to guide users to operate step by step. As a developer, it would be tedious to set up each information-perfecting process as a separate Activity, and it would be difficult to handle the logic in the application. However, using fragments combined with a rollback is quite appropriate.

Write the flow of each set as a Fragment, display different fragments through state control, and use the function of back stack to return to the previous step. For example, when entering SecondStepFragment from FirstStepFragment, you can do this in loginActivity.java:

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.hide(firstStepFragment);
if (secondStepFragment==null){
    ft.add(R.id.fl_content, secondStepFragment);
}else {
    ft.show(secondStepFragment);
}
ft.addToBackStack(null);
ft.commit();Copy the code

Note: We use the hide() method instead of the replace() method, because of course we want the previous Settings not to disappear when the user goes back to the previous step.

Communication mode

Generally, there are three ways for a Fragment to communicate with an Activity: an Activity operates on an embedded Fragment, a Fragment operates on a host Activity, or a Fragment operates on another Fragment within the same Activity.

Since the Activity holds all instances of the embedded Fragment object (the Fragment object saved when the instance was created), Or the findFragmentById() and findFragmentByTag() methods provided by the FragmentManager class), so you can manipulate the Fragment directly. Fragment The Fragment method getActivity() can be used to retrieve the host Activity object (cast type) and manipulate the host Activity. Naturally, then, the Fragment that gets the host Activity object can operate on other Fragment objects.

Although the above operation can solve the communication problem between Activity and Fragment, it will cause the result of disordered code logic, which is extremely inconsistent with this programming idea: high cohesion, low coupling. Fragments do their own thing. All operations involving display and control between fragments should be managed by the host Activity.

Therefore, we strongly recommend using an open interface to pass some external operations of the Fragment to the host Activity. The specific implementation is as follows:

public class OneFragment extends Fragment implements View.OnClickListener{ public interface IOneFragmentClickListener{ void onOneFragmentClick(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View contentView = inflater.inflate(R.layout.fragment_one, null); contentView.findViewById(R.id.edt_one).setOnClickListener(this); return contentView; } @Override public void onClick(View v) { if (getActivity() instanceof IOneFragmentClickListener){ ((IOneFragmentClickListener) getActivity()).onOneFragmentClick(); }}}Copy the code

As long as the host Activity implement IOneFragmentClickListener fragments defined external interface, can realize the fragments to invoke the function of the Activity.

Of course, you can do this:

public class OneFragment extends Fragment implements View.OnClickListener{ private IOneFragmentClickListener clickListener; public interface IOneFragmentClickListener{ void onOneFragmentClick(); } public void setClickListener(IOneFragmentClickListener clickListener) { this.clickListener = clickListener; } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View contentView = inflater.inflate(R.layout.fragment_one, null); contentView.findViewById(R.id.edt_one).setOnClickListener(this); return contentView; } @Override public void onClick(View v) { clickListener.onOneFragmentClick(); }}Copy the code

The principle is the same, except that compared to the first method, you need to add an additional listening setting to the host Activity:

oneFragment.setClickListener(this);
Copy the code

GetActivity () reference problem

Use getActivity() to retrieve the host Activity object in your Fragment, but this can cause two problems:

First, the Activity instance destruction problem. For example, if there is an asynchronous, time-consuming task in a Fragment, such as a network request, the host Activity object may have been destroyed by the time the Fragment method is called back to the host Activity object. NullPointException and other exceptions are raised, and the program crashes. Therefore, asynchronous callbacks need to add null values and other judgments (e.g., fragment.isadd (), getActivity()! GetActivity ().getApplicationContext(); getActivity().getApplicationContext();

Second, memory leaks. If the Fragment holds a reference to the host Activity, the host Activity cannot be reclaimed, causing a memory leak. So, if possible, try not to hold references to the host Activity in your Fragment.

To solve the problem of Context references, the Fragment provides an onAttach(Context) method, in which we can retrieve the Context object, such as:

@Override
public void onAttach(Context context) {
    super.onAttach(context);
    this.context = context;
}Copy the code

Fragment overlap problem

When the onCreate() method is re-executed, the Fragment will be created and displayed again. The existing Fragment instance will be destroyed and created again, so it will be displayed at the same time as the second Fragment created in the onCreate() method.

As a rule of thumb, we usually set the Activity to landscape mode in AndroidManifest, so there is no screen rotation that causes this problem. A common occurrence is when the application is in the background for a long time, but the Activity is destroyed due to memory constraints on the device, and when the user opens the application again, the Fragment overlaps. This problem is hard to see in the development phase due to the frequent use of applications, but it does exist. Pay attention to these issues during development.

Once you know the root of the problem, you have a solution. When creating a Fragment instance in an Activity, add a judgment to the Fragment instance.

The first approach is handled in the onAttachFragment() method provided by the Activity:

@Override public void onAttachFragment(Fragment fragment) { super.onAttachFragment(fragment); if (fragment instanceof OneFragment){ oneFragment = (OneFragment) fragment; }}Copy the code

The second way is to add a judgment before creating a Fragment to check whether it already exists:

Fragment tempFragment = getSupportFragmentManager().findFragmentByTag("OneFragment");
if (tempFragment==null) {
    oneFragment = OneFragment.newInstance();
    ft.add(R.id.fl_content, oneFragment, "OneFragment");
}else {
    oneFragment = (OneFragment) tempFragment;
}Copy the code

The third way, which is simpler, can be directly judged by savedInstanceState:

if (savedInstanceState==null) {
    oneFragment = OneFragment.newInstance();
    ft.add(R.id.fl_content, oneFragment, "OneFragment");
}else {
    oneFragment = (OneFragment) getSupportFragmentManager().findFragmentByTag("OneFragment");
}Copy the code

onActivityResult()

The Fragment class provides the startActivityForResult() method for jumping between activities and sending back data. It is also the internal method for calling activities. Note, however, that the Fragment does not provide the setResult() method when the page returns, which can be implemented by hosting the Activity.

For example, call startActivityForResult() from FragmentA in ActivityA to jump to ActivityB and return to ActivityA from FragmentB in ActivityB, The return code is as follows:

Intent intent = new Intent();
// putExtra
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().finish();Copy the code

The onActivityResult() method in ActivityA is called back, and the onActivityResult() method in FragmentA is then distributed.

/** * Dispatch incoming result to the correct fragment. */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { mFragments.noteStateNotSaved(); int requestIndex = requestCode>>16; if (requestIndex ! = 0) { requestIndex--; String who = mPendingFragmentActivityResults.get(requestIndex); mPendingFragmentActivityResults.remove(requestIndex); if (who == null) { Log.w(TAG, "Activity result delivered for unknown Fragment."); return; } Fragment targetFragment = mFragments.findFragmentByWho(who); if (targetFragment == null) { Log.w(TAG, "Activity result no fragment exists for who: " + who); } else { targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data); } return; } super.onActivityResult(requestCode, resultCode, data); }Copy the code

To extend this, if FragmentA inserts another layer of FragmentAA, and then jumps from FragmentAA to ActivityB, does the onActivityResult() method in FragmentAA receive a callback? Obviously not. As you can see from the source code above, FragmentActivity is only distributed at one level. So, if you want to achieve multilevel distribution, you have to manually add distribution code to each Fragment and then to the next.

State transition monitor

Fragment hide, show and other state changes are reflected in the corresponding callback functions, we can use these listener functions to do some interface refresh and other functions. One of the more common listener functions is the onHiddenChanged() method, changes to which directly affect the return value of the isHidden() method.

In addition to the isHidden() method, there is also an isVisible() method, which is also used to determine the state of the Fragment, indicating whether the Fragment isVisible to the user. If true, three conditions must be met: 1, Fragment has been added to the Activity; 2. The view content has been associated with the window. 3. Not hidden, that is, isHidden() is false. These three points can be seen from isVisible() source code:

/** * Return true if the fragment is currently visible to the user. This means * it: (1) has been added, (2) has its view attached to the window, and * (3) is not hidden. */ final public boolean isVisible() { return isAdded() && ! isHidden() && mView ! = null && mView.getWindowToken() ! = null && mView.getVisibility() == View.VISIBLE; }Copy the code

Note: The onHiddenChanged() method listens for hide() and show() operations, unlike the setUserVisibleHint() method, The latter is commonly used in FragmentPagerAdapter with a combination of ViewPager and Fragment. This method is used to change the state of the Fragment when the ViewPager slides. This method can be used to implement lazy loading of the Fragment, which will be described in more detail in a future article.

Refer to the link

As you can see from the above introduction, fragments are very convenient to use, but they have many problems. After using them for a long time, you will find that there are endless holes waiting for you. Of course, there’s a solution for every pit, Google it, pop it up everywhere, and you’ll always find what you need. These articles, for example, are full of dry stuff:

  • Android Fragment fully parses, everything you need to know about fragments

  • Android Fragment Everything you need to know

  • Holes you stepped on in those years