多模块架构下,未引入任何Android库的data模块如何通过Dependency Inversion访问SharedPreferences?
我完全懂你这种困惑!当年第一次接触依赖倒置在多模块架构里的实际应用时,也绕了好一会儿才摸透门道。咱们一步步拆解这个问题,把依赖倒置的思路落地:
核心思路:用抽象隔离平台依赖
依赖倒置原则(Dependency Inversion)的核心就是高层模块不依赖低层模块,两者都依赖抽象;抽象不依赖细节,细节依赖抽象。放到你的场景里:
- data模块是「业务数据处理层」,它不能碰Android的具体类(比如SharedPreferences),所以我们要给它一个纯抽象的接口;
- app模块是「细节实现层」,它能访问Android框架,负责把SharedPreferences包装成这个抽象接口的实现;
- 最后通过依赖注入把实现传递给data模块,让它能间接使用SharedPreferences的功能。
第一步:在data模块定义抽象存储接口
在data模块里写一个完全不涉及Android API的接口,只定义你需要的存储操作(比如存字符串、取字符串,按需扩展)。这样data模块完全不需要引入任何Android库:
// data模块的纯Kotlin/Java代码,无Android依赖 interface LocalStorage { fun saveString(key: String, value: String) fun getString(key: String, defaultValue: String?): String? // 可以根据业务需求添加其他方法,比如saveInt、getBoolean等 }
这个接口就是data模块依赖的「抽象」,它只关心要做什么,不关心具体怎么实现。
第二步:在app模块实现抽象接口
app模块可以自由访问Android框架,所以我们在这里写一个基于SharedPreferences的LocalStorage实现,把Android的具体操作封装起来:
// app模块的代码,可访问Android API class SharedPreferencesLocalStorage(private val sharedPreferences: SharedPreferences) : LocalStorage { override fun saveString(key: String, value: String) { sharedPreferences.edit().putString(key, value).apply() } override fun getString(key: String, defaultValue: String?): String? { return sharedPreferences.getString(key, defaultValue) } }
这里我们把SharedPreferences的操作转换成了LocalStorage接口定义的方法,相当于给Android的具体实现套了一层「抽象壳」。
第三步:通过依赖注入把实现注入到data模块
不管你用Hilt、Dagger这类DI框架,还是手动做依赖注入,核心都是把SharedPreferencesLocalStorage的实例传递给data模块需要用它的地方。比如data模块的Repository:
// data模块的Repository,只依赖抽象的LocalStorage class UserRepository(private val localStorage: LocalStorage) { fun saveUserName(userName: String) { localStorage.saveString("USER_NAME", userName) } fun getUserName(): String? { return localStorage.getString("USER_NAME", null) } }
然后在app模块的DI配置里,把LocalStorage和它的具体实现绑定。比如用Hilt的话:
// app模块的Hilt配置模块 @Module @InstallIn(SingletonComponent::class) object StorageModule { @Provides fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences { return context.getSharedPreferences("APP_PREFS", Context.MODE_PRIVATE) } @Provides fun provideLocalStorage(sharedPreferences: SharedPreferences): LocalStorage { return SharedPreferencesLocalStorage(sharedPreferences) } }
这样当data模块的UserRepository需要LocalStorage时,DI框架会自动把SharedPreferencesLocalStorage的实例传进去,data模块完全不知道背后是SharedPreferences在工作。
为什么这就是依赖倒置?
如果按照传统的思路,你可能会让data模块直接依赖SharedPreferences(低层的Android具体类),但这样data模块就被绑定到了Android平台,还违反了「不能加Android库」的限制。
而现在的做法:
- data模块只依赖自己定义的抽象(LocalStorage),不依赖任何Android细节;
- app模块的具体实现(SharedPreferencesLocalStorage)依赖这个抽象;
- 高层模块(data的Repository)和低层模块(SharedPreferences实现)都依赖抽象,完全符合依赖倒置的原则。
而且这么做还有额外好处:如果以后要把data模块移植到其他平台(比如桌面端),只需要写一个对应平台的LocalStorage实现(比如用Java的Preferences类),根本不需要修改data模块的代码!
备注:内容来源于stack exchange,提问作者Varian Wrynn

