使用navhost停止底部导航中的片段刷新

时间:2020-02-05 05:59:43

标签: android android-layout android-fragments kotlin

这个问题已经被问过几次了,但是我们现在是2020年,有没有人找到一个好的解决方案呢?

我希望能够使用底部导航控件进行导航,而无需每次选择都刷新片段。这是我目前所拥有的:

navigation / main.xml:

<?xml version="1.0" encoding="utf-8"?>
<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/main"
    app:startDestination="@id/home">

    <fragment
        android:id="@+id/home"
        android:name="com.org.ftech.fragment.HomeFragment"
        android:label="@string/app_name"
        tools:layout="@layout/fragment_home" />
    <fragment
        android:id="@+id/news"
        android:name="com.org.ftech.fragment.NewsFragment"
        android:label="News"
        tools:layout="@layout/fragment_news"/>
    <fragment
        android:id="@+id/markets"
        android:name="com.org.ftech.fragment.MarketsFragment"
        android:label="Markets"
        tools:layout="@layout/fragment_markets"/>
    <fragment
        android:id="@+id/explore"
        android:name="com.org.ftech.ExploreFragment"
        android:label="Explore"
        tools:layout="@layout/fragment_explore"/>
</navigation>

activity_mail.xml:

<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<androidx.drawerlayout.widget.DrawerLayout
    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/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/main" />

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottomNavigationView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:itemIconTint="@color/nav"
            app:itemTextColor="@color/nav"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:menu="@menu/main">

        </com.google.android.material.bottomnavigation.BottomNavigationView>

    </androidx.constraintlayout.widget.ConstraintLayout>

    <com.google.android.material.navigation.NavigationView
        app:menu="@menu/main"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/navigationView"
        android:layout_gravity="start">
    </com.google.android.material.navigation.NavigationView>

</androidx.drawerlayout.widget.DrawerLayout>

MainActivity.kt:

class MainActivity : AppCompatActivity() {

    private var drawerLayout: DrawerLayout? = null
    private var navigationView: NavigationView? = null
    private var bottomNavigationView: BottomNavigationView? = null
    private lateinit var appBarConfiguration: AppBarConfiguration

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        drawerLayout = findViewById(R.id.drawer_layout)
        navigationView = findViewById(R.id.navigationView)
        bottomNavigationView = findViewById(R.id.bottomNavigationView)

        val navController = findNavController(R.id.nav_host_fragment)
        appBarConfiguration = AppBarConfiguration(setOf(R.id.markets, R.id.explore, R.id.news, R.id.home), drawerLayout)

        setupActionBarWithNavController(navController, appBarConfiguration)

        findViewById<NavigationView>(R.id.navigationView)
            .setupWithNavController(navController)

        findViewById<BottomNavigationView>(R.id.bottomNavigationView)
            .setupWithNavController(navController)

    }


    override fun onSupportNavigateUp(): Boolean {
        val navController = findNavController(R.id.nav_host_fragment)
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.itemId == R.id.search) {
            startActivity(Intent(applicationContext, SearchableActivity::class.java))
        }
        return super.onOptionsItemSelected(item)
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.options_menu, menu)
        return super.onCreateOptionsMenu(menu)
    }
}

在该片段中,我对服务进行了几次调用以获取onCreateView中的数据,在恢复该片段时,我假设这些调用将不再执行,并且应保留该片段的状态。

10 个答案:

答案 0 :(得分:5)

如果您使用的是Jetpack,最简单的解决方法是使用ViewModel

每次访问另一个片段时,您都必须保存所有有价值的数据,而不必进行不必要的数据库加载或网络调用。

UI controllers such as activities and fragments are primarily intended to display UI data, react to user actions, or handle operating system communication, such as permission requests.

这是我们使用ViewModels

的时候

ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance.

因此,如果重新创建该片段,则所有数据将立即存在,而无需再次调用数据库或网络。重要的是要知道,如果重新创建了保存ViewModel的活动或片段,您将收到之前创建的相同ViewModel实例。

但是在这种情况下,如果对所有片段使用共享的ViewModel,或者对每个片段使用不同的ViewModel,则必须指定ViewModel具有活动范围而不是片段范围。

这也是使用LiveData的小例子:

//Using KTX
val model by activityViewModels<MyViewModel>()
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
        // update UI
    })

//Not using KTX
val model by lazy {ViewModelProvider(activity as ViewModelStoreOwner)[MyViewModel::class.java]}
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
        // update UI
    })

就是这样! Google积极致力于对底部标签导航的多个后向堆栈支持,并声称如果您愿意和/或您的问题更相关,它将如hereon this issue tracker所述到达Navigation 2.4.0到多个后堆栈,您可以查看这些链接

记住片段仍然会被重新创建,通常您不更改组件行为,而是将数据调整为适合它们!

我给您留下了一些有用的链接:

ViewModel Overview Android Developers

How to communicate between fragments and activities with ViewModels - on Medium

