Koin in Android: 更简单的依赖注入

Dagger 之殇 如果还不清楚什么是依赖注入,那么请参考之前写的 Dagger2 in Android(一)通俗基础开头部分。如果你不了解 Dagger 倒也无妨,本文会进行一定的对比,但仅针对接触过 Dagger 的同学,否则大可以忽略。 Dagger2 作为著名优秀的依赖注入框架广为流传,何况还是 Android 的亲爸爸 - Google 在维护,因此相信很多人会将其作为 Android 开发的首选 DI 框架。Dagger 从入门到放弃一定是很多很多人必经甚至多次经历的历程:anger: 诚然,Dagger 很强大。但它的学习曲线太过于陡峭,即使好不容易搞清楚了各种注解与概念,也很难适当地运用到项目中。同时对于 Activity 之类重要但却不能我们自己初始化的类 Dagger 明显水土不服。为此,Google 搞了个 .android 扩展库来「简化」使用。我不否认最终确实简化了代码,但是这玩意本身就很难度,学习成本堪称指数级。 除此之外,.android 扩展库对于 ViewModel 依然是严重的水土不服,甚至 Google 官方 Demo 的实现也是一堆问题。 Koin 基础 Koin 是纯 Kotlin 编写的轻量级依赖注入框架,轻量是因为它只使用 Kotlin 的函数解析特性,没有代理,没有代码生成,没有反射!官方声称5分钟快速上手。随着 Kotlin 的推广,Koin 这个后起之秀也获得了越来越多的关注。当然它也提供了 implementation "org.koin: koin-java:1.0.0" 扩展库来支持 java,但本文不会涉及。 不建议新手阅读 Koin 源码。作为 DSL,它大量使用了 Kotlin 的高级特性,例如 inline 函数。相对来说难以理解。 使用 Koin 所需的依赖在官方文档已经说得很明确了。这里因为使用了 AndroidX 库,所以引入 org....

July 5, 2019 · Chenhe

[译] Coroutines on Android(三)实战

