使用导航主机在底部导航中停止片段刷新

fai*_*gir 19 android android-layout android-fragments kotlin

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

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

导航/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>
Run Code Online (Sandbox Code Playgroud)

活动邮件.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>
Run Code Online (Sandbox Code Playgroud)

主活动.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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

bas*_*nce 11

Kotlin 2020 谷歌推荐的解决方案

许多这些解决方案在主活动中调用 Fragment 构造函数。但是,按照 Google 推荐的模式,这不是必需的。

设置导航图选项卡

首先为目录下的每个选项卡创建一个导航图xml res/navigation

文件名: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>
Run Code Online (Sandbox Code Playgroud)

对其他选项卡重复上述模板。重要的所有片段和导航图都有一个 id(例如@+id/tab0、@+id/fragmentA)。

设置底部导航视图

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

<?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>
Run Code Online (Sandbox Code Playgroud)

设置活动主 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"
/>
Run Code Online (Sandbox Code Playgroud)

主要活动 XML

将以下代码复制到您的主要活动 Kotlin 文件中,并setupBottomNavigationBar在 OnCreateView 中调用。确保您使用 navGraphIdsR.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
}
Run Code Online (Sandbox Code Playgroud)

复制 NavigationExtensions.kt 文件

将以下文件复制到您的代码库

[编辑] 上面的链接已损坏。在分叉仓库中找到它

来源

  • 我遵循了你的指导,非常有帮助。不幸的是,导航的工作方式与以前完全相同,所有片段都被重新创建。有什么建议或者我是否误解了谷歌解决方案的用途。 (2认同)
  • @RasoulMiri 虽然新的 alpha 版本可能会修复它,但它仍然是 alpha 版本。他们每两周都会发布这个版本的新 alpha。在使用生产代码部署 alpha 更改时请记住这一点。我很高兴看到谷歌至少已经解决了这个问题 (2认同)

Mah*_*Raj 10

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

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

其中binding.navView是用于参考BottomNavigationView使用Android数据绑定。


Jam*_*mim 8

尝试这个:

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


}
Run Code Online (Sandbox Code Playgroud)

或者你可以按照谷歌推荐的解决方案:谷歌链接

  • 我们中的许多人都使用 nav Host xml 来定义我们的片段。有关将此解决方案应用于较新版本的任何建议,因为我们不会直接在代码中的活动中显式创建片段 (3认同)

rui*_*f3r 6

如果您使用的是 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
    })
Run Code Online (Sandbox Code Playgroud)

就是这样!Google 正在积极致力于为底部选项卡导航提供多个后退堆栈支持,并声称它将Navigation 2.4.0按照此处此问题跟踪器的说明到达,如果您愿意和/或您的问题与多个后退堆栈更相关,您可以查看这些链接

记住片段仍然会被重新创建,通常你不会改变组件的行为,而是让你的数据适应它们!

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

ViewModel 概览 Android 开发者

如何使用 ViewModel 在片段和活动之间进行通信 - 在 Medium

使用 ViewModels 恢复 UI 状态 - 在 Medium