Skip to content

Android Architecture Core Principles: Single Source of Truth (SSOT) and Unidirectional Data Flow (UDF) Practice Guide

In Android development, as application complexity increases, problems such as data chaos, inconsistent states, and difficult debugging occur frequently. Single Source of Truth (SSOT) and Unidirectional Data Flow (UDF) are the core architectural principles to solve these pain points. Working together, they can make the application architecture clearer, more maintainable, and more testable, serving as the cornerstone of modern Android architectures (such as MVVM and MVI). This guide will help developers fully grasp the application of SSOT and UDF from four dimensions: concept explanation, core value, practical implementation, and common problems, enabling them to build robust Android applications with Jetpack components.

I. Core Concept Explanation: What are SSOT and UDF?

1.1 Single Source of Truth (SSOT)

Single Source of Truth means that in an application architecture, for any type of data, there is always and only one "authoritative source". All components must read and modify the data through this source, and direct sharing or modification of data copies between components is prohibited.

Core Significance: Solve the pain point of "inconsistent multiple data copies" in Android development—for example, chaotic synchronization of multiple data copies in local databases, network caches, and memory caches leads to abnormal UI display and logical judgment errors. At the same time, it reduces data maintenance costs and simplifies the debugging process (only need to track changes in the single data source).

Core Requirements:

  • Uniqueness: There is only one authoritative data source for the same type of data (such as user information, product lists) to avoid multi-source modifications.

  • Uniformity: All components (UI, ViewModel, UseCase, etc.) read data from this source, and data modification must go through a unified entry (such as Repository methods). Direct operation on the underlying data source (such as Room Dao, Retrofit interface) is prohibited.

  • Consistency: The data source itself ensures data integrity and consistency (for example, when synchronizing local and remote data, the merging logic is uniformly handled through the Repository).

1.2 Unidirectional Data Flow (UDF)

Unidirectional Data Flow means that the flow direction of all data in the application is fixed, forming a closed loop of "data input → processing → output → display". Reverse flow is prohibited (such as the UI directly modifying the data source, or data being transmitted from the UI layer back to the data layer).

Core Significance: Solve the problem of "chaotic data flow and untraceable states"—for example, UI click events directly modifying the database make it impossible to track the cause of state changes. UDF makes every state change predictable, debuggable, and reproducible through a fixed flow direction.

Standard Flow (Taking MVVM Architecture as an Example):

  1. User Interaction/External Events (Input): The UI layer triggers events (such as clicking the refresh button, pull-down loading) or external data pushes (such as push notifications).

  2. Event Processing: The ViewModel receives the event, calls methods of the domain layer (UseCase) or data layer (Repository), and processes business logic.

  3. Data Acquisition/Modification: The Repository acquires or modifies data from the single data source (local/remote) and returns the processing result.

  4. State Distribution (Output): The ViewModel converts the processed result into a UI-displayable state (such as loading, success, failure) and distributes it to the UI layer through observable objects (Flow/LiveData).

  5. UI Rendering (Display): The UI layer observes the state of the ViewModel, updates the interface according to the state, and does not handle any business logic, only responsible for display.

Core Requirements: Data flow is irreversible, and states are immutable (the UI layer only reads states and does not modify them; the ViewModel only distributes states and does not directly receive state modification requests from the UI layer, which must be triggered by events).

1.3 The Relationship Between SSOT and UDF: Complementary and Indispensable

SSOT is the foundation of UDF: without a single data source, the "data output" of UDF will have multi-source and inconsistent problems, leading to chaotic UI display states; UDF is the guarantee of SSOT: without unidirectional data flow, components may directly modify the data source, destroying the uniqueness and consistency of SSOT. Only when combined can they achieve the goal of "traceable data, predictable states, and maintainable architecture".

II. Practical Preparation: Technology Stack Selection and Architecture Foundation

