New Things in Android Fragments

Kacper Kogut
6 min readNov 15, 2019

--

Photo by Patrick Tomasso on Unsplash

Many of Android Developers had bad experiences using Fragments. There are many issues connected to them, starting from the lifecycle and ending on animations. Fortunately, the Android team addressed some of these problems in the two latest releases of the Fragments library. The newest version is still the Release Candidate, but in this article, I want to show what functionalities it will bring and what will be the future of Fragments.

Getting started

I’ve decided to check out these new features in a simple project. It allows creating and replacing Fragment in the old way, and the new one. On every Fragment, there will be displayed its number, which is fetched from a mocked service.

To start working with new features that I will be explaining, new dependencies have to be added to build.gradle file:

dependencies {
def fragment_version = "1.1.0"

// For Java
implementation "androidx.fragment:fragment:$fragment_version"
// For Kotlin
implementation "androidx.fragment:fragment-ktx:$fragment_version"
// For testing fragments
implementation "androidx.fragment:fragment-testing:$fragment_version"
}

Fragment Container

One of the most important things introduced was a new view for holding Fragments, called FragmentContainer. Until now, when we wanted to add a single Fragment to our layout, we had to use the <fragment> tag. Since many Android developers treat it as an antipattern and instead of using it they would rather inflate FrameLayout with desired fragment, we can say that until now, there has not been a dedicated and well-working XML view for storing Fragments.

FragmentContainer extends FrameLayout but allows only fragments as its children. Except for being one and true container for the Fragments, it also resolves the issues with animations between each transition.

In the project that I have created, I have added custom animation between each transition, to check how these components will behave.

parentFragmentManager.commit {
setCustomAnimations(
R.anim.enter_from_right,
R.anim.exit_to_left,
R.anim.enter_from_left,
R.anim.exit_to_right
)
replace(R.id.fragment, BaseFragment)
addToBackStack(null)
}

Below are the results of this transaction using these two components.

Pay attention to the bottom of each view, especially the button. It’s easy to see that the transition in FragmentContainer looks and works much better. This happens thanks to fixes with z-ordering of Fragments in the FragmentContainer.

Similarly to <fragment> tag, FragmentContainer allows us to use the class tag, to inflate view with the desired Fragment. But unlike the old way, FragmentContainer uses FragmentTransaction for adding Fragments.

<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment"
android:name="com.example.fragmentsexample.feature.common.BaseFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

Fragment Factory

Until now, whenever you wanted to create a new Fragment you had to call its no-argument constructor and put all the variables in a Bundle, which should be then passed as the Fragment arguments using setArguments setter. It was advised to do it this way because whenever the Android system would recreate this Fragment it will call this empty constructor.

Bundle works fine if you want to pass some object or list to Fragment. But this requirement makes it impossible to implement constructor Dependency Injection.

To start working with FragmentFactory you need to create a class which will create an instance of fragments based on their class name:

class BaseFragmentFactory : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String) =
when (className) {
BaseFragment::class.java.name -> BaseFragment()
SecondFragment::class.java.name -> SecondFragment()
else -> super.instantiate(classLoader, className)
}
}

Then, in an Activity that will show these Fragments, you should attach created FragmentFactory:

class ContainerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = BaseFragmentFactory()
super.onCreate(savedInstanceState)
}
}

Now, whenever you want to replace or add Fragments, you can do it via its class name, rather than its instance, thanks to which Fragment can have as many parameters in the constructor as you want:

parentFragmentManager.commit {
replace<BaseFragment>(R.id.fragment_container)
}

On back pressed dispatcher

When I was implementing some app a long time ago, I wanted to listen for back press events in Fragment. To achieve this, I had to send an event from Activity to my target class from onBackPressed method. It was annoying when I had to do this many times, and it generated a lot of boilerplate code.

Fortunately, now we can make use of BackPressedDispacher which could be used in any component that can get an instance of its Activity:

lateinit var dispatcher : OnBackPressedDispatcher
lateinit var callback: OnBackPressedCallback

override fun onAttach(context: Context) {
super.onAttach(context)
dispatcher = requireActivity().onBackPressedDispatcher
callback = dispatcher.addCallback(
//Lifecycle owner
this
) {
fragmentService.fragmentsCount--

//Called when user should be navigated back
callback.isEnabled = false
dispatcher.onBackPressed()
}
}

In the following example, on every back press, I am decrementing fragment number and then calling onBackPressed to navigate the user back. Calling dispatcher methods can be done anywhere in code, so for example, you can show a confirmation dialog at back arrow press and after user clicks on confirm — call the onBackPressed method.

Both of these use cases contain the same view, and as you can see, it doesn’t work as it supposed to with old <fragment> tags. After pressing the next button, then returning and once again pressing the next button Fragment number should be shown as 2, but instead, it is displayed as 1. Both examples use the same Fragment class, and since it only works as it supposed to with FragmentContainer, it is another reason to use them instead of the old <fragment> tag.

Alternatively, you can inject the OnBackPressedDispatcher from Activity to Fragment. A similar example was presented at Android Dev Summit.

Fragment Scenario

FragmentScenario allows you to isolate a single fragment and test how it will behave on click, recreation, state changes, etc. Thanks to the usage of FragmentFactory we can simply create FragmentScenario with MockFactory, which will provide mocked dependencies.

val scenario = 
launchFragmentInContainer<BaseFragment>(factory = MockBaseFragmentFactory())

To test each of the use-cases you have to call an appropriate method on the created scenario. Test assertions could be done with onFragment method.

//Move Fragment to onCreated state
scenario.moveToState(State.CREATED)

//Recreate Fragment
scenario.recreate()

//Check fragment on click
onView(withId(R.id.nextFragmentButton)).perform(click())

//Test assertions
scenario.onFragment{ fragment ->
//Check if fragment responded as it should
}

Future of Fragments

One more thing that was addressed at Android Dev Summit was the future of Fragments. The features, that they described are not yet available to be tested, but they look very promising. Since these are still things, that they are working on, you have to keep in mind that their plans might still change.

Multiple back stacks — Until now, there was one stack, that was responsible for holding fragments that had been started one after another. This approach made working with things that navigate to different Fragments in parallel on one screen (like BottomNavigationView) very painful. The solution that the Android team will propose is to have multiple back-stacks, each connected to starting fragments. Thanks to this, the state of all Fragments will be stored.

Returning results — For now, if you want to pass data from one fragment to another, you were supposed to use targetFragment, and keep the hard reference to it. Since we did not know in which state the referenced Fragment will be, this solution generated plenty of problems. Android team will be working on improving the method onResult so that you could receive results from different components, not only from Activity to Activity.

Fragment lifecycle — Now there are two lifecycles connected to each Fragment. What the Android team is trying to achieve is to merge these two lifecycles into one, based on the view lifecycle. So when the view is destroyed, the Fragment lifecycle will be destroyed as well.

Summary

I think that this update is a big step forward in dealing with Fragments in Android applications. Finally, developers have a dedicated view to store Fragments, and finally, they can inject variables in its constructor.

These changes might not look like a real game-changer, but it’s good to know that people working on the Android platform paid attention to occurring issues, and made work easier for Android developers. I’m excited for the future of Fragments, and I think that you should too.

Useful links

Android Dev Summit presentation

AndroidX official documentation and changelog

Sample project source code

Originally published at https://www.netguru.com.

--

--