Android Architecture Samples

repository·main·Indexed 13 days ago

https://github.com/android/architecture-samples

A collection of Android application samples demonstrating architectural patterns through a TODO app. Features include Jetpack Compose for UI, Hilt for dependency injection, Room for local data, and a Repository pattern with Kotlin Flow and Coroutines for asynchronous operations. Includes implementations of ViewModels (AddEditTaskViewModel, StatisticsViewModel, TasksViewModel) and a TaskRepository API for managing task data with an offline-first approach.

Tokens
4.1K
Snippets
10
Records
14
Agent score
100%

What's inside Android Architecture Samples

  1. Overview of Android Architecture Samples

    main

    This repository contains multiple samples showcasing different architectural approaches to Android development. Each sample implements a simple TODO app to demonstrate specific design decisions, testing scenarios, and architectural patterns.

    This specific branch implements:

    • UI: Built with Jetpack Compose using a single-activity architecture and Navigation Compose.
    • Presentation Layer: Uses a Compose screen (View) and a ViewModel per screen/feature.
    • Asynchronous Operations: Reactive UIs powered by Kotlin Flow and Coroutines.
    • Data Layer: Uses a Repository pattern with two data sources (local Room database and a fake remote source).
    • Dependency Injection: Managed via Hilt.
    • Product Flavors: Includes mock and prod flavors to facilitate development and testing.
    • Testing: Includes unit, integration, and end-to-end (e2e) tests, including shared tests for emulators/devices.
  2. Open a sample in Android Studio

    main

    To run a sample, you must first check out the specific branch containing the architecture you wish to study. Once the desired branch is checked out, open the root directory in Android Studio.

    Follow these steps:

    1. Clone the repository.
    2. Checkout the desired branch (not shown in this specific README, but required).
    3. Open the architecture-samples/ directory in Android Studio.
    git clone git@github.com:android/architecture-samples.git
  3. Configure Dependency Injection with Hilt in TodoApplication

    main

    The TodoApplication class serves as the entry point for the application and is annotated with @HiltAndroidApp. This annotation triggers Hilt's code generation, allowing for dependency injection throughout the application. Additionally, it initializes Timber for logging, but only when BuildConfig.DEBUG is true, ensuring debug logs are not active in production builds.

    @HiltAndroidApp
    class TodoApplication : Application() {
    
        override fun onCreate() {
            super.onCreate()
            if (BuildConfig.DEBUG) Timber.plant(DebugTree())
        }
    }
  4. Navigate to different screens with TodoNavigationActions

    main

    Use the following methods on TodoNavigationActions to perform navigation tasks:

    • navigateToTasks(userMessage: Int = 0): Navigates to the tasks screen. If userMessage is non-zero, it passes the message as a query parameter. It manages the back stack to prevent deep stacks when navigating from the drawer.
    • navigateToStatistics(): Navigates to the statistics screen. It uses popUpTo the start destination and launchSingleTop to avoid building a large back stack.
    • navigateToTaskDetail(taskId: String): Navigates to the task detail screen for a specific task identified by taskId.
    • navigateToToAddEditTask(title: Int, taskId: String?): Navigates to the add/edit task screen. The title is a string resource ID. If taskId is provided, it navigates in edit mode; otherwise, it navigates in add mode.
  5. Manage Add/Edit Task screen state with AddEditTaskViewModel

    main

    The AddEditTaskViewModel manages the UI state and business logic for the screen where users create or edit tasks. It uses a single AddEditTaskUiState data class to represent the entire screen state, which is exposed as a StateFlow.

    UI State Structure

    The AddEditTaskUiState contains:

    • title: The task title.
    • description: The task description.
    • isTaskCompleted: Completion status.
    • isLoading: Whether the task is currently being loaded from the repository.
    • userMessage: An optional string resource ID (Int?) used to display error messages (e.g., via a Snackbar).
    • isTaskSaved: A flag indicating if the task was successfully saved, used to trigger navigation.

    Key Operations

    • Updating Fields: Use updateTitle(newTitle: String) and updateDescription(newDescription: String) to mutate the state as the user types.
    • Saving: Call saveTask() to persist the task. If the title or description is empty, the state is updated with R.string.empty_task_message. If valid, it either creates a new task or updates an existing one based on whether a taskId was provided via navigation arguments.
    • Handling Messages: Call snackbarMessageShown() after displaying a user message (like an error) to clear the userMessage from the state.
    // Example of interacting with the ViewModel in a UI component
    
    // Observe the state
    lifecycleScope.launch { 
        viewModel.uiState.collect { uiState -> 
            // Update UI elements based on uiState
            titleEditText.setText(uiState.title)
            if (uiState.isLoading) showLoadingSpinner()
        }
    }
    
    // Update state on user input
    titleEditText.doAfterTextChanged { text ->
        viewModel.updateTitle(text.toString())
    }
    
    // Save the task
    saveButton.setOnClickListener {
        viewModel.saveTask()
    }
    
    // Clear error messages after showing them
    snackbar.addCallback(object : Snackbar.Callback() {
        override fun onDismissed(transientBottomBar: Snackbar?, event: DismissedTranscript) {
            viewModel.snackbarMessageShown()
        }
    })
  6. TaskRepository API Reference

    main

    The TaskRepository interface defines the following public methods for task management:

    Task Creation and Modification

    • suspend createTask(title: String, description: String): String: Creates a new task with a unique ID. Returns the generated taskId.
    • suspend updateTask(taskId: String, title: String, description: String): Updates the title and description of an existing task. Throws an Exception if the task is not found.
    • suspend completeTask(taskId: String): Marks a task as completed.
    • suspend activateTask(taskId: String): Marks a task as not completed.

    Task Retrieval

    • suspend getTasks(forceUpdate: Boolean): List<Task>: Returns a list of all tasks. If forceUpdate is true, it triggers a refresh() from the network first.
    • fun getTasksStream(): Flow<List<Task>>: Returns a Flow that emits the list of all tasks whenever the local data changes.
    • suspend getTask(taskId: String, forceUpdate: Boolean): Task?: Retrieves a specific task by ID. Returns null if not found. If forceUpdate is true, it triggers a refresh() first.
    • fun getTaskStream(taskId: String): Flow<Task?>: Returns a Flow that emits the specific task by ID whenever it changes.

    Bulk Operations and Sync

    • suspend refresh(): Deletes all local tasks and replaces them with tasks loaded from the network.
    • suspend refreshTask(taskId: String): A convenience method that calls refresh().
    • suspend deleteTask(taskId: String): Deletes a specific task locally and syncs the deletion to the network.
    • suspend clearCompletedTasks(): Deletes all tasks marked as completed locally and syncs to the network.
    • suspend deleteAllTasks(): Deletes all tasks locally and syncs to the network.
  7. Use TodoNavigationActions to manage screen transitions

    main

    The TodoNavigationActions class provides a high-level API for navigating between different screens in the Todo app using a NavHostController. It abstracts the underlying route strings and argument construction, ensuring consistent navigation behavior and proper back stack management (e.g., using popUpTo, launchSingleTop, and restoreState).

    // Example of initializing and using navigation actions
    val navController = // ... obtained from NavHost
    val navigationActions = TodoNavigationActions(navController)
    
    // Navigate to the task list
    navigationActions.navigateToTasks()
    
    // Navigate to a specific task detail screen
    navigationActions.navigateToTaskDetail("task_id_123")
    
    // Navigate to add or edit a task
    navigationActions.navigateToToAddEditTask(R.string.new_task, null)
  8. Use TaskRepository to manage task data

    main

    The TaskRepository (implemented by DefaultTaskRepository) serves as the single entry point for managing task data in the application. It abstracts the complexity of coordinating between a local data source (via TaskDao) and a remote network data source (NetworkDataSource).

    Key behaviors:

    • Offline-first approach: Most operations update the local data source first and then trigger a background synchronization to the network.
    • Asynchronous Sync: Methods like createTask, updateTask, and deleteTask call saveTasksToNetwork(), which launches a coroutine in the ApplicationScope to sync data without blocking the caller.
    • Data Mapping: The repository handles the conversion between local database models and external/network models using extension functions like .toLocal(), .toExternal(), and .toNetwork().
    // Example usage of TaskRepository
    
    // Create a new task
    val taskId = taskRepository.createTask("Title", "Description")
    
    // Get a stream of all tasks to observe changes in the UI
    val tasksFlow: Flow<List<Task>> = taskRepository.getTasksStream()
    
    // Get a specific task by ID
    val task = taskRepository.getTask(taskId, forceUpdate = false)
    
    // Complete a task
    taskRepository.completeTask(taskId)
  9. Use TasksViewModel methods to control task list behavior

    main

    The TasksViewModel provides several public methods to perform actions on the task list:

    • setFiltering(requestType: TasksFilterType): Updates the current filter (e.g., ALL_TASKS, ACTIVE_TASKS, COMPLETED_TASKS). This state is persisted across process death via SavedStateHandle.
    • clearCompletedTasks(): Removes all tasks marked as completed from the repository and shows a confirmation snackbar.
    • completeTask(task: Task, completed: Boolean): Toggles the completion status of a specific task.
    • showEditResultMessage(result: Int): Displays a snackbar message based on the result of an add, edit, or delete operation. Use the following constants:
      • EDIT_RESULT_OK
      • ADD_EDIT_RESULT_OK
      • DELETE_RESULT_OK
    • refresh(): Triggers a refresh of the task data from the repository.
    • snackbarMessageShown(): Call this when a snackbar is dismissed to clear the userMessage in the UI state.
  10. Manage the Tasks list screen with TasksViewModel

    main

    The TasksViewModel manages the state and business logic for the main task list screen. It exposes a single uiState flow that combines task data, loading status, filtering information, and user messages (snackbars) into a unified TasksUiState object. This allows the UI to reactively update to data changes, loading states, or errors.

    Key UI State Properties

    • items: A List<Task> representing the tasks currently visible based on the active filter.
    • isLoading: A boolean indicating if a background operation (like refreshing) is in progress.
    • filteringUiInfo: A FilteringUiInfo object containing string resources and icons for the current filter state.
    • userMessage: An optional integer resource ID used to display snackbar messages.
    // Observe the UI state in your Fragment or Activity
    viewModel.uiState.collect { uiState ->
        // Update UI components based on uiState.items, uiState.isLoading, etc.
    }
  11. TasksUiState data structure

    main

    The TasksUiState is the single source of truth for the Tasks screen UI. It is a data class containing:

    PropertyTypeDescription
    itemsList<Task>The list of tasks to display.
    isLoadingBooleanWhether the screen is currently loading data.
    filteringUiInfoFilteringUiInfoMetadata for the current filter (labels and icons).
    userMessageInt?A string resource ID for displaying snackbars.
    data class TasksUiState(
        val items: List<Task> = emptyList(),
        val isLoading: Boolean = false,
        val filteringUiInfo: FilteringUiInfo = FilteringUiInfo(),
        val userMessage: Int? = null
    )
  12. StatisticsUiState data class

    main

    Represents the UI state for the statistics screen. It tracks whether the screen is empty, currently loading, and the percentage of active versus completed tasks.

    Fields:

    • isEmpty: Boolean indicating if there are no tasks.
    • isLoading: Boolean indicating if data is currently being fetched.
    • activeTasksPercent: Float representing the percentage of active tasks.
    • completedTasksPercent: Float representing the percentage of completed tasks.
    data class StatisticsUiState(
        val isEmpty: Boolean = false,
        val isLoading: Boolean = false,
        val activeTasksPercent: Float = 0f,
        val completedTasksPercent: Float = 0f
    )