Stock Market App Note
1: data layer model vs domain layer model
data层放的model是直接映射api获取的接口或数据库存储的,当presentation层不一定需要全部字段,或者需要经过一定的transfer才能用在数据层,如果presentaiton层直接使用data 层的数据结构,那么presentation层就会被data 层的数据逻辑所污染,因此需要map,把data层的model做一个映射到domain层的model. 映射在Repository中完成。
需要定义三个部分,一个是Mapper文件,放在data层,里面定义若干个函数,是关于data model -> domain model,还需要分别在data package 和 domain package 定义一个model package.(Of course it is ok to Subdivide these categories into local , remote ...)
presentation的UI reference the model in domain layer.Domain layer的model没有任何第三方有关的东西。比如说Room的id字段...
好处就是当想更换数据来源或第三方库,比如说把Room换成其他数据库框架之类的,那我们不需要修改domain层和presentation层,只需要修改data层的数据结构和相关map就可以了。

2: Room you need to konw
2.1 自增id
Philipp使用了这种方式来auto generate id:
点击查看代码
@Entity
data class CompanyListingEntity(
val name: String,
val symbol: String,
val exchange: String,
@PrimaryKey val id: Int? = null
)
2.2 删除整个表
点击查看代码
@Query("DELETE FROM companylistingentity")
suspend fun clearCompanyListings()
2.3 Query使用Like来查询
querywithlike
Philipp的例子:
点击查看代码
@Query(
"""
SELECT *
FROM companylistingentity
WHERE LOWER(name) LIKE '%' || LOWER(:query) || '%' OR
UPPER(:query) == symbol
"""
)
suspend fun searchCompanyListing(query: String): List<CompanyListingEntity>
3: Make repository an interface
在之前实习的项目中,我建立的repository不是一个接口,我当时建立的就是一个类,然后依赖注入Dao还有Retrofit来进行数据库访问和网络访问。这样的缺点有几个:
- 破坏架构。presentation层不应该直接和具体的数据层——data联系,就像上面说过的那个例子,我在data的model和domain的model之间建立一个映射map,然后presentation层只用domain层的model,这样可以避免更改data层的时候也需要重构presentation层,避免污染presentation层。
- 第二点其实和第一点相联系,当我想更换数据来源,比如说我想用假数据来测试,那么我要直接对presentation层引用的repository动刀,修改实现。
更好的做法是:
- 在domian层定义一个Repository interface,相关的函数签名写在里面。
- 在data层写Repository的具体实现。
这样presentation层就是在和domain层一个抽象的类所联系,我可以随意更改具体实现,比如说我想用假数据,我就直接建一个Repository实现接口的所有方法,但是我的函数所返回的不是从数据库和网络中获取的,而是假数据。
4: 封装后端的状态
由于repo的接口返回一个flow,而在响应式编程之中一个重要的点就是流的状态,presentation如何知道data层的数据是成功获取还是失败还是正在请求?这个时候封装一个类来指示状态和装有数据就很有必要。
点击查看代码
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Success<T>(data: T?): Resource<T>(data)
class Error<T>(message: String, data: T? = null): Resource<T>(data, message)
class Loading<T>(val isLoading: Boolean = true): Resource<T>(null)
}
5:Retrofit错误处理
之前在一个项目之中,处理retrofit的异常是通过给Okhttp加interceptor的方式,现在看起来十分不好。
- 无法根据相应请求接口进行不同的异常处理,全都被Okhttp捕获了。
- 无法通知UI层发生了异常,不能已经异常提醒等操作。
之前的代码:
点击查看代码
.addInterceptor(Interceptor { chain ->
// 添加重试,以及报错处理
var retryNum = 0
val request: Request = chain.request()
var response: Response
do {
retryNum++
try {
response = chain.proceed(request)
} catch (e: Exception) {
e.printStackTrace()
response = Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(999)
.message("okhttp error")
.body(Constants.Error_Custom_Json.toResponseBody(null)).build() // 在这里设置了json为错误的json
}
}while (!response.isSuccessful && retryNum < Constants.Max_Retry)
return@Interceptor response
})
虽然返回一个表示错误的Json了,但还是过于死板不够优雅。
Philipp的代码:
点击查看代码
val remoteListings = try {
val response = api.getListings()
companyListingsParser.parse(response.byteStream())
} catch(e: IOException) {
e.printStackTrace()
emit(Resource.Error("Couldn't load data"))
null
} catch (e: HttpException) {
e.printStackTrace()
emit(Resource.Error("Couldn't load data"))
null
}
remoteListings?.let { listings ->
dao.clearCompanyListings()
dao.insertCompanyListings(
listings.map { it.toCompanyListingEntity() }
)
emit(Resource.Success(
data = dao
.searchCompanyListing("")
.map { it.toCompanyListing() }
))
emit(Resource.Loading(false))
}
在一个Flow里面,catch到异常就emit Error,成功就emit Success。是不是感觉有Rxjava onError onSuccess那味了?
6:函数默认值
感觉函数的所有参数都可以给它一个默认值啊,默认值遵循最普遍的情况,这样代码可能会更clean.
7:Hilt trick
Hilt依赖注入ViewModel saveStateHandler.设置
下面的代码:
点击查看代码
@HiltViewModel
class CompanyInfoViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: StockRepository
): ViewModel() {}
viewModel: CompanyInfoViewModel = hiltViewModel()
待续

浙公网安备 33010602011771号