咱們寫程序大部分時候都是經過 new Object()來建立對象,當該對象須要在多個地方被建立時,咱們有可能會封裝成工廠方法。有沒有更優雅的實現方式呢?
依賴注入相信你們都能說出個一二,可是在Android 端不多人會去用,下面經過一個小例子,來介紹依賴注入的使用,但願能爲您帶來一點幫助。先介紹下2個名詞。android
傳統程序建立對象由調用者經過Obj obj = new Obj()建立,而依賴注入是把對象的建立和對象生命週期的管理交給容器來管理,定義註解標識,如:@Inject Obj obj,容器會根據註解在合適的時機自動爲咱們建立實例,咱們在程序中直接使用obj。git
在使用MVVM模式進行網絡請求時,ViewModel 依賴 Repository 層,Repository 依賴Remote Data Source 和 Room,先忽略Room,把MainApi 當成 Remote Data Source,下面我分別用普通寫法、泛型反射寫法、hilt寫法進行舉例說明 github
// 定義網絡接口
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,這種代碼是重複且不優雅的,經過反射能夠減小這些重複代碼。
api
// 定義網絡接口
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了,有沒有其餘實現方式呢?
markdown
@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以前,先說下依賴注入在Android中的歷史,Dagger是由square在2012年推出的,基於反射來實現的。後來谷歌在此基礎上進行了重構,也就是Dagger2,基於Java註解來實現的,在編譯期就會檢查錯誤,若是編譯經過,項目正常運行是沒問題的,適用於Java、kotlin。而 hilt 則是谷歌面向Android寫的一套依賴注入框架,相比Dagger2 簡單易用,提供了android端專屬api。架構
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'
複製代碼
必須在Application子類上添加註解@HiltAndroidAppapp
@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}
複製代碼
sunflower這是谷歌Android官方的示例框架
在沒有使用依賴注入框架以前,咱們在代碼中建立對象正常是經過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 |
經過上面圖片咱們要明白如下幾點:
問題: 在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 忽略。
讓 hilt 生效的必要條件,使用hilt的模塊必須在Application的子類中聲明@HiltAndroidApp
做用在類名上面,如:@InstallIn(SingletonComponent::class),標識提供當前類建立的對象都經過該容器建立,具體使用見下面示例
做用在類名上面,通常和@InstallIn一塊兒使用,標識當前類是一個模塊,類中的方法會被指定的容器類建立。一半用來建立:第3方類,接口,build 模式的構造等。和@InstallIn 同時使用,指定該模塊經過哪一個容器建立。具體使用見下面示例
做用在方法上面,標識經過該方法建立的對象是單例,該方法只會執行一次。具體使用見下面示例
做用在方法上面,標識方法返回的對象是經過容器建立的,方法主體會告知 Hilt 如何提供相應類型的實例。每當須要提供該類型的實例時,Hilt 都會執行方法主體。具體使用見下面示例。
@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {
@Singleton
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit()
}
}
複製代碼
做用在如下指定的類上面,除了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{}
複製代碼
能夠做用在類和方法上面。做用在類上面,當在 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
}
複製代碼
scope一、scope二、scope三、scope4 是同一個對象,由於在provideScopedBinding()加了@ActivityScoped,表示該方法建立出的對象的生命週期在Activity範圍內時,實例只會建立一次,MyLinearLayout的context也是MainActivity,因此 scope1 == scope3 == scope4
unscope一、unscope二、unscope三、unscope4 是4個不一樣的對象
@Provides 做用在方法上是讓容器在建立對象的時候執行該方法, 若是不加@Provides,那麼容器建立對象不會走provideScopedBinding()方法。 @Module和@InstallIn是同時使用的,不加@Module編譯不經過
@xxScoped註解,能夠定義在方法和類上,若是對象的建立是經過容器建立的,即便在類上面定義了@xxScoped註解,也會被 hilt 忽略。若是定義在類上面,而且對象的建立沒有經過容器,那麼類上面的@xxScoped註解會生效。
使用 @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)
}
}
複製代碼
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()
...
}
}
複製代碼
必須做用在一個抽象方法上,抽象方法返回的必須是接口
// 定義一個水果模塊,模塊裏方法的返回對象由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的示例
@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的示例
意思是:被注入的類,在構造方法中能夠默認持有不一樣對象的引用,持有的對象根據當前入口點進行分析。具體見示例。
// 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)
複製代碼
@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組件支持。