cottage

Multiple Fragments as Children in Android Apps

December 3, 2021
3 minute read
label

Searching for “adding fragments programmatically” will yield plenty of answers for replacing a fragment using transactions, but you won’t find a lot of guidance if you need to add fragments into a list of an unknown size. (I ran into this problem while implementing dialogs which might need to stack on top of another.) I thought I would share my solution for this problem along with some pitfalls that I ran into while attempting to engineer a solution.

Adding Multiple Fragments

In the activity layout XML, we can add a View such as a LinearLayout or a FrameLayout to act as a container for our fragments. Give this view an android:id so we can reference it later in the activity class.

<FrameLayout
  android:id="@+id/dialog_container"
  android:layout_width="match_parent"
  android:layout_height="match_parent"/>

Once the container is in the layout, we can now reference it using its android:id. This will come in handy later once we start adding our fragment container views.

findViewById(R.id.dialog_container);

Adding the fragment container view

As you probably know, a view can have children. We can add a FrameLayout as a child to this container for each fragment.

FrameLayout dialogFrame = new FrameLayout(this);
mDialogContainer.addView(dialogFrame);

At this point, the tree looks something like this:

- Activity
-- FrameLayout // @id/dialog_container
--- FrameLayout // The dialogFrame we just added

But to use a FragmentTransaction to add our fragment to this FrameLayout, we need the ID of the layout.

FragmentManager manager = activity.getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction
  .replace(containerId, fragment)
  .commit();

Note the containerId in the replace() call.

Container view ID

You can use View.getId() to get the ID of any View. But, by default, instantiating a new View does not set its own ID and getId() will return View.NO_ID. We are going to have to set this ID ourselves, but there are some things to consider:

  • View IDs need to be unique (at least per fragment/activity) - searching with findViewById() will become ambiguous otherwise.
  • View IDs set for views in layout files are dynamically generated at compile-time.

These two factors make it difficult to generate our own view IDs without some amount of integration with the resource library - we don’t any collisions with existing IDs. This is where View.generateViewId() comes in. This static function will generate an ID that is guaranteed to not already be in the R.id that was generated at compile-time.

FrameLayout dialogFrame = new FrameLayout(this);
int containerId = View.generateViewId();
dialogFrame.setId(containerId);
mDialogContainer.addView(dialogFrame);

From this point, we can use the containerId to reference the FrameLayout that we want the FragmentTransaction to add our fragment to.

After a couple fragments are added this way, the tree will look like this:

- Activity
-- FrameLayout // @id/dialog_container
--- FrameLayout
---- Fragment
--- FrameLayout
---- Fragment
--- FrameLayout
---- Fragment
--- ...

Pitfalls

Cryptic exceptions

Something I ran into while implementing this solution was a rather cryptic exception thrown from the bowels of the Android lifecycle:

IllegalStateException: Restarter must be created only during owner’s initialization stage

It turns out that the fragment being added to the FrameLayout must be a new instance for each new FrameLayout, even if the old one no longer exists. At least, that was my solution to this exception.

Scoping IDs in your fragment layout

Another interesting bug that you might run into involves views in your fragment layout that share the same ID. If multiple instances of the same fragment are being added to your activity tree, that means all the views in your fragment layout that have an android:id now share the same ID. This can be problematic if you use findViewById() as opposed to a binding. If you do use findViewById() in your fragment, make sure you call it from the scope of the fragment view as opposed to the parent activity, or you’ll run into some unintended behaviors. In my case, touch input was being passed to the first fragment added as opposed to the current fragment.