23. LiveData与BroadcastReceiver:网络状态监听、电量变化监听、实战:网络变化提示

各位同学,今天我们来聊一个非常接地气的话题——LiveData 与 BroadcastReceiver 的联姻

说实话,BroadcastReceiver 在 Android 里算是个老面孔了。从最早的 Android 1.0 开始,它就在那里。但问题是,很多同学用 BroadcastReceiver 的方式还停留在「注册-接收-注销」的原始阶段,代码散落在 Activity 或 Service 里,维护起来那叫一个酸爽。

我个人习惯是:能用 LiveData 包装的,绝不裸写 Receiver。为什么?因为 LiveData 自带生命周期感知,不会泄漏,不会空指针,而且 UI 层订阅起来极其优雅。

今天我们就拿两个最典型的场景开刀:网络状态监听电量变化监听。最后再做一个实战——网络变化提示弹窗。

23.1 为什么要把 BroadcastReceiver 包装成 LiveData?

你想想看,传统的 BroadcastReceiver 写法是什么样的?

// 传统写法:在 Activity 里注册和注销
class MainActivity : AppCompatActivity() {
    private lateinit var networkReceiver: BroadcastReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        networkReceiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                // 处理网络变化
            }
        }
        registerReceiver(networkReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(networkReceiver) // 容易忘!
    }
}

这段代码有什么问题?我列几个:

  • 生命周期耦合:你必须在 onDestroy 里手动注销,忘了就泄漏。
  • 无法感知生命周期状态:如果 Activity 在后台,Receiver 还在跑,白白浪费资源。
  • 测试困难:Receiver 逻辑和 Activity 绑在一起,单元测试无从下手。

而用 LiveData 包装后,这些问题全部消失。LiveData 会自动在 onStart 时开始观察,onStop 时停止观察,onDestroy 时自动清理。说白了,LiveData 就是 BroadcastReceiver 的「智能管家」

23.2 网络状态监听:LiveData 封装实战

我们先写一个网络状态监听器。核心思路是:创建一个自定义 LiveData,在它 active 时注册 Receiver,inactive 时注销 Receiver

class NetworkStateLiveData(context: Context) : LiveData<Boolean>() {

    private val connectivityManager =
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val activeNetwork = connectivityManager.activeNetworkInfo
            val isConnected = activeNetwork?.isConnectedOrConnecting == true
            postValue(isConnected) // 注意:这里用 postValue,因为是在子线程回调
        }
    }

    private var appContext: Context = context.applicationContext

    override fun onActive() {
        super.onActive()
        // LiveData 开始被观察时注册 Receiver
        appContext.registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
        // 立即获取一次当前状态
        val activeNetwork = connectivityManager.activeNetworkInfo
        value = activeNetwork?.isConnectedOrConnecting == true
    }

    override fun onInactive() {
        super.onInactive()
        // LiveData 不再被观察时注销 Receiver
        appContext.unregisterReceiver(receiver)
    }
}

关键点解析

  • onActive()onInactive() 是 LiveData 的生命周期回调,由系统自动管理。
  • 使用 applicationContext 避免 Activity 泄漏。
  • postValue() 用于跨线程更新数据,setValue() 只能在主线程调用。

我在项目中遇到过一个问题:Android 7.0 之后,CONNECTIVITY_ACTION 这个广播不再支持动态注册。嗯,这里要注意,从 Android 7.0(API 24)开始,系统限制了这个隐式广播。那怎么办?

解决方案是改用 registerNetworkCallback()。我们升级一下:

class NetworkStateLiveData(context: Context) : LiveData<Boolean>() {

    private val connectivityManager =
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            postValue(true)
        }

        override fun onLost(network: Network) {
            postValue(false)
        }
    }

    private var appContext: Context = context.applicationContext

    override fun onActive() {
        super.onActive()
        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        connectivityManager.registerNetworkCallback(request, networkCallback)
        // 获取当前状态
        val activeNetwork = connectivityManager.activeNetworkInfo
        value = activeNetwork?.isConnectedOrConnecting == true
    }

    override fun onInactive() {
        super.onInactive()
        connectivityManager.unregisterNetworkCallback(networkCallback)
    }
}

避坑指南:我曾经在旧项目里混用了两种方式,结果在 Android 10 上出现了重复回调。建议统一使用 registerNetworkCallback(),兼容性更好。

23.3 电量变化监听:同样的套路

网络状态能监听,电量当然也可以。原理一模一样:

class BatteryStateLiveData(context: Context) : LiveData<Int>() {

