在Android Jetpack Compose中使用Hilt依赖注入

在Android Jetpack Compose中使用Hilt依赖注入

引言

依赖注入是一种编程模式,建议类不应构建其依赖项的实例,而是由外部提供这些实例。这种模式能够实现关注点分离,提高可测试性、重用性和维护性。Hilt是一个基于Dagger的Android库,实现了Android应用的依赖注入解决方案。Hilt的两个重要特性:

  • 提供依赖项:如何构建对象及其依赖项,并由需要它们的类获取。
  • 作用域依赖项:定义对象的保留位置及其有效生命周期。
    Compose正在迅速成为构建Android应用UI的新标准,使用函数式编程控制屏幕显示内容。
    文章将讨论Hilt在传统Android应用中提供和作用域依赖项的细节,以及Compose如何改变我们的处理方式。

依赖提供(Providing Dependencies)

Hilt通过注解生成源代码,实例化对象并将其作为依赖项提供给其他对象。在依赖图中,每个类型及其构造方法必须是已知的,以确保所有依赖项都能满足,否则代码生成器将失败,应用程序也无法构建。

虽然这种预先工作会导致稍长的构建时间,但它带来的好处是高性能的代码和类型安全。生成的代码准确知道在哪需要哪些对象,以及如何正确创建它们,这样可以避免因缺少依赖项或接收错误类型的依赖项而引发的运行时错误。

示例:在Fragment中注入依赖

假设我们需要使用依赖注入创建实现业务逻辑的对象,并在某个时刻将它们连接到UI。在Android应用中,通过Activity和Fragment与UI层桥接,但这些类由Android框架实例化,无法由Hilt或其他DI解决方案创建。

幸运的是,Hilt可以通过添加几个注解,自动将成员注入到Activity和Fragment等框架类中。例如,CheckoutFragment需要一个PaymentApi以便用户下订单,以下展示了实现方式:

class PaymentApi @Inject constructor() {
    fun submitPayment(...) { /* other business逻辑 */ }
}

@AndroidEntryPoint
class CheckoutFragment : Fragment() {
    @Inject
    lateinit var paymentApi: PaymentApi // 示例依赖

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        submitButton.setOnClickListener {
            paymentApi.submitPayment(...)
        }
    }
}

推荐的做法:在ViewModel中注入依赖

推荐的做法是在ViewModel中通过构造函数注入依赖。这不仅有助于将依赖项以一种与UI框架无关的方式进行分组,还允许这些对象在Activity或Fragment重新创建时得以保留。以下是将PaymentApi放在ViewModel中的示例:

@AndroidEntryPoint
class CheckoutFragment : Fragment() {
    private val viewModel: CheckoutViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        submitButton.setOnClickListener {
            viewModel.submitPayment(...)
        }
    }
}

@HiltViewModel
class CheckoutViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val paymentApi: PaymentApi, // 示例依赖
) : ViewModel() { 

    fun submitPayment(...) {
        paymentApi.submitPayment(...)
    }
}

特殊情况:在框架类中注入依赖

有时将依赖注入ViewModel中并不现实。如果PaymentApi需要与Activity实例一起构造,则ViewModel持有该依赖会在配置更改后导致旧Activity泄漏。在这种情况下,退回到框架类注入是更好的选择。

依赖作用域(Scoping Dependencies)

Hilt允许声明依赖作用域和它们保留的容器,这些容器被称为组件(Components)。一个作用域及其对应的组件定义了对象的生命周期。

Hilt为Android提供了一套意见明确的作用域和组件,这些作用域与Activity、Fragment和ViewModel等关键类型对齐,因为这些类型都有明确的生命周期,且通常希望依赖项根据这些生命周期进行保留。

选择合适的作用域对依赖的正确性和性能有重要影响。如果你在一个特定屏幕需要一个对象,但将其注入到应用程序的更广泛生命周期中,该对象在不使用时也会占用内存。同样,为管理某些共享数据而选择过小的作用域会导致对象过早消失,引发问题。

