zoukankan      html  css  js  c++  java
  • 【从零开始撸一个App】Fragment和导航中的使用

    Fragment简介

    Fragment自从Android 3.0引入开始,刚接触的同学会把它与Activity的关系类比于html片段和html页面的关系,其实是不准确的。前者更多的是组件的概念,需要在运行时有一套管理机制;而后者侧重于布局编写阶段,Android中复用布局我们一般使用<include layout="@layout/xxx"/>标签。

    Fragment实例由Activity的FragmentManager管理,其生命周期和Activity一样,都不是由开发人员而是由系统维护的。自然而然的,每当它们被重建时,系统只会去调用它们的无参构造器,带参构造器会被无视。那如果要在它们创建时传入初始化数据咋办呢?这也是为什么会有Bundle这个玩意儿的存在,就是用于开发人员存取相关数据,如下所示:

    /**
     * Use the [ThumbnailsFragment.newInstance] factory method to
     * create an instance of this fragment.
     */
    class ThumbnailsFragment() : Fragment() {
        private var albumTag: String? = null
    
        companion object {
            @JvmStatic
            fun newInstance(albumTag: String?) =
                ThumbnailsFragment().apply {
                    arguments = Bundle().apply {
                        putString("albumTag", albumTag)
                    }
                }
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            arguments?.let {
                albumTag = it.getString("albumTag")
            }
        }
        
        /*other code*/
    }
    

    arguments由FragmentManager维护(跨fragment生命周期),可参看Android解惑 - 为什么要用Fragment.setArguments(Bundle bundle)来传递参数

    底部导航栏切换Fragment

    效果如下

    BottomNavigationView

    底部是BottomNavigationView组件,各个菜单在另外xml中定义,然后通过app:menu="xxx"指定。此处菜单定义如下

    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android">
    
        <item
            android:id="@+id/navigation_home"
            android:icon="@drawable/ic_home_black_24dp"
            android:title="@string/title_home" />
    
        <item
            android:id="@+id/navigation_dashboard"
            android:icon="@drawable/ic_dashboard_black_24dp"
            android:title="@string/title_dashboard" />
    
        <item
            android:id="@+id/navigation_notifications"
            android:icon="@drawable/ic_notifications_black_24dp"
            android:title="@string/title_notifications" />
    
    </menu>
    

    然后在代码中设置BottomNavigationView.setOnNavigationItemSelectedListener,判断当前选中的菜单项,手动切换Fragment,需要用到FragmentTransaction。如下示例

        override fun onClick(view: View?) {
            val trans = activity.supportFragmentManager.beginTransaction()
            val fragments = activity.supportFragmentManager.fragments
            fragments.forEach {
                if (it.isVisible) {
                    trans.hide(it) //隐藏当前显示的fragment
                }
            }
            val tag = (view as TextView).text.toString()
            val thumbnailsFragment = ThumbnailsFragment.newInstance(tag)
            //fragment_main_container就是居中的那块区域,用于显示各个fragment
            trans.add(R.id.fragment_main_container, thumbnailsFragment, tag)
            trans.show(thumbnailsFragment)
            trans.addToBackStack(null) //将本次操作入栈
            trans.commitAllowingStateLoss() //提交
        }
    

    注意addToBackStack方法,该方法是为了实现回退时——用户按返回按钮或程序执行回退(配合popBackStack)——界面能返回到本次操作前的状态。也可指定tag,在跨[多次]操作回退时有用。注意此处入栈的是操作信息,而非fragment。

    BottomNavigationView也可搭配ViewPager使用,但回退操作依然需要另外实现。

    上述手动控制Fragment的切换太麻烦。2018 I/O大会上,Google隆重推出一个新的架构组件:Navigation。它提供了多Fragment之间的转场、栈管理。在抽屉式/底部/顶部导航栏的需求中都可以使用这个组件。

    使用:在res目录下新建navigation文件夹,然后新建一个navigation graph设为bottom_navigation:

    <navigation xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/mobile_navigation"
        app:startDestination="@+id/navigation_home">
    
        <fragment
            android:id="@+id/navigation_home"
            android:name="com.eixout.presearchapplication.ui.home.HomeFragment"
            android:label="@string/title_home"
            tools:layout="@layout/fragment_home" />
    
        <fragment
            android:id="@+id/navigation_dashboard"
            android:name="com.eixout.presearchapplication.ui.dashboard.DashboardFragment"
            android:label="@string/title_dashboard"
            tools:layout="@layout/fragment_dashboard" />
    
        <fragment
            android:id="@+id/navigation_notifications"
            android:name="com.eixout.presearchapplication.ui.notifications.NotificationsFragment"
            android:label="@string/title_notifications"
            tools:layout="@layout/fragment_notifications" />
    </navigation>
    

    注意每个fragment的id要和之前定义的menu的id保持一致。可以设置转场动画,还可以设置每个fragment跳转的目标(destination),目标可以是 Activity或Fragment,也可以自定义。

    然后在Activity布局文件中添加一个Fragment,设置name属性为android:name="androidx.navigation.fragment.NavHostFragment"。在传统的单Activity多Fragment场景中,我们往往需要为Activity添加一个FrameLayout作为Fragment的容器。在Navigation中HavHostFragment就是Fragment的容器(HavHostFragment继承了NavHost。The NavHost interface enables destinations to be swapped in and out.),其中设置app:navGraph="@navigation/bottom_navigation"使之与navigation graph建立联系。

        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"        
            app:defaultNavHost="true"
            app:navGraph="@navigation/bottom_navigation"
            other_config="..." />
    

    app:defaultNavHost: If set to true, the navigation host will intercept the Back button.

    最后将导航栏与Navigation关联

    val navController = findNavController(R.id.nav_host_fragment)
    bottomNavigationView.setupWithNavController(navController)
    

    如此便大功告成了。

    如果不依赖导航栏,而是手动跳转,则可以使用NavController的相关方法,比如在Activity里navController.navigate(actionId)

    问题

    Navigation和FragmentTransaction方式最好不要同时使用,它俩的返回堆栈似乎不是同一个,回退时会有问题。不能同时使用还使得下面两个问题不好解决。

    1. 使用Navigation,Fragment可以相互跳转没问题,但状态丢失了。比如A下滑一定距离后跳转到B,B回退到A,A的下滑状态丢失,仍是从头部开始显示。

    2. 每次点击BottomNavigationView的菜单项,对应的Fragment会recreate,这其实不是我们想要的,我们预期的应该是Fragment第一次创建后就一直复用,既保留了当前状态也不会对后端造成不必要的调用。

    如果使用FragmentTransaction很好处理,只要缓存一个Fragment集合即可(若要保留Fragment的状态,比如滑动位置,可以使用supportFragmentManager.saveFragmentInstanceState(fragment)fragment.setInitialSavedState(savedState)加载,也可以使用hide/show(fragment)的方式),但用了Navigation后就没办法了。可以看看Navigation, Saving fragment state评论区的吐槽,里面也有临时的一些解决方案(不实用)。

    FragmentTransaction本身也有对状态信息的处理考量,参看commit(), commitNow()和commitAllowingStateLoss()

    参考资料

    嵌套Fragment的使用及常见错误
    Fragment 生命周期和使用
    BottonNavigationView+Fragment切换toolbar标题栏
    手把手教你使用Android官方组件Navigation
    Playing with Navigation Architecture Components
    The Navigation Architecture Component Tutorial: Getting Started
    Handle Complex Navigation Flow with Single-Activity Architecture and Android Jetpack’s Navigation component
    导航到目的地-popUpTo 和 popUpToInclusive
    Difference between add(), replace(), and addToBackStack()

  • 相关阅读:
    LeetCode 275. H-Index II
    LeetCode 274. H-Index
    LeetCode Gray Code
    LeetCode 260. Single Number III
    LeetCode Word Pattern
    LeetCode Nim Game
    LeetCode 128. Longest Consecutive Sequence
    LeetCode 208. Implement Trie (Prefix Tree)
    LeetCode 130. Surrounded Regions
    LeetCode 200. Number of Islands
  • 原文地址:https://www.cnblogs.com/newton/p/14411325.html
Copyright © 2011-2022 走看看