Android Vehicle Application Development and Analysis (Extra) - Analysis of Fragment Usage in 2022 (Part 1)

References: https://developer.android.google.cn/guide/fragments?hl=zh_cn
https://zhuanlan.zhihu.com/p/149937029
Although this article is titled "vehicle", all the knowledge points involved are Android APP development general
This article is based on the AndroidX Fragment library, reading this article requires a certain Fragment usage foundation

The reason for writing this blog is that when I recently communicated with my colleagues about the problems encountered in a certain project, I found that our use of Fragments has hardly improved compared to 4-5 years ago. When encountering problems, we often think about how to get around instead of Analyzing the reasons, we ridiculed ourselves as "the craftsman of ancient Android", so we have this blog. How should Fragment be used in 2022?

1. Introduction to Fragment

Fragment represents a reusable part of the application interface. A Fragment defines and manages its own layout, has its own lifecycle, and can handle its own input events. A Fragment cannot stand alone, but must be hosted by an Activity or another Fragment. The fragment's view hierarchy becomes part of, or attached to, the host's view hierarchy.

In actual development, Fragment is also one of the most important components in the application. Some people even regard Fragment as the fifth largest component of Android. There are few vehicle application interfaces, and we almost always develop HMI around Fragment.

2. Create Fragment

The AndroidX Fragment library needs to be introduced before creating a Fragment.

dependencies {
    def fragment_version = "1.4.1"
    // Java language implementation
    implementation "androidx.fragment:fragment:$fragment_version"
    // Kotlin
    implementation "androidx.fragment:fragment-ktx:$fragment_version"
}

After introducing the library, there are usually the following methods to create a Fragment

2.1 Construction method

class ExampleKotlinFragment(param1: String, param2: String) : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_example, container, false)
    }
}

// 使用
supportFragmentManager.commit(true) {
    setReorderingAllowed(true)
    add(R.id.container,Example1Fragment("1","2"))
}

Using a parameterized construction method to create a Fragment is one of our uncommon ways, and it has many disadvantages. For example: If a ConfigChange occurs in the Activity (screen rotation, sharing from the main screen to the secondary screen), an exception will occur when the Fragment is destroyed and rebuilt, causing the program to crash.

2.2 Static factory

private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

class ExampleKotlinFragment : Fragment() {

    private var param1: String? = null
    private var param2: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_example, container, false)
    }

    companion object {
        /**
         * 使用此工厂方法,使用提供的参数创建此片段的新实例.
         */
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            ExampleKotlinFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

Static factory is our most commonly used method. Its advantage is to save the incoming parameters in arguments and take them out for use in onCreate. This approach can cover the scene during ConfigChange, and the parameters required when Fragment is destroyed and rebuilt can still be obtained normally. If you use the first "construction method" to instantiate a Fragment, an exception will be thrown when the Fragment is reconstructed after being destroyed by an exception, causing the program to crash.

2.3 FragmentFactory

Using a static factory requires us to modify the implementation of Fragment, so Android currently recommends that we use FragmentFactory to instantiate Fragment.
For Fragment, we can choose to pass in the required parameters in the construction method.

class Example1Fragment(val param1: String, val param2: String) : Fragment(R.layout.fragment_example) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.e("TAG", "onViewCreated: $param1" )
        // .....
    }

}

Then define a Factory integrated from FragmentFactory, implement instantiate() and instantiate Fragment according to the class type.

class ExampleFactory(val param1: String, val param2: String) : FragmentFactory() {

    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
        when (loadFragmentClass(classLoader, className)) {
            Example1Fragment::class.java -> Example1Fragment(param1, param2)
            else -> super.instantiate(classLoader, className)
        }
}

The last is to use, you need to onCreate()set the custom FragmentFactory to FragmentManager before the method of Activity.

class MainActivity : AppCompatActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = ExampleFactory("1", "2")

        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            supportFragmentManager.commit {
                setReorderingAllowed(true)
                add(R.id.container, Example1Fragment::class.java, null)
            }
        }
    }
}

Using FragmentFactory can not only cover the abnormal scene of ConfigChange, but also concentrate the instantiation of Fragment into one class, which facilitates the maintenance of the program.

2.4 Add Fragment to Activity

In AndroidX, Google abandons the previous practice of using tags in XML or dynamically replacing them in XML, and currently recommends the following two methods.

2.4.1 Add Fragment through FragmentContainerView

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/container"
    android:name="com.wujia.learning.fragment.Example1Fragment"
    android:tag="example_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

