全局获取 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 对象即可。

相关推荐
叶羽西1 小时前
Android15系统中(娱乐框架和车机框架)中对摄像头的朝向是怎么定义的
android
Java小白中的菜鸟1 小时前
安卓studio链接夜神模拟器的一些问题
android
莫比乌斯环1 小时前
【Android技能点】深入解析 Android 中 Handler、Looper 和 Message 的关系及全局监听方案
android·消息队列
编程之路从0到11 小时前
React Native新架构之Android端初始化源码分析
android·react native·源码阅读
行稳方能走远1 小时前
Android java 学习笔记2
android·java
编程之路从0到12 小时前
React Native 之Android端 Bolts库
android·前端·react native
爬山算法2 小时前
Hibernate(38)如何在Hibernate中配置乐观锁?
android·java·hibernate
行稳方能走远2 小时前
Android java 学习笔记 1
android·java
zhimingwen2 小时前
【開發筆記】修復 macOS 上 JADX 啟動崩潰並實現快速啟動
android·macos·反編譯