This practice will be based on the modern mainstream Android technology stack, combined with the MVVM architecture, to implement the SSOT and UDF principles. The technology stack selection is as follows (all are Jetpack core components, reducing learning costs and adapting to most Android projects):

  • Language: Kotlin (null safety, coroutines, Flow, adapting to UDF data flow processing).

  • Architecture Pattern: MVVM (Model-View-ViewModel), clearly distinguishing the UI layer, ViewModel layer, and data layer.

  • Data Layer: Room (local data source, SQLite encapsulation), Retrofit (remote data source, network requests), Repository (single data source entry).

  • ViewModel Layer: ViewModel (holds UI states and handles events), Flow (unidirectional data flow distribution, replacing LiveData to achieve more flexible state management).

  • UI Layer: Compose (declarative UI, more suitable for UDF state-driven rendering; if using XML layout, the logic is consistent, only the rendering method is different).

  • Others: Coroutines (handle asynchronous tasks such as network requests and database operations), Dagger Hilt (dependency injection, simplifying dependencies between components and improving testability).

Architecture Layer Convention (strictly follow the single responsibility principle to support SSOT and UDF):

  • UI Layer: Only responsible for displaying the UI, receiving user interactions, observing the state of the ViewModel, and does not contain any business logic or data processing.

  • ViewModel Layer: Receives events from the UI layer, calls UseCase/Repository to process logic, converts results into UI states, distributes them through Flow, and does not directly operate the data source.

  • Domain Layer (optional, can be omitted in small and medium-sized projects and integrated into the Repository): Encapsulates core business logic (such as data filtering and conversion) to decouple the ViewModel from the Repository.

  • Data Layer: Includes Repository, local data source (Room), and remote data source (Retrofit). It is the core carrier of SSOT and is responsible for data acquisition, storage, and synchronization.

III. Core Practice: Complete Implementation Case of SSOT + UDF

This case will implement a "user list" function, covering two core interactions: "pull-down refresh to obtain the latest user data" and "click item to view user details". It will fully implement the SSOT and UDF principles, focusing on displaying: encapsulation of a single data source (Repository), circulation of unidirectional data flow (UI → ViewModel → Repository → ViewModel → UI), and state-driven UI rendering.

3.1 Requirement Sorting (Clarify Input and Output)

  • Input Events: Pull-down refresh (request the latest remote user data), click user item (trigger the event to view details).

  • Data Processing: When refreshing, first request remote data; if successful, update the local database (single data source); if failed, display local cached data. When clicking an item, pass the user ID to obtain the corresponding user details (read from the single data source).

  • Output States: Loading (when refreshing), user list display (success), loading failure (error prompt), user detail display (success).

3.2 Step 1: Encapsulate the Single Data Source (SSOT Core: Repository)

Repository is the only entry for the single data source. It is responsible for integrating local (Room) and remote (Retrofit) data sources, uniformly handling data synchronization logic, and providing a unified API externally to ensure that all components only obtain/modify data through the Repository.

3.2.1 Define Data Models (Entity and DTO)

Distinguish between local database entities (Entity) and remote interface return data (DTO) to avoid data coupling, and unify data formats through mapping conversion.

kotlin
// 1. Remote DTO (Retrofit interface return format)
data class UserDto(
    @SerializedName("id") val id: Int,
    @SerializedName("name") val name: String,
    @SerializedName("avatar") val avatarUrl: String
)

// 2. Local Entity (Room database entity, storage carrier of single data source)
@Entity(tableName = "user")
data class UserEntity(
    @PrimaryKey val id: Int,
    val name: String,
    val avatarUrl: String,
    @ColumnInfo(defaultValue = "0") val updateTime: Long // Used for data synchronization judgment
)

// 3. UI Display Model (Model distributed by ViewModel to UI layer, decoupled from data source)
data class UserUiModel(
    val id: Int,
    val name: String,
    val avatarUrl: String
)

// Mapping extension functions (convert DTO→Entity, Entity→UiModel)
fun UserDto.toEntity(): UserEntity = UserEntity(
    id = id,
    name = name,
    avatarUrl = avatarUrl,
    updateTime = System.currentTimeMillis()
)

fun UserEntity.toUiModel(): UserUiModel = UserUiModel(
    id = id,
    name = name,
    avatarUrl = avatarUrl
)

3.2.2 Implement Local and Remote Data Sources