FragmentContainerViewInherited from FrameLayout, but it only allows filling FragmentView. It also replaces tags by passing the class name in the class attribute. Since FragmentContainerView uses FragmentTransaction internally, there is no need to worry, and there will be no problem when replacing this Fragment later.

According to Google officials, FragmentContainerViewsome animation issues have been fixed. For example, the level of Fragment on the Z axis. As shown in the figure below, you can see that in FrameLayout, Fragment does not display animation when switching, but jumps out to the screen as a whole. This problem is caused by the cut-in Fragment and its animations being below the level of the previous Fragment. And FragmentContainerViewit will ensure that the hierarchical relationship between Fragments is in the correct state, and we can see the switching animation.
image

2.4.2 Add Fragment through FragmentManager

Dynamically adding Fragment is our most common way, and it also needs to be introduced FragmentContainerViewas a container.

<!-- res/layout/example_activity.xml -->
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Then add Fragment to Activity.

class HostActivity : AppCompatActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = ExampleFactory("1", "2")

        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            supportFragmentManager.commit {
                setReorderingAllowed(true)
                add(R.id.container, Example1Fragment::class.java, null)
            }
        }
    }
}

3. FragmentManager

The FragmentManager class is responsible for performing operations on the application's Fragments, such as adding, removing or replacing them, and adding them to the back stack.

Although Google strongly recommends using the NavigationUI component instead of FragmentManger, FragmentManager is actually the most frequently used at present.

3.1 Get Fragment Manager

3.1.1 Get in Activity

getSupportFragmentManagerUse FragmentManager in the Activity of FragmentActivity and its subclasses .

3.1.2 Get in Fragment

In a Fragment you can getChildFragmentManager()get a reference to FragmentManagerthe . If you need to access its host FragmentManager, you can use it getParentFragmentManager().

Note that getFragmentManager() is deprecated and should no longer be used.

image

Through the above view, we can deepen our understanding getChildFragmentManager()of getParentFragmentManager().

3.2 Using FragmentManager

We are familiar with using FragmentManager to perform transactions. Here we mainly introduce some common APIs of FragmentManager.

3.2.1 Common APIs of FragmentManager

  • addFragmentOnAttachListener(FragmentOnAttachListener listener)

Add a FragmentOnAttachListener that should receive calls to onAttachFragment when a new fragment is attached to this FragmentManager.

  • addOnBackStackChangedListener(FragmentManager.OnBackStackChangedListener listener)

Add a listener to monitor Fragment's backStack.

  • clearBackStack(String name)

Clear the back stack previously saved by saveBackStack.

  • clearFragmentResult(String requestKey)

Clear the stored results for the given requestKey.

  • clearFragmentResultListener(String requestKey)

Clears the stored FragmentResultListener for the given requestKey.

  • F findFragment(View view)

Find the Fragment associated with a given View.

  • Fragment findFragmentById(@IdRes int id)

Find a Fragment, where the ID can be the ID of the container (FrameLayout or FragmentContainerView), or the ID generated when the Fragment is inflated from XML

  • Fragment findFragmentByTag(String tag)

Finds the Fragment identified by the given tag, either inflated from XML or set when added to a transaction

  • FragmentManager.BackStackEntry getBackStackEntryAt(int index)

Returns the BackStackEntry at index in the return stack; index 0 is the bottom of the stack.

  • Int getBackStackEntryCount()

Returns the number of entries currently on the back stack.

  • putFragment(Bundle bundle,String key,Fragment fragment)

Put the reference to the Fragment into a Bundle.

  • Fragment getFragment(Bundle bundle, String key)

Retrieves the current Fragment instance for the reference previously placed with putFragment.

  • List getFragments()

Get a list of all Fragments currently added to the FragmentManager.

  • registerFragmentLifecycleCallbacks(FragmentManager.FragmentLifecycleCallbacks cb,boolean recursive)

Register FragmentLifecycleCallbacks to listen to Fragment lifecycle events that occur in FragmentManager.

  • executePendingOperations

After a FragmentTransaction is committed using FragmentTransaction.commit()it, it is scheduled to execute asynchronously on the main thread of the process.

3.2.2 FragmentManager return stack

Dealing with the return stack is one of the most troublesome things when using FragmentManager, so we list the API of the return stack separately

  • popBackStack()

Popping the top element of the return stack is an asynchronous method. The synchronization method corresponds to popBackStackImmediate()

  • popBackStack(String name, int flags)

  • popBackStack(int id, int flags)

