综合实战:构建一个完整的单Activity多Fragment应用
说实话,讲到这里,我觉得是时候把前面学的所有东西串起来了。Navigation组件、单Activity架构、Fragment生命周期、参数传递……这些知识点单独看都不难,但真正把它们揉进一个真实项目里,才是考验功底的时候。
我记得刚带团队做第一个单Activity项目时,大家都很兴奋——终于不用再被Activity跳转的动画卡顿折磨了。结果呢?第一个版本就翻车了,因为Fragment的back stack管理没处理好,用户按返回键直接退出了应用。嗯,从那以后,我对Navigation组件的理解就深刻多了。
项目结构概览
我们先看看这个应用长什么样。它包含四个核心模块:
- 登录模块:用户认证入口,支持账号密码登录
- 主页模块:应用主界面,展示核心内容列表
- 详情模块:内容详情页,展示选中项的详细信息
- 设置模块:用户偏好设置,主题切换、账号管理等
整个应用只有一个Activity,所有页面切换都靠Navigation组件驱动。你想想看,这样做的好处是什么?最直观的就是:页面跳转的动画统一了、返回栈管理交给系统了、深层链接也变得简单了。
导航图的设计
我们先画导航图。我个人习惯先用纸笔画个草图,再在Android Studio里用可视化编辑器拖拽。这样思路更清晰。
核心导航路径:
- LoginFragment → HomeFragment(登录成功后)
- HomeFragment → DetailFragment(点击列表项)
- HomeFragment → SettingsFragment(点击设置按钮)
- SettingsFragment → LoginFragment(退出登录)
下面这张图展示了完整的导航结构:
导航图的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