一、背景
我们写程序大部分时候都是通过 new Object()来创建对象,当该对象需要在多个地方被创建时,我们有可能会封装成工厂方法。有没有更优雅的实现方式呢?
依赖注入相信大家都能说出个一二,但是在Android 端很少人会去用,下面通过一个小例子,来介绍依赖注入的使用,希望能为您带来一点帮助。先介绍下2个名词。
1.1、控制反转和依赖注入
IOC(控制反转):全称是 Inverse of Control , 是一种思想.指的是让第3方去控制去创建对象.
DI(依赖注入):全称是 Dependency Injection , 对象的创建是通过注入的方式实现. 是IOC的一种具体实现.
传统程序创建对象由调用者通过Obj obj = new Obj()创建,而依赖注入是把对象的创建和对象生命周期的管理交给容器来管理,定义注解标识,如:@Inject Obj obj,容器会根据注解在合适的时机自动为我们创建实例,我们在程序中直接使用obj。
二、 用依赖注入的目的是解耦
举个例子
在使用MVVM模式进行网络请求时,ViewModel 依赖 Repository 层,Repository 依赖Remote Data Source 和 Room,先忽略Room,把MainApi 当成 Remote Data Source,下面我分别用普通写法、泛型反射写法、hilt写法进行举例说明
2.1、普通写法
// 定义网络接口
interface MainApi {
@GET("goods/list")
List<String> requestList()
}
// 仓库
class MainRepo {
private MainApi api;
public MainRepo(MainApi api) {
this.api = api;
}
List<String> requestList() {
// 具体调用接口
return api.requestList();
}
}
// ViewModel层
class MainViewModel extends ViewModel {
private MainRepo repo = new MainRepo(new MainApi() {});
void requestList(){
// 通过repo请求接口
List<String> list = repo.requestList();
}
}
问题: 因为根据MVVM架构,每个Activity和Fragment都依赖ViewModel,每个ViewModel都依赖Repository,但是在ViewModel实例是通过谷歌提供的api创建的,在每一个ViewModel中通过new的方式进行创建Repo,这种代码是重复且不优雅的,通过反射可以减少这些重复代码。
2.2、反射写法
// 定义网络接口
interface MainApi {
@GET("goods/list")
List<String> requestList()
}
// 仓库抽象类
abstract class BaseRepo<Api> {
private Api api;
public Api getApi() {
return api;
}
public void setApi(Api api) {
this.api = api;
}
}
// 首页仓库
class MainRepo extends BaseRepo<MainApi> {
void requestList() {
// 具体调用接口
getApi().requestList();
}
}
// 抽象ViewModel层
abstract class BaseViewModel<R extends BaseRepo> {
private R repo;
public BaseViewModel() {
try {
repo = crateRepoAndApi(this);
} catch (Exception e) {
e.printStackTrace();
}
}
public R getRepo() {
return repo;
}
// 反射创建Repo和Api
public R crateRepoAndApi(BaseViewModel<R> model) throws Exception {
Type repoType = ((ParameterizedType) model.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
R repo = (R) repoType.getClass().newInstance();
Type apiType = ((ParameterizedType) repoType.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
String apiClassPath = apiType.getClass().toString().replace("class ", "").replace("interface ", "");
repo.setApi(Class.forName(apiClassPath));
return repo;
}
}
// ViewModel层
class MainViewModel extends BaseViewModel<MainRepo> {
void requestList() {
// 通过repo请求接口
getRepo().requestList();
}
}
新增了BaseRepo和BaseViewModel 2个类,并定义了泛型,在BaseViewModel实例化时获取子类的泛型,然后反射创建Repo,再根据Repo的泛型,反射创建api。通过反射确实能够解耦,并且不用在每个ViewModel手动创建Repo和Api了,有没有其他实现方式呢?
2.3、Hilt写法
@HiltViewModel
public class MainViewModel extends ViewModel {
@Inject
public MainRepo repo ;
}
class MainRepo extends BaseRepo {
@Inject
public MainRepo() {}
@Inject
public MainApi api;
}
@InstallIn(SingletonComponent.class)
@Module
public class ApiModule {
@Singleton
@Provides
public MainApi provideMainApi() {
// 通过Retrofit创建api,这里只是举例,
return new MainApi() {};
}
}
public interface MainApi {
@GET("goods/list")
List<String> requestList()
}
3种不同写法都讲完了,各有优势劣势,hilt写法虽然高级,但是在编译期新增了不少的类,模块化开发需要在每个模块的build.gradle添加依赖,用kotlin写法代码量会更少点,为了方便阅读我用Java代码举的例子。至于实战中用哪种方式来实现就仁者见仁了,我个人偏向于反射泛型实现,虽然在ViewModel中创建Repo不符合MVVM架构的思想,但是使用起来也方便和解耦。
三、hilt的具体使用步骤
在介绍hilt之前,先说下依赖注入在Android中的历史,Dagger是由square在2012年推出的,基于反射来实现的。后来谷歌在此基础上进行了重构,也就是Dagger2,基于Java注解来实现的,在编译期就会检查错误,如果编译通过,项目正常运行是没问题的,适用于Java、kotlin。而 hilt 则是谷歌面向Android写的一套依赖注入框架,相比Dagger2 简单易用,提供了android端专属api。
3.1、引入包
1 在项目最外层build.gralde引入
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.37'
2 在app模块顶部
plugin "dagger.hilt.android.plugin"
plugin "kotlin-kapt"
3 在app模块内
kapt { // 纠正错误类型,可选
correctErrorTypes true
}
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
4 添加依赖
implementation 'com.google.dagger:hilt-android:2.37'
kapt 'com.google.dagger:hilt-compiler:2.37'
3.2、添加@HiltAndroidApp
必须在Application子类上添加注解@HiltAndroidApp
@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}
sunflower这是谷歌Android官方的示例
四、Hilt 常用注解的含义
在没有使用依赖注入框架之前,我们在代码中创建对象正常是通过new xxx(),这个对象的创建过程其实是交给了虚拟机本身,我们不关心其内部是怎么创建的。而使用依赖注入后,对象的创建就交给 hilt 控制了,hilt内部是怎么创建的呢? 这时也引入一个概念: 容器。
容器负责创建对象,不需要手动我们去new,只需要在指定位置添加不同的注解,hilt内部选择不同的容器帮我们创建对象,怎么选择的呢?主要是由我们自己通过@InstallIn(容器.class)指定 ,容器创建的对象的生命周期由hilt决定,单例类就由单例容器创建,ViewModel容器创建出来的对象生命周期和ViewModel是一致的。在Activity中使用的对象就由Activity容器创建,不同容器创建的对象,其生命周期也不一样。
准确的应该叫组件,我喜欢叫容器,更加通俗点,哈哈。下面是官方的图,我在此基础上添加了ViewModelComponent,更加全面点。
容器/组件 (接口) | 作用范围(注解) | 创建于 | 销毁于 |
---|---|---|---|
SingletonComponent | @Singleton | Application#onCreate() | Application#onDestroy() |
ActivityRetainedComponent | @ActivityRetainedScoped | Activity#onCreate() | Activity#onDestroy() |
ServiceComponent | @ServiceScoped | Service#onCreate() | Service#onDestroy() |
ViewModelComponent | @ViewModelScoped | ViewModel创建 | ViewModel销毁 |
ActivityComponent | @ActivityScoped | Activity#onCreate() | Activity#onDestroy() |
FragmentComponent | @FragmentScoped | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | @ViewScoped | View#super() | View destroyed |
ViewWithFragmentComponent | @ViewScoped | View#super() | View destroyed |
通过上面图片我们要明白以下几点:
- 容器/组件:@InstallIn用在类上,作用范围: @Singleton、@ActivityScoped等用在方法上。
- SingletonComponent容器的生命周期是最长的,ServiceComponent 和 ActivityRetainedComponent 继承于SingletonComponent的ActivityComponent 和 ViewModelComponent 继承于 ActivityRetainedComponent,以此类推...
- 通过@ActivityScoped标识的方法返回的类,可以在Fragment和View中进行依赖注入,通过@FragmentScoped标识的类,如果是ViewWithFragmentComponent容器创建的对象也可以在View中注入。
问题: 在MainActivity注入一个User类时,hilt 内部是怎么创建对象的呢?理解这点非常重要。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var scope1: User
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
class User @Inject constructor() { }
@Module
@InstallIn(ActivityComponent::class)
class UserModule {
@Provides
@ActivityScoped
fun createUser(): User {
return User()
}
}
以上面代码举例:
-
当前注入类是在入口点Activity中声明的,hilt 内部会去依次寻找咱们项目中通过@installIn定义的ActivityComponent、ActivityRetainedComponent、SingletonComponent 3个容器。同理如果注入类是在Fragment声明的,hilt 内部会去依次寻找FragmentComponent、ActivityComponent、ActivityRetainedComponent、SingletonComponent。
-
寻找容器中创建User对象的方法,如果该方法添加了@Provides,则命中,通过该方法进行实例化。如果该方法添加了@ActivityScoped,那么在该Activity里的Fragment 和 自定义的View 中注入User类,都是和Activity里的user对象是同一个。
-
如果该方法没有添加@Provides注解,那么hilt 会找到User类,看User类的上方有没有@ActivityScoped注解,如果有,则通过User的构造实例化,那么在该Activity里注入多个User,其实都是同一个对象,在Fragment和View中注入的User,和Activity中的User也是同一个对象,因为User的作用域已经声明成和Activity同生命周期的,在Activity的onCreate()和onDestory()范围内只会创建一个user对象。
-
所以这就解释了为什么ActivityComponent除了指向FragmentComponent,也指向了ViewComponent,因为对象是由ActivityComponent容器创建的,并且在创建对象的方法上声明了@ActivityScoped注解,那么在Fragment和View中注入的对象,都会通过该容器创建。
-
@xxScoped注解,可以定义在方法和类上,如果对象的创建是通过容器创建的,即使在类上面定义了@xxScoped注解,也会被 hilt 忽略。
@HiltAndroidApp
让 hilt 生效的必要条件,使用hilt的模块必须在Application的子类中声明@HiltAndroidApp
@InstallIn
作用在类名上面,如:@InstallIn(SingletonComponent::class),标识提供当前类创建的对象都通过该容器创建,具体使用见下面示例
@Module
作用在类名上面,一般和@InstallIn一起使用,标识当前类是一个模块,类中的方法会被指定的容器类创建。一半用来创建:第3方类,接口,build 模式的构造等。和@InstallIn 同时使用,指定该模块通过哪个容器创建。具体使用见下面示例
@Singleton
作用在方法上面,标识通过该方法创建的对象是单例,该方法只会执行一次。具体使用见下面示例
@Provides
作用在方法上面,标识方法返回的对象是通过容器创建的,方法主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行方法主体。具体使用见下面示例。
@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {
@Singleton
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit()
}
}
- @InstallIn(容器.class),只能在()里写上面指定的容器.class
- @Module和@InstallIn是同时使用的,不加@Module编译不通过
- @Singleton 表示当前方法只会被执行一次,方法返回的对象是单例
- @Provides 作用在方法上是让容器在创建Retrofit对象的时候执行该方法,
@AndroidEntryPoint
作用在以下指定的类上面,除了Application Hilt 一共有 6 个入口点,分别是:
Application
Activity
Fragment
View
Service
BroadcastReceiver
除了Application由@HiltAndroidApp进行标识,Activity仅支持ComponentActivity的子类,Fragment仅支持androidx下的Fragment,其他入口点的子类都用@AndroidEntryPoint, 比如:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}
// 错误写法,编译不通过
@AndroidEntryPoint
class User{}
@ActivityScoped
可以作用在类和方法上面。作用在类上面,当在 Activity 注入该 Activity
class UnscopedBinding @Inject constructor() {}
@ActivityScoped
class ScopedBinding @Inject constructor() {}
@Module
@InstallIn(ActivityComponent::class)
class MainModule {
@Provides
fun provideUnscopedBinding() = UnscopedBinding()
@Provides
@ActivityScoped
fun provideScopedBinding() = ScopedBinding()
}
@Inject
lateinit var unscope1: UnscopedBinding
@Inject
lateinit var unscope2: UnscopedBinding
@Inject
lateinit var scope1: ScopedBinding
@Inject
lateinit var scope2: ScopedBinding
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
// MyLinearLayout 是 activity_main 里面的一个控件
class MyLinearLayout : LinearLayout {
...
@Inject
lateinit var scope3: ScopedBinding
@Inject
lateinit var scope4: ScopedBinding
}
scope1、scope2、scope3、scope4 是同一个对象,因为在provideScopedBinding()加了@ActivityScoped,表示该方法创建出的对象的生命周期在Activity范围内时,实例只会创建一次,MyLinearLayout的context也是MainActivity,所以 scope1 == scope3 == scope4
unscope1、unscope2、unscope3、unscope4 是4个不同的对象
@Provides 作用在方法上是让容器在创建对象的时候执行该方法,
如果不加@Provides,那么容器创建对象不会走provideScopedBinding()方法。
@Module和@InstallIn是同时使用的,不加@Module编译不通过
@ViewModelScoped
@FragmentScoped
@ViewScoped
@ServiceScoped
@xxScoped注解,可以定义在方法和类上,如果对象的创建是通过容器创建的,即使在类上面定义了@xxScoped注解,也会被 hilt 忽略。如果定义在类上面,并且对象的创建没有通过容器,那么类上面的@xxScoped注解会生效。
@Inject
使用 @Inject 来告诉 Hilt 创建该类的实例,常用于构造,非私有字段,非静态方法中。
// 作用在构造
class User @Inject constructor() {
// 作用在非静态方法中
@Inject
fun autoCallByHilt(){
// 当User对象创建完成,hilt 会自动调用该方法
}
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// 作用在非私有字段
@Inject
lateinit var user: User
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
@EntryPoint
hilt 已经默认支持6个入口点Application、Activity、Fragment、View、Service、BroadcastReceiver。@EntryPoint 是hilt提供给我们自定义的入口点,必须和@InstallIn同时使用。
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
// 必须得通过容器实例化Retrofit,否则编译不通过
fun getRetrofit(): Retrofit
}
@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Singleton
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit()
}
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
fun doSomething(context: Context) {
val myEntryPoint = EntryPoints.get(context, MyEntryPoint::class.java)
val retrofit = myEntryPoint.getRetrofit()
...
}
}
@Binds 和 @Qualifier
必须作用在一个抽象方法上,抽象方法返回的必须是接口
// 定义一个水果模块,模块里方法的返回对象由Activit容器创建
@Module
@InstallIn(ActivityComponent::class)
abstract class FruitModule {
// 苹果注解标识
@AppleAnnotation
@Binds
abstract fun provideApple(pear: Apple): Fruit
// 梨子注解标识
@PearAnnotation
@Binds
abstract fun providePear(apple: Pear): Fruit
}
// 定义苹果注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppleAnnotation
// 定义梨子注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PearAnnotation
// 定义水果类
interface Fruit {
fun getName(): String
}
class Apple @Inject constructor() : Fruit {
override fun getName(): String {
return "苹果"
}
}
class Pear @Inject constructor() : Fruit {
override fun getName(): String {
return "梨子"
}
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@AppleAnnotation
@Inject
lateinit var apple: Fruit
@PearAnnotation
@Inject
lateinit var pear: Fruit
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
System.out.println(apple.getName()) // 苹果
System.out.println(pear.getName()) // 梨子
}
}
- @Binds只能作用在抽象方法上,并且抽象方法里的参数必须是具体类,返回值必须是接口
- @Qualifier作用在自定义的注解上,自定义注解和@Binds共同作用在抽象方法上,hilt 会根据方法里的参数对象,创建具体实例。
- 如果接口只有一个子类,就不需要用自定义注解和@Qualifier
五、如何注入接口
见上面@Binds 和 @Qualifier的示例
六、如何注入第3方类
@Module
@InstallIn(SingleComponent::class)
object NetworkModule {
@Provides
@SingleScoped
fun provideAnalyticsService(
): Retrofit {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
}
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var retrofit: Retrofit
...
}
定义@InstallIn注解的时候 , 记得加上 @Module,在创建第3方对象的方法上必须得加上@Provides , 告诉hilt 通过该方法创建对象,而不是通过第3方类的构造方法。@SingleScoped 注解表示该方法只会被执行一次,创建出来的对象是单例。
七、如何注入同一个接口的不同子类
见上面@Binds 和 @Qualifier的示例
八、hilt 内部 组件/容器 默认绑定
意思是:被注入的类,在构造方法中可以默认持有不同对象的引用,持有的对象根据当前入口点进行分析。具体见示例。
// context、act、frag、view、会被 hilt 赋值
class User @Inject constructor(var context: Application)
class UserByAct @Inject constructor(var act: FragmentActivity)
class UserByFragment @Inject constructor(var frag: Fragment)
class UserByView @Inject constructor(var view: View)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var user: User
@Inject
lateinit var userAct: UserByAct
// 错误写法,当前入口点无法把View注入进UserByView类中
@Inject
lateinit var userView: UserByView
// 错误写法
@Inject
lateinit var userByFrag: UserByFragment
...
}
@AndroidEntryPoint
class MyFragment : Fragment() {
@Inject
lateinit var user: User
@Inject
lateinit var userAct: UserByAct
@Inject
lateinit var userByFrag: UserByFragment
}
@AndroidEntryPoint
class MyView : View {
@Inject
lateinit var user: User
@Inject
lateinit var userAct: UserByAct
// 正确
@Inject
lateinit var userView: UserByView
// 错误写法
@Inject
lateinit var userByFrag: UserByFragment
...
}
也可以把Application转换成我们自己的 MyApplication
@Module
@InstallIn(SingleComponent::class)
object ApplicationModule {
@Provides
fun provideMyApplication(context: Application): MyApplication {
return context as MyApplication
}
}
@Module
@InstallIn(FragmentComponent::class)
class BaseFragmentModule {
@Provides
fun provideBaseFragment(fragment: Fragment): BaseFragment {
return fragment as BaseFragment
}
}
class User @Inject constructor(var context: MyApplication)
class UserByFragment @Inject constructor(var frag: BaseFragment)
九、注入ViewModel
@HiltViewModel
class MyViewModel @Inject constructor( val repo: MyRepo) : ViewModel()
class MyRepo @Inject constructor()
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// 注意: 这里ViewModel的创建还是得交给ViewModelProvider进行创建,不能new
val vm by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
}
只需要在ViewModel的上面添加@HiltViewModel , 创建ViewModel的方式得通过ViewModelProvider
十、结尾
hilt 的常用用法也基本讲完了,引入hilt会默认把dagger库也引入进来,想要快速上手hilt,得先理解 hilt 容器创建对象的方式,每种注解的用法。在多模块的应用中使用依赖注入,还得借助 dagger中的api来实现。 目前hilt 和 Jetpack的集成只支持 ViewModel 和 WorkManager,相信谷歌未来会对hilt提供更多的Jetpack组件支持。