我一直在玩 Hilt 在一个小应用程序中对视图模型的支持,并且需要我的视图模型来启动共享活动:
@HiltViewModel
class MyCuteLittleViewModel @Inject constructor(
) : ViewModel() {
// ... some code that invokes share()
private fun share(content: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, content)
}
val chooserIntent = Intent.createChooser(intent, "Share with…")
val activity = TODO("Need an activity here!")
activity.startActivity(chooserIntent)
}
}
视图模型在活动配置更改中保留,因此活动不可注入,这完全有道理:在视图模型中注入活动会导致配置更改泄漏。
不幸的是,Activity该类提供了很多实用程序,因此需要访问它是相当普遍的。
大多数在线资源建议将代码移动到 Activity 或有权访问它的协作者,让其监听指示要执行的操作的事件,然后从 ViewModel 发送事件。
我不在乎这些“最佳”做法。我想要那个代码在它被使用的地方,我不想要不必要的解耦。
无论如何,这里有一点 Hilt 黑客技术可以在不更改任何Activity代码的情况下支持这一点。
首先,让我们创建一个CurrentActivityProvider作用域 to @ActivityRetainedScoped,它将负责保存当前的活动实例:
@ActivityRetainedScoped
class CurrentActivityProvider @Inject constructor() {
// TODO Set and clear currentActivity
private var currentActivity: Activity? = null
fun <T> withActivity(block: Activity.() -> T) : T {
checkMainThread()
val activity = currentActivity
check(activity != null) {
"Don't call this after the activity is finished!"
}
return activity.block()
}
}
然后我们可以根据需要使用它。请注意,这withActivity()使得将活动实例意外存储在错误的位置变得更加困难:
@HiltViewModel
class MyCuteLittleViewModel @Inject constructor(
private val activityProvider: CurrentActivityProvider
) : ViewModel() {
private fun share(content: String) {
// ...
activityProvider.withActivity {
startActivity(chooserIntent)
}
}
}
CurrentActivityProvider.currentActivity现在我们需要为每个ActivityRetainedComponent范围进行设置。为此,我们创建了一个作用域为活动 ( ActivityComponent) 的入口点,它将提供对CurrentActivityProvider(位于父ActivityRetainedComponent作用域中的)的访问。入口点:
@EntryPoint
@InstallIn(ActivityComponent::class)
interface ActivityProviderEntryPoint {
val activityProvider: CurrentActivityProvider
}
现在我们可以从一个活动实例中检索作用域活动提供者:
val entryPoint: ActivityProviderEntryPoint =
EntryPointAccessors.fromActivity(this)
val activityProvider = entryPoint.activityProvider
这仅在活动是 Hilt 感知的情况下才有效,所以让我们检查它是否实现GeneratedComponentManagerHolder(🤫 它在 Hilt 的内部包中,但它也是公共的,所以🤷♂️),让我们为此制作一个小Activity.withProvider()实用程序:
activity.withProvider { activityProvider ->
// TODO
}
private fun Activity.withProvider(
block: CurrentActivityProvider.() -> Unit
) {
if (this is GeneratedComponentManagerHolder) {
val entryPoint: ActivityProviderEntryPoint =
EntryPointAccessors.fromActivity(this)
val provider = entryPoint.activityProvider
provider.block()
}
}
注意:Android 应用可以同时有多个处于创建状态的活动。这里的代码通过依赖ActivityRetainedComponent范围来支持这一点,这将为堆栈中的每个活动提供一个新组件,但当通过配置更改重新创建活动时仍然返回相同的逻辑组件。
现在让我们添加方法来更新生命周期更改的活动引用:
@ActivityRetainedScoped
class CurrentActivityProvider @Inject constructor() {
private var currentActivity: Activity? = null
fun <T> withActivity(block: Activity.() -> T) : T { /* ... */ }
companion object {
private fun Activity.withProvider(
block: CurrentActivityProvider.() -> Unit
) { /* ... */ }
fun onActivityCreated(activity: Activity) {
activity.withProvider {
currentActivity = activity
}
}
fun onActivityDestroyed(activity: Activity) {
activity.withProvider {
if (currentActivity === activity) {
currentActivity = null
}
}
}
}
}
最后让我们从我的Application班级挂钩生命周期回调:
@HiltAndroidApp
class MyCuteLittleApp : Application() {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
CurrentActivityProvider.onActivityCreated(activity)
}
override fun onActivityDestroyed(activity: Activity) {
CurrentActivityProvider.onActivityDestroyed(activity)
}
})
}
}
有了这个,我们现在可以注入CurrentActivityProvider任何ActivityRetainedComponent范围(以及较低的范围)并轻松地使用activityProvider.withActivity().
测试测试 1 2 3 🎤
为了MyCuteLittleViewModel更容易测试,我们可以将共享责任转移给注入的协作者,例如Sharer:
interface Sharer {
fun share(content: String)
}
class ActivitySharer @Inject constructor(
private val activityProvider: CurrentActivityProvider
) : Sharer {
override fun share(content: String) {
// ...
activityProvider.withActivity {
startActivity(chooserIntent)
}
}
}
@Module
@InstallIn(ActivityRetainedComponent::class)
interface SharerModule {
@Binds fun bindSharer(sharer: ActivitySharer): Sharer
}