Restoring UI State using ViewModels - on Medium

答案 1 :(得分:4)

Kotlin 2020 Google推荐的解决方案

许多这些解决方案在Main Activity中调用Fragment构造函数。但是,不需要遵循Google的推荐模式。

设置导航图标签

首先为res/navigation目录下的每个标签创建一个navigation graph xml。

文件名:tab0.xml

<?xml version="1.0" encoding="utf-8"?>
<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/tab0"
    app:startDestination="@id/fragmentA"
    tools:ignore="UnusedNavigation">

    <fragment
        android:id="@+id/fragmentA"
        android:label="@string/fragment_A_title"
        android:name="com.app.subdomain.fragA"
    >
    </fragment>
</navigation>

为其他标签重复上述模板。重要的是所有片段,导航图都有一个ID(例如@ + id / tab0,@ + id / fragmentA)。

设置底部导航视图

确保导航ID与底部菜单xml中指定的导航ID相同。

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

    <item android:title="@string/fragment_A_title"
        android:id="@+id/tab0"
        android:icon="@drawable/ic_baseline_book_24"/>

    <item android:title="@string/fragment_B_title"
        android:id="@+id/tab1"
        android:icon="@drawable/ic_baseline_add_alert_24"/>

    <item android:title="@string/fragment_C_title"
        android:id="@+id/tab2"
        android:icon="@drawable/ic_baseline_book_24"/>

    <item android:title="@string/fragment_D_title"
        android:id="@+id/tab3"
        android:icon="@drawable/ic_baseline_more_horiz_24"/>

</menu>

设置活动主XML

确保使用FragmentContainerView,而不使用<fragment,并且不要设置app:navGraph属性。稍后将在代码中设置


<androidx.fragment.app.FragmentContainerView
      android:id="@+id/fragmentContainerView"
      android:name="androidx.navigation.fragment.NavHostFragment"
      android:layout_width="0dp"
      android:layout_height="0dp"
      app:defaultNavHost="true"
      app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/main_toolbar"
/>

主要活动XML

将以下代码复制到您的主要活动Kotlin文件中,并在OnCreateView中调用setupBottomNavigationBar。确保您的navGraphIds使用R.navigation.whatever而不是R.id.whatever

private lateinit var currentNavController: LiveData<NavController>

private fun setupBottomNavigationBar() {
  val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
  val navGraphIds = listOf(R.navigation.tab0, R.navigation.tab1, R.navigation.tab2, R.navigation.tab3)
  val controller = bottomNavigationView.setupWithNavController(
      navGraphIds = navGraphIds,
      fragmentManager = supportFragmentManager,
      containerId = R.id.fragmentContainerView,
      intent = intent
  )
  controller.observe(this, { navController ->
      val toolbar = findViewById<Toolbar>(R.id.main_toolbar)
      val appBarConfiguration = AppBarConfiguration(navGraphIds.toSet())
      NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
      setSupportActionBar(toolbar)
  })
  currentNavController = controller
}

override fun onSupportNavigateUp(): Boolean {
  return currentNavController?.value?.navigateUp() ?: false
}

复制NavigationExtensions.kt文件

将以下file复制到您的代码库

来源

答案 2 :(得分:3)

尝试一下:

public class MainActivity extends AppCompatActivity {


    final Fragment fragment1 = new HomeFragment();
    final Fragment fragment2 = new DashboardFragment();
    final Fragment fragment3 = new NotificationsFragment();
    final FragmentManager fm = getSupportFragmentManager();
    Fragment active = fragment1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);


        BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);

        fm.beginTransaction().add(R.id.main_container, fragment3, "3").hide(fragment3).commit();
        fm.beginTransaction().add(R.id.main_container, fragment2, "2").hide(fragment2).commit();
        fm.beginTransaction().add(R.id.main_container,fragment1, "1").commit();

    }


    private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
            = new BottomNavigationView.OnNavigationItemSelectedListener() {

        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            switch (item.getItemId()) {
                case R.id.navigation_home:
                    fm.beginTransaction().hide(active).show(fragment1).commit();
                    active = fragment1;
                    return true;

                case R.id.navigation_dashboard:
                    fm.beginTransaction().hide(active).show(fragment2).commit();
                    active = fragment2;
                    return true;

                case R.id.navigation_notifications:
                    fm.beginTransaction().hide(active).show(fragment3).commit();
                    active = fragment3;
                    return true;
            }
            return false;
        }
    };


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_settings) {
            startActivity(new Intent(MainActivity.this, SettingsActivity.class));
            return true;
        }

        return super.onOptionsItemSelected(item);
    }


}

答案 3 :(得分:2)

快速提示,如果您只想阻止加载已选择的片段,只需覆盖 setOnNavigationItemReselectedListener 并且什么都不做,但这不会保存片段状态

binding.navBar.setOnNavigationItemReselectedListener {  }

答案 4 :(得分:1)

