Android结构设计系列[2]--解耦合

声明:

本文是对Mark Allison系列博客的翻译和学习笔记,感谢作者提供的demo和这么好的博客。

在上一篇《Android结构设计系列[1]–初识工程》中我们认识了我们显示天气的APP,接下来将开始我们的结构优化。

在优化项目结构之前我们得先知道什么样的工程才是结构合理的工程。在最早的一篇博文中我对这个问题进行了简要归纳,请参考《六大设计原则浅析》,符合这些设计原则的工程就是一个结构合理的工程,我们要实现这样的结构得先从耦合性方面考虑,第一步我们先做分离。

逻辑分离

接下来我们回顾一下上一篇中的工程目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
src
│ Converter.kt #转换工具类
│ WeatherStationApplication.kt #自定义的Application

├─model
│ Common.kt #天气数据具体的Json实体类
│ Current.kt #天气数据接口接收实体类

├─net
│ OpenWeatherMap.kt #Retrofit的Api Service定义接口

└─ui
CurrentWeatherFragment.kt #天气显示Fragment
MainActivity.kt #主界面
NoPermissionFragment.kt #无权限fragment
PreferencesFragment.kt #设置fragment

从整个结构和上一篇的分析中会发现大部分逻辑都集中在CurrentWeatherFragment,包括获取位置信息、初始化网络请求框架、请求数据、绑定显示数据。获取位置信息的方式有很多种,部分手机并不支持系统获取定位,这个时候我们需要更换定位系统,则会发现我们的定位是和逻辑混合在一起,不容易拆解而且更加麻烦的是不容易进行单元测试。接下来我们将定位功能先拆分出来。

位置服务

定义位置服务接口:

1
2
3
4
5
6
interface LocationProvider {
//注册更新位置信息
fun requestUpdates(callback: (latitude: Double, longitude: Double) -> Unit)
//取消注册
fun cancelUpdates(callback: (latitude: Double, longitude: Double) -> Unit)
}

在设计这个接口过程中应该把整个定位服务想成一个服务提供者,只需要订阅它然后会定时回调位置数据信息(经纬度),不要考虑具体的实现细节,只需要综合考虑对外交互的接口定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override fun onResume() {
super.onResume()
//注册位置服务
locationProvider.requestUpdates(::retrieveForecast)
}

//位置结果回调
private fun retrieveForecast(latitude: Double, longitude: Double) {
//使用经纬度查询天气数据
currentWeatherProvider.request(latitude, longitude, ::bind)
}

override fun onPause() {
//取消订阅
locationProvider.cancelUpdates(::retrieveForecast)
super.onPause()
}

天气数据服务

我们在考虑一下我们应该如何将天气数据请求服务剥离出去,首先得向外提供一个通过经纬度获取天气数据的方法,还得提供一个取消请求的方法。

1
2
3
4
interface CurrentWeatherProvider {
fun request(latitude: Double, longitude: Double, callback: (CurrentWeather) -> Unit)
fun cancel()
}

注意,这里重新定义了CurrentWeather实体类来提供更加符合业务场景的实体类(删除无用数据),重新组装对象,这样就可以和请求结果对象之间解耦合。

1
2
3
4
5
6
7
8
9
10
11
12
13
override fun onAttach(context: Context) {
super.onAttach(context)

//创建位置服务的实现类实例(具体的实现类)
locationProvider = FusedLocationProvider(context)
//创建天气数据服务的实现类实例(具体的实现类)
currentWeatherProvider = OpenWeatherMapProvider(context, BuildConfig.API_KEY)
converter = Converter(context)
}

private fun retrieveForecast(latitude: Double, longitude: Double) {
currentWeatherProvider.request(latitude, longitude, ::bind)
}

上面代码中我们的FusedLocationProvider类和OpenWeatherMapProvider类分别实现了两个接口的定义,并在CurrentWeatherFragment中创建两个类的实例,调用对应服务的方法。

依赖注入

上面实现了对位置服务和天气数据服务的分离,但是如果让你看看CurrentWeatherProvider接口的实现类OpenWeatherMapProvider的定义你可能会发现很糟糕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class OpenWeatherMapProvider(
context: Context,
private val appId: String,
okHttpClient: OkHttpClient = OkHttpClient.Builder()
.cache(Cache(context.cacheDir, cacheSize))
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build(),
converterFactory: Converter.Factory = MoshiConverterFactory.create(
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
),
retrofit: Retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.openweathermap.org/")
.addConverterFactory(converterFactory)
.build(),
private val service: OpenWeatherMap = retrofit.create(OpenWeatherMap::class.java),
private val calls: MutableList<Call<Current>> = mutableListOf()

) : CurrentWeatherProvider { //实现CurrentWeatherProvider接口
...
}

上面代码中我们只所以能方便的构造OpenWeatherMapProvider对象(只需要传入context和apikey)是因为构造中做了大量默认值和初始化,这段代码很显然让我们感觉到不舒服,因为它的可读性比较差。

而且我们还是面临着一个耦合问题就是在CurrentWeatherFragment类中必须具体的去创建OpenWeatherMapProvider对象,这样就造成了它们之间的强关联关系。

这个时候你可能会想到工厂方法模式来实现解耦,如下:

1
2
3
4
5
6
7
class DependencyFactory {
fun createCurrentWeatherProvider(context: Context): CurrentWeatherProvider =
OpenWeatherMapProvider(context, BuildConfig.API_KEY)

fun createLocationProvider(context: Context): LocationProvider =
FusedLocationProvider(context)
}

这个方案依然不够完美,这个工厂类同样面临着强耦合关系。下面我们用依赖注入框架Dagger2来解决上面的问题。有关Dagger2的详细知识请参考我的以下博文:

