基于MVVM与RxJava的Repository数据加载逻辑优化咨询(Android Jetpack Navigation项目)
Hey there! Your current logic gets the job done, but we can refine it to be more reactive, robust, and aligned with modern Android Jetpack best practices. Let’s break down a better approach that leverages single source of truth principles and reactive streams (using Kotlin Coroutines/Flow instead of RxJava, though we’ll touch on RxJava adjustments too).
Key Issues with Your Current Implementation
While functional, your setup has a few potential gaps:
- You’re manually managing
Disposableinstances, which can lead to leaks if not handled carefully. - The
getDataFromDBJobMissions()call might execute beforemakeApiCallAndSaveToDBJobMissions()finishes (since it’s asynchronous), leading to temporary empty data in the UI. - There’s no built-in support for loading/error states, which are critical for good UX.
Optimized Solution Using Kotlin Flow & Room
Room natively supports returning Flow for queries, which automatically emits updates whenever the underlying database changes. This lets us build a reactive pipeline where the UI always gets the latest data without manual refreshes.
Step 1: Update Your DAO
Replace the count query with a direct list query that returns a Flow:
@Dao interface MissionDao { @Query("SELECT * FROM MissionsTable") fun getJobMissions(): Flow<List<Mission>> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(missions: List<Mission>) }
Step 2: Refine the Repository
We’ll create a single reactive stream that prioritizes local data, fetches remote data only if local is empty, and automatically propagates database updates to the UI:
// First, define a sealed class to handle loading/success/error states 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>() } class MissionRepository( private val dao: MissionDao, private val missionApi: MissionApi // Your remote API service ) { // Single source of truth: UI observes this Flow for all data updates fun getCurrentJobMissions(): Flow<Result<List<Mission>>> = flow { // Emit loading state first (optional but helpful for UX) emit(Result.Loading) // Fetch initial local data val localMissions = dao.getJobMissions().first() emit(Result.Success(localMissions)) // If local data is empty, fetch from remote and save to DB if (localMissions.isEmpty()) { try { val remoteMissions = missionApi.fetchJobMissions() dao.insertAll(remoteMissions) // No need to emit here: Room's Flow will auto-emit the updated local data } catch (e: Exception) { emit(Result.Error(e.message ?: "Failed to fetch missions")) } } }.flowOn(Dispatchers.IO) // Add a refresh method for pull-to-refresh scenarios suspend fun refreshJobMissions() { val remoteMissions = missionApi.fetchJobMissions() dao.insertAll(remoteMissions) } }
Step 3: Integrate with ViewModel
Use ViewModel and StateFlow to manage the data lifecycle and ensure the UI only receives relevant updates:
class MissionViewModel( private val repository: MissionRepository ) : ViewModel() { private val _missionsState = MutableStateFlow<Result<List<Mission>>>(Result.Loading) val missionsState: StateFlow<Result<List<Mission>>> = _missionsState init { viewModelScope.launch { repository.getCurrentJobMissions().collect { result -> _missionsState.value = result } } } fun onRefresh() { viewModelScope.launch { try { repository.refreshJobMissions() } catch (e: Exception) { _missionsState.value = Result.Error(e.message ?: "Refresh failed") } } } }
If You Prefer RxJava
If you want to stick with RxJava instead of Coroutines, you can adjust the DAO and repository like this:
// Updated DAO for RxJava @Dao interface MissionDao { @Query("SELECT * FROM MissionsTable") fun getJobMissions(): Observable<List<Mission>> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(missions: List<Mission>): Completable } // RxJava Repository class MissionRepository( private val dao: MissionDao, private val missionApi: MissionApi ) { fun getCurrentJobMissions(): Observable<Result<List<Mission>>> { return dao.getJobMissions() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .flatMap { localMissions -> if (localMissions.isEmpty()) { missionApi.fetchJobMissions() .flatMapCompletable { remoteMissions -> dao.insertAll(remoteMissions) } .andThen(dao.getJobMissions()) .map { Result.Success(it) } .startWithItem(Result.Loading) .onErrorReturn { Result.Error(it.message ?: "Failed to fetch") } } else { Observable.just(Result.Success(localMissions)) } } } }
Why This Is Better
- Single Source of Truth: The UI only observes one stream, eliminating data inconsistency.
- Automatic Updates: Room’s
Flow/Observableemits new data whenever the database is updated, so the UI refreshes automatically after saving remote data. - Clean State Management: The
Resultsealed class makes it easy to handle loading, success, and error states in the UI. - Lifecycle Safety: Using
viewModelScopeensures coroutines are canceled when the ViewModel is destroyed, preventing leaks.
内容的提问来源于stack exchange,提问作者Shqipdon

