综合实战:构建一个完整的单Activity多Fragment应用

说实话,讲到这里,我觉得是时候把前面学的所有东西串起来了。Navigation组件、单Activity架构、Fragment生命周期、参数传递……这些知识点单独看都不难,但真正把它们揉进一个真实项目里,才是考验功底的时候。

我记得刚带团队做第一个单Activity项目时,大家都很兴奋——终于不用再被Activity跳转的动画卡顿折磨了。结果呢?第一个版本就翻车了,因为Fragment的back stack管理没处理好,用户按返回键直接退出了应用。嗯,从那以后,我对Navigation组件的理解就深刻多了。

项目结构概览

我们先看看这个应用长什么样。它包含四个核心模块:

  • 登录模块:用户认证入口,支持账号密码登录
  • 主页模块:应用主界面,展示核心内容列表
  • 详情模块:内容详情页,展示选中项的详细信息
  • 设置模块:用户偏好设置,主题切换、账号管理等

整个应用只有一个Activity,所有页面切换都靠Navigation组件驱动。你想想看,这样做的好处是什么?最直观的就是:页面跳转的动画统一了、返回栈管理交给系统了、深层链接也变得简单了

导航图的设计

我们先画导航图。我个人习惯先用纸笔画个草图,再在Android Studio里用可视化编辑器拖拽。这样思路更清晰。

核心导航路径:

  • LoginFragment → HomeFragment(登录成功后)
  • HomeFragment → DetailFragment(点击列表项)
  • HomeFragment → SettingsFragment(点击设置按钮)
  • SettingsFragment → LoginFragment(退出登录)

下面这张图展示了完整的导航结构:

LoginFragment HomeFragment DetailFragment SettingsFragment 登录成功 点击列表项 点击设置 退出登录 正向导航 返回/退出

导航图的XML配置

有了上面的结构图,写导航配置就顺理成章了。我在项目中习惯把导航图放在 res/navigation/ 目录下,命名为 nav_graph.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/nav_graph"
    app:startDestination="@id/loginFragment">

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.example.app.ui.login.LoginFragment"
        android:label="登录"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_login_to_home"
            app:destination="@id/homeFragment"
            app:popUpTo="@id/loginFragment"
            app:popUpToInclusive="true" />
    </fragment>

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.app.ui.home.HomeFragment"
        android:label="主页"
        tools:layout="@layout/fragment_home">
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/detailFragment" />
        <action
            android:id="@+id/action_home_to_settings"
            app:destination="@id/settingsFragment" />
    </fragment>

    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.app.ui.detail.DetailFragment"
        android:label="详情"
        tools:layout="@layout/fragment_detail">
        <argument
            android:name="itemId"
            app:argType="integer" />
    </fragment>

    <fragment
        android:id="@+id/settingsFragment"
        android:name="com.example.app.ui.settings.SettingsFragment"
        android:label="设置"
        tools:layout="@layout/fragment_settings">
        <action
            android:id="@+id/action_settings_to_login"
            app:destination="@id/loginFragment"
            app:popUpTo="@id/loginFragment"
            app:popUpToInclusive="true" />
    </fragment>
</navigation>

个人经验:注意看登录跳转主页时,我用了 popUpToInclusive="true"。这意味着登录成功后,登录页面会从返回栈中移除。用户按返回键时不会回到登录页,而是直接退出应用。这个细节很多新手会忽略,结果用户登录后按返回键又回到登录页,体验很差。

单Activity的搭建

MainActivity 的代码其实很简单,它只负责两件事:加载布局、设置导航控制器。

class MainActivity : AppCompatActivity() {

    private lateinit var navController: NavController
    private lateinit var appBarConfiguration: AppBarConfiguration

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

        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController

        appBarConfiguration = AppBarConfiguration(
            setOf(R.id.loginFragment, R.id.homeFragment)
        )

        setupActionBarWithNavController(navController, appBarConfiguration)
    }

    override fun onSupportNavigateUp(): Boolean {
        return navController.navigateUp(appBarConfiguration)
                || super.onSupportNavigateUp()
    }
}

你可能会问:为什么 AppBarConfiguration 只传了 loginFragment 和 homeFragment?因为这两个页面是顶层目的地,不应该显示返回按钮。而 detailFragment 和 settingsFragment 是从主页跳转过去的,需要显示返回箭头。