《Dagger2入门学习记录》
《Dagger2入门学习之MVP项目整合(上)》
《Dagger2入门学习之MVP项目整合(下)》

官方参考链接:https://google.github.io/dagger/android

引入dagger依赖

1
2
3
4
implementation 'com.google.dagger:dagger:2.16'
implementation 'com.google.dagger:dagger-android-support:2.16'
kapt 'com.google.dagger:dagger-compiler:2.16'
kapt "com.google.dagger:dagger-android-processor:2.16"

接下来声明一个Dagger模块,它定义了我们希望注入的Android组件。

1
2
3
4
5
6
@Module
abstract class AndroidBuilder {

@ContributesAndroidInjector
abstract fun bindCurrentWeatherFragment(): CurrentWeatherFragment
}

然后,需要将此Module和AndroidInjectionModule(它是库的一部分)添加为Dagger组件中的模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Singleton
@Component(modules = [
AndroidInjectionModule::class,
AndroidBuilder::class,
WeatherStationModule::class,
LocationModule::class,
WeatherModule::class
])
interface WeatherStationComponent {

@Component.Builder
interface Builder {

@BindsInstance
fun application(application: Application) : Builder

fun build(): WeatherStationComponent
}

fun inject(application: WeatherStationApplication)
}

现在需要在Application类中实现HasSupportFragmentInjector:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class WeatherStationApplication : Application(), HasSupportFragmentInjector {

@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>

private val weatherStationComponent: WeatherStationComponent by lazy {
DaggerWeatherStationComponent.builder()
.application(this)
.build()
}

override fun onCreate() {
super.onCreate()

weatherStationComponent.inject(this)
AndroidThreeTen.init(this)
}

override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
}

接下来就很容易解决上面对于具体实现的依赖问题了,实现依赖注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CurrentWeatherFragment : Fragment() {

@Inject lateinit var locationProvider: LocationProvider //抽象类型
@Inject lateinit var currentWeatherProvider: CurrentWeatherProvider //抽象类型

private lateinit var converter: Converter

override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.main_menu, menu)
super.onCreateOptionsMenu(menu, inflater)
}

override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState)
}
...
}

源码请参考:https://github.com/StylingAndroid/WeatherStation/tree/di

评论

Ajax Android AndroidStudio Animation Anroid Studio AppBarLayout Banner Buffer Bulma ByteBuffer C++ C11 C89 C99 CDN CMYK COM1 COM2 CSS Camera Raw, 直方图 Chrome ContentProvider CoordinatorLayout C语言 DML DOM Dagger Dagger2 Darktable Demo Document DownloadManage Element Error Exception Extensions File FileProvider Fresco GCC Git GitHub GitLab Gradle Groovy HTML5 Handler HandlerThread Hexo Hybrid I/O IDEA IO ImageMagick IntelliJ Intellij Interpolator JCenter JNI JS Java JavaScript JsBridge Kotlin Lab Lambda Lifecycle Lint Linux Looper MQTT MVC MVP Maven MessageQueue Modbus Momentum MySQL NDK NIO NexT Next Nodejs ObjectAnimator Oracle VM Permission PhotoShop Physics Python RGB RS-232 RTU Remote-SSH Retrofit Runnable RxAndroid RxJava SE0 SSH Spring SpringBoot Statubar Task Theme Thread Tkinter UI UIKit UML VM virtualBox VS Code ValueAnimator ViewPropertyAnimator Web Web前端 Workbench api apk bookmark by关键字 compileOnly css c语言 databases demo hexo hotfix html iOS icarus implementation init jQuery javascript launchModel logo merge mvp offset photos pug query rxjava2 scss servlet shell svg tkinter tomcat transition unicode utf-8 vector virtual box vscode 七牛 下载 中介者模式 串口 临潼石榴 主题 书签 事件 享元模式 仓库 代理模式 位运算 依赖注入 修改,tables 光和色 内存 内核 内部分享 函数 函数式编程 分支 分析 创建 删除 动画 单例模式 压缩图片 发布 可空性 合并 同向性 后期 启动模式 命令 命令模式 响应式 响应式编程 图层 图床 图片压缩 图片处理 图片轮播 地球 域名 基础 增加 备忘录模式 外观模式 多线程 大爆炸 天气APP 太白山 头文件 奇点 字符串 字符集 存储引擎 宇宙 宏定义 实践 属性 属性动画 岐山擀面皮 岐山肉臊子 岐山香醋 工具 工厂模式 年终总结 开发技巧 异常 弱引用 恒星 打包 技巧 指针 插件 摄影 操作系统 攻略 故事 数据库 数据类型 数组 文件 新功能 旅行 旋转木马 时序图 时空 时间简史 曲线 杂谈 权限 枚举 架构 查询 标准库 标签选择器 样式 核心 框架 案例 桥接模式 检测工具 模块化 模板引擎 模板方法模式 油泼辣子 泛型 洛川苹果 浅色状态栏 源码 瀑布流 热修复 版本 版本控制 状态栏 状态模式 生活 留言板 相册 相对论 眉县猕猴桃 知识点 码云 磁盘 科学 笔记 策略模式 类图 系统,发行版, GNU 索引 组件 组合模式 结构 结构体 编码 网易云信 网格布局 网站广播 网站通知 网络 美化 联合 膨胀的宇宙 自定义 自定义View 自定义插件 蒙版 虚拟 虚拟机 补码 补齐 表单 表达式 装饰模式 西安 观察者模式 规范 视图 视频 解耦器模式 设计 设计原则 设计模式 访问者模式 语法 责任链模式 贪吃蛇 转换 软件工程 软引用 运算符 迭代子模式 适配器模式 选择器 通信 通道 配置 链表 锐化 错误 键盘 闭包 降噪 陕西地方特产 面向对象 项目优化 项目构建 黑洞
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×