Detailed explanation of changes in using Activity and Fragment under AndroidX

  • 2021-11-30 01:37:25
  • OfStack

In the past period of time, many changes have taken place in API of Activity/Fragmet under AndroidX software package. Let's see how they improve the efficiency of Android development and how they adapt to the current popular programming rules and patterns.

All of the features described in this article are now available in stable AndroidX packages, which were released or moved to stable versions last year.

Pass in the layout ID in the constructor

Starting with AndroidX AppCompat 1.1. 0 and Fragment 1.1. 0, you can use constructors that take layoutId as arguments:


class MyActivity : AppCompatActivity(R.layout.my_activity)
class MyFragmentActivity: FragmentActivity(R.layout.my_fragment_activity)
class MyFragment : Fragment(R.layout.my_fragment)

This approach reduces the number of method overrides in Activity/Fragment and makes the class more readable. You can call the setContentView () method without overriding onCreate () in Activity. In addition, you can manually call Inflater to extend the view without having to manually override onCreateView in Fragment.

Extend the flexibility of Activity/Fragment

With the new API of AndroidX, it is possible to reduce the processing of certain functions in Activity/Fragment. Typically, instead of overriding the methods in Activity/Fragment, you can get an object that provides some functionality and register your processing logic with it. In this way, you can now compose several separate classes on the screen, gain more flexibility, reuse code, and have more control over code structure, usually without introducing your own abstractions. Let's see how this works in two examples.

OnBackPressedDispatcher

Sometimes, you need to prevent users from returning to level 1. In this case, you need to override the onBackPressed () method in Activity. However, when you use Fragment, there is no direct way to intercept the return. There is no onBackPressed () method available in the Fragment class to prevent unexpected behavior when multiple Fragment exist at the same time.

However, starting with AndroidX Activity 1.0. 0, you can use OnBackPressedDispatcher to register OnBackPressedCallback anywhere (for example, in Fragment) that you can access the code of the Activity.


class MyFragment : Fragment() {
 override fun onAttach(context: Context) {
  super.onAttach(context)
  val callback = object : OnBackPressedCallback(true) {
   override fun handleOnBackPressed() {
    // Do something
   }
  }
  requireActivity().onBackPressedDispatcher.addCallback(this, callback)
 }
}

You may notice two other useful features here:

Boolean-type parameters in the constructor of OnBackPressedCallback help to dynamically turn the press on/off according to the current state

The optional first parameter of the addCallback () method is LifecycleOwner to ensure that the callback is only used if your lifecycle-aware object (for example, Fragment) is at least in the STARTED state.

By using OnBackPressedDispatcher, you can not only get a convenient way to handle the return key outside of Activity. Depending on your needs, you can define the OnBackPressedCallback anywhere, make it reusable, or do anything depending on the architecture of your application. You no longer need to override the onBackPressed method in Activity, nor do you need to provide your own abstract code to implement the requirements.

SavedStateRegistry

If you want Activity to return to its previous state after it is terminated and restarted, you may want to use the saved state functionality. In the past, you needed to override two methods in Activity: onSaveInstanceState and onRestoreInstanceState. You can also access the recovered state in the onCreate method. Similarly, in Fragment, you can use the onSaveInstanceState method (and restore state in onCreate, onCreateView, and onActivityCreated methods).

Starting with AndroidX SavedState 1.0. 0 (which is an internal dependency of AndroidX Activity and AndroidX Fragment), you can access SavedStateRegistry, which uses a mechanism similar to the OnBackPressedDispatcher described earlier: You can get SavedStateRegistry from Activity/Fragment and then register your SavedStateProvider:


class MyActivity : AppCompatActivity() {

 companion object {
  private const val MY_SAVED_STATE_KEY = "my_saved_state"
  private const val SOME_VALUE_KEY = "some_value"
 }

 private lateinit var someValue: String

 private val savedStateProvider = SavedStateRegistry.SavedStateProvider {
  Bundle().apply {
   putString(SOME_VALUE_KEY, someValue)
  }
 }

 override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  savedStateRegistry
   .registerSavedStateProvider(MY_SAVED_STATE_KEY, savedStateProvider)
 }

 fun someMethod() {
  someValue = savedStateRegistry
   .consumeRestoredStateForKey(MY_SAVED_STATE_KEY)
   ?.getString(SOME_VALUE_KEY)
   ?: ""
 }
}

As you can see, SavedStateRegistry forces you to use the key for data. This prevents your data from being corrupted by an attach to another SavedStateProvider of the same Activity/Fragment. Just like 1 in OnBackPressedDispatcher, you can, for example, extract SavedStateProvider into another class and use it with Data 1 using any logic you need to achieve clear saved state behavior in your application.

In addition, if you use ViewModel in your application, consider using AndroidX ViewModel-SavedState so that your ViewModel can save its state. For convenience, starting with AndroidX Activity 1.1. 0 and AndroidXFragment 1.2. 0, SavedStateViewModelFactory with SavedState enabled is the default factory used in all ways to get ViewModel: delegate the ViewModelProvider constructor and the ViewModelProviders. of () method.

FragmentFactory

One of the most common problems with Fragment is that you cannot use constructors with arguments. For example, if you use Dagger2 for dependency injection, you cannot annotate the Fragment constructor with Inject and specify parameters. You can now reduce similar problems in the Fragment creation process by specifying the FragmentFactory class. By registering FragmentFactory in FragmentManager, you can override the default method of instantiating Fragment:


class MyFragmentFactory : FragmentFactory() {

 override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
  // Call loadFragmentClass() to obtain the Class object
  val fragmentClass = loadFragmentClass(classLoader, className)

  // Now you can use className/fragmentClass to determine your prefered way
  // of instantiating the Fragment object and just do it here.

  // Or just call regular FragmentFactory to instantiate the Fragment using
  // no arguments constructor
  return super.instantiate(classLoader, className)
 }
}

As you can see, this API is very generic, so you can do whatever you want to create an instance of Fragment. Going back to the Dagger2 example, for example, you can inject FragmentFactory Provider < Fragment > And use it to get an Fragment object.

Test Fragment

Beginning with AndroidX Fragment 1.1. 0, you can use the Fragment test component to provide the FragmentScenario class, which helps instantiate Fragment in tests and test it separately:


// To launch a Fragment with a user interface:
val scenario = launchFragmentInContainer<FirstFragment>()

// To launch a headless Fragment:
val scenario = launchFragment<FirstFragment>()

// To move the fragment to specific lifecycle state:
scenario.moveToState(CREATED)

// Now you can e.g. perform actions using Espresso:
onView(withId(R.id.refresh)).perform(click())

// To obtain a Fragment instance:
scenario.onFragment { fragment ->

More Kotlin!

It's nice to see that many useful Kotlin extension methods are available in the-ktx AndroidX package, and new methods are added periodically. For example, in AndroidX Fragment-KTX 1.2. 0, extensions using fragmented types can be used for the replace () method on FragmentTransaction. Combining this with the commit () extension method, we can get the following code:


// Before
supportFragmentManager
 .beginTransaction()
 .add(R.id.container, MyFragment::class.java, null)
 .commit()

// After
supportFragmentManager.commit {
 replace<MyFragment>(R.id.container)
}

FragmentContainerView

One small but important thing. If you use FrameLayout as a container for Fragment, you should use FragmentContainerView instead. It fixes some animation z axis index order problems and window insertion scheduling. You can use FragmentContainerView from AndroidXFragment 1.2. 0.


Related articles: