Understanding Fragments in Android: Part 2

Written by azamatnurkhojayev | Published 2022/10/29
Tech Story Tags: android | android-app-development | androiddev | kotlin | android-tutorial | android-development | mobile-app-development | tutorial

TLDRIn 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. We can describe transactions in a DSL style and the functions `beginTransaction()` and `commit()` or `commitAllowStateLoss()` are called under the hood. 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.via the TL;DR App

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"
}

Fragment-ktx

FragmentManager

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
}

FragmentTransaction

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)
}

Transaction optimization

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:

  • do not create a fragment if it is replaced in the same transaction;
  • interrupt the life cycle at any time before RESUMED, if a transaction to replace the added fragment has begun;
  • cause 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.

Change visibility

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) 
  }
}

Destroying the View

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) 
  }
}

FragmentFactory

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:

  • before super.onCreate();
  • in the 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) {
    …
  }
}

LayoutId in the constructor

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
}

Summary

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.


Written by azamatnurkhojayev | I'm an Android developer. Teaches courses at the programming school, and writes articles about development.
Published by HackerNoon on 2022/10/29