前语

经过总结 Hilt 的运用办法,感受一下依靠注入的魅力。

Android 中运用 Hilt

关于如何运用 Hilt,参阅 Jetpack新成员,一篇文章带你玩转Hilt和依靠注入 作为入门教程即可。

无论是在 Java 仍是 Kotlin 中,日常开发中,咱们总是从类 Class 的维度去看待咱们编写的代码文件。可是从依靠注入的维度出发,更关怀的是容器这个概念。(不是 docker 那个容器哈,不要混了)。

经过在 Application 添加 @HiltAndroidApp 注解。 Application 便是一个应用级的容器了,这就意味着在这个容器里的一切依靠项大局通用。

同理关于 Hilt 官方支撑的组件,经过添加 @AndroidEntryPoint (或者是 @HiltViewModel)注解,咱们就拥有了一个支撑依靠注入的类,也便是所谓的 Ioc容器。

有了容器(类)接下来便是容器内部字段的初始化,初始化这些字段之后调用相应的办法完成各种功用。

当咱们要完成一个页面内容时,无论是 MVC/MVP 仍是 MVVM 结构。其实便是在用组合的办法把各种功用组件窜到一起,完结各自的初始,开始调用各类办法 。从 MVC 一步步发展到 MVVM, 组合的办法也许变了,但不变的是依靠项的初始化。无论是创立 Presenter 仍是实例化 ViewModel,这个步骤仍是耦合在页面这一级别。当然了,导致这个问题的原因仍是因为 Context,在 Android 中很多组件的实例化终究仍是会依靠到 Context 上。

通过 Hilt 反观依赖注入

以上图官方引荐的 MVVM 架构暗示图为例,每一次写一个新的事务时,写 UI(Activity/Frament) 时依靠 ViewModel,然后又去写 ViewModel ,ViewModel 又依靠 Repository ,Repository 又依靠 Retrofit/Http 或者是 Room。 最后便是按照依靠的反向顺序把一切的组件准备好,然后开始逐层创立依靠项的实例。这个进程十分繁琐,适当所以手动注入每一个依靠项。

而运用 Hilt 进行依靠注入的管理之后,就能够十分便利了。 以 Android 官方示例 sunflower 代码为例

@AndroidEntryPoint
class PlantListFragment : Fragment() {
    private val viewModel: PlantListViewModel by viewModels()
    ....
    private fun subscribeUi(adapter: PlantAdapter) {
        viewModel.plants.observe(viewLifecycleOwner) { plants ->
            adapter.submitList(plants)
        }
    }
}
@HiltViewModel
class PlantListViewModel @Inject internal constructor(
    plantRepository: PlantRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
}
@Singleton
class PlantRepository @Inject constructor(private val plantDao: PlantDao) {
  ...
}
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {
    // AppDataBase 虽然是抽象类,但在 Room 规范运用办法中,能够经过创立单例的办法供给。
    // 仅有需求依靠的 Context ,经过内建的 @ApplicationContext 供给支撑。
    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return AppDatabase.getInstance(context)
    }
    // PlantDao 是个接口,没有结构函数,经过 Provides 供给 
    // 这个办法依靠的 AppDatabase 经过上面的 provideAppDatabase 供给,而且都是单例
    @Provides
    fun providePlantDao(appDatabase: AppDatabase): PlantDao {
        return appDatabase.plantDao()
    }
}

一个列表页需求的数据,沿着 ViewModel–>Repository–>Dao–>Database–>Context 的依靠链,开发者只需求清晰好各个依靠项具体实例化的办法就好,在实践运行时 Hilt 会沿着依靠链反向顺次初始化各个依靠项。用 @Inject 注解的依靠项(属性、字段)都会自动的完结实例化。

依靠也不会无限套娃,在 Android 开发中,很多组件的实例化终究会依靠 Context (ApplicationContext 或者是 Activity Context),关于这些依靠,官方现已供给了内建的支撑,需求的时分直接运用即可,如上面 provideAppDatabase 中运用 Context 相同。

关于实例具体创立的办法,普通的类能够直接修正结构函数,添加 @Inject 注解,其他的无法进行直接实例化的合作运用 @Module 和 @Provides 注解自界说办法即可。@Binds 感觉有些鸡肋,无法经过结构函数进行注入的类,统一用 @Provides 就好了。

依靠注入的实质便是将容器内实例的实例化进程剥离到了外部,Hilt 相比传统的结构函数、setXXX(Obj o) 注入的办法更彻底,把解耦做到了极致。

依靠注入容器

随着依靠项越来越多,依靠联络越来越复杂,咱们需求考虑依靠项的生命周期作用域。 好在 Hilt 现已结合 Android 组件的生命周期供给了完善的支撑。

通过 Hilt 反观依赖注入

将模块安装到组件后,其绑定就能够用作该组件中其他绑定的依靠项,也能够用作组件层次结构中该组件下的任何子组件中其他绑定的依靠项

这儿就需求就结合实践运用的场景,挑选适宜的办法进行 Install ,具体来说便是 @Installin 这个注解的参数挑选了。挑选单例,亦或是 Activity 作用域。关于依靠项,创立适宜而且合理的 Ioc 容器,能够保证代码履行的性能和稳定性。

为同一类型供给多个绑定

Hilt 中供给的一个十分有用的功用是为同一个实例供给不同的完成办法。仍是以之前的列表页为例,关于 RecyclerView 来说,adapter 就能够决定列表具体显示的内容(数据相同的情况下)。因而,咱们能够完成 adapter 的注入,简化 Android 中完成一个列表的操作。

  • 界说不同的 Adapter