本系列文章主要翻译自 medium-AndroidDevelopers. 使用协程解决实际问题 前两章重点研究了协程如何简化代码,在 Android 中提供主线程安全,以及如何避免协程泄露。在此基础上,协程是一个在 Android 中进行后台处理以及以及简化回调的优秀方案。 到目前为止,我们主要关注的是什么是协程以及如何管理它们。在这篇文章中,我们将看看如何使用它们来完成实战任务。协程是一个通用的语言特性,与函数在同一个级别——你可以用它实现任何可以用函数或对象来实现的东西。当然,实际工作中有两种情况是非常适合使用协程解决的: 一次性的请求。一旦得到结果那个这次请求就结束了。 流式请求。它持续地观察数据改变并通知调用者——即使得到结果也会持续执行。 这两种任务都非常适合协程。这篇文章中,我们将深入研究一次性请求,并探索如何在 Android 中用协程实现它们。 一次性请求 一次性请求当被调用时执行,当得到结果时返回并结束。就像普通的函数那样——调用它,执行一些工作,然后返回。由于与函数调用的相似性,它比流式请求更容易理解。 一次性请求当被调用时执行,得到结果时结束。 A one shot request is performed each time it’s called. It stops executing as soon as a result is ready. 举个例子,想想浏览器是如何加载网页的。当你点击这篇文章的链接,浏览器会向服务器发送一个请求来加载页面。一旦页面成功传输到浏览器,便会停止与服务器的通讯——浏览器已经得到了所有它需要的数据。如果服务器修改了文章,除非你刷新网页,否则浏览器不会及时修改。 尽管这缺少流式请求的实时推送特性,但一次性请求还是非常强大。一个应用的许多功能都可以通过一次性请求来实现,比如获取、保存、更新数据。对于排序列表之类的事情,它也是一种很好的模式。 需求:显示一个排序的列表 让我们通过探索如何显示排序列表来研究一次性请求。为了更加具体,让我们构建一个库存管理应用,供商店的员工使用。它可以根据最后一次进货的时间查找商品——他们希望能够对列表进行升序和降序排序。但是商品太多了,排序需要几乎1秒才能完成——所以我们使用协程来避免阻塞主线程! 在这个应用中,所有的商品都存储在一个数据库中,使用了 Room 框架。这是一个很好的用例,因为它不需要涉及网络请求,我们可以专注于一次性请求模式本身,麻雀虽小,五脏俱全。 要实现此功能,你需要将协程引入 ViewModel, Repository 以及 Dao。让我们逐个看看怎么与协程进行结合。 class ProductsViewModel(val productsRepository: ProductsRepository): ViewModel() { private val _sortedProducts = MutableLiveData<List<ProductListing>>() val sortedProducts: LiveData<List<ProductListing>> = _sortedProducts /** * 当用户点击排序按钮时调用这些函数。 */ fun onSortAscending() = sortPricesBy(ascending = true) fun onSortDescending() = sortPricesBy(ascending = false) private fun sortPricesBy(ascending: Boolean) { viewModelScope....

July 2, 2019 · Chenhe

[译] Coroutines on Android(二)起步

本系列文章主要翻译自 medium-AndroidDevelopers. 本篇将开始整合协程与 Android,探索如何启动并跟踪协程,以便适配 UI 生命周期。 为何跟踪协程 在第一篇中,我们探索了协程能解决的问题。总结一下,协程是解决这两个问题的优秀方案: 在主线程运行长时间任务导致阻塞。 从主线程上安全地调用一切 suspend 函数。也即主线程安全(Main-safety) 为了解决这些问题,协程通过给常规函数添加 suspend 与 resume 操作,使得协程被挂起时在单独线程执行,主线程可以继续执行其他工作。 但是,协程本身不能跟踪正在执行的工作。协程本身很轻量,你完全可以同时启动大量的协程,甚至成百上千个。 如果要手动编写代码跟踪这几千个协程的进度将非常困难。也许能够追踪他们,确保他们完成或者取消,但这非常枯燥且容易出错。稍有不注意,就会失去对协程的跟踪,也就是协程泄露。 协程泄露就像内存泄露,但是更严重。如果协程泄露,除了占用内存,还会占用 CPU、硬盘甚至启动网络请求。 泄露的协程会浪费内存、CPU、硬盘、网络,而这些都是不需要的。 A leaked coroutine can waste memory, CPU, disk, or even launch a network request that’s not needed. 为了帮助减少协程泄露,Kotlin 介绍了 结构化并发。结构化并发是协程特性与最佳实践的结合,帮助你保持对所有协程的追踪。 在 Android,我们可以利用结构化并发完成三件事: 当不再需要时取消任务。 对运行中的任务保持追踪。 捕获协程中的异常。 让我们深入研究每一个,看看结构化并发如何帮助我们确保始终跟踪所有协程。 使用 scopes 取消任务 在 Kotlin,协程必须在一个叫做 CoroutineScope 内执行。CoroutineScope 保持了对所有协程的跟踪,尽管被挂起也是这样。不像之前我们讨论的 Dispatchers,Scope 实际上不执行协程,只是确保你不会跟丢或忘记它们。 为了保证所有协程都被跟踪,Kotlin 不允许在没有 CoroutineScope 的情况下启动协程。Scope 能够启动具备我们上一节讨论的挂起与恢复能力的协程。...

July 2, 2019 · Chenhe

[译] Coroutines on Android(一)背景知识

本系列文章主要翻译自 medium-AndroidDevelopers. 本系列文章主要关注 Kotlin Coroutine(协程) 是如何工作的,以及如何解决实际的 Android 问题。但是相对来说不会过于深入底层,更偏向于应用。 协程解决了什么? Kotlin 协程提供了一个全新的,更加简单地方式来实现异步。协程在 Kotlin 1.3 中正式发布,API 已经稳定,可以用于生产环境。事实上,协程的概念始终存在,最早是 Simula 语言在1967年探索了协程的使用。 在过去的几年,协程正逐步流行开来,众多的语言,例如 js, c#, python, ruby, go 都有所支持。Kotlin 协程基于这些基础,可以用于构建大型应用程序。 在 Android 上,协程可以优雅地解决下面两个问题: 在主线程运行长时间任务导致阻塞。 从主线程上安全地调用一切 suspend 函数。也即主线程安全(Main-safety) 主线程安全指的是:调用者不必关心这个函数应当在那个线程(调度器)调用,总能“同步”取得正确的返回值且不会阻塞。 下面从这两个角度入手,深入看看协程任何高效整洁地解决这些问题。 长时间运行的任务 调用 API 等行为会涉及到网络请求。读取数据库或加载配置文件涉及到文件读写。这类工作就是所谓的长时间任务,因为相对来说他们花费的时间很长,应用必须停下来等待他们。 在 Android 每个应用都有一个主线程来处理 UI 与交互。如果在这个线程执行了太繁重的工作,应用就会变卡,导致非常糟糕的用户体验。任何长时间运行的任务都不应该阻塞主线程,这样应用才能流畅运行,避免掉帧或未响应的出现。 为了在子线程执行网络请求,最常见的模式是使用回调(Callback)。回调提供了一个引用,它可以在在未来的某个时间通知代码继续运行,使用回调,代码可能是这样的: class ViewModel: ViewModel() { fun fetchDocs() { get("developer.android.com") { result -> show(result) } } } 尽管 get 函数在主线程被调用,但他会在另一个线程执行网络请求。然后一旦得到相应,回调函数就会在主线程执行。这是一个很棒的模式来出来长时间运行的任务。Retorfit 就是一个非常著名的网络请求库帮助你完成这些工作。 使用协程来解决 协程可以大幅简化上面的代码,现在我们用协程来重写上面的功能:...

July 1, 2019 · Chenhe

Dagger2 in Android(四).android 扩展库

问题 在之前我们讨论过,Dagger 在 Android 上普遍的结构是:定义一个全局的 AppComponent,其他组件依赖或继承它。假设现在有 AppComponent 与 ActivityComponent 两个,他们可能是这样编写的: @Module(subcomponents = [ActivityComponent::class]) class AppModule(val context: Context) { @Provides @Singleton fun provideContext() = context } @Component(modules = [AppModule::class]) @Singleton interface AppComponent { fun inject(app: MyApplication) fun activityComponent(): ActivityComponent.Builder } @Module class ActivityModule { @Provides fun provideSp(context: Context) = context.getSharedPreferences("Cooker", Context.MODE_PRIVATE) } @SubComponent(modules = [ActivityModule::class]) interface ActivityComponent { fun inject(activity: MainActivity) @Subcomponent.Builder interface Builder { fun build(): ActivityComponent } } 上面定义了这两个 Component 并且他们是包含关系。然后我们必须在 Appliction 中实例化 AppComponent 来保证单例:...

June 10, 2019 · Chenhe

Dagger2 in Android(三)Scope与生命周期

前言 之前我们已经学习了 Dagger 的基础知识、模块化管理,本章将是 Dagger 基础使用的最后一章。 Scope 被误称 Dagger 的黑科技,但实际上它非常简单,但错误理地解它的人却前仆后继。希望小伙伴们认真阅读这一章,第一次学习时一定要正确理解,不然后边再纠正会感觉世界观都被颠覆了。 @Scope 终于来了。Scope 正如字面意思,它可以管理所创建对象的“生命周期”。Scope 的定义方式类似 Qualifier,都需要利用这个注解来定义新的注解,而不是直接使用。 重点!!!这里所谓的「生命周期」是与 Component 相关联的。与 Activity 等任何 Android 组件没有任何关系! 下面是典型的错误案例: 定义一个 @PerActivity 的 Scope, 于是认为凡是被这个 PerActivity 注解的 Provides 所创建的实例, 就会自动与当前 Activity 的生命周期同步。 上述想法非常可爱,非常天真,所以很多很多程序猿们都是可爱的 (o´・ェ・`o) 要是仅仅靠一个注解就能全自动同步生命周期,那也太智能了。 下面开始好好学习啦。先来说说正常的注入流程:目标类首先需要创建一个 Component 的实例,然后调用它定义的注入方法,传入自身。Component 就会查找需要注入的变量,然后去 Module 中查找对应的函数,一旦找到就调用它来获取对象并注入。 这里我们可以发现一个关键,也就是对象最终是 Module 里的函数提供的。这个函数当然也是我们自己编写的,大部分情况下在这里会直接 new 一个出来。因此,如果多次注入同一类型的对象,那么这些对象将分别创建,各不相同。看下面的例子: class MainAty : AppCompatActivity() { @Inject lateinit var chef1: Chef @Inject lateinit var chef2: Chef override fun onCreate(savedInstanceState: Bundle?...

June 9, 2019 · Chenhe

Dagger2 in Android(二)进阶

前面已经讲了 Dagger 的基础注解,并且最后我们也搭建了一个最简单的 Dagger 注入。 这一篇我们继续学习 Dagger 更多的注解,以及如何模块化地管理。这些将帮助我们妥善组织不同的组件、明确各自的生命周期。 @Named 依赖注入迷失 之前说过 @Module 和 @Provides 配合可以包装没有 @Inject 标注的构造函数。但如果包装了一个已经有了 @Inject 的类会怎么样?其实这俩有优先级的。Dagger 会优先从 Module 中查找实例化方法,如果找不到再去找被 Inject 的标记的构造函数。 这也非常好理解,一般人肯定会选择优先去超市买东西,而不是直接去拜访工厂。 一般来说,为了便于管理,我们会统一用 Module 封装一层,无论构造函数有没有被标注。这可以帮助我们更好地管理依赖结构与生命周期,这些后面会讲到。 但如果 Module 里有两个返回值类型一样的 Provides 呢?考虑下面的代码: class Stove() { var name: String? = null constructor(name: String) : this() { this.name = name } } @Module class MainModule() { @Provides provideStove():Stove { return Stove() } @Provides provideStove():Stove { // 现在有两个Provides都返回炉子 return Stove("Boom") } } 现在家乐福里有两个炉子,Dagger 不知道该买哪一个,我们给这种情况起个名字叫「依赖注入迷失」。依赖注入迷失会在编译期报错,很容易发现。...

June 9, 2019 · Chenhe

Dagger2 in Android(一)通俗基础

背景知识 Dagger2 是一个由 Google (之前是 Square)维护的开源依赖注入框架。我曾两次试图学习 Dagger 最终被乱七八糟的名词弄得晕头转向,连个 demo 都没写出来就放弃了。所以本文也会重点解释 Dagger 的各个名词,只有熟悉了它们的作用,才能顺畅无阻地使用,也才能看懂别人的 demo。 虽然标题叫 Dagger2 in Android,但是前几节都是 Dagger 通用的基础知识,与 Android 没有关系。 本系列使用 Kotlin 语言,其与 Java 100% 兼容,仅仅是语法不同而已,不会造成太大影响。Kotlin 最终依然会被编译成 jvm 字节码。 例如 java 定义函数: public String getName() {return "David"} kotlin 版本:fun getName(): String {return "David"},甚至可以更简洁:fun getName() = "David" 依赖注入 那么首先我们得先清楚什么是依赖注入。依赖注入是实现控制反转(IoC)的一个方案。 那什么叫控制反转?别急,我尽量用最通俗的方式解释所涉及的所有概念。 通常在面向对象语言中,一个类往往依赖其他类,一个对象依赖其他对象。那么最直接的方案就是 new 一个出来。这很好理解,我依赖计算机,所以我就自己造一个出来。那么造出的计算机当然是我自己控制的,这就是“控制正转”。那么反转就是,我需要计算机,但是我不自己造,而是厂家造好之后交给我。这就是控制反转。因为我不再控制计算机的生产,此时厂家就叫做 IoC 容器,它负责生产维持对象,而我只负责拿来用。所以 IoC 不是技术,而是思想,利用这种思想可以降低对象间的耦合,提高代码重用率。 倘若计算机的配置发生了变更,在之前每个人都要自己更新图纸,但是现在只需要厂家更新就好了,我总可以拿到最新款的计算机。 为了实现控制反转,有很多方案,常见的有 依赖注入、依赖查找、服务定位器。这里我们讲依赖注入。 其实依赖注入不是什么新东西,我们天天都在用。依赖注入有三种常见的手段:构造函数、Setter、注解。举个厨师的例子:厨师依赖炉子。那么代码可以这么写: // 通过构造函数注入炉子 class Chef(val stove: Stove) { } // 通过 Setter 注入 class Chef() { var stove: Stove // 其实 Kotlin 默认实现了 setter,为了更加清晰我手动写了一个。 fun setStove(stove: Stove) { this....

June 9, 2019 · Chenhe

Android 构建 MVVM 应用 with Kotlin(一)概述

各种模式 MVVM 是一种开发模式,对应的还有 MVC、MVP。这些模式最早在前端领域比较火,后来因为便于维护,被应用到各个平台。但 Android 上却发展的很慢。先来看一下各个模式的区别,以及他们在 Android 中的对应关系。 MVC Model:业务数据模型。用于获取、储存数据。 View:视图。Activity、Fragment 等,当然也包括 xml 布局文件。 Controller:控制器。处理用户交互,向 Model 发送数据。。 乍一看似乎这个架构还不错,而且和 Android 基本组件也能对应。但事实上,xml 作为 View,其功能实在是太差劲了,基本上仅仅用了描述一个视图。而 Activity 不得不处理大量的 UI 问题,干脆很多时候顺便连数据一起处理了。最后 Android 开发成为了 面向 Activity 编程。所有的逻辑全部糅杂在一起,在加上生命周期问题,简直就是噩梦。 MVP 为了解决上面 Activity 承担太多工作的问题。聪明的程序猿们专为 Android 改造了 MVC,变成 MVP。MAC 的本质问题是 Activity 同时承担了 View 与 Controller 的角色。而 MVP 把 Activity 中的 UI 逻辑抽象成 View 接口,把业务逻辑抽象成 Presenter 接口,Model 还是原来的 Mdel。 如此一来 Activity 的工作就简单多了,只用来同步生命周期,其他乱七八糟的东西都交给 Presenter。 但是,这并没有解决实际问题,不过把一堆恶心的代码从 V 搬到 P 而已。而且 P 必定还要持有 V 接口引用以便进行 UI 操作。这下 VP 又耦合在一起。要是改变 UI,又得改一堆接口,想想都很难受。...

June 9, 2019 · Chenhe