Learning outcome
Follow state, concurrency and lifecycle boundaries through the Android client.
CommunityRepository separates network I/O from orchestration. CommunityViewModel owns the processor, coroutine job, conversation state, selected event and retry request. Compose observes state and dispatches user intent. The manual factory supplies dependencies once; recomposition should not create a new repository or launch another model request.
Repository contract from the pinned implementation
interface CommunityRepository {
suspend fun discover(): List<CommunityEvent>
suspend fun ask(endpoint: String, request: AgentRequest): AgentReply
}
data class AgentReply(val text: String, val messages: List<String>)
The HTTP implementation uses Dispatchers.IO for blocking OkHttp calls and closes responses with use. It bounds response data before decoding. That contract allows tests to return synthetic protocol messages without a Gemini key while exercising the actual renderer. A fake dependency in a test is useful; silently selecting it in production would defeat the product and hide failures.
Key lifecycle guards: abbreviated source logic
// send()
if (prompt.isBlank() || state.value.busy) return
// Missing endpoint shows setup instead of a generated result.
// submit(): remember which conversation owns the request.
val epoch = generation
// After repository.ask(...) returns:
ensureActive()
if (epoch != generation) return@launch
// reset(): invalidate the old conversation before clearing surfaces.
generation++
job?.cancel()
The busy guard prevents overlapping requests from normal user input. The epoch comparison prevents a result from the old conversation being appended after reset. Cancellation is rethrown rather than converted into a network error. However, cancelling the coroutine does not guarantee the synchronous OkHttp call or the provider computation stops immediately; timeout bounds the call. Do not equate UI cancellation with refunded model usage.
The endpoint persists in SharedPreferences. ViewModel conversation state survives normal Activity recreation but not process death. New conversation clears surfaces and selected event; session-only saved IDs are retained by the reset implementation. Inspect holds a bounded in-memory trace. None of this is durable chat history or a production audit log.
Practice and checkpoint
Trace two quick Send taps and a New conversation action while the network is slow. Explain which guards prevent duplicate requests and stale appends. Propose a repository change that connects coroutine cancellation to OkHttp Call.cancel(), but keep it separate from the tested baseline unless you implement and verify it.
Source and next steps
- Pinned implementation — The exact app and server revision used by this lesson.
Back to roadmap · Practice this unit in the codelab