示例:定义作用域

假设我们有一个仅在特定Activity中使用的对象,可以为其定义Activity作用域。以下是一个简单的例子:

@ActivityScoped
class SomeActivityScopedDependency @Inject constructor() {
    // 业务逻辑
}

@AndroidEntryPoint
class SomeActivity : AppCompatActivity() {
    @Inject
    lateinit var someDependency: SomeActivityScopedDependency

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 使用someDependency
    }
}

在Jetpack Compose中使用Hilt

与传统的Android开发不同,Jetpack Compose使用函数式编程来控制UI展示,这带来了新的挑战。Compose中的Composable函数只是函数,它们没有构造函数或成员,因此既不能使用构造函数注入,也不能使用成员注入。

在Compose中使用ViewModel和导航

如果依赖项可以注入到ViewModel中,那么这仍然是将对象连接到UI层的推荐方法。ViewModel有明确的生命周期,不依赖于特定的Composable,并且可以按需获取。

@HiltViewModel
class CheckoutViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val paymentApi: PaymentApi // 示例依赖
) : ViewModel() {

    fun submitPayment(...) {
        paymentApi.submitPayment(...)
    }
}

@Composable
fun CheckoutScreen(
    viewModel: CheckoutViewModel = hiltViewModel()
) {
    // ...
    Button(
        onClick = { viewModel.submitPayment(...) }
    ) {
        Text("Submit")
    }
}

hiltViewModel()函数负责查找最近的适合所有者,通常是Activity或Fragment,这样ViewModel会在导航目的地的返回堆栈条目存在期间被保留,从而更好地与其使用位置对齐。

使用包含类进行构造函数注入

如果某些依赖项无法注入到ViewModel中,但仍希望将它们逻辑上分组到使用它们的内容附近,可以创建一个包含Composable函数的类,并将依赖项注入其中。然后,将包含类注入到需要它的Activity或Fragment中,并从那里调用Composable函数。包含类不应包含除注入依赖项外的任何状态,也不应使用任何作用域注解。

class PaymentComposable @Inject constructor(
    private val paymentApi: PaymentApi // 示例依赖
) {
    @Composable
    fun PaymentScreen() {
        Button(
            onClick = { paymentApi.submitPayment(...) }
        ) {
            Text("Submit")
        }
    }
}

@AndroidEntryPoint
class CheckoutActivity : AppCompatActivity() {
    @Inject
    lateinit var paymentComposable: PaymentComposable

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            paymentComposable.PaymentScreen()
        }
    }
}

避免在CompositionLocal中存储依赖项

尽管将@Injected对象存储在CompositionLocal中并让Composable通过这种方式获取这些对象似乎更简单,但这种方法放弃了Hilt提供的一些保护措施,并引入了可能的运行时错误。期望的对象可能不存在,或可能被其他Composable替换,这会引发问题。推荐使用Entry Points来获取依赖项。

使用Entry Points获取依赖项

Hilt提供了一种通过@EntryPoint从依赖图中获取对象的方法。这种方法更类似于“请求”对象,而不是“注入”对象,但与CompositionLocal不同,使用Entry Point可以保证你收到的对象是正确的类型并且适用于当前的作用域。以下代码展示了如何在CheckoutScreen Composable中获取PaymentApi:

@EntryPoint
@InstallIn(ActivityComponent::class)
interface PaymentApiEntryPoint {
    fun paymentApi(): PaymentApi
}

@Composable
fun CheckoutScreen() {
    val activity = LocalContext.current as Activity
    val paymentApi = remember {
        EntryPointAccessors.fromActivity(
            activity,
            PaymentApiEntryPoint::class.java
        ).paymentApi()
    }
    // ...
}

使用remember是为了避免在每次CheckoutScreen重新组合时访问依赖图。需要注意的是,paymentApi将在CheckoutScreen离开组合时被忘记,因此这种模式应保留给直到用户导航到应用的其他部分或发生其他重大UI更改时才会消失的高层Composable。

使用自定义依赖组件

Hilt中的依赖组件具有固定的生命周期,无法更改,因此我们可以创建一个新的组件。由于它不是预定义的组件,Hilt不会知道在何处创建它或保留多长时间,所以这些部分必须由我们自己实现。首先定义一个PaymentComponent来持有我们的PaymentApi:

@DefineComponent(parent = ActivityComponent::class)
interface PaymentComponent {

    @DefineComponent.Builder
    interface PaymentComponentBuilder {
        fun build(): PaymentComponent
    }
}

@EntryPoint
@InstallIn(ActivityComponent::class)
interface PaymentComponentBuilderEntryPoint {
    fun paymentComponentBuilder(): Provider<PaymentComponentBuilder>
}

@EntryPoint
@InstallIn(PaymentComponent::class)
interface PaymentApiEntryPoint {
    fun paymentApi(): PaymentApi
}

自定义组件需要两样东西:一个父组件(在本例中是ActivityComponent)和一个用于构建它的Builder接口。我们新增了一个Entry Point PaymentComponentBuilderEntryPoint,它将使我们能够访问组件的构建器,并将PaymentApiEntryPoint安装到PaymentComponent中。

回到CheckoutScreen Composable,我们可以获取一个PaymentComponentBuilder并构建它,然后从生成的PaymentComponent中获取PaymentApi。与之前一样,使用remember避免在每次重新组合时重复此操作。

@Composable
fun CheckoutScreen() {
    val activity = LocalContext.current as Activity
    val paymentApi = remember {
        val paymentComponent = EntryPointAccessors.fromActivity(
            activity,
            PaymentComponentBuilderEntryPoint::class.java
        ).paymentComponentBuilder().get().build()
        EntryPoints.get(
            paymentComponent,
            PaymentApiEntryPoint::class.java
        ).paymentApi()
    }
    // ...
}

现在CheckoutScreen控制PaymentComponent的生命周期。当CheckoutScreen首次组合并在remember块中构建组件时,其生命周期开始,当CheckoutScreen离开组合并记忆计算被忘记时,其生命周期结束。

结论

通过Hilt,我们能够在Android应用中实现高效且安全的依赖注入管理。它通过注解生成源代码,简化了依赖的提供和作用域的定义,使得代码更加简洁、可维护和可测试。无论是在Fragment、Activity还是ViewModel中,Hilt都提供了灵活的解决方案,帮助开发者专注于业务逻辑和功能实现。
本文通过具体示例展示了如何在Android应用中使用Hilt进行依赖注入,希望对开发者们有所帮助。如果你还没有使用过Hilt,不妨在你的下一个项目中尝试一下,相信它会让你的开发过程更加高效和愉快。

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Android Jetpack Compose ,可以使用 Modifier 的 combinedClickable 和 draggable 属性来实现同时监听点击和拖动的功能。具体实现步骤如下: 1. 导入 Compose 的 Modifier 和 rememberDraggableState。 ```kotlin import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize ``` 2. 在需要添加点击和拖动的组件上,使用 Modifier 的 combinedClickable 和 draggable 属性,同时设置 interactionSource 和 draggableState。 ```kotlin // 设置 interactionSource val interactionSource = remember { MutableInteractionSource() } // 设置 draggableState val draggableState = rememberDraggableState { delta -> // 处理拖动过程的回调 } // 添加 combinedClickable 和 draggable 属性 Box( modifier = Modifier .combinedClickable( interactionSource = interactionSource, indication = null, onClick = { // 处理点击事件的回调 } ) .draggable( state = draggableState, orientation = Orientation.Horizontal, onDragStopped = { velocity -> // 处理拖动结束的回调 }, interactionSource = interactionSource, ) ) { // 添加需要点击和拖动的组件 } ``` 这样就可以同时实现监听点击和拖动的功能了。需要注意的是,点击和拖动的回调分别在 combinedClickable 和 draggable 属性处理。同时,我们也可以通过 draggableState 的回调来处理拖动过程的事件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Calvin880828

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值