iosched项目是Google I/O的官方app,可以在GooglePlay上下载使用 :
https://play.google.com/store/apps/details?id=com.google.samples.apps.iosched
同时可以在github上获取项目源码:https://github.com/google/iosched,项目中使用了大量的Google官方推荐的最佳实践,是一个学习Jetpack、MaterialDesign等技术的非常好的demo。本文想通过iosched源码的学习寻找一些Android应用架构的设计思路。
Android应用架构
Android开发中常见的应用架构,例如MVP、MVVM、Clean等,虽然各有特点,但其基本思想都是通过定义不同的抽象层,来实现关注点分离,降低各层级之间不必要的耦合。无论何种架构各层级间的通信都是架构实现的关键,通过iosched源码的阅读,我们可以学习一种层级间通信的实现方案,既然是Google官方出品的,相比有一定参考价值。
iosched代码架构
iosched
├ mobile → Presentation层(View/ViewModel)
├ model → 各种Bean
└ shared
├ data → Repository层/Data Source
└ domain → Service层
iosched的代码按照分层进行管理,其中mobile中包含了UI展示相关实现;shared中包含了domin层以及repo层的实现。这种分层很好地贯彻了Clean Architecture 的共同封闭原则(Common Closure Principle)
The classes in a component should be closed together against the same kind of changes. A change that affects a component affects all the classes in that component and no other components.
--- Common Closure Principle of Clean Architecture
每个层级中的Class应该高度内聚高度相关,不相关的Class就应该划分到不同的层级。iosched给出了一种代码结构的划分原则,当然,根据domain和repo的相关性,也可以进一步分到不同module中(ioshed只是分在了不同文件夹)
层级依赖
iosched采用了MVVM+Clean的架构,各层级的依赖关系如下:
代码中通过dagger实现构造函数注入,所以通过构造函数可以清晰的看清依赖关系,例如
class AgendaViewModel @Inject constructor(
loadAgendaUseCase: LoadAgendaUseCase,
getTimeZoneUseCase: GetTimeZoneUseCase
) : ViewModel() {
// ...
}
推荐在项目中引入dagger或者koin等DI框架,除了可更好地实现解耦之外,还可以让各组件之间的依赖关系更加一目了然。
各层级的实现
接下来以相对简单的Agenda获取模块为例,介绍源码中ViewModel、Service、Repository等各层级以及层级间通信的实现。
Repository
class DefaultAgendaRepository(private val appConfigDataSource: AppConfigDataSource)
: AgendaRepository {
private val blocks by lazy {
generateBlocks(appConfigDataSource)
}
/**
* Generates a list of [Block]s. Default values of each [Block] is supplied from the
* default values stored as shared/src/main/res/xml/remote_config_defaults.xml.
* Add a corresponding entry in RemoteConfig is any [Block]s need to be overridden.
*/
private fun generateBlocks(dataSource: AppConfigDataSource): List<Block> {
return listOf(
Block(
title = "Badge pick-up",
type = "badge",
color = 0xffe6e6e6.toInt(),
startTime = ZonedDateTime.parse(
dataSource.getStringLiveData(BADGE_PICK_UP_DAY0_START_TIME).value),
endTime = ZonedDateTime.parse(
dataSource.getStringLiveData(BADGE_PICK_UP_DAY0_END_TIME).value)
),
Block(
title = "Badge pick-up",
type = "badge",
color = 0xffe6e6e6.toInt(),
startTime = ZonedDateTime.parse(
dataSource.getStringLiveData(BADGE_PICK_UP_DAY1_START_TIME).value),
endTime = ZonedDateTime.parse(
dataSource.getStringLiveData(BADGE_PICK_UP_DAY1_END_TIME).value)
),
Block(
title = "Breakfast",
type = "meal",
color = 0xfffad2ce.toInt(),
startTime = ZonedDateTime.parse(
dataSource.getStringLiveData(BREAKFAST_DAY1_START_TIME).value),
endTime = ZonedDateTime.parse(
dataSource.getStringLiveData(BREAKFAST_DAY1_END_TIME).value)
)
// ...
)
}
override fun getAgenda(): List<Block> = blocks
/**
* Returns a list of [Block]s as [LiveData].
* This is needed because each start/end time could be overridden from RemoteConfig.
* When the time is updated from RemoteConfig, the List needs to be observable otherwise
* the value change in RemoteConfig isn't effective unless restarting the app.
*/
override fun getObservableAgenda(): LiveData<List<Block>> {
val result: MutableLiveData<List<Block>> = MutableLiveData()
result.postValue(getAgenda())
appConfigDataSource.syncStringsAsync(object : StringsChangedCallback {
override fun onChanged(changedKeys: List<String>) {
if (!changedKeys.isEmpty()) {
result.postValue(generateBlocks(appConfigDataSource))
}
}
})
return result
}
}
Repository中通过dataSource获取block,同时通过syncStringAsync监听远程配置的变化
UseCase
open class LoadAgendaUseCase @Inject constructor(
private val repository: AgendaRepository
) : MediatorUseCase<Unit, List<Block>>() {
override fun execute(parameters: Unit) {
try {
val observableAgenda = repository.getObservableAgenda()
result.removeSource(observableAgenda)
result.addSource(observableAgenda) { //通过addSource桥接Repo与ViewModel
result.postValue(Result.Success(it))
}
} catch (e: Exception) {
result.postValue(Result.Error(e))
}
}
}
值得注意的是异常的通知方式:try..catch捕获到异常后,通过liveData通知给ViewModel,ViewModel通过as?进行容错处理,将error转为emptyList:
(it as? Result.Success)?.data ?: emptyList()
再者需要注意的是LoadAgendaUseCase继承了MediatorUseCase。MediatorUseCase中封装了以下处理:Repository获取的数据不直接返回给ViewModel,而是通过result.addSource()之后,再通过将数据传递给ViewModel。
abstract class MediatorUseCase<in P, R> {
protected val result = MediatorLiveData<Result<R>>()
// Make this as open so that mock instances can mock this method
open fun observe(): MediatorLiveData<Result<R>> {
return result
}
abstract fun execute(parameters: P)
}
下图可以帮助我们理清数据的传递过程:
execute明明可以直接返回LiveData,为什么要引人Result呢?这是为了便于request和observe的分离,有时候我们并不希望订阅的同时进请求,此时通常会用RxJava的Subject或者Coroutine的Channel来实现,这里LiveData实现了同样的功能。
ViewModel
class AgendaViewModel @Inject constructor(
loadAgendaUseCase: LoadAgendaUseCase,
getTimeZoneUseCase: GetTimeZoneUseCase
) : ViewModel() {
val loadAgendaResult: LiveData<List<Block>>
private val preferConferenceTimeZoneResult = MutableLiveData<Result<Boolean>>()
val timeZoneId: LiveData<ZoneId>
init {
val showInConferenceTimeZone = preferConferenceTimeZoneResult.map {
(it as? Result.Success<Boolean>)?.data ?: true
}
timeZoneId = showInConferenceTimeZone.map { inConferenceTimeZone ->
if (inConferenceTimeZone) {
TimeUtils.CONFERENCE_TIMEZONE
} else {
ZoneId.systemDefault()
}
}
// Load agenda blocks.
getTimeZoneUseCase(preferConferenceTimeZoneResult)
val observableAgenda = loadAgendaUseCase.observe()
loadAgendaUseCase.execute(Unit)
loadAgendaResult = observableAgenda.map {
(it as? Result.Success)?.data ?: emptyList()
}
}
}
AgendaViewModel最终通过databinding在fragment_agenda.xml中与UI进行绑定。AgendaViewModel在init同时进行数据请求,当然也可以在Fragment的onCreate时进行数据请求。
最后
iosched的源码中给出了一种架构分层方案以及基于LiveData的层级间通信方案,在不借助RxJava等三方框架的情况下实现各种Cold流/Hot流的数据获取和数据传递,适合在一些简单的项目中参考使用。但对于有复杂数据变化的项目,个人推荐使用RxJava,其各种丰富的操作符以及线程切换能力可以帮助我们大大提高开发效率。