class AlbumAdapter(val context: Context, val imageList: ArrayList<Uri>, val imageSize: Int) :
    RecyclerView.Adapter<AlbumAdapter.ViewHolder>() {
    ...
}
class GifAdapter(val context: Context, val imageList: ArrayList<Uri>, val imageSize: Int) :
    RecyclerView.Adapter<GifAdapter.ViewHolder>() {
    ...
}
  • Adapter 的注入供给不同的完成

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AlbumAdapterAnnotation
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GifAdapterAnnotation
@Module
@InstallIn(FragmentComponent::class)
object AlbumAdapterProvider {
    private lateinit var list: ArrayList<Uri>
    @Provides
    fun provideList(): ArrayList<Uri> {
        list = ArrayList()
        return list
    }
    @AlbumAdapterAnnotation
    @Provides
    fun providerAdapter(
        @ActivityContext context: Context,
    ): AlbumAdapter {
        val size = context.resources.displayMetrics.widthPixels / 3
        return AlbumAdapter(context, list, size)
    }
    @GifAdapterAnnotation
    @Provides
    fun providerGifAdapter(
        @ActivityContext context: Context,
    ): GifAdapter {
        val size = context.resources.displayMetrics.widthPixels / 3
        return GifAdapter(context, list, size)
    }
}

前面咱们提到,很多实例的初始化需求依靠 Context (有些还必须是 Activity 级别的 Context)。因而,在手动注入依靠的时分,咱们始终绕不开 Activity 。 这儿经过内建的 @ActivityContext 直接注入了 Activity 的 Context。几乎有种玩游戏时对面直接开了一个大,随便就来一个 Context,官方出手便是吊。

  • 基于参数运用不同的 adapter
@AndroidEntryPoint
class PictureBottomDialog(val type: GalleryType) : BaseBottomSheetDialog() {
    @AlbumAdapterAnnotation
    @Inject
    lateinit var albumAdapter: AlbumAdapter
    @GifAdapterAnnotation
    @Inject
    lateinit var gifAdapter: GifAdapter
    private lateinit var adapter: RecyclerView.Adapter<*>
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.bottom_sheet_layout, container, false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        adapter = if (type == GalleryType.GIF) gifAdapter else albumAdapter
        val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.layoutManager = GridLayoutManager(context, columns)
        recyclerView.adapter = adapter
}

能够看到这儿咱们能够基于 Hilt 的自界说注解界说多种类型的 adapter ,在运行时基于实践情况决定运用哪种 adapter。尤其是在 Adapter 的初始化依靠运行时其他依靠项的时分,结合 Hilt 的特性能够协助咱们更好的保护各个组件的依靠联络和生命周期,尽可能的避免空指针之类的问题。

EntryPoint

Hilt 供给的另一个利器便是 @EntryPoint 了。上面提到的一切内容都支撑 Android 开发者十分熟悉的 Android 组件。关于官方默许没有供给支撑的其他组件,咱们能够经过 @EntryPoint 让他和 Hilt 支撑的规范组件发生联络。

经过 EntryPoint 咱们能够完成接口的注入,十分便利的完成面向接口编程。一切组件都依靠于接口,而具体的完成在哪里并不重要。

假定咱们现在有一个操控播放器行为的接口

界说接口

interface IVideoPlayer {
    fun play()
    fun pause()
    fun resume()
    fun stop()
}

完成接口

@Singleton
class IVideoPlayerImpl : IVideoPlayer {
    override fun play() {
        Log.d(TAG, "play() called")
    }
    override fun pause() {
        Log.d(TAG, "pause() called")
    }
    override fun resume() {
        Log.d(TAG, "resume() called")
    }
    override fun stop() {
        Log.d(TAG, "stop() called")
    }
    @Module
    @InstallIn(SingletonComponent::class)
    class VideoPlayerModule {
        @Singleton
        @Provides
        fun provideVideoPlayer(): IVideoPlayer {
            return IVideoPlayerImpl()
        }
    }
}
  • 接口的行为应该是仅有的,因而咱们用 @Singleton 注解保证这个注入只会大局实例化统一的一个
  • 经过 @Module 注解界说的类,供给 IVideoPlayer 这个接口的具体完成
  • 这个接口的完成能够在恣意模块中,只需保证运用到的当地对这个模块有依靠联络即可

注入接口

界说 Hilt 能够履行到进口点。

@EntryPoint
@InstallIn(SingletonComponent::class)
interface MiniEntryPoint {
   fun videoPlayer(): IVideoPlayer
}

在一个项目里,如果说 Hint 经过 @HiltAndroidApp 构建了一个依靠容器依靠联络树的话,那么经过 @EntryPoint 便是把这儿界说的内容和这棵树搭了一条线。

运用接口

EntryPointAccessors.fromApplication(MinApp.INSTANCE, MiniEntryPoint::class.java).videoPlayer().play()

咱们能够在任何需求运用 IVideoPlayer 的当地经过上述办法获取到这个接口真正的完成并调用。

这儿再次着重一下,IVideoPlayer 的完成能够在恣意模块中。经过 @EntryPoint 进口点供给注入办法后,就能够运用 EntryPointAccessors.fromApplication() 办法获取到这个接口的完成了。 Hilt 经过编译期的工作,为运行期供给了十分便利的完成。

依靠注入

每次提到依靠注入,总会面临一个尴尬的问题。为什么需求运用?上面举的一切例子,没有一个是缺少依靠注入结构干不了的。甚至有些内容的完成运用依靠注入结构有种在炫技的感觉。同时这类依靠注入结构的运用,会添加编译耗时,甚至是报错。在复杂度较低的项目中,运用依靠注入的确有点过渡设计,为了用而用的意味。可是在项目规模加大,需求多人协作开发的时分,合理运用依靠注入结构是能够提升效率的。

参阅文档

  • 运用 Hilt 完成依靠注入

  • Jetpack新成员,一篇文章带你玩转Hilt和依靠注入