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就可以了。


df

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
)
Google文档有另一种写法:https://developer.android.com/reference/androidx/room/PrimaryKey#autoGenerate()

2.2 删除整个表

点击查看代码
@Query("DELETE FROM companylistingentity")
    suspend fun clearCompanyListings()
companylistingentity是定义的数据结构名,在Room中一个数据结构会被作为一个表的名字。然后表中的每一行就是一个model.

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来进行数据库访问和网络访问。这样的缺点有几个:

  1. 破坏架构。presentation层不应该直接和具体的数据层——data联系,就像上面说过的那个例子,我在data的model和domain的model之间建立一个映射map,然后presentation层只用domain层的model,这样可以避免更改data层的时候也需要重构presentation层,避免污染presentation层。
  2. 第二点其实和第一点相联系,当我想更换数据来源,比如说我想用假数据来测试,那么我要直接对presentation层引用的repository动刀,修改实现。

更好的做法是:

  1. 在domian层定义一个Repository interface,相关的函数签名写在里面。
  2. 在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的方式,现在看起来十分不好。

  1. 无法根据相应请求接口进行不同的异常处理,全都被Okhttp捕获了。
  2. 无法通知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()
hilt会为ViewModel注入从Navigation传过来的argument. [链接](https://stackoverflow.com/questions/67350331/how-to-use-hilt-to-inject-a-safe-args-argument-into-a-viewmodel "链接")

待续

posted @ 2022-05-28 16:59  ou尼酱~~~  阅读(98)  评论(0)    收藏  举报