λ:
仓库地址: github.com/lzyprime/an…
开发分支dev
加入了compose
, 图片库由 glide
换为 coil
, DataStore
代替SharedPreference
。 同时剔除掉LiveData
, 用Flow
代替。
本来想完全用compose
完成UI实现。但是目前compose
组件贫瘠,与其他库的配合库也都没有稳定。部分场景下实现反倒费力。所以开两个分支:
dev
:compose
只做部分控件实现,主体仍保留传统库和其他方式。dev_compose
:view
层完全用compose
实现,包括路由导航。删除layout, navgation, menu
等文件夹,删除compose 以外
的依赖。
hilt
或者 koin
做依赖注入是贯穿全局的。所以得先会这个
关于文档,还是尽量看英文原版。原版文档本身有一定延迟,而中文文档翻译又会延迟一段。就导致文档里api可能已过时,内容废弃等问题。比如hilt
在android官网的文档。中文版本还是alpha
版本的,@ViewModelInject
接口已经废弃等.
当然中文文档中介绍和原理部分还是可以参考,即使api改变了,这种东西一般也不会变。
共性:
无论是哪个库,原理都是一样的:
-
在
Application, Activity, Fragment
中开一个容器Container
-
所依赖的实例从
Container
中获得,并以单例存在于Container
中
//example
class MyApplication : Application() {
val appContainer = AppContainer()
...
}
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as MyApplication).appContainer
loginViewModel = LoginViewModel(appContainer.userRepository)
}
}
复制代码
手动依赖注入问题:
- 必须自行管理
Container
,手动为依赖项创建实例,根据生命周期移除实例 - 样板代码
ViewModel
依赖注入,要靠ViewModelProvider.Factory
才可以复用Jetpack
中的内容. 或者放进容器,自己维护ViewModel
生命周期。
依赖注入有什么好处是老生常谈。当然可以不用,通篇object
, 直接就单例,到处可以使用。
依赖注入优势,官网给的总结:
- 重用类以及分离依赖项:更容易换掉依赖项的实现。由于控制反转,代码重用得以改进,并且类不再控制其依赖项的创建方式,而是支持任何配置。
- 易于重构:依赖项成为 API Surface 的可验证部分,因此可以在创建对象时或编译时进行检查,而不是作为实现详情隐藏。
- 易于测试:类不管理其依赖项,因此在测试时,您可以传入不同的实现以测试所有不同用例。
Hilt
Hilt
做法是改变并拓展基类,比如继承自Application
的,通过生成的Hilt_XXApplication
,在里边实现容器维护。可以看生成的代码。
开容器
通过@HiltAndroidApp, @AndroidEntryPoint
标记,生成对应基类,在基类里实现容器的维护逻辑。
@HiltAndroidApp
class UnsplashApplication : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity()
复制代码
@Inject 标记需要注入项
class UnsplashRepository @Inject constructor(private val service: UnsplashService)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var service: XXService
...
}
复制代码
注册实例获取方式
创建 @Module
, 通过 @Provides
和 @Binds
标记实例获取方式,并通过 @InstallIn
标记实例放在哪个容器里。
@Binds
interface XXService { ... }
class XXServiceImpl @Inject constructor(
...
) : XXService { ... }
@Module
@InstallIn(ActivityComponent::class)
abstract class XXModule {
@Binds
abstract fun bindAnalyticsService(
xxServiceImpl: XXServiceImpl
): XXService
}
复制代码
@Provides
interface XXService {
...
companion object : XXService {
...
}
}
@Module
@InstallIn(ActivityComponent::class)
object XXModule {
@Provides
fun provideXXService(): XXService = XXService
}
复制代码
两种方式都可以标记某个实例的获取方式。@InstallIn(ActivityComponent)
标记存放在Activity的容器Container
中。参照表:
component | 注入到 | create at | destroyed at |
---|---|---|---|
SingletonComponent | Application | Application#onCreate() | Application#onDestroy() |
ActivityRetainedComponent | N/A | Activity#onCreate() | Activity#onDestroy() |
ViewModelComponent | ViewModel | ViewModel created | ViewModel destroyed |
ActivityComponent | Activity | Activity#onCreate() | Activity#onDestroy() |
FragmentComponent | Fragment | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | View | View#super() | View destroyed |
ViewWithFragmentComponent | View annotated with @WithFragmentBindings | View#super() | View destroyed |
ServiceComponent | Service | Service#onCreate() | Service#onDestroy() |
除此之外,也可以通过标记作用域的方式,标记存放在哪个容器中,如:
@Singleton // 放入Application容器中,相当于全局单例
class UserRepository @Inject constructor(...)
复制代码
对照表:
class | component | Scope |
---|---|---|
Application | SingletonComponent | @Singleton |
Activity | ActivityRetainedComponent | @ActivityRetainedScoped |
ViewModel | ViewModelComponent | @ViewModelScoped |
Activity | ActivityComponent | @ActivityScoped |
Fragment | FragmentComponent | @FragmentScoped |
View | ViewComponent | @ViewScoped |
View annotated with @WithFragmentBindings | ViewWithFragmentComponent | @ViewScoped |
Service | ServiceComponent | @ServiceScoped |
这些花里胡哨的最后影响的不过是实例的生存周期,以及单例范围,如标记了Activity内,则每个Activity都会各自维护一份单例。以此类推
为实例提供多个获取方式
// 限定符
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DebugService
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ReleaseService
复制代码
// module
@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {
@DebugService
@Provides
fun provideDebugService(): XXService = XXService(debug_url)
@ReleaseService
@Provides
fun provideReleaseService(): XXService = XXService(release_url)
}
复制代码
// use
class UserRepository @Inject constructor(
@DebugService service: XXService
)
// or
class Example {
@Release
@Inject lateinit var okHttpClient: OkHttpClient
}
复制代码
预设限定符:@ApplicationContext
和 @ActivityContext
class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) { ... }
复制代码
ViewModel
使用@HiltViewModel
标记,在Activity中仍然可以通过 by viewModels()
的方式获取到。
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() {
...
}
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
private val exampleViewModel: ExampleViewModel by viewModels()
...
}
复制代码
要了解如何做到的,首先要知道by viewModel()
如何实现。完整的函数是要传ViewModel Factory
获取方式的,缺省值为空,当什么都不穿时,会调用Activity#getDefaultViewModelProviderFactory
。会创建个默认工厂。
而Hilt
实现的注入方式,就是改变并拓展基类。所以在生成的Activity基类里,override
掉这个函数,先去Hilt中找,没有匹配的则返回默认行为。
这是Koin
与Hilt
明显差别之一。 Koin
类似于把手动注入的过程封装一下,所以 by viewModel()
是 koin
库额外实现的一版,与原有同名,是两个不同函数。
Navigation
可以以导航图为单位共享一个 ViewModel
val viewModel: ExampleViewModel by hiltNavGraphViewModels(R.id.my_graph)
复制代码
gradle plugin 生成代码
得益于Hilt
通过Gradle Plugin
生成代码, 否则我们就要:
@HiltAndroidApp(Application.class)
class FooApplication : Hilt_FooApplication
@AndroidEntryPoint(FragmentActivity.class)
class FooActivity : Hilt_FooActivity
...
复制代码
Navigation SafeArgs
同样也是用 Gradle Plugin
生成代码的库,所以它并不在dependencies{ implementation "xxx" }
而是在 plugins{ id "xxx" }
Koin
当了解了容器的概念,再来看Koin
,或者其他依赖注入库就很容易理解。Koin
源码在github
都有,可以自己扒。或者猜测一下实现,看库是不是与自己一样。或者觉得库哪些内容实现不优雅不好,有无更好的方案或写法。
开容器
Koin
通过 startKoin
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
// Koin Android logger
androidLogger()
//inject Android context
androidContext(this@MainApplication)
// use modules
modules(myAppModules)
}
}
}
复制代码
标记需要注入项
by inject()
class MySimpleActivity : AppCompatActivity() {
val firstPresenter: MySimplePresenter by inject()
}
复制代码
get()
class MySimpleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val firstPresenter: MySimplePresenter = get()
}
}
复制代码
注册实例获取方式
val appModule = module {
// single instance of HelloRepository
single<HelloRepository> { HelloRepositoryImpl() }
// Simple Presenter Factory
factory { MySimplePresenter(get()) }
}
复制代码
startKoin {
...
// use modules
modules(myAppModules)
}
复制代码
module
有详细文档。至于实例的生命周期和作用域,肯定靠module
和startKoin
注册方式维护。
ViewModel
class MyViewModel(val repo : HelloRepository) : ViewModel() {...}
复制代码
val appModule = module {
// single instance of HelloRepository
single<HelloRepository> { HelloRepositoryImpl() }
// MyViewModel ViewModel
viewModel { MyViewModel(get()) }
}
复制代码
由于注入方式相当于手动维护容器,所以ViewModel
也需要注册获取方式。
class MyViewModelActivity : AppCompatActivity() {
val myViewModel: MyViewModel by viewModel()
}
复制代码
调用Koin
库里的viewModel函数
, 获取实例。
~λ:
其他详细内容可看文档和源码。两个库各有优劣,相比Koin
, Hilt
拓展基类,生成代码的方式可能更舒服。