Fragment之间的参数传递

从主页跳转到详情页时,我们需要传递一个 itemId。在 Navigation 组件里,这通过 Safe Args 实现。

先定义参数:

// 在 nav_graph.xml 中已经定义了 argument
// 现在生成对应的 Directions 类

// HomeFragment 中跳转
val item = listAdapter.currentList[position]
val action = HomeFragmentDirections.actionHomeToDetail(item.id)
findNavController().navigate(action)

// DetailFragment 中接收
val args: DetailFragmentArgs by navArgs()
val itemId = args.itemId

避坑指南:我曾经在项目里直接用 Bundle 传参,结果类型不匹配导致崩溃。Safe Args 的好处是编译时检查类型,而且自动生成代码,省去了手动写 Bundle 的麻烦。记住:能用 Safe Args 就别手写 Bundle

登录状态的全局管理

登录模块有个特殊之处:它决定了用户能否访问其他页面。我通常用 SharedPreferences 保存登录状态,在 Navigation 的跳转逻辑里做判断。

// 在 LoginFragment 中
private fun login() {
    // 调用登录API...
    // 登录成功后
    val prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
    prefs.edit().putBoolean("is_logged_in", true).apply()
    
    findNavController().navigate(R.id.action_login_to_home)
}

// 在 HomeFragment 中检查登录状态
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    
    val prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
    if (!prefs.getBoolean("is_logged_in", false)) {
        findNavController().navigate(R.id.action_home_to_login)
        return
    }
    // 继续加载主页数据...
}

说白了,这就是一个简单的守卫模式。用户没登录就踢回登录页,登录了就放行。虽然简单,但很实用。

设置模块的退出登录逻辑

退出登录时,我们需要清空用户数据,然后跳回登录页。注意这里也要用 popUpToInclusive,把设置页和主页都清掉。

// SettingsFragment 中
private fun logout() {
    // 清空登录状态
    val prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
    prefs.edit().clear().apply()
    
    // 跳转到登录页,并清空返回栈
    findNavController().navigate(R.id.action_settings_to_login)
}

完整的页面切换流程

我们把所有流程串起来,看看用户的操作路径:

用户操作 当前页面 目标页面 返回栈变化
打开应用 LoginFragment [Login]
登录成功 LoginFragment HomeFragment [Home](Login被移除)
点击列表项 HomeFragment DetailFragment [Home, Detail]
按返回键 DetailFragment HomeFragment [Home]
点击设置 HomeFragment SettingsFragment [Home, Settings]
退出登录 SettingsFragment LoginFragment [Login](Home和Settings被移除)

这个表格看起来简单,但背后是 Navigation 组件帮我们处理了所有返回栈的入栈出栈逻辑。你想想看,如果用手动管理 FragmentTransaction,得写多少代码?

UI层面的统一处理

单Activity架构下,所有页面的标题栏、状态栏、动画效果都应该统一。我习惯在 Activity 里设置一个全局的 Toolbar,然后每个 Fragment 通过 Navigation 的 Label 自动更新标题。

// 在 activity_main.xml 中
<androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    app:titleTextColor="@android:color/white" />

<fragment
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_graph" />

然后在 Activity 里监听导航变化,更新标题:

navController.addOnDestinationChangedListener { _, destination, _ ->
    toolbar.title = destination.label
}

这样做的好处是:每个 Fragment 不需要自己管理标题,导航图里定义的 label 就是标题。改标题只需要改导航图,不用改代码。

核心要点总结:

  • 导航图是单Activity架构的骨架,先画图再写代码
  • Safe Args 是参数传递的最佳实践,编译时检查类型安全
  • popUpTo 和 popUpToInclusive 控制返回栈,避免页面混乱
  • 登录状态用 SharedPreferences 保存,配合导航守卫实现权限控制
  • 标题栏统一在 Activity 层管理,Fragment 只关注业务逻辑

嗯,这个综合实战项目到这里就基本成型了。从登录到主页,从列表到详情,再到设置和退出登录,整个流程被 Navigation 组件串联得干干净净。我个人觉得,单Activity架构最大的价值不是技术上的炫技,而是让页面管理变得可预测、可维护。你想想看,当项目从5个页面膨胀到50个页面时,这种架构的优势就体现出来了。

公众号:蓝海资料掘金营,微信deep3321