闻过则喜,知过不讳,改过不惮。
这是NIA系列文章中,数据层部分的最后一篇。在这篇博客中我将首先从宏观角度纵览数据的分层设计思路,随后分析数据仓库层:core:data
的实现。
数据的分层设计
采用自底向上顺序,将整个NIA项目的数据层拆分如下表。
层次 | 说明 | 对上层暴露接口 |
---|---|---|
数据源Source | 数据的存储方式,如服务器、数据库、文件 | 单例,NetworkDataSource(服务器)、XXXDao(数据库),以Bean作为函数返回 |
数据仓库Repository | 封装底层具体存取实现,通常会组合2~3个底层接口对象 | 单例,XXXRepository,以Flow对象作为函数返回 |
领域Domain | 拆分后的业务基本能力,类似"数据中台" | XXXUseCase,供ViewModel使用,以Flow对象作为函数返回值 |
特性Feature | 对应到页面粒度的ViewModel | 向上传递数据,向下传递控制 |
按照package拆分
- di: 通过依赖注入生成各个Repository的单例
- model: network层的数据对象,同样会将其转换为通用的Topic、NewsResource等来使用
- repository: 依照业务实体划分的各个Repository,对外(上层业务)暴露的主要接口,返回的数据格式为Flow
- util: 请求监控、数据同步等工具类
di------依赖注入绑定默认的Repository
di包内部有两个类,分别是生成UserNewsResourceRepository
的interface UserNewsResourceRepositoryModule
和生成TopicsRepository
、NewsResourceRepository
等的abstrace class DataModule
。
疑问:同样是借助Hilt注入能力自动生成单例对象,为什么使用了接口、抽象类两种不同的写法?是否可以只采用接口来实现?
是否可以只采用接口来实现?------在当前项目的场景中,这个问题的答案是肯定的。首先来学习了解下这两种方式的区别。
知识点:Hilt的两种注入方法
Hilt框架提供了抽象类、接口两种注入方式,这两种注入方式的区别其实也就是抽象类、接口之间的区别,比如抽象类可以含有属性及非抽象函数等。
特性 | 抽象类写法 | 接口写法 |
---|---|---|
类类型 | 必须声明为 abstract class |
必须声明为 interface |
方法修饰符 | 需显式添加 abstract 关键字 | 方法默认抽象,无需 abstract 关键字 |
构造函数 | 可以有构造函数(但 Hilt 模块不需要) | 不能有构造函数 |
成员变量 | 可声明非静态属性 | 不能有属性(仅支持抽象方法) |
多继承 | 单继承(Kotlin 单继承限制) | 支持多实现多个接口 |
代码风格 | 更贴近传统 Java 风格 | 更符合 Kotlin 简洁风格 |
在具体代码中,这两种写法有些微的不同。
kotlin
// 抽象类写法(必须显式声明 abstract)
@Module
abstract class DataModule {
@Binds
abstract fun bindsRepo(impl: Impl): Interface
}
// 接口写法(默认抽象,无需关键字)
@Module
interface UserRepoModule {
@Binds
fun bindsRepo(impl: Impl): Interface // 隐式抽象
}
如果需要结合@Binds
和@Provides
,则只能使用抽象类,因为后者依赖于完整的函数实现。
kotlin
@Module
abstract class HybridModule {
@Binds
abstract fun bindInterface(impl: Impl): Interface
@Provides
fun provideUtility(): Utility { // 接口无法添加此方法
return Utility()
}
}
如果不需要@Provides
,那么使用抽象类和接口是等价的,两种写法最终都会被 Hilt 处理为相同的依赖注入逻辑,没有性能或功能上的差异。出于接口契约性 和编码简洁 的考虑,最佳编程实践是使用接口Interface
。
Repository------封装后的数据仓库
repository包内部大部分都是以XXXRepository
命名的类,