Challenge 3: The Full Stack, One More Time — Solution data class NotesUiState( val notes: List = emptyList(), val isLoading: Boolean = true, val errorMessage: String? = null ) @HiltViewModel class NoteViewModel @Inject constructor( private val repository: NoteRepository // the INTERFACE, not the impl ) : ViewModel() { private val _uiState = MutableStateFlow(NotesUiState()) val uiState: StateFlow = _uiState.asStateFlow() init { viewModelScope.launch { repository.notes.collect { notesList -> _uiState.value = _uiState.value.copy(notes = notesList, isLoading = false) } } refresh() } fun refresh() { viewModelScope.launch { _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) try { repository.refresh() } catch (e: Exception) { _uiState.value = _uiState.value.copy(errorMessage = "Couldn't refresh notes") } } } } @Composable fun NotesScreen(viewModel: NoteViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() Column(modifier = Modifier.padding(16.dp)) { uiState.errorMessage?.let { message -> Text(text = message, color = Color.Red) } if (uiState.isLoading && uiState.notes.isEmpty()) { Text(text = "Loading...") } else { uiState.notes.forEach { note -> Text(text = note.title) } } Button(onClick = { viewModel.refresh() }) { Text("Refresh") } } } Notes: - NoteViewModel's constructor takes "repository: NoteRepository" — the INTERFACE from Challenge 2 — never NoteRepositoryImpl, Room, or Retrofit directly. This is dependency inversion in practice: the ViewModel is written against an abstraction and has no idea Room or Retrofit exist underneath. - init { } does two things: collects repository.notes (the local, always-current Room-backed Flow) into uiState continuously, AND triggers an initial refresh() to pull fresh data from the network — these are two separate, independently-running coroutines launched via viewModelScope. - The composable's rendering logic branches on uiState.isLoading && uiState.notes.isEmpty() specifically (not just isLoading alone) — this means a background refresh (Button-triggered) doesn't blank out already-loaded notes while it runs, only the very first load (with no cached data yet) shows the loading text, matching the offline-first philosophy from Challenge 1. - This example pulls together: sealed-class-free but StateFlow-based UiState (Chapter 2), Room via the repository (Chapter 3), the interface/@Binds pattern (this chapter), viewModelScope (Chapter 6), and collectAsStateWithLifecycle() in Compose (Chapter 1) — the entire course's architecture in one screen.