    private val appContext = context.applicationContext
    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
            val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
            val percentage = (level.toFloat() / scale.toFloat() * 100).toInt()
            postValue(percentage)
        }
    }

    override fun onActive() {
        super.onActive()
        appContext.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
        // 注意:ACTION_BATTERY_CHANGED 是粘性广播,注册后会立即收到一次
    }

    override fun onInactive() {
        super.onInactive()
        appContext.unregisterReceiver(receiver)
    }
}

这里有个小细节:ACTION_BATTERY_CHANGED粘性广播(Sticky Broadcast)。注册后系统会立即发送最后一次的状态,所以我们不需要手动获取初始值。

但粘性广播在 Android 8.0 之后被标记为废弃。我个人建议:不要依赖粘性特性,还是在 onActive 里手动获取一次初始值更稳妥

23.4 实战:网络变化提示弹窗

好,理论讲完了,我们来做一个完整的实战。需求很简单:

  • 当网络从有到无时,弹出一个 Snackbar 提示「网络已断开」。
  • 当网络从无到有时,弹出一个 Snackbar 提示「网络已恢复」。
  • 只在 Activity 可见时弹出,不可见时不弹。

我们用 ViewModel + LiveData 来实现:

class NetworkViewModel : ViewModel() {
    private val networkState = NetworkStateLiveData(MyApp.instance)

    // 对外暴露不可变的 LiveData
    val isConnected: LiveData<Boolean> = networkState

    // 用于触发提示的 SingleLiveEvent(避免重复弹出)
    private val _networkToastEvent = SingleLiveEvent<Boolean>()
    val networkToastEvent: LiveData<Boolean> = _networkToastEvent

    init {
        // 监听网络变化,当状态改变时发送事件
        networkState.observeForever { connected ->
            _networkToastEvent.value = connected
        }
    }

    override fun onCleared() {
        super.onCleared()
        networkState.removeObserver { } // 清理
    }
}

这里用到了 SingleLiveEvent,它是一个只会发送一次事件的 LiveData,非常适合「弹一次 Toast」这种场景。代码我就不贴了,网上有很多实现,核心就是重写 observe() 方法,每次只分发一次。

然后在 Activity 里:

class MainActivity : AppCompatActivity() {

    private val viewModel: NetworkViewModel by viewModels()

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

        // 观察网络状态(用于 UI 更新)
        viewModel.isConnected.observe(this) { connected ->
            // 比如更新一个图标
            updateNetworkIcon(connected)
        }

        // 观察网络变化事件(用于弹提示)
        viewModel.networkToastEvent.observe(this) { connected ->
            val message = if (connected) "网络已恢复" else "网络已断开"
            Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
        }
    }
}

注意:千万不要在 isConnected 的观察回调里直接弹 Snackbar。因为每次 Activity 重建(比如旋转屏幕)时,LiveData 会重新发送最后一次的值,导致重复弹窗。用 SingleLiveEvent 就是为了避免这个问题。

23.5 知识体系总览

下面这张图总结了本章的核心逻辑:

LiveData + BroadcastReceiver 架构图 BroadcastReceiver 网络状态变化 电量变化 其他系统广播 LiveData 包装 onActive() 注册 onInactive() 注销 postValue() 更新 生命周期感知 UI 层 Activity / Fragment observe() 订阅 Snackbar 提示 图标更新 系统广播 → LiveData 包装(生命周期感知)→ UI 层自动订阅

这张图其实就说了三件事:

  1. 数据源:系统广播(网络、电量等)通过 BroadcastReceiver 接收。
  2. 中间层:自定义 LiveData 负责注册/注销 Receiver,并把数据转为 LiveData 流。
  3. 消费端:Activity/Fragment 通过 observe() 订阅,自动感知生命周期。

说白了,这就是一个观察者模式的经典应用。BroadcastReceiver 是事件源,LiveData 是事件总线,UI 是观察者。

23.6 总结与最佳实践

最后,我给大家几个建议:

场景 推荐做法 不推荐做法
网络状态监听 使用 registerNetworkCallback() + LiveData 直接注册 CONNECTIVITY_ACTION 广播
电量变化监听 使用 ACTION_BATTERY_CHANGED + LiveData 在 Activity 里直接注册不注销
弹窗提示 使用 SingleLiveEvent 避免重复 直接在 LiveData 回调里弹 Toast
生命周期管理 依赖 LiveData 的 onActive/onInactive 手动在 onResume/onPause 里注册注销

嗯,今天就到这里。记住一句话:能用 LiveData 包装的广播,就不要裸写。你的代码会变得更干净、更安全、更好测试。


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