MVVM架构下ViewModel结合UseCase的实现优化问询
Hey there! Great question—let’s break this down clearly, since this is a common pain point when shifting from MVP to MVVM, especially when adding UseCases into the mix.
First: Is the ViewModel + UseCase Pattern Valid?
Absolutely! This pattern isn’t just valid—it’s highly recommended for medium-to-large projects where you want to keep your codebase clean and maintainable. Here’s why:
- Single Responsibility Principle: UseCases encapsulate specific business logic (e.g., "fetch user data and validate it", "submit a form with error checking"), leaving your ViewModel focused only on UI-related tasks like exposing state to the UI or handling user input triggers.
- Decoupling: By placing UseCases between ViewModel and Repository, you avoid bloating ViewModels with data-fetching or business rules. Plus, UseCases can be reused across multiple ViewModels if needed.
- Testability: UseCases are easy to unit test in isolation (you just mock their Repository dependencies), and ViewModels become simpler to test too—no need to deal with data layer details directly.
The reason you see fewer examples of this is that basic MVVM tutorials often skip UseCases to keep examples minimal. But in real-world apps, UseCases fill a crucial gap between the UI and data layers.
Optimizing UseCase-ViewModel Interaction (Ditching Callbacks)
Callbacks can lead to messy, nested code and make error handling tricky. Luckily, Android has modern tools to replace them—here are the best approaches:
1. Coroutines + Flow (Official Recommended Approach)
Jetpack Coroutines and Kotlin Flow are the go-to solution now. They let you write sequential, readable code for async operations and integrate seamlessly with ViewModels and state holders like StateFlow.
Example Implementation:
Step 1: Define a UseCase that returns a Flow
// UseCase encapsulates business logic class FetchUserUseCase @Inject constructor( private val userRepository: UserRepository ) { // Operator invoke lets you call the UseCase like a function operator fun invoke(userId: String): Flow<Result<User>> = flow { emit(Result.Loading) val user = userRepository.getUserFromApi(userId) // Add any business logic here (e.g., validate user data) if (user.isValid()) { emit(Result.Success(user)) } else { emit(Result.Error("Invalid user data")) } }.catch { exception -> emit(Result.Error(exception.localizedMessage ?: "Unknown error")) } } // Sealed class for consistent state handling sealed class Result<out T> { object Loading : Result<Nothing>() data class Success<out T>(val data: T) : Result<T>() data class Error(val message: String) : Result<Nothing>() }
Step 2: Use the UseCase in ViewModel with StateFlow
class UserViewModel @Inject constructor( private val fetchUserUseCase: FetchUserUseCase ) : ViewModel() { // MutableStateFlow for internal updates, exposed as immutable StateFlow private val _userState = MutableStateFlow<Result<User>>(Result.Loading) val userState: StateFlow<Result<User>> = _userState.asStateFlow() fun loadUser(userId: String) { // viewModelScope automatically cancels when ViewModel is destroyed viewModelScope.launch { fetchUserUseCase(userId).collect { result -> _userState.value = result } } } }
Step 3: Observe in UI
- For Jetpack Compose: Use
collectAsStateWithLifecycle()to observe the StateFlow safely. - For traditional Views/Fragments: Use
lifecycle.repeatOnLifecycleto collect the Flow, or convert it to LiveData withasLiveData().
2. RxJava (If Your Project Already Uses It)
If your team relies on RxJava, have UseCases return Observable/Flowable instead of callbacks. Then subscribe to these streams in the ViewModel to update state.
Example:
class FetchUserUseCase @Inject constructor( private val userRepository: UserRepository ) { fun execute(userId: String): Observable<Result<User>> { return userRepository.getUserObservable(userId) .map { user -> if (user.isValid()) Result.Success(user) else Result.Error("Invalid data") } .startWithItem(Result.Loading) .onErrorReturn { Result.Error(it.localizedMessage) } } } // In ViewModel class UserViewModel @Inject constructor( private val fetchUserUseCase: FetchUserUseCase ) : ViewModel() { private val _userState = MutableLiveData<Result<User>>() val userState: LiveData<Result<User>> = _userState private val disposables = CompositeDisposable() fun loadUser(userId: String) { fetchUserUseCase.execute(userId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ result -> _userState.value = result }, { error -> _userState.value = Result.Error(error.localizedMessage) }) .addTo(disposables) } override fun onCleared() { super.onCleared() disposables.clear() } }
3. LiveData (Less Flexible, But Simple for Basic Cases)
You could have UseCases return LiveData directly, though this is less flexible than Flow (LiveData is designed for UI observation, not general async streams). It’s an option if you want to stick with LiveData everywhere:
class FetchUserUseCase @Inject constructor( private val userRepository: UserRepository ) { fun invoke(userId: String): LiveData<Result<User>> { val liveData = MutableLiveData<Result<User>>() liveData.value = Result.Loading userRepository.fetchUser(userId) { user, error -> if (error != null) { liveData.postValue(Result.Error(error.message)) } else { liveData.postValue(Result.Success(user)) } } return liveData } } // In ViewModel class UserViewModel @Inject constructor( private val fetchUserUseCase: FetchUserUseCase ) : ViewModel() { val userState: LiveData<Result<User>> = fetchUserUseCase.invoke("123") }
Final Notes
- Dagger Integration: Injecting UseCases into ViewModels works perfectly with Dagger—just ensure your UseCases are properly scoped (e.g.,
@Singletonor@ViewModelScoped) and included in your Dagger modules. - State Consistency: Using a sealed class like
Resultmakes it easy to handle loading, success, and error states uniformly across your app.
内容的提问来源于stack exchange,提问作者4gus71n