Pop the top element of the return stack, the name and id are both identifiers, and the flag has only two values ​​0 or POP_BACK_STACK_INCLUSIVE.

When the name and id are not empty, all the elements above the identifier will be popped up (excluding the elements marked), if flag=POP_BACK_STACK_INCLUSIVE, the elements of the identifier will also be popped up. Equivalent when name and id are empty popBackStack().

  • saveBackStack(String name)

Save the return stack. Used to support APIs that need to maintain multiple return stacks.

  • restoreBackStack(String name)

Restore the return stack. Used to support APIs that need to maintain multiple return stacks.

4. Fragment affairs

When the program is running, FragmentManagereach commit is called a transaction by performing operations such as add, show, hide, and replace.

4.1 Allow reordering of Fragment state changes

It is recommended to enable Reordering in AndroidX. Especially when running on the back stack and running animations and transitions. Enabling this flag ensures that any intermediate fragments (that is, fragments that are added and then immediately replaced) do not undergo lifecycle changes or perform their animations or transitions if multiple transactions are performed concurrently.

supportFragmentManager.commit {
    ...
    setReorderingAllowed(true)
}

4.2 Add and delete Fragment

Use add()can add a Fragment to the container.

// 添加Fragment
supportFragmentManager.commit {
    setReorderingAllowed(true)
    add<Example1Fragment>(R.id.container)
}

Use remove()to remove the Fragment in the container, and the removed Fragment will be directly destroyed.

supportFragmentManager.commit {
    setReorderingAllowed(true)
    remove(fragment)
}

Use replace()can replace the current Fragemnt in the container with the specified Fragment, which is equivalent to remove and then add

// 替换Fragment
supportFragmentManager.commit {
    setReorderingAllowed(true)
    replace<Example1Fragment>(R.id.container)
}

4.3 Show and hide Fragment

Use show()the and hide()methods to show or hide views of Fragments that have been added to the container. These methods only affect the visibility of the Fragment's views, not the Fragment's lifecycle.

4.4 Attaching and detaching Fragments

Use detach()can detach a Fragment from the UI, breaking its view hierarchy. At this point the Fragment's life cycle advances onDestroyView(), and the Fragment remains in the same state as when it was placed on the back stack. This means the Fragment has been removed from the UI but is still managed by the FragmentManager.

Use attach()the method to reattach a Fragment that was previously detached from it. Fragments recreate their view hierarchies, attach to the UI, and display those hierarchies.

4.5 Add to return stack

By default, our commit transaction is not added to the back stack. If you want to save these changes, you can need to call addToBackStack(name), name can be empty.

supportFragmentManager.commit {
        setReorderingAllowed(true)
        hide(example1Fragment)
        add<Example2Fragment>(R.id.container)
        addToBackStack(null)
    }

After executing the above code, Example2Fragment is displayed on the current screen, and *example1Fragment** is displayed after pressing the back button. *It should be noted that if you replace()add a Fragment and then addToBackStack()add it to the return stack, the replaced Fragment will not be destroyed, and the life cycle of the replaced Fragment will only advance onDestroyView.

4.6 Limit the life cycle of Fragment

FragmentTransactionsCan affect the lifecycle state of individual fragments added within the scope of a transaction. Use setMaxLifecycle(Fragment,Lifecycle.State)to set the lifecycle maximum state for a given Fragment. For example, ViewPager2to constrain offscreen fragments to this state.

4.7 Submit a transaction

Calling commit()does not execute a transaction immediately. Transactions are scheduled to run on the UI thread for as long as it is able to do so. But if necessary, commitNow() can be called to run the committed transaction immediately on the UI thread. Alternatively, it is also possible to execute a not-yet-running executePendingTransactions()by commit().

If the host Activity has already been executed when the transaction is committed onSaveInstanceState, commit will throw an exception at this time and can be used commitAllowingStateLossto commit the transaction.

commitAllowingStateLoss is a dangerous operation because the commit may be lost if the activity needs to resume from its state later, so this should only be used in situations where the UI state can change unexpectedly on the user.

In most cases we commit()can use the method.

5. Transition animation between Fragments

Fragment's API provides setting animation and transition effects to connect the switching between two Fragments. Due to concerns about application performance, in most cases we will not design complex transition animations. Here we only need to understand the commonly used animation effects.

5.1 Setting up animation

Setting animation requires us to create the corresponding xml animation file in res/anim. The following code creates the effect of fading in and out of View when Fragment enters and exits.

