全局获取 Context:从静态方案到 Hilt 依赖注入

前言

我们很多地方都会用到 Context,比如弹出提示时、启动 Activity 时。目前我们很多代码都写在了 Activity 中,获取 Context 对象很容易,因为 Activity 本身就是一个 Context 对象。

但当应用的架构逐渐复杂起来后,我们的很多逻辑代码都会脱离 Activity。如果这时你需要 Context,你不得不通过参数把 Context 传递进来。虽说这也能解决问题,但这样层层传递不仅写起来非常麻烦,也让代码的调用关系变得不那么纯粹了。

不过,你不用担心。我们将介绍两种获取 Context 的方法,带你从能用到好用。

方案一:全局 Context

要解决这个问题,思路其实很简单,我们只需要找到一个在应用启动时创建、在应用的整个生命周期中存在的全局对象,让它来帮我们保管 Context,这样我们就能够随时随地获取了。

Android 提供了一个 Application 类,它就能完美满足我们的需求。每当应用启动时,系统都会初始化这个类的实例。我们可以对这个类进行定制,存放一些全局的状态信息,就比如 Context

首先,创建一个 MyApplication 类,继承自 Application

kotlin 复制代码
class MyApplication : Application() {

    companion object {
        lateinit var context: Context
    }

    override fun onCreate() {
        super.onCreate()
        context = applicationContext
    }
    
}

不过,写完后你会发现 context 变量报了一个警告:Do not place Android context classes in static fields; this is a memory leak

这是因为静态持有 Context 可能会导致内存泄漏问题。

为什么静态持有 Context 会有风险?

当一个长生命周期的对象,持有了一个短生命周期的对象引用时,就可能导致内存泄漏。例如我们持有了 ActivityContext,那么当 Activity 需要销毁时,由于 MyApplication 这个全局单例还持有着它的引用,Activity 的内存就无法被回收,内存泄漏就发生了。

但由于我们赋给 context 变量的是 applicationContext(应用上下文)。这个 Context 的生命周期和 Application 保持一致,在应用的整个生命周期内都存在。所以不存在持有短生命周期对象引用导致的内存泄露问题。

我们可以通过注解来告诉 Lint 忽略这个警告,并让 context 只能在内部被修改:

kotlin 复制代码
class MyApplication : Application() {

    companion object {
        @SuppressLint("StaticFieldLeak")
        lateinit var context: Context
            private set // 确保只能在 MyApplication 内部被赋值,防止被外部意外修改
    }

    override fun onCreate() {
        super.onCreate()
        context = applicationContext
    }

}

然后,我们要告诉系统,应用启动时应该初始化我们自定义的 MyApplication。我们需要在 AndroidManifest.xml 文件中通过 <application> 标签的 name 属性来指定:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:name=".MyApplication" ...>
        ...
    </application>

</manifest>

这样,当我们需要 Context 时,我们可以通过 MyApplication.context 的方式来获取。例如:

kotlin 复制代码
object ToastUtils {
    fun showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
        // 直接使用,无需传递 context 参数
        Toast.makeText(MyApplication.context, message, duration).show()
    }
}

方案二:依赖注入

我们刚才的解决方案很直接,也很好用,但这并不是最好的选择。ToastUtils 为了完成自己的功能,需要明确知道有 MyApplication 这个类的存在。

这带来了两个问题:一是 ToastUtilsMyApplication 耦合性太高ToastUtils 必须与 MyApplication 配合使用。二是可测试性太低 ,很难对 ToastUtils 进行单元测试,因为在单元测试的环境中,并没有真实的应用在运行,导致 MyApplication.context 根本不会被初始化,当调用时应用就会崩溃。

为此,我们会采用依赖注入(Dependency Injection, DI)的设计思想。我们并不会主动去获取依赖,而是在需要时,由外部将依赖传递进来。

Google 给我们提供了 Hilt 来完成这一点,要使用 Hitl,首先得进行配置:

  1. 在项目根路径的 build.gradle.kts 文件中,声明 Hilt 和 KSP 的 Gradle 插件。
kotlin 复制代码
// build.gradle.kts (Project-level)
plugins {
    id("com.google.dagger.hilt.android") version "2.56.2" apply false
    id("com.google.devtools.ksp") version "2.1.0-1.0.28" apply false
}
  1. app/build.gradle.kts 文件中启用插件,并添加 Hilt 的运行时库和编译器。
kotlin 复制代码
// app/build.gradle.kts (Module-level)
plugins {
    id("com.google.devtools.ksp")
    id("com.google.dagger.hilt.android")
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.56.2")
    ksp("com.google.dagger:hilt-android-compiler:2.56.2")
}
  1. MyApplication 类加上 @HiltAndroidApp 注解,来启用 Hilt。
kotlin 复制代码
@HiltAndroidApp
class MyApplication : Application() 

配置步骤参考于官方文档

配置完成后,我们就可以开始使用 Hilt 了:

kotlin 复制代码
// Context 会通过构造函数传递
class ToastHelper @Inject constructor(
    @ApplicationContext private val context: Context
) {
    fun showToast(message: String) {
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}
kotlin 复制代码
// 在 Activity 或者 ViewModel 中使用
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var toastHelper: ToastHelper // Hilt 会自动创建该实例

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
       
        toastHelper.showToast("Hello from Hilt!")

    }


}

可以看到,ToastHelper 只是在构造函数上声明了它所需要的参数,Hilt 就会给它提供一个 Application Context。

这样 ToastHelperMyApplication 完全解耦了,可以拿到任何地方使用。并且方便测试,我们只需创建一个 ToastHelper 实例,并传给它一个 Context 对象即可。

相关推荐
Industio_触觉智能31 分钟前
量产技巧之RK3588 Android12默认移除导航栏&状态栏
android·rk3588·开发板·核心板·瑞芯微·rk3588j
小馬佩德罗33 分钟前
Android系统的问题分析笔记 - Android上的调试方式 bugreport
android·调试
VividnessYao33 分钟前
Android Handler 消息机制
android
iReaShare2 小时前
如何将华为文件传输到电脑
android
火柴就是我2 小时前
每日见闻之Rust中 trait 的孤儿规则
android·rust
IT 前端 张2 小时前
uni-app在安卓设备上获取 (WIFI 【和】以太网) ip 和 MAC
android·tcp/ip·uni-app
iReaShare2 小时前
如何轻松将音乐从安卓设备传输到安卓设备
android
狂浪天涯3 小时前
Android 16 | Display Framework - 2 | Surface
android·操作系统
没有了遇见3 小时前
Android 异常处理机制全解析:虚拟机层、Java 层与 Native 层
android