导航与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
    }
}
注意:WeakReference虽然能防止内存泄漏,但如果Activity已经被销毁,导航请求就丢了。所以最好加一个fallback机制——比如发送一个通知,让用户手动点击进入。

解决方案二:使用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))
}
我的建议:如果项目里已经用了SharedFlow或者StateFlow做统一事件管理,那就用这个方案。如果只是偶尔需要,全局导航对象更轻量。

解决方案三: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)
            }
        }
    }
}
我曾经踩过的坑:如果App已经被系统杀死,Activity会重新创建,走的是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 BroadcastReceiver 方案一:全局导航 GlobalNavigator 单例 WeakReference 持有 Activity 注册/注销 直接调用 navigate() 方案二:事件总线 SharedFlow/LiveData 密封类定义事件 Activity 收集事件 Service 发送事件 方案三:PendingIntent Notification 通知 Intent 携带参数 onNewIntent 解析 支持 App 被杀死 目标 NavController Fragment 跳转 适用场景:方案一适合前台,方案二适合常规后台,方案三适合被杀后恢复

避坑指南

  • 我曾经犯过的错:在Service里直接持有Activity的Context,结果Activity销毁后Service还在运行,导致内存泄漏。后来全部改用Application Context + 事件总线。
  • 导航重复问题:如果用户已经在这个页面了,再触发导航会重复跳转。记得用currentDestination?.id判断一下。
  • 生命周期安全:导航操作一定要在Activity的onResume之后执行。我见过有人在onCreate里就导航,结果Fragment还没初始化好,直接崩溃。
  • 协程作用域:BroadcastReceiver里不要用lifecycleScope,它没有Lifecycle。用GlobalScope或者Application级别的协程作用域。

核心要点:

  • Service和BroadcastReceiver不能直接持有NavController
  • 全局导航对象适合简单场景,事件总线适合复杂场景
  • PendingIntent方案能处理App被杀死的情况
  • 始终考虑生命周期安全,避免内存泄漏

好了,这一章的内容就到这里。后台导航其实不难,关键是要想清楚「谁持有导航能力,谁触发导航事件」这个关系。把职责分清楚,代码自然就清晰了。

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