kotlin
// 1. Remote Data Source (Retrofit interface)
interface UserRemoteDataSource {
    @GET("/users")
    suspend fun getRemoteUsers(): List<UserDto>
    
    @GET("/users/{id}")
    suspend fun getRemoteUserDetail(@Path("id") id: Int): UserDto
}

// 2. Local Data Source (Room Dao)
@Dao
interface UserLocalDataSource {
    @Query("SELECT * FROM user ORDER BY updateTime DESC")
    fun getLocalUsers(): Flow<List<UserEntity>> // Use Flow to observe local data changes
    
    @Query("SELECT * FROM user WHERE id = :id")
    suspend fun getLocalUserDetail(id: Int): UserEntity?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE) // Replace on conflict to ensure data uniqueness
    suspend fun insertUsers(users: List<UserEntity>)
    
    @Delete
    suspend fun deleteAllUsers()
}

3.2.3 Implement Repository (Single Data Source Entry)

Repository integrates local and remote data sources, provides unified methods externally, handles data synchronization logic (such as remote first, then local when refreshing), and ensures that all components only operate data through the Repository to implement SSOT.

kotlin
// Inject local and remote data sources (dependency injection to simplify dependency management)
class UserRepository @Inject constructor(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) {
    // 1. Get user list: single data source entry (prioritize remote, use local if failed, update local if successful)
    suspend fun getUsers(refresh: Boolean = false): Result<List<UserEntity>> {
        return try {
            if (refresh) {
                // Pull-down refresh: request remote data and update local database
                val remoteUsers = remoteDataSource.getRemoteUsers()
                val userEntities = remoteUsers.map { it.toEntity() }
                localDataSource.insertUsers(userEntities)
            }
            // Whether to refresh or not, finally read from the local data source (core of single data source: all reads go through local)
            val localUsers = localDataSource.getLocalUsers().first() // Get the first value of Flow
            Result.success(localUsers)
        } catch (e: Exception) {
            // Remote request failed, return local cache (if no local data, return failure)
            val localUsers = localDataSource.getLocalUsers().firstOrNull()
            localUsers?.let { Result.success(it) } ?: Result.failure(e)
        }
    }
    
