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