Multiple Fragments as Children in Android Apps
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.