    // 2. Get user details: read from local data source (single data source)
    suspend fun getUserDetail(id: Int): Result<UserEntity?> {
        return try {
            val user = localDataSource.getLocalUserDetail(id)
            Result.success(user)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    // 3. Observe changes in local user list (used for real-time UI refresh, such as automatic synchronization after database update)
    fun observeUsers(): Flow<List<UserEntity>> = localDataSource.getLocalUsers()
}

// Note: Repository only exposes data operation methods, does not handle business logic and UI state conversion, and maintains single responsibility

3.3 Step 2: Implement Unidirectional Data Flow (UDF Core: ViewModel + UI)

Follow the standard flow of UDF to implement the closed loop of "UI event → ViewModel processing → Repository data acquisition → ViewModel state conversion → UI rendering", ensuring that the data flow is unidirectional and the state is predictable.

3.3.1 ViewModel: Event Processing and State Distribution

The core responsibility of ViewModel: receive events from the UI layer, call Repository methods to process data, convert data results into UI-displayable states, distribute them to the UI layer through Flow, do not hold UI references, and do not directly operate the data source.

kotlin
// 1. Define UI State (immutable, only for display, cannot be modified by UI layer)
sealed class UserUiState {
    object Loading : UserUiState() // Loading
    data class Success(val users: List<UserUiModel>) : UserUiState() // Success (user list)
    data class DetailSuccess(val user: UserUiModel?) : UserUiState() // Success (user details)
    data class Error(val message: String) : UserUiState() // Failure
}

// 2. Define UI Event (input, triggered by UI layer, received by ViewModel)
sealed class UserUiEvent {
    object RefreshUsers : UserUiEvent() // Pull-down refresh
    data class ClickUserItem(val userId: Int) : UserUiEvent() // Click user item
}

// 3. ViewModel Implementation (process events and distribute states)
class UserViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    // Private event flow (receives events from UI layer, uses MutableSharedFlow to support single sending)
    private val _uiEvent = MutableSharedFlow<UserUiEvent>()
    // Public state flow (distributes UI states, uses StateFlow to ensure states are observable and immutable)
    private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    init {
        // Listen to UI events and process logic (data flow entry)
        viewModelScope.launch {
            _uiEvent.collect { event ->
                when (event) {
                    is UserUiEvent.RefreshUsers -> handleRefreshUsers()
                    is UserUiEvent.ClickUserItem -> handleClickUserItem(event.userId)
                }
            }
        }
        // Initialization: load local cached data
        viewModelScope.launch {
            handleRefreshUsers(refresh = false)
        }
    }

    // Process pull-down refresh event
    private suspend fun handleRefreshUsers(refresh: Boolean = true) {
        _uiState.value = UserUiState.Loading // Send loading state
        val result = userRepository.getUsers(refresh)
        _uiState.value = when (result) {
            is Result.Success -> {
                val userUiModels = result.data.map { it.toUiModel() }
                UserUiState.Success(userUiModels) // Send success state (user list)
            }
            is Result.Failure -> UserUiState.Error(result.exception.message ?: "Loading failed") // Send failure state
        }
    }

    // Process click user item event
    private suspend fun handleClickUserItem(userId: Int) {
        _uiState.value = UserUiState.Loading // Send loading state
        val result = userRepository.getUserDetail(userId)
        _uiState.value = when (result) {
            is Result.Success -> {
                val userUiModel = result.data?.toUiModel()
                UserUiState.DetailSuccess(userUiModel) // Send success state (user details)
            }
            is Result.Failure -> UserUiState.Error(result.exception.message ?: "Failed to get details") // Send failure state
        }
    }

    // Provide methods externally: UI layer triggers events (data flow entry, only allows UI layer to send events, not directly modify states)
    fun sendEvent(event: UserUiEvent) {
        viewModelScope.launch {
            _uiEvent.emit(event)
        }
    }
}

// Key Notes:
// 1. State (uiState) uses StateFlow, which is immutable (exposed as asStateFlow externally), and the UI layer can only observe but not modify it;
// 2. Event (uiEvent) uses MutableSharedFlow, and the UI layer sends events through sendEvent, which are uniformly processed by the ViewModel;
// 3. All data processing calls Repository methods, does not directly operate local/remote data sources, and follows SSOT;
// 4. Data flow is irreversible: UI→ViewModel (events), ViewModel→UI (states), no reverse flow.

3.3.2 UI Layer: Event Triggering and State Rendering (Compose Implementation)

The core responsibility of the UI layer: display states, trigger events, do not contain any business logic or data processing, strictly follow "state-driven rendering", and only observe the ViewModel's uiState to update the interface according to the state.

kotlin
// UserListScreen: User list interface (UI layer)
@Composable
fun UserListScreen(
    viewModel: UserViewModel = hiltViewModel() // Inject ViewModel
) {
    // Observe the state of the ViewModel (data flow exit)
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Pull-down refresh component (trigger refresh event)
    val pullRefreshState = rememberPullRefreshState(
        refreshing = uiState is UserUiState.Loading,
        onRefresh = { viewModel.sendEvent(UserUiEvent.RefreshUsers) }
    )

    Box(modifier = Modifier.fillMaxSize()) {
        PullRefresh(
            state = pullRefreshState,
            modifier = Modifier.fillMaxSize()
        ) {
            // Render UI according to state
            when (val state = uiState) {
                is UserUiState.Loading -> {
                    // Loading: display progress bar
                    CircularProgressIndicator(
                        modifier = Modifier.align(Alignment.Center)
                    )
                }
                is UserUiState.Success -> {
                    // Success: display user list
                    LazyColumn(modifier = Modifier.fillMaxSize()) {
                        items(state.users) { user ->
                            UserItem(
                                user = user,
                                onClick = {
                                    // Click event: send click event to ViewModel
                                    viewModel.sendEvent(UserUiEvent.ClickUserItem(user.id))
                                }
                            )
                        }
                    }
                }
                is UserUiState.DetailSuccess -> {
                    // Detail state: navigate to detail page (or display in pop-up window)
                    state.user?.let { user ->
                        UserDetailDialog(
                            user = user,
                            onDismiss = {
                                // Close details: reload list state
                                viewModel.sendEvent(UserUiEvent.RefreshUsers)
                            }
                        )
                    } ?: run {
                        Text(
                            text = "User does not exist",
                            modifier = Modifier.align(Alignment.Center)
                        )
                    }
                }
                is UserUiState.Error -> {
                    // Failure: display error prompt and provide retry button
                    Column(
                        modifier = Modifier.align(Alignment.Center),
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Text(text = state.message)
                        Button(
                            onClick = { viewModel.sendEvent(UserUiEvent.RefreshUsers) },
                            modifier = Modifier.topPadding(16.dp)
                        ) {
                            Text(text = "Retry")
                        }
                    }
                }
            }
        }
    }
}

// Auxiliary Component: User item
@Composable
fun UserItem(user: UserUiModel, onClick: () -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClick = onClick)
            .padding(16.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(text = user.name)
        Image(
            painter = rememberAsyncImagePainter(user.avatarUrl),
            contentDescription = "User Avatar",
            modifier = Modifier.size(40.dp)
        )
    }
}

