前言
我们很多地方都会用到 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 会有风险?
当一个长生命周期的对象,持有了一个短生命周期的对象引用时,就可能导致内存泄漏。例如我们持有了 Activity
的 Context
,那么当 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
这个类的存在。
这带来了两个问题:一是 ToastUtils
和 MyApplication
耦合性太高 ,ToastUtils
必须与 MyApplication
配合使用。二是可测试性太低 ,很难对 ToastUtils
进行单元测试,因为在单元测试的环境中,并没有真实的应用在运行,导致 MyApplication.context
根本不会被初始化,当调用时应用就会崩溃。
为此,我们会采用依赖注入(Dependency Injection, DI)的设计思想。我们并不会主动去获取依赖,而是在需要时,由外部将依赖传递进来。
Google 给我们提供了 Hilt 来完成这一点,要使用 Hitl,首先得进行配置:
- 在项目根路径的
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
}
- 在
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")
}
- 给
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。
这样 ToastHelper
与 MyApplication
完全解耦了,可以拿到任何地方使用。并且方便测试,我们只需创建一个 ToastHelper
实例,并传给它一个 Context
对象即可。