<!-- res/anim/fade_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />

<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />

Then we create a transition animation and set the way the Fragment enters and exits.

<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />

<!-- res/anim/slide_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />    

Finally, submit the animation effect in the transaction

supportFragmentManager.commit {
    setCustomAnimations(
        enter = R.anim.slide_in,
        exit = R.anim.fade_out,
        popEnter = R.anim.fade_in,
        popExit = R.anim.slide_out
    )
    setReorderingAllowed(true)
    replace<Example2Fragment>(R.id.container)
    addToBackStack(null)
}

Then you can see the animation effect as shown in the figure
image
image

5.2 Setting transitions

In addition to setting animation, we can also use transition to set the animation effect of Fragment.
First create the xml of fade and slide under res/transition

<!-- res/transition/fade.xml -->
<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@integer/material_motion_duration_short_1"/>

<!-- res/transition/slide_right.xml -->
<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@integer/material_motion_duration_short_1"
    android:slideEdge="end" />    

onCreate()Then set it in the method of Fragment to exitTransition or enterTransition

class Example1Fragment(val param1: String, val param2: String) :
    Fragment(R.layout.fragment_example1) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }

class Example2Fragment(val param1: String, val param2: String) : Fragment(R.layout.fragment_example2) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }

5.3 Transitions using shared elements

Shared element transition is also one of our commonly used transition effects, and it is also very simple to use in Fragment.
image
First, you need to use android:transitionName to set the required shared elements between the two Fragments

<!-- exampleFragment1 -->
<ImageView
    android:id="@+id/iv_pic1"
    android:transitionName="pic1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@mipmap/ic_launcher" />

<!-- exampleFragment2 -->
<ImageView
    android:id="@+id/iv_pic2"
    android:transitionName="pic2"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@mipmap/ic_launcher"/>  

Then define the transitionSet animation effect in res/transition

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeImageTransform/>
</transitionSet>

Set transition to Fragment's sharedElementEnterTransition . By default, sharedElement Exit Transition will share the same animation effect.

class Example2Fragment(val param1: String, val param2: String) : Fragment(R.layout.fragment_example2) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        sharedElementEnterTransition = inflater.inflateTransition(R.transition.shared_image)
    }

Finally, you need to set the shared elements in FragmentManager.

supportFragmentManager.commit {
    // 注意pic1的View与Fragment2中命名为pic2的View是共享元素
    addSharedElement(example1Fragment.pic1,"pic2")
    setReorderingAllowed(true)
    replace<Example2Fragment>(R.id.container)
    addToBackStack(null)
}

5.4 Postponing transition

Sometimes the shared elements on the second page need to be loaded for a period of time, for example, pictures need to be loaded from the network, etc. At this time, we need to postpone the transition.

Delay transition must show settingssetReorderingAllowed(true)

Declare in example2Fragment that transition needs to be postponed.

// 
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    postponeEnterTransition()
}

Then turn on the animation transition when the image loads incorrectly or completes

Glide.with(this)
            .load(url)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)

When dealing with situations such as a user with a slow internet connection, it may be desirable to initiate a delayed transition after a certain amount of time rather than waiting for all the data to load. For these cases, it can be called in the method of the input fragment Fragaction.postponeEnterTransition(long,TimeUnit), passing in the duration and time unit. Once the specified time has elapsed, the postponed time will start automatically.

5.5 Using shared element animation in RecyclerView

startPostponedEnterTransition should not execute until all views entering the Fragment have been measured and laid out . RecyclerViewWhen using , you must wait for all the data to load and for the item to be ready to draw before starting the transformation. Here is an example:

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // 等待数据加载
        viewModel.data.observe(viewLifecycleOwner) {
            // 在 RecyclerView 适配器上设置数据
            adapter.setData(it)
            // 测量和布局所有视图后开始过渡
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}

When using a shared element to transition from a Fragment RecyclerViewusing to another Fragment, you must use RecyclerViewto startPostponedEnterTransition to ensure that the returned shared element transition still works normally when returning RecyclerViewto .

The transitionName in RecyclerView needs to be set in ViewHolder, or it can be set in XML using DataBinding, as long as the set name is a unique value.

class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}


Limited by space and time, this is the end of the basic knowledge of Fragment. The important things such as the life cycle of Fragment in various states and the way of message delivery will be written later.

Guess you like

Origin blog.csdn.net/linkwj/article/details/123787344