// Auxiliary Component: User Detail Dialog
@Composable
fun UserDetailDialog(user: UserUiModel, onDismiss: () -> Unit) {
    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text(text = "User Details") },
        text = {
            Column {
                Text(text = "ID: ${user.id}")
                Text(text = "Name: ${user.name}")
            }
        },
        confirmButton = {
            Button(onClick = onDismiss) {
                Text(text = "Confirm")
            }
        }
    )
}

// Key Notes:
// 1. The UI layer only observes the ViewModel's state through collectAsStateWithLifecycle and renders different interfaces according to the state;
// 2. All user interactions (pull-down refresh, click item, retry) send events to the ViewModel through sendEvent, without directly processing logic;
// 3. Does not hold any data source references, does not modify any data, only responsible for "display" and "triggering events", following the unidirectional flow of UDF.

3.4 Step 3: Dependency Injection and Operation Verification (Hilt)

Use Dagger Hilt to implement dependency injection, simplify dependencies between components (such as Repository injected into ViewModel, local/remote data sources injected into Repository), avoid hard coding, and improve testability and maintainability.

kotlin
// 1. Configure Hilt (Application layer)
@HiltAndroidApp
class MyApp : Application()

// 2. Provide Remote Data Source Instance (Retrofit)
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/") // Replace with actual interface address
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Singleton
    @Provides
    fun provideUserRemoteDataSource(retrofit: Retrofit): UserRemoteDataSource {
        return retrofit.create(UserRemoteDataSource::class.java)
    }
}

// 3. Provide Local Data Source Instance (Room)
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "app_database"
        ).build()
    }

    @Provides
    fun provideUserLocalDataSource(database: AppDatabase): UserLocalDataSource {
        return database.userDao()
    }
}

// 4. Room Database Instance
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserLocalDataSource
}

Operation Verification Points:

  • Pull-down Refresh: Trigger a remote request; after success, the local database is updated, and the UI displays the latest list (SSOT takes effect).

  • Refresh Without Network: Remote request fails, and the UI displays local cached data (fault tolerance of SSOT).

  • Click Item: Trigger the detail event; the ViewModel obtains local data from the Repository and displays the details (UDF takes effect).

  • Debug Verification: All data changes go through the Repository, and all state changes are distributed through the ViewModel. Data flow and state changes can be tracked through logs.

IV. Common Problems and Solutions (Practical Pitfalls Avoidance)

4.1 Problem 1: Multiple Components Modify the Data Source, Destroying SSOT

Phenomenon: The UI layer or ViewModel directly calls Room Dao and Retrofit interfaces to modify data, resulting in inconsistent multiple data copies.

Solutions:

  • Strict Encapsulation: Only the Repository holds references to local/remote data sources, and no Dao or Retrofit interfaces are exposed externally.

  • Dependency Injection: Inject the Repository through Hilt, and prohibit components from creating local/remote data source instances by themselves.

  • Code Inspection: Prohibit direct calls to Dao and Retrofit interfaces through lint rules (optional).

4.2 Problem 2: Reverse Data Flow, Destroying UDF

Phenomenon: The UI layer directly modifies the state of the ViewModel, or the ViewModel directly receives state modification requests from the UI layer, leading to chaotic data flow.

Solutions:

  • Immutable State: The ViewModel's uiState is exposed as StateFlow (immutable) externally and can only be modified internally.

  • Event-Driven: The UI layer can only send events through sendEvent, and the ViewModel modifies the state by processing events, not directly receiving state modification requests.

  • Responsibility Separation: The UI layer only renders states and does not process any data; the ViewModel only processes events and distributes states, and does not hold UI references.

4.3 Problem 3: Chaotic Synchronization Between Local and Remote Data, SSOT Invalid

Phenomenon: After the remote data is updated, the local database is not synchronized, or the synchronization logic is wrong, leading to inconsistent read data.

Solutions:

  • Unified Synchronization Logic: All data synchronization (remote → local) is implemented in the Repository, such as requesting remote first when refreshing, and inserting into local (replacing conflicting data) after success.

  • Timestamp Judgment: Add an updateTime field to local data, and judge the freshness of data according to the timestamp during synchronization to avoid invalid synchronization.

  • Exception Handling: When a remote request fails, give priority to using local cache, and provide an error prompt to allow users to retry.

4.4 Problem 4: Too Many States, Chaotic UI Rendering Logic

Phenomenon: The ViewModel defines a large number of states, and the UI layer needs to handle complex state judgments, leading to redundant and error-prone code.

Solutions:

  • Sealed Class Encapsulation State: Use sealed class to uniformly manage all UI states (such as UserUiState in this case) to avoid scattered states.

  • State Merging: Merge related states into a composite state (such as merging loading, success, and failure into a list state) to reduce the number of states.

  • Auxiliary Components: Extract complex UI rendering logic into independent components (such as UserItem and UserDetailDialog in this case) to simplify the main interface code.

V. Best Practice Summary (Core Points of SSOT + UDF)

  • SSOT Best Practices:

  • There is only one authoritative source for all data (prioritize local database, and remote data is used to synchronize and update local data).

  • Repository is the only data entry/exit, uniformly handling the synchronization, reading, and modification of local and remote data.

  • Direct sharing of data copies between components is prohibited, and all data operations are performed through the Repository.

  • UDF Best Practices:

  • Strictly follow the unidirectional flow of "input → processing → output → display", and the data flow is irreversible.

  • Immutable State: The UI layer only reads states, the ViewModel only distributes states, and state modification must be triggered by events.

  • Use Flow/StateFlow to manage data flow, replacing LiveData to achieve more flexible state monitoring (such as coroutine adaptation and backpressure handling).

  • Architecture Collaboration:

  • SSOT and UDF must be used together, which are indispensable to achieve the robustness and maintainability of the architecture.

  • Clear Layering: UI layer (display), ViewModel layer (events + states), data layer (SSOT), strictly following the single responsibility principle.

  • Dependency Injection: Use Hilt to simplify component dependencies and improve testability and scalability.

VI. Extension Suggestions (Advanced Directions)

  • Combine with MVI Architecture: MVI is the ultimate embodiment of UDF, which further standardizes UI states and events. You can try to transform this case into MVI architecture.

  • State Persistence: Use SavedStateHandle to save the state of the ViewModel to avoid state loss when the screen is rotated.

  • Unified Exception Handling: Encapsulate a global exception handling mechanism to uniformly capture and handle exceptions in the Repository or ViewModel, simplifying the error handling logic of the UI layer.

  • Testing Practice: Write unit tests (test the data synchronization logic of the Repository and the event processing of the ViewModel) and UI tests (test state rendering) to verify the correctness of SSOT and UDF.

Conclusion: Single Source of Truth (SSOT) and Unidirectional Data Flow (UDF) are not "silver bullets", but they are the core principles to solve Android architecture chaos and data inconsistency. In actual projects, there is no need to be overly complex. As long as we strictly follow the core idea of "single data source entry and unidirectional data flow", and implement it with Jetpack components, we can significantly improve the maintainability and testability of the application and reduce the cost of later iterations.

Just something casual. Hope you like it.