In this article, we will analyze the interesting points of the Fragment API, I think that it will be of interest to all developers who develop an application for Android.
First, you need to add dependencies:
dependencies {
def fragment_version = "1.5.4"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
}
Now you can describe transactions in a DSL style and the functions beginTransaction()
and commit()
or commitAllowStateLoss()
are called under the hood:
fun FragmentManager.commit(
allowStateLoss: Boolean = false,
block: FragmentTransaction.() -> Unit
)
Example:
fragmentManager.commit {
// transaction
}
Added replacement for FragmentTransaction.add(Int, Class<out Fragment>, Bundle?)
method overloads. A similar extension has been added for FragmentTransaction.replace()
:
fun <reified T: Fragment> FragmentTransaction.add(
containerId: Int,
tag: String? = null,
args: Bundle? = null
): FragmentTransaction
fun <reified T: Fragment> FragmentTransaction.replace(
containerId: Int,
tag: String? = null,
args: Bundle? = null
): FragmentTransaction
Example:
fragmentManager.commit {
val args = bundleOf("key" to "value")
add<FragmentA>(R.id.fragment_container, "tag", args)
replace<FragmentB>(R.id.fragment_container)
}
Optimization is a pretty important thing. To figure out how FragmentManager can do everything for us, let's try to optimize transactions by hand.
fragmentManager.commit {
add<FragmentA>(R.id.fragment_container)
replace<FragmentB>(R.id.fragment_container)
replace<FragmentC>(R.id.fragment_container)
}
How to speed up a transaction? During the most complex technical analysis, we see that, following its results, the user will see FragmentC
. We are simple people and just throw out the extra two actions, immediately showing FragmentC
. Another example is with two transactions running one after the other:
// 1
fragmentManager.commit {
add<FragmentA>(R.id.fragment_container)
}
// 2
fragmentManager.commit {
replace<FragmentB>(R.id.fragment_container)
}
In this case, we could abort the addition of FragmentA
and immediately add FragmentB
. We cannot do this, but the problem is rather theoretical. All of the above FragmentManager can do on its own. We just need to allow it by adding setReorderingAllowed(true)
to the transaction we want to optimize.
fragmentManager.commit {
setReorderingAllowed(true)
add<FragmentA>(R.id.fragment_container)
replace<FragmentB>(R.id.fragment_container)
replace<FragmentC>(R.id.fragment_container)
}
In the second place, you need to set the focus in the first place, because it is her permission to interrupt, and the second, in turn, must be fully controlled:
// 1
fragmentManager.commit {
setReorderingAllowed(true)
add<FragmentA>(R.id.fragment_container)
}
// 2
fragmentManager.commit {
replace<FragmentB>(R.id.fragment_container)
}
In fact, we allow the FragmentManager
to behave lazily and not execute unnecessary commands, which gives some performance gain. Moreover, it helps to handle animations, transitions, and back stack correctly.
It is worth remembering that an optimized FragmentManager
can:
RESUMED
, if a transaction to replace the added fragment has begun;onCreate()
of the new fragment to be called before onDestroy()
of the old one.
During a transaction, there are several ways to manage the life cycle of a fragment, in some cases, this may be useful.
We can hide and show a fragment without changing its lifecycle state. This behavior is similar to View.visibility = View.GONE
and View.visibility = View.VISIBLE
.
We can just hide the container. This is true, but hiding the container in the back stack will not work, and a transaction with a similar command is easy. To hide a fragment from prying eyes, just call the FragmentTransaction.hide(Fragment)
method:
fragmentManager.commit {
setReorderingAllowed(true)
fragmentManager.findFragmentById(R.id.fragment_container)?.let {
hide(it)
}
}
To show it again, you need to call the FragmentTransaction.show(Fragment)
method:
fragmentManager.commit {
setReorderingAllowed(true)
fragmentManager.findFragmentById(R.id.fragment_container)?.let {
show(it)
}
}
We can destroy the Fragment's View, but not the Fragment
itself, by calling the FragmentTransaction.detach(Fragment)
method. As a result of such a transaction, the fragment will enter the STOPPED
state:
fragmentManager.commit {
setReorderingAllowed(true)
fragmentManager.findFragmentById(R.id.fragment_container)?.let {
detach(it)
}
}
To recreate the Fragment View, just call the FragmentTransaction.attach(Fragment)
method:
fragmentManager.commit {
setReorderingAllowed(true)
fragmentManager.findFragmentById(R.id.fragment_container)?.let {
attach(it)
}
}
In Fragments 1.1.0, we can control the creation of fragment instances, including adding any parameters and dependencies to the constructor.
To do this, it is enough to replace the standard implementation of FragmentFactory
with our own, where we are our kings and gods.
fragmentManager.fagmentFactory = MyFragmentFactory(Dependency())
The main thing is to have time to replace the implementation before the FragmentManager
needs it, that is, before the first transaction and restoring the state after re-creation. The sooner we replace the bad implementation, the better.
For Activity, the best scenario would be to replace:
super.onCreate()
;init
block.
Fragments do not immediately have access to their FragmentManager
. Therefore, we can only perform the substitution between onAttach()
and onCreate()
inclusive, otherwise, we will see terrible red text in the logs after launch.
But it's important to remember that parentFragmentManager
is the FragmentManager
through which the commit was made.
Therefore, if you previously replaced FragmentFactory
in it, you do not need to do this a second time.
Now let's figure out how we can implement our factory. We create a class, inherit from FragmentFactory
, and override the instantiate()
method.
class MyFragmentFactory(
private val dependency: Dependency
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when(className) {
FirstFragment::class.java.name -> FirstFragment(dependency)
SecondFragment::class.java.name -> SecondFragment()
else -> super.instantiate(classLoader, className)
}
}
}
We get a classLoader
as input, which can be used to create a Class<out Fragment>
, and className
is the full name of the desired fragment. We determine which fragment we need to create and return based on the name. If we do not know such a fragment, we transfer control to the parent implementation.
This is what super.instantiate()
looks like under the hood of FragmentFactory
:
open fun instantiate(classLoader: ClassLoader, className: String): Fragment {
try {
val cls: Class<out Fragment> = loadFragmentClass(classLoader, className)
return cls.getConstructor().newInstance()
} catch (java.lang.InstantiationException e) {
…
}
}
Let's remember how we learned to work with fragments. We create a class, create a markup file, and inflate it in onCreateView()
:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_example, container, false)
We've typed these familiar strings hundreds of times, but in version 1.1.0 of Fragments
, the folks at Google have decided they won't take it anymore. They added a second constructor to fragments that take @LayoutRes
as input, so you no longer need to override onCreateView()
.
class ExampleFragment : Fragment(R.layout.fragment_example)
And under the hood, the same boilerplate works:
constructor(@LayoutRes contentLayoutId: Int) : this() {
mContentLayoutId = contentLayoutId
}
open fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) : View? {
if (mContentLayoutId != 0) {
return inflater.inflate(mContentLayoutId, container, false)
}
return null;
}
The less code we have to write, the better.
If suddenly you have previously initialized the View in onCreateView()
, it is more correct to use a special onViewCreated()
callback called immediately after onCreateView()
.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
button.setOnClickListener {
// do something
}
// some view initialization
}
This article looked at the features and new features of creating Fragments and how you can pass LayoutId in the Fragment constructor. In the next part of the article, we will look at animations during the transition and closing of the Fragment.