Android MVVM架构如何避免重复函数?Jetpack Compose场景
问题背景
我用Jetpack Compose开发一款简易游戏,包含3个页面:
- HeroesScreen:展示所有英雄,支持选择单个/多个相同角色
- HeroDetailsScreen:展示英雄详情,可多次选择同一角色增加持有数量
- ShoppingCartScreen:调整各角色数量(增减操作)
每个页面对应一套ViewModel+Repository:
HeroesScreen -> HeroesViewModel -> HeroesRepository HeroDetailsScreen -> HeroDetailsViewModel -> HeroDetailsRepository ShoppingCartScreen -> ShoppingCartViewModel -> ShoppingCartRepository
每个Repository包含8-12个业务API,但增减数量的两个函数在三个Repository和ViewModel中完全重复。我考虑过把这两个函数放到单个Repository中,再注入到其他ViewModel,但担心ShoppingCartRepository和HeroDetailsViewModel没有直接关联,这种方式是否合理?
补充代码示例(重复部分)
ViewModel重复逻辑(以ShoppingCartViewModel为例):
class ShoppingCartViewModel @Inject constructor( private val repo: ShoppingCartRepository ): ViewModel() { var incrementQuantityResult by mutableStateOf<Result<Boolean>>(Result.Success(false)) private set var decrementQuantityResult by mutableStateOf<Result<Boolean>>(Result.Success(false)) private set fun incrementQuantity(heroId: String) = viewModelScope.launch { repo.incrementQuantity(heroId).collect { result -> incrementQuantityResult = result } } fun decrementQuantity(heroId: String) = viewModelScope.launch { repo.decrementQuantity(heroId).collect { result -> decrementQuantityResult = result } } }
Repository重复逻辑(以ShoppingCartRepositoryImpl为例):
class ShoppingCartRepositoryImpl( private val db: FirebaseFirestore, ): ShoppingCartRepository { override fun incrementQuantity(heroId: String) = flow { try { emit(Result.Loading) val heroIdRef = db.collection("shoppingCart").document(heroId) heroIdRef.update("quantity", FieldValue.increment(1)).await() emit(Result.Success(true)) } catch (e: Exception) { emit(Result.Failure(e)) } } override fun decrementQuantity(heroId: String) = flow { try { emit(Result.Loading) val heroIdRef = db.collection("shoppingCart").document(heroId) heroIdRef.update("quantity", FieldValue.increment(-1)).await() emit(Result.Success(true)) } catch (e: Exception) { emit(Result.Failure(e)) } } }
可行的代码复用方案
1. 提取独立的数量操作组件(推荐)
把增减数量的逻辑抽成一个独立的Repository/UseCase,比如HeroQuantityRepository,专注处理数量变更的核心逻辑,不绑定任何页面的业务。这样所有需要数量操作的ViewModel只需要注入这个组件,无需依赖其他页面的Repository。
第一步:定义独立的数量操作接口与实现
interface HeroQuantityRepository { fun incrementQuantity(heroId: String): Flow<Result<Boolean>> fun decrementQuantity(heroId: String): Flow<Result<Boolean>> } class HeroQuantityRepositoryImpl( private val db: FirebaseFirestore ): HeroQuantityRepository { override fun incrementQuantity(heroId: String) = flow { try { emit(Result.Loading) val cartRef = db.collection("shoppingCart").document(heroId) cartRef.update("quantity", FieldValue.increment(1)).await() emit(Result.Success(true)) } catch (e: Exception) { emit(Result.Failure(e)) } } override fun decrementQuantity(heroId: String) = flow { try { emit(Result.Loading) val cartRef = db.collection("shoppingCart").document(heroId) cartRef.update("quantity", FieldValue.increment(-1)).await() emit(Result.Success(true)) } catch (e: Exception) { emit(Result.Failure(e)) } } }
第二步:在ViewModel中注入使用
以HeroDetailsViewModel为例:
class HeroDetailsViewModel @Inject constructor( private val heroDetailsRepo: HeroDetailsRepository, private val quantityRepo: HeroQuantityRepository // 注入独立组件 ): ViewModel() { var incrementQuantityResult by mutableStateOf<Result<Boolean>>(Result.Success(false)) private set fun incrementHeroQuantity(heroId: String) = viewModelScope.launch { quantityRepo.incrementQuantity(heroId).collect { incrementQuantityResult = it } } // 其他HeroDetails专属业务逻辑... }
这种方式完全解耦,每个ViewModel只依赖自身业务需要的组件,避免跨页面Repository的不合理依赖。
2. 使用抽象类封装通用逻辑
如果不想新增独立组件,可以定义一个抽象基础Repository,把增减数量的逻辑放在里面,让其他Repository继承这个抽象类:
abstract class BaseHeroRepository(protected val db: FirebaseFirestore) { fun incrementQuantity(heroId: String) = flow { try { emit(Result.Loading) val cartRef = db.collection("shoppingCart").document(heroId) cartRef.update("quantity", FieldValue.increment(1)).await() emit(Result.Success(true)) } catch (e: Exception) { emit(Result.Failure(e)) } } fun decrementQuantity(heroId: String) = flow { try { emit(Result.Loading) val cartRef = db.collection("shoppingCart").document(heroId) cartRef.update("quantity", FieldValue.increment(-1)).await() emit(Result.Success(true)) } catch (e: Exception) { emit(Result.Failure(e)) } } } // 让各业务Repository继承 class ShoppingCartRepositoryImpl(db: FirebaseFirestore): BaseHeroRepository(db), ShoppingCartRepository { // ShoppingCart专属业务逻辑... } class HeroDetailsRepositoryImpl(db: FirebaseFirestore): BaseHeroRepository(db), HeroDetailsRepository { // HeroDetails专属业务逻辑... }
ViewModel层面也可以用类似的抽象基类封装重复的状态收集逻辑,进一步减少VM中的代码重复。
3. 关于“把函数放到单个Repository注入到其他VM”的合理性
这种方式不推荐,会造成不必要的依赖耦合:HeroDetailsViewModel本来只需要处理英雄详情业务,却要依赖ShoppingCartRepository,违反了单一职责原则,后续维护时容易出现逻辑混淆,也不利于单元测试(需要Mock无关的ShoppingCartRepository)。
内容的提问来源于stack exchange,提问作者Always Learner