如果您使用NavigationUI.setupWithNavController(),则NavOptions将为您定义NavigationUI.onNavDestinationSelected()。这些选项包括launchSingleTop,如果菜单项不是第二项,则包括popUpTo图形的根。

问题在于,launchSingleTop仍然用新的片段替换了最上面的片段。要解决此问题,您必须创建自己的setupWithNavController()onNavDestinationSelected()函数。在onNavDestinationSelected()中,您可以根据需要调整NavOptions

答案 5 :(得分:1)

停止对同一导航项的多次单击停止刷新的简单解决方案可能是

 binding.navView.setOnNavigationItemSelectedListener { item ->
        if(item.itemId != binding.navView.selectedItemId)
            NavigationUI.onNavDestinationSelected(item, navController)
        true
    }

其中 binding.navView 是使用Android数据绑定的 BottomNavigationView 的引用。

答案 6 :(得分:0)

尝试这样的事情

navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)

private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
    when (item.itemId) {
        R.id.home -> {
            fragmentManager.beginTransaction().hide(active).show(homeFragment).commit()
            active = homeFragment
            return@OnNavigationItemSelectedListener true
        }
        R.id.news -> {
            fragmentManager.beginTransaction().hide(active).show(newsFragment).commit()
            active = newsFragment
            return@OnNavigationItemSelectedListener true
        }
        R.id.markets -> {
            fragmentManager.beginTransaction().hide(active).show(marketsFragment).commit()
            active = marketsFragment
            return@OnNavigationItemSelectedListener true
        }
        R.id.explore -> {
            fragmentManager.beginTransaction().hide(active).show(exploreFragment).commit()
            active = exploreFragment
            return@OnNavigationItemSelectedListener true
        }
    }
    false
}

答案 7 :(得分:0)

创建一个类:

@Navigator.Name("keep_state_fragment") // `keep_state_fragment` is used in navigation xml 
class KeepStateNavigator(
private val context: Context,
private val manager: FragmentManager, // Should pass childFragmentManager.
private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {

override fun navigate(
    destination: Destination,
    args: Bundle?,
    navOptions: NavOptions?,
    navigatorExtras: Navigator.Extras?
): NavDestination? {
    val tag = destination.id.toString()
    val transaction = manager.beginTransaction()

    var initialNavigate = false
    val currentFragment = manager.primaryNavigationFragment
    if (currentFragment != null) {
        transaction.detach(currentFragment)
    } else {
        initialNavigate = true
    }

    var fragment = manager.findFragmentByTag(tag)
    if (fragment == null) {
        val className = destination.className
        fragment = manager.fragmentFactory.instantiate(context.classLoader, className)
        transaction.add(containerId, fragment, tag)
    } else {
        transaction.attach(fragment)
    }

    transaction.setPrimaryNavigationFragment(fragment)
    transaction.setReorderingAllowed(true)
    transaction.commitNow()

    return if (initialNavigate) {
        destination
    } else {
        null
    }
}
}

使用keep_state_fragment代替nav_graph中的片段

活动中:

val navController = findNavController(R.id.nav_host_fragment)

    // get fragment
    val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!
    // setup custom navigator
    val navigator = KeepStateNavigator(this, navHostFragment.childFragmentManager, R.id.nav_host_fragment)
    navController.navigatorProvider += navigator

    // set navigation graph
    navController.setGraph(R.navigation.nav_graph)
    bottom_navigation.setupWithNavController(navController)

答案 8 :(得分:0)

使用以下代码段:

private fun attachFragment(fragmentTag: String) {
    val fragmentTransaction = supportFragmentManager.beginTransaction()
    supportFragmentManager.findFragmentByTag(fragmentTag)?.let {
        if (supportFragmentManager.backStackEntryCount == 0) return
        val currentFragmentTag = supportFragmentManager.getBackStackEntryAt(supportFragmentManager.backStackEntryCount - 1).name
        (supportFragmentManager.findFragmentByTag(currentFragmentTag) as? FragmentBase)?.let { curFrag ->
            fragmentTransaction.hide(curFrag)
        }
        fragmentTransaction.show(it)
    } ?: run {
        when (fragmentTag) {
            FragmentHome.TAG -> FragmentBase.newInstance<FragmentHome>()
            FragmentAccount.TAG -> FragmentBase.newInstance<FragmentAccount>()
            else -> null
        }?.let {
            fragmentTransaction.add(R.id.container, it, fragmentTag)
            fragmentTransaction.addToBackStack(fragmentTag)
        }
    }
    fragmentTransaction.commit()
}

您可以通过attachFragment(FragmentHome.TAG)方法使用此通行证想要显示的特定片段的标签

答案 9 :(得分:0)

如果您使用导航组件,除了 this answer From version:'2.4.0-alpha01' 它还内置了对多个返回堆栈的支持。因此不需要导航扩展

有关详细信息,请参阅此链接。 https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f