导航与Service/广播:从Service或BroadcastReceiver触发导航
这一章我们来聊一个很实际的问题——后台任务完成后,怎么跳转页面?
我在项目中遇到过好几次这样的场景:用户上传文件,进度条在后台跑着,突然网络断了,或者上传完成了,这时候需要弹个通知或者直接跳转页面。但问题是,Service和BroadcastReceiver里没有NavController,你没法直接调用navigate()。
嗯,这其实是个典型的「跨组件导航」问题。说白了,就是怎么让一个没有界面的组件,去指挥有界面的组件干活。
为什么不能直接导航?
你想想看,NavController是绑定在Activity和Fragment上的。Service和BroadcastReceiver是独立于UI生命周期的组件。它们根本不知道当前屏幕上显示的是哪个Fragment,更别提拿到NavController的引用了。
强行持有Activity引用?我见过有人这么干,结果就是内存泄漏、空指针满天飞。尤其是App被系统杀掉重启的时候,Service还在,Activity已经没了。
解决方案一:全局导航对象
我个人习惯的做法是,在Application级别维护一个全局导航对象。它不直接持有NavController,而是通过弱引用或者回调的方式,把导航请求转发给当前Activity。
// 全局导航管理器
object GlobalNavigator {
private var navControllerRef: WeakReference<NavController>? = null
fun register(navController: NavController) {
navControllerRef = WeakReference(navController)
}
fun unregister() {
navControllerRef?.clear()
navControllerRef = null
}
fun navigate(destinationId: Int, args: Bundle? = null) {
navControllerRef?.get()?.let { controller ->
if (controller.currentDestination?.id != destinationId) {
controller.navigate(destinationId, args)
}
}
}
}
然后在Activity的onResume里注册,onPause里注销:
class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
val navController = findNavController(R.id.nav_host_fragment)
GlobalNavigator.register(navController)
}
override fun onPause() {
super.onPause()
GlobalNavigator.unregister()
}
}
这样Service里就可以直接调用了:
class UploadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 模拟上传完成
Thread {
Thread.sleep(3000)
GlobalNavigator.navigate(R.id.uploadResultFragment)
}.start()
return START_NOT_STICKY
}
}
解决方案二:使用LiveData或SharedFlow
另一种更「Android化」的做法,是用LiveData或者SharedFlow来做事件总线。Service发出事件,Activity观察事件并执行导航。
// 导航事件
sealed class NavigationEvent {
data class GoToDestination(val destinationId: Int, val args: Bundle? = null) : NavigationEvent()
object GoBack : NavigationEvent()
}
// 事件总线
object NavigationBus {
private val _events = MutableSharedFlow<NavigationEvent>()
val events = _events.asSharedFlow()
suspend fun send(event: NavigationEvent) {
_events.emit(event)
}
}
Activity里收集事件:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 在ViewModel或Activity中收集
lifecycleScope.launch {
NavigationBus.events.collect { event ->
when (event) {
is NavigationEvent.GoToDestination -> {
findNavController(R.id.nav_host_fragment)
.navigate(event.destinationId, event.args)
}
is NavigationEvent.GoBack -> {
findNavController(R.id.nav_host_fragment).navigateUp()
}
}
}
}
}
}
Service里发送事件:
lifecycleScope.launch {
NavigationBus.send(NavigationEvent.GoToDestination(R.id.resultFragment))
}
解决方案三:PendingIntent + Notification
这个方案适用于App在后台,甚至被杀死的情况。通过Notification的PendingIntent,把导航意图包装成一个Intent,系统会帮你启动Activity并导航到指定页面。
// 在Service中构建通知
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("navigate_to", "upload_result")
}
val pendingIntent = PendingIntent.getActivity(
this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("上传完成")
.setContentText("点击查看结果")
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
startForeground(NOTIFICATION_ID, notification)
然后在Activity的onNewIntent里解析参数并导航:
class MainActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleNavigationIntent(intent)
}
private fun handleNavigationIntent(intent: Intent?) {
when (intent?.getStringExtra("navigate_to")) {
"upload_result" -> {
findNavController(R.id.nav_host_fragment)
.navigate(R.id.uploadResultFragment)
}
}
}
}
onCreate而不是onNewIntent。所以记得在onCreate里也处理一下Intent参数。
从BroadcastReceiver触发导航
BroadcastReceiver和Service的情况类似,但它更短命——onReceive执行完就结束了。所以不能在里面做耗时操作,也不能直接持有Activity引用。
我的做法是:在Receiver里发送一个本地广播或者事件,让Activity去处理。
class NetworkChangeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == ConnectivityManager.CONNECTIVITY_ACTION) {
val isConnected = // 检查网络状态
if (isConnected) {
// 发送事件,让Activity导航到重试页面
lifecycleScope.launch {
NavigationBus.send(NavigationEvent.GoToDestination(R.id.retryFragment))
}
}
}
}
}
这里要注意,lifecycleScope在Receiver里可能拿不到,因为Receiver没有Lifecycle。我一般用GlobalScope或者自己维护一个协程作用域:
// 在Application中定义
object AppScope {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
}
// Receiver中使用
AppScope.scope.launch {
NavigationBus.send(...)
}
知识体系总览
下面这张图总结了从Service/BroadcastReceiver触发导航的三种核心方案,以及各自的适用场景:
避坑指南
- 我曾经犯过的错:在Service里直接持有Activity的Context,结果Activity销毁后Service还在运行,导致内存泄漏。后来全部改用Application Context + 事件总线。
- 导航重复问题:如果用户已经在这个页面了,再触发导航会重复跳转。记得用
currentDestination?.id判断一下。 - 生命周期安全:导航操作一定要在Activity的
onResume之后执行。我见过有人在onCreate里就导航,结果Fragment还没初始化好,直接崩溃。 - 协程作用域:BroadcastReceiver里不要用
lifecycleScope,它没有Lifecycle。用GlobalScope或者Application级别的协程作用域。
核心要点:
- Service和BroadcastReceiver不能直接持有NavController
- 全局导航对象适合简单场景,事件总线适合复杂场景
- PendingIntent方案能处理App被杀死的情况
- 始终考虑生命周期安全,避免内存泄漏
好了,这一章的内容就到这里。后台导航其实不难,关键是要想清楚「谁持有导航能力,谁触发导航事件」这个关系。把职责分清楚,代码自然就清晰了。