David Chung · Eight Sleep · 30-minute Android interview
Pick a problem. Build the feature.
11 independent Android practice problems to build from an empty project, with requirements, sample data, and follow-ups. Study the 18 fundamentals lessons with extra depth on Flow and coroutines, Compose recomposition and drawing, architecture, offline sync, Hilt, and unit/UI tests. Use the 20 recall cards afterward.
Updated October 12, 2026 · Recovered exercises and adaptations are labeled. Exact Eight Sleep technical questions were not verified.
Learn the fundamentals, then rebuild them in your own empty project. Flow and coroutines get the most depth. Compose lessons cover state, recomposition, layout, drawing, stability and performance diagnosis. The examples share one small chat contract so the ViewModel and tests fit together.
Original educational examples synthesized from primary Android and Kotlin documentation checked October 9, 2026. Framework snippets need the listed Android dependencies and your package names. The build-along workshop has separate MVVM and MVI reference projects with the folder structure shown below.
AF01Start an empty project and build one vertical slice
Fundamentals lesson · original examples
Start with Android Studio Empty Activity, Kotlin, Compose and Material 3. Keep one app module for the timed attempt. Create model/, data/ and ui/ packages, then implement sample data, repository, ViewModel, and screen. Make loading, empty, content and failure observable before adding persistence.
The workshop uses these package boundaries in its actual snippets and reference downloads. Use plain String IDs for nights and notes. Keep ViewModels and UI state beside their screen; database entities and network DTOs stay in data.
Need
Dependency or setup
Screen + ViewModel
Use the template Compose setup. Add lifecycle-viewmodel-ktx, lifecycle-runtime-compose, and lifecycle-viewmodel-compose.
Asynchronous work
kotlinx-coroutines-android. Pick a Kotlin-compatible stable version.
HTTP + JSON
Choose Retrofit with a converter or Ktor with serialization. Add INTERNET permission. Map DTOs to domain models.
Local data
Room runtime + room-ktx and its KSP compiler. Use DataStore for preferences.
DI
Hilt Android, its compiler through KSP, and the Hilt Gradle plugin. Use matching Hilt versions.
Unit tests
JUnit 4 + kotlinx-coroutines-test. Match the coroutines-core and test versions.
UI tests
Compose ui-test-junit4 plus androidx.test.ext:junit and test:runner in androidTestImplementation. Use ui-test-manifest in debugImplementation and AndroidJUnitRunner.
Use the current Compose BOM for Compose artifacts. Lifecycle, Room, Hilt and coroutines are versioned separately. Use KSP compatible with the project Kotlin/compiler setup. Sync and run an empty screen first; dependency repair should happen before your timed rehearsal.
30-minute order
First 3 minutes: restate the behavior, identify the state owner, and show the sample model.
Next 15 minutes: finish one usable path through repository, ViewModel and Compose.
Next 7 minutes: handle failure/retry and one meaningful unit test.
Last 5 minutes: run the feature, explain lifecycle/cancellation, and show a UI test or describe the remaining coverage.
For the longer follow-up, add Room, WorkManager and Hilt. Ask what libraries the interview project already provides.
MVVM assigns screen decisions to a ViewModel and persistence/network behavior to a repository. MVI usually makes intents, state transitions and side effects explicit. The terms overlap: a ViewModel can expose immutable state and process intents with a pure reducer. Neither name requires a framework or a new module.
Use the same folder structure for MVVM and MVI. Put screen state and ViewModel code in ui/journal. MVI adds its intent and reducer beside them. Domain models do not import UI controls; repositories, Room entities and API adapters stay in data. The workshop provides separate implementation paths with the same storage and user-visible behavior.
For the workshop, JournalUiState holds everything Compose renders. UserInput groups the query, favorite filter, selected night and draft. LoadState is an enum with Loading, Ready and Failure; the journal data and error message are fields in UiState. Compose uses if checks to show the matching UI. The JournalRepository interface lets us replace memory storage with Room; a single concrete repository is enough when you do not need that swap.
The data path
UI action -> ViewModel or intent handler -> repository write
Room or in-memory state -> Flow -> ViewModel UI state -> Compose
Reducer: previous state + action -> next state
Effect runner: send request -> dispatch result action
Use one screen state with explicit content and operation status. Keep transient input separate from persisted entities. A domain use case is useful when rules are reused; a pass-through use case adds little to a small interview feature.
Choose the state lifetime
remember: composition-local state. rememberSaveable: small saveable UI values across recreation.
ViewModel: survives configuration changes while its owner remains; it does not survive process death.
SavedStateHandle: small draft/query or ID needed to reconstruct a screen after eligible state restoration.
Room: durable records, favorites, pending operations. DataStore: small preferences.
Represent important user feedback as state with an acknowledgement ID. A SharedFlow with no replay can lose it while the screen is stopped.
A small, pure MVI transition
sealed interface InputAction {
data class Changed(val text: String) : InputAction
data object Cleared : InputAction
}
data class InputState(val text: String = "")
fun reduce(state: InputState, action: InputAction): InputState =
when (action) {
is InputAction.Changed -> state.copy(text = action.text)
InputAction.Cleared -> state.copy(text = "")
}
Keep network calls outside the reducer. Serialize result actions if they can race.
AF03Coroutines, suspension and structured concurrency
Fundamentals lesson · original examples
A coroutine is a cancellable computation, not a thread. A suspending call can release its thread while waiting. Marking a function suspend does not move blocking work off Main. Dispatchers.Main is for UI, IO for blocking I/O, and Default for CPU work. A suspend network library may already be main-safe.
launch returns a Job for work without a result. async returns Deferred for a result obtained with await. withContext changes the context for a block and returns its result. coroutineScope waits for children; a child failure normally cancels its siblings. Keep the parent-child relationship and avoid GlobalScope.
Parallel reads with a shared lifetime
suspend fun loadOverview(api: OverviewApi): Overview = coroutineScope {
val profile = async { api.profile() }
val history = async { api.history() }
Overview(profile.await(), history.await())
}
Design sketch: OverviewApi and Overview are your domain contracts. If the reads depend on each other, call them sequentially.
viewModelScope is appropriate for screen-owned work. For work that must survive leaving the screen, select an explicitly owned longer scope or persistent work. A Job is cancellation/lifetime; a dispatcher is execution context. Dispatchers.IO alone is not a durable lifetime.
Senior distinctions
Use withContext on an injected dispatcher around blocking file/database adapters or expensive parsing. Room suspend/Flow adapters already manage their work.
A suspend call does not guarantee cancellation of a blocking SDK call. Use a cancellable adapter or runInterruptible when the API supports thread interruption.
Do not use runBlocking or Thread.sleep on Main. Prefer delay for asynchronous waits.
Never launch a fresh disconnected scope inside a suspending repository method merely to return early.
Cancellation is cooperative. Suspending library calls check it; long CPU loops should periodically call ensureActive or yield. Clean up resources in finally. Catch CancellationException only to rethrow it. A broad runCatching also catches cancellation, so inspect the failure before turning it into a user error.
coroutineScope is appropriate when all child results are required. supervisorScope isolates child failures, but each child still needs explicit handling. A SupervisorJob does not make exceptions disappear. CoroutineExceptionHandler handles uncaught failures for applicable launch jobs; it is not a substitute for recovery around an awaited operation.
Handle an expected failure and preserve cancellation
Illustrative calls. withTimeoutOrNull returns null for the timeout it owns; cancellation from an outer scope still propagates. A UI timeout does not undo a server-side operation.
For replaceable reads, cancel the previous Job or use flatMapLatest. A cancelled client request may still finish on a server. Protect writes with an idempotency key, and protect displayed responses with a request identity when the adapter cannot cancel reliably. A request ID and a server/device revision describe different orderings.
Rehearse races
Search for "s" then "sleep": a late "s" result must not overwrite "sleep".
Tap Send twice before the first coroutine starts: claim the in-flight state synchronously.
Clear a chat while a delayed reply is pending: cancel its owner or reject the reply using a conversation generation.
Device slider: latest requested temperature can differ from acknowledged temperature; preserve both.
AF05Flow fundamentals: cold, hot, StateFlow and SharedFlow
Fundamentals lesson · original examples
Type
What collection means
Use and limitation
flow { ... }
Cold. The builder executes separately for each collector.
A repeatable read/stream; two collectors may create two requests or listeners.
StateFlow
Hot state holder. A new collector gets the current value.
Has an initial value; equality-conflated and slow collectors may skip intermediate states.
SharedFlow
Hot broadcast with configurable replay and buffering.
Each active subscriber receives emissions subject to buffering. No durable storage guarantee.
Channel
Queue with receive semantics, not a Flow state holder.
Multiple receivers compete for elements. Buffering is not an exactly-once or process-death guarantee.
Flow is an interface, so seeing Flow<T> alone does not prove a source is cold. MutableStateFlow can change with no collector. A cold flow does no builder work until collection. A shared stream has one producer lifetime, which its owning scope and sharing policy control.
StateFlow compares values with equals. Publish immutable snapshots with copy; mutating a list inside the existing state can leave observers unaware. MutableStateFlow.update is atomic, but its lambda can run more than once. Keep network calls, counters with external effects, and ID generation outside that lambda.
With MutableSharedFlow(replay = 0), values emitted without subscribers are lost. extraBufferCapacity supports slow active subscribers; it does not store an offline backlog for future subscribers. replay = 1 re-delivers the latest item to new subscribers, which can repeat navigation or a snackbar.
Cold versus hot, predict the counts
var starts = 0
val cold = flow { emit(++starts) }
println(cold.first())
println(cold.first())
val hot = MutableStateFlow(7)
println(hot.first())
println(hot.first())
Run in a coroutine with kotlinx.coroutines.flow imports.
AF06Choose Flow operators by ordering and loss policy
Fundamentals lesson · original examples
Need
Operator
Senior caveat
Transform each value
map
Sequential by default; expensive CPU mapping needs an appropriate dispatcher.
Compose current values
combine
Emits after each source has emitted; uses the latest value from each.
Pair corresponding values
zip
Pairs one-for-one and ends when pairing can no longer continue.
Ignore unchanged values
distinctUntilChanged
Useful after normalization. StateFlow already suppresses equal state values.
Wait until typing pauses
debounce
Limits search starts, but old work may continue during the debounce window.
Replace the previous inner stream
flatMapLatest
Cancels the previous inner collection; suitable for queries or selected IDs.
Replace collector work
collectLatest
Cancels the previous collector block, not arbitrary external side effects.
Serialize inner streams
flatMapConcat
Preserves order; an infinite inner stream prevents later ones from starting.
Concurrent inner streams
flatMapMerge
Interleaves results; bound concurrency and explain why ordering is unnecessary.
Allow producer/consumer overlap
buffer
A bounded queue absorbs bursts; it does not fix sustained consumer overload.
Keep latest pending value
conflate
Drops older values awaiting collection. Fine for progress, unsafe for transaction records.
Move upstream context
flowOn
Changes operators above it, not the final collector or everything in the pipeline.
Recover upstream errors
catch / retryWhen
Placement determines what is caught or restarted. Do not retry permanent failures forever.
Cancel a search immediately, then debounce the next start
@OptIn(ExperimentalCoroutinesApi::class)
fun searchResults(query: Flow<String>, api: SearchApi): Flow<SearchUiState> =
query.map { it.trim() }.distinctUntilChanged().flatMapLatest { text ->
flow<SearchUiState> {
if (text.isBlank()) {
emit(SearchUiState.Empty)
} else {
emit(SearchUiState.Loading(text))
delay(300)
emit(SearchUiState.Content(text, api.search(text)))
}
}.catch { error ->
if (error is IOException) emit(SearchUiState.Error(text))
else throw error
}
}
Design sketch: define SearchApi and the sealed SearchUiState. The delay inside the new inner flow cancels old work before the wait. Loading includes the query so the UI can associate results with their input.
catch handles failures upstream of its position, not exceptions in the terminal collector. Emitting a fallback ends that failing upstream collection; it does not automatically reconnect. Catching inside flatMapLatest lets future queries create new streams. Use bounded retryWhen for recoverable transport failures.
AF07Share once and collect with the Android lifecycle
Fundamentals lesson · original examples
stateIn creates shared state; shareIn creates a shared broadcast. Choose the scope first, then the start policy and replay. Eagerly starts immediately. Lazily starts on the first subscriber and continues until the owning scope ends. WhileSubscribed follows subscribers with configurable stop and replay timeouts.
One state stream for a screen
val uiState: StateFlow<HistoryUiState> = repository.observeHistory()
.map<List<Night>, HistoryUiState> { HistoryUiState.Content(it) }
.catch { error ->
if (error is IOException) emit(HistoryUiState.Error)
else throw error
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
HistoryUiState.Loading
)
Illustrative domain types. Put stateIn in a stable ViewModel property, not in a composable or a getter that creates it on every access.
The 5-second stop timeout delays stopping upstream after the last subscriber leaves. It is not polling, a network timeout, or disk caching. The replay cache is retained by default; replayExpirationMillis = 0 resets stateIn to its initial value after stopping. Collection can restart upstream when a subscriber returns.
Compose and Views collection
// Compose route
val state by viewModel.uiState.collectAsStateWithLifecycle()
// Fragment, use the view lifetime
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.uiState.collect { render(it) } }
launch { viewModel.connection.collect { renderConnection(it) } }
}
}
Two infinite flows need two child collectors. Sequential collect calls never reach the second stream.
An upstream failure in stateIn/shareIn terminates the sharing coroutine and goes to its scope, rather than becoming an exception delivered to subscribers. Put expected recovery before sharing. If the resulting flow completes after catch, define how retry will start new upstream work.
This small chat example connects immutable messages, a repository contract, draft input and send status. It is a teaching slice, not a completed answer to EP01. Add the ping/pong delay, deletion, image handling and clear-history rules yourself. The in-memory fake is not offline storage.
Chat contract and ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.io.IOException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
data class ChatMessage(val id: String, val text: String)
interface ChatRepository {
fun observeMessages(): Flow<List<ChatMessage>>
suspend fun send(text: String)
}
sealed interface SendState {
data object Idle : SendState
data object Sending : SendState
data object Failed : SendState
}
data class ChatUiState(
val messages: List<ChatMessage> = emptyList(),
val draft: String = "",
val send: SendState = SendState.Idle
)
class ChatViewModel(private val repository: ChatRepository) : ViewModel() {
private val draft = MutableStateFlow("")
private val sendState = MutableStateFlow<SendState>(SendState.Idle)
val uiState: StateFlow<ChatUiState> = combine(
repository.observeMessages(), draft, sendState
) { messages, text, status -> ChatUiState(messages, text, status) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ChatUiState())
fun onDraftChanged(text: String) { draft.value = text }
fun onSend() {
val original = draft.value
val text = original.trim()
if (text.isEmpty() || sendState.value == SendState.Sending) return
sendState.value = SendState.Sending
viewModelScope.launch {
try {
repository.send(text)
draft.compareAndSet(original, "")
sendState.value = SendState.Idle
} catch (cancelled: CancellationException) {
throw cancelled
} catch (failed: IOException) {
sendState.value = SendState.Failed
} finally {
if (sendState.value == SendState.Sending) {
sendState.value = SendState.Idle
}
}
}
}
}
Calls to onDraftChanged/onSend come from Main. The repository contract assumes an observable local store that keeps producing; handle storage failures separately. compareAndSet prevents a completed send from clearing a newer draft.
The in-flight guard changes synchronously before launch. This covers two taps even if uiState has not emitted its new value yet. UI disabling is useful feedback, but the state owner still enforces the rule. The coroutine test in AF13 uses this exact contract and ViewModel.
Keep the route responsible for obtaining the ViewModel and collecting it. Pass plain state and callbacks into a stateless screen. Hoist shared state to its common owner. Use stable item IDs, avoid network calls during composition, and model loading/empty/error explicitly.
Call this in a size-constrained Scaffold content area and apply its padding. The route imports lifecycle-compose collectAsStateWithLifecycle and delegates state/events.
LaunchedEffect keys control restarting a composition-owned effect. DisposableEffect is for cleanup of a registration/resource. rememberCoroutineScope supports a UI interaction tied to the composition. Do not create a player or start collection on every recomposition.
Small-feature details
Navigate with an ID rather than a large DTO; reconstruct detail data through the repository. Keep NavController in UI.
Use rememberSaveable for a simple local toggle or SavedStateHandle for a ViewModel-owned query. They are not replacements for a database.
For image selection, use the photo picker and a URI. Do not assume it is a filesystem path; plan URI access/copying for persistence.
Make content descriptions useful and keep touch targets accessible. Assert semantic behavior in UI tests.
Continue with AF17 for recomposition versus layout/redraw and AF18 for remember, derivedStateOf, snapshotFlow, stability and profiling.
For an offline-first feature, the UI observes persisted local data and refresh writes network results into that store. Keep DTOs and database entities inside the data layer. Room fits messages/history/favorites; DataStore fits preferences. A StateFlow cache disappears with process death.
Small persisted chat table and DAO
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Entity(tableName = "messages")
data class MessageEntity(
@PrimaryKey val id: String,
val text: String,
val createdAtMillis: Long,
val pending: Boolean
)
@Dao
interface MessageDao {
@Query("SELECT * FROM messages ORDER BY createdAtMillis, id")
fun observeAll(): Flow<List<MessageEntity>>
@Upsert
suspend fun upsert(messages: List<MessageEntity>)
@Query("SELECT * FROM messages WHERE pending = 1 ORDER BY createdAtMillis, id")
suspend fun pending(): List<MessageEntity>
}
@Database(entities = [MessageEntity::class], version = 1, exportSchema = true)
abstract class ChatDatabase : RoomDatabase() {
abstract fun messages(): MessageDao
}
Teaching schema, not a complete sync implementation. Persist migrations and exported schemas in a production project. Include an account/conversation key before supporting multiple users or conversations.
Offline read exercise
EP05 or EP11: store favorites by stable destination/venue ID.
Persist the favorite, restart the app with network disabled, and confirm it stays selected.
Refresh venue/catalog data while preserving local favorites; keep server data separate from local preference fields.
Distinguish first-load empty from cached empty, and refresh failure from failure to read the database.
Use a transaction for related writes that must become visible together. Avoid destructive migration as a default.
Choose write behavior per operation. A favorite or chat draft can update locally first. A payment or device command may require online acknowledgement. Do not label a queued operation as completed. Pending, acknowledged and failed are meaningful states for EP01 and EP09.
Durable outbox design
Create a stable operation ID once. In one database transaction, write the optimistic local record and an outbox row with payload, account, status and attempt metadata.
Enqueue persistent sync with WorkManager using appropriate constraints and bounded/backoff retry. It is deferred work, not an immediate real-time channel.
Worker selects or atomically claims pending work. Send the same idempotency key on each retry; the backend must support deduplication.
On acknowledgement, transactionally mark the operation complete and merge canonical server IDs/revisions. If the process dies after server success but before local acknowledgement, retry remains safe.
A unique worker name reduces overlapping work; it cannot by itself prevent duplicate server effects. Recover abandoned claims after crashes.
Retry transient connectivity/5xx failures. Re-authenticate before retrying authorization failures. Mark permanent validation failures for user correction.
For conflicts, define which fields the client owns and which the server owns. Use server revisions or explicit conflict responses rather than trusting device wall-clock ordering. Use tombstones or versioned deletes so a refresh cannot resurrect deleted records. Persist the incremental cursor only after successfully committing its batch.
The failure window to explain
1. Client persists operation op-123 and a pending message.
2. Server accepts op-123 and creates message server-456.
3. App dies before marking op-123 complete.
4. Worker retries op-123 after restart.
5. Server returns the original result for the same key.
6. Client merges server-456 and clears pending atomically.
Server idempotency and client reconciliation are required. This is an original design exercise.
Connectivity is a scheduling hint, not proof a request will succeed. If UI observes Room, a worker update naturally reaches the screen through Flow. For a device, queued desired state and confirmed hardware state must remain distinct.
Constructor injection exposes dependencies and makes replacement with fakes straightforward. Hilt generates the Android object graph. Bind an interface to an implementation; provide third-party objects such as a database. Singleton, ActivityRetained and ViewModel scopes describe lifetimes, not where work executes.
Minimal Hilt wiring for the AF08 contract
import android.app.Application
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
import javax.inject.Qualifier
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
@HiltAndroidApp
class PracticeApplication : Application()
@Singleton
class InMemoryChatRepository @Inject constructor() : ChatRepository {
private val messages = MutableStateFlow<List<ChatMessage>>(emptyList())
override fun observeMessages(): Flow<List<ChatMessage>> = messages.asStateFlow()
override suspend fun send(text: String) {
val item = ChatMessage(java.util.UUID.randomUUID().toString(), text)
messages.update { it + item }
}
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun chat(impl: InMemoryChatRepository): ChatRepository
}
@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {
@Provides @IoDispatcher
fun io(): CoroutineDispatcher = Dispatchers.IO
}
Set android:name to .PracticeApplication in the manifest. Add @AndroidEntryPoint to the Activity. Change the AF08 ViewModel to @HiltViewModel class ChatViewModel @Inject constructor(...), with the matching imports. Use the current Android Hilt ViewModel Compose integration in the route. The in-memory implementation is a warm-up; replace the binding with a Room-backed repository for offline practice.
A main-safe data source can inject @IoDispatcher CoroutineDispatcher to move blocking work with withContext. A local unit test can instantiate ChatViewModel(fake) directly. Use Hilt test replacement only when testing the actual DI integration; AF14 tests the stateless screen without the whole graph.
AF13Unit tests for coroutines, Flow and ViewModels
Fundamentals lesson · original examples
Use runTest with one shared TestCoroutineScheduler. StandardTestDispatcher queues work until the test yields or runs the scheduler. advanceTimeBy moves virtual time; runCurrent executes tasks at the new boundary. advanceUntilIdle drains finite queued work. Delays on real IO/Default dispatchers are not controlled by virtual time, so inject dispatchers.
Replace Main before constructing a ViewModel and reset it afterward. stateIn with WhileSubscribed needs an active collector. Put never-ending collectors in backgroundScope so runTest cancels them. Assert final StateFlow values when testing state; use an explicitly collected event list or Turbine when emission order itself is the contract.
src/test/.../ChatViewModelTest.kt, uses AF08
import java.io.IOException
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.*
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
val dispatcher: TestDispatcher = StandardTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) { Dispatchers.setMain(dispatcher) }
override fun finished(description: Description) { Dispatchers.resetMain() }
}
class FakeChatRepository : ChatRepository {
val messages = MutableStateFlow<List<ChatMessage>>(emptyList())
var calls = 0
var fail = false
override fun observeMessages(): Flow<List<ChatMessage>> = messages
override suspend fun send(text: String) {
calls++
delay(1_000)
if (fail) throw IOException("Offline")
val item = ChatMessage("m$calls", text)
messages.update { it + item }
}
}
@OptIn(ExperimentalCoroutinesApi::class)
class ChatViewModelTest {
@get:Rule val main = MainDispatcherRule()
@Test fun blankInputDoesNotSend() = runTest {
val repo = FakeChatRepository()
val vm = ChatViewModel(repo)
vm.onDraftChanged(" ")
vm.onSend()
runCurrent()
assertEquals(0, repo.calls)
}
@Test fun duplicateTapSendsOnceAndPublishesResult() = runTest {
val repo = FakeChatRepository()
val vm = ChatViewModel(repo)
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
vm.uiState.collect {}
}
vm.onDraftChanged(" ping ")
vm.onSend()
vm.onSend()
runCurrent()
assertEquals(1, repo.calls)
assertEquals(SendState.Sending, vm.uiState.value.send)
advanceTimeBy(1_000)
runCurrent()
assertEquals(listOf(ChatMessage("m1", "ping")), vm.uiState.value.messages)
assertEquals("", vm.uiState.value.draft)
assertEquals(SendState.Idle, vm.uiState.value.send)
}
@Test fun failedSendPreservesDraftAndCanBeRetried() = runTest {
val repo = FakeChatRepository().apply { fail = true }
val vm = ChatViewModel(repo)
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
vm.uiState.collect {}
}
vm.onDraftChanged("hello")
vm.onSend()
runCurrent()
advanceTimeBy(1_000)
runCurrent()
assertEquals("hello", vm.uiState.value.draft)
assertEquals(SendState.Failed, vm.uiState.value.send)
assertTrue(vm.uiState.value.messages.isEmpty())
repo.fail = false
vm.onSend()
runCurrent()
advanceTimeBy(1_000)
runCurrent()
assertEquals(listOf(ChatMessage("m2", "hello")), vm.uiState.value.messages)
assertEquals(SendState.Idle, vm.uiState.value.send)
}
@Test fun completedSendDoesNotEraseNewDraft() = runTest {
val repo = FakeChatRepository()
val vm = ChatViewModel(repo)
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
vm.uiState.collect {}
}
vm.onDraftChanged("first")
vm.onSend()
runCurrent()
vm.onDraftChanged("second")
advanceTimeBy(1_000)
runCurrent()
assertEquals("second", vm.uiState.value.draft)
assertEquals(listOf(ChatMessage("m1", "first")), vm.uiState.value.messages)
}
}
These are original teaching tests for the AF08 slice. Run them in your Android project; they are not tests added to the existing downloadable apps.
src/test/.../FlowFundamentalsTest.kt
import org.junit.Assert.*
import org.junit.Test
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
@OptIn(ExperimentalCoroutinesApi::class)
class FlowFundamentalsTest {
@Test fun coldFlowRunsPerCollector() = runTest {
var starts = 0
val stream = flow { emit(++starts) }
assertEquals(1, stream.first())
assertEquals(2, stream.first())
assertEquals(2, starts)
}
@Test fun hotStateUpdatesWithoutCollectors() = runTest {
val state = MutableStateFlow(1)
state.value = 2
assertEquals(2, state.first())
assertEquals(2, state.first())
}
@Test fun broadcastWithoutReplayLosesUnobservedValue() = runTest {
val events = MutableSharedFlow<String>(replay = 0)
events.emit("before")
val seen = mutableListOf<String>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
events.collect { seen += it }
}
events.emit("after")
assertEquals(listOf("after"), seen)
}
@Test fun sharedStateStartsOneUpstreamForTwoCollectors() = runTest {
var starts = 0
val state = flow {
starts++
emit(42)
awaitCancellation()
}.stateIn(backgroundScope, SharingStarted.WhileSubscribed(5_000), 0)
assertEquals(0, state.value)
assertEquals(0, starts)
repeat(2) {
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
state.collect {}
}
}
runCurrent()
assertEquals(42, state.value)
assertEquals(1, starts)
}
@Test fun cancellationRunsCleanupAndPreventsDelayedWork() = runTest {
var cleanedUp = false
var replied = false
val job = launch {
try {
delay(1_000)
replied = true
} finally {
cleanedUp = true
}
}
runCurrent()
job.cancelAndJoin()
advanceTimeBy(1_000)
runCurrent()
assertTrue(cleanedUp)
assertFalse(replied)
}
}
Five outcome-based tests for the stream and cancellation contracts. No Android framework dependency is required.
Add the hard cases in your own attempt
Query race: arrange a slow first result and a faster second result; prove only the current query is shown.
Cancellation: cancel a screen-owned job and prove it does not emit a user-visible failure.
Sharing: two collectors must not start two upstream requests after stateIn/shareIn.
Offline sync: retry the same operation ID after an interrupted acknowledgement and prove one logical server effect.
Algorithm: test empty input, boundaries, repeated visits and simultaneous times for EP07.
Verified here: all nine published ChatViewModelTest and FlowFundamentalsTest methods compiled and passed on the JVM using the cached AndroidX ViewModel 2.8.7, coroutines 1.9.0 and JUnit 4.13.2 libraries. This is not an APK, device or UI test run.
Test what a user sees and can do. Render a stateless screen with explicit state and fake callbacks for component coverage. Add a separate route/integration test with a fake repository to prove ViewModel state reaches the UI. Use semantic text, roles and stable tags; avoid coordinate taps and wall-clock sleeps.
src/androidTest/.../ChatScreenTest.kt, uses AF08 and AF09
These tests intentionally supply fixed state; callback assertions do not prove the whole app updates. Add a route-level test with a fake repository for input -> send -> displayed reply.
Deterministic UI checks
Compose synchronizes its own work. For external asynchronous work use a controlled fake and waitUntil for an observable condition.
mainClock controls Compose frames/animations. It is separate from the kotlinx-coroutines-test scheduler. Do not advance one and assume the other advances.
Verify loading, empty, retry, selection/detail/back, disabled actions, and accessibility semantics.
Use an in-memory Room database for repository integration tests and persistent storage/relaunch coverage for offline requirements.
Run ./gradlew :app:testDebugUnitTest, then ./gradlew :app:connectedDebugAndroidTest with an emulator/device. Adapt the task prefix for build flavors.
The four Compose UI tests are supplied for your practice project. They have not been executed on an emulator in this environment.
AF15Adapters for real-time data, location and media
Fundamentals lesson · original examples
Clean up callback registrations
fun positions(source: PositionSource): Flow<Position> = callbackFlow {
val listener = object : PositionListener {
override fun onPosition(value: Position) {
// Position is replaceable snapshot data.
trySend(value)
}
override fun onFailure(error: Throwable) { close(error) }
}
source.register(listener)
awaitClose { source.unregister(listener) }
}.buffer(Channel.CONFLATED)
Design sketch with domain-specific PositionSource, PositionListener and Position. Import kotlinx.coroutines.channels.awaitClose and Channel plus flow APIs. Registration/unregistration must obey the SDK thread and concurrency contract. Conflation is deliberate for positions; use an explicit overflow/recovery policy for non-droppable messages.
callbackFlow is cold, so each collector registers its own listener unless you share the stream. awaitClose ties cleanup to cancellation. Handle trySend failure according to the data contract. For reliable messages, persist stable IDs and detect gaps; reconnect with a cursor and bounded backoff instead of assuming the socket delivered everything.
Problem-specific adapters
EP10 video: one active player policy, release/stop at the owner lifecycle, track visible item by stable ID, pause offscreen media. Media3 is a useful implementation option.
EP11 venues: request only the needed location permission, handle denied/approximate location, adapt the location source to Flow, cancel old venue queries. Do not prompt from recomposition.
EP06 files: use URI/storage access contracts, map loading and permission failure, keep debug/release or flavor configuration explicit.
EP01 images: use the picker contract, keep URI ownership clear, and store accessible image data when the feature must survive restart.
EP09 hardware: separate desired, sent and acknowledged values. Sequence commands and reject stale replies according to the device revision contract.
The goal is to finish a small feature and explain its failure behavior. Use this guide for the shared building blocks, then read the specific brief for algorithm, media, location or device rules. These lessons are a preparation plan, not a guarantee of the actual interview content.
Before calling an attempt complete
State: one clear owner, immutable snapshots, explicit loading/empty/error/pending behavior.
Recomposition reruns affected composable code to update the UI description. Layout measures and places nodes. Drawing renders their content. A state change can invalidate just one phase. Recomposition does not mean the whole screen was measured or redrawn, and a redraw can happen without recomposition.
Where state is read
What can be invalidated
Example
Composition
The reading restart scope, with layout/drawing if its output changes.
Text(text = draft.value)
Measurement
The affected measurement work and dependent later work.
A custom layout reads a size preference while measuring.
Placement
Affected placement work; measurement can sometimes remain unchanged.
Modifier.offset { IntOffset(position.value, 0) }
Drawing
Affected drawing work, without requiring composition or layout for that read.
Modifier.drawBehind { drawRect(tint.value) }
The point where value is read matters. Passing a State<T> or a provider lambda to a child can defer the read. Evaluating value in the parent first still subscribes that parent scope. A lambda only helps when the API invokes it in the later phase you intend.
positionPx is pixels. These examples deliberately avoid reading either value in composition. Supply remembered snapshot state and put the marker in a visible, size-constrained parent. They illustrate phase invalidation; device rendering has not been measured here.
This caches a pure calculation for these inputs. For a large list, do filtering in the ViewModel on the appropriate dispatcher. It does not move work to another thread.
Use a derived threshold rather than every scroll position
val listState = rememberLazyListState()
val showJumpButton by remember(listState) {
derivedStateOf { listState.firstVisibleItemIndex >= 3 }
}
Requires Compose runtime getValue/remember/derivedStateOf and foundation lazy rememberLazyListState imports. Use listState on the actual LazyColumn. The boolean changes less often than the underlying position.
Observe a UI threshold with a cancellable effect
val latestOnPastIntro by rememberUpdatedState(onPastIntro)
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex >= 3 }
.filter { it }
.collect { latestOnPastIntro() }
}
Imports Compose runtime snapshotFlow, LaunchedEffect and rememberUpdatedState plus Flow filter/collect. onPastIntro is your callback. This reports each transition into the threshold while the effect is alive; it is not durable analytics. Use take(1) for a once-per-effect rule.
Stability affects whether unchanged parameters can allow skipping. Strong skipping is enabled by default with Kotlin 2.0.20 and later. Restartable composables can then be skipped even with unstable parameters. Stable parameters use equals; unstable ones use identity comparison. State invalidation can still require a rerun.
A val List<T> is read-only through that reference, not necessarily an immutable object. In-place mutation may be invisible to state observation. Publish new snapshots or use observable snapshot collections appropriately. @Stable and @Immutable promise contracts to the compiler; adding them does not repair mutable data. Verify your compiler configuration instead of applying old rules about lambdas or unstable inputs blindly.
Lazy lists and effect identity
Use a stable unique key so remembered item state follows its item when rows move. contentType can help lazy-layout reuse for different row shapes; it serves a different purpose from identity.
Store derived values with the right keys. A remembered sort with no items key can retain old data.
Use LaunchedEffect(itemId) when selecting a new item should restart its work. Use DisposableEffect for listener or player cleanup.
Keep expensive work and state writes out of ordinary composition. Composition can be repeated or abandoned; a println counter in the body is not a reliable frame metric.
Diagnose before optimizing
Use Layout Inspector recomposition/skip counters to locate active areas. It shows composition activity, not a complete count of drawing or GPU work.
Inspect Compose compiler stability/skippability reports when parameter behavior is unclear.
Reproduce the actual interaction and use system tracing or profiler tools to identify main-thread, layout, drawing, allocation or media work.
Assess performance on a representative device with a release or appropriate benchmark configuration; debug overhead can mislead.
Change one suspected cause, then repeat the same interaction. A lower recomposition count alone does not prove a smoother app.
Hands-on Compose experiment
Add a marker beside your sleep list. Use snapshot state to change its text, position and tint separately.
First read the position in the composable body, then move the read into Modifier.offset { ... }. Observe which composition scopes restart.
Move the tint read into drawBehind and compare the observed phase work.
Type in search while inspecting list rows. Confirm stable item keys preserve per-row state during filtering/reordering.
Explain the result using a state-read location and actual trace evidence, without promising a fixed recomposition count.
Pick one problem and create a fresh Android Studio Empty Activity project with Kotlin and Compose. Implement your own models, state holder, repository, and UI from the brief. Use the supplied data or linked data source.
Saved prompts, reconstructed code, and guide-based adaptations are labeled separately. These briefs are not claimed to be Eight Sleep questions. No original private documents or repository files are published here.
Lyft · 30-minute core; longer app practice can take 60 to 90 minutes
Adapted from the Android guide. The saved 2022 Lyft guide describes a mini app followed by feature additions. This ride board is an original practice specification.
Build from an empty project
Create a ride request app. Parse the sample data, show the available rides, and let the user accept a requested ride locally.
Core requirements
Show each passenger, pickup, destination, ETA, and status.
Show Accept only for requested rides.
Tapping Accept changes that ride to accepted without changing other rows.
Use stable ride IDs. Make the list scrollable and show a message when it is empty.
EP06Build a file browser and configure build variants
Dropbox · 30-minute app; 60-minute app and build-variants round
Adapted from screening questions. The saved Dropbox document has eight build-tool screening questions. Its debugging preparation project omits the actual task source.
Build from an empty project
Create a file-list app with a detail screen. Then use it to practice the Gradle and build-variant topics from the Dropbox questions.
Core requirements
Show file name, type, and size from the sample data.
Open a file detail and provide Back navigation.
Build and run the debug app.
For the longer round, add demo and production flavors with distinct application IDs. Show the active flavor in the app.
EP09Build device temperature control from an empty project
Eight Sleep preparation · 30-minute basic flow; 60-minute full race and reconnect behavior
Original role-focused practice. Original simulator exercise. Levels and revisions are teaching contracts, not Pod specifications.
Build from an empty project
Create a device-control screen with a level slider and an Apply button. Implement the state model and simulated repository yourself.
Core requirements
Levels range from 0 to 10. Keep desired, pending, and confirmed values distinct.
Selecting a level changes only the desired value. Apply starts a request.
Confirm a new device value only when its revision is newer. Clear pending only for the matching request ID.
Preserve the desired and confirmed values on failure and allow retry.
Offline Apply sends no command. Reconnect refreshes device status and does not automatically apply the draft.
Simulator contract
Initial: desired=5, confirmed=5, revision=0, connected=true.
A request has id and level. A completion has id, level, and deviceRevision.
You control the demo delays, connection, and failure mode.
An old request can report newer device truth. Request ID and device revision have different roles.
Run the feature
Select 8 and see desired 8 while confirmed remains 5.
Apply and show pending before confirmation.
Complete two requests out of order. Preserve newer intent and use revisions for confirmed truth.
Disconnect and confirm that Apply sends nothing.
Follow-ups after the core
Explain cancellation when a physical command may already have been applied.
Save only the draft through process recreation and reconcile device truth after reconnect.
Hints, open after your attempt
Model actions and state before wiring the slider.
Compare device revision separately from the identity of the latest UI request.
Wolt · 30-minute first slice; complete the full task in a longer practice session
Published hiring exercise. Published Wolt 2025 Android internship assignment. Its historical service availability is unconfirmed.
Build from an empty project
Build a venue list that updates as a simulated user location changes.
Core requirements
Show up to 15 nearby venues.
Advance through the supplied coordinates every ten seconds and loop.
Refresh the list with a smooth transition.
Persist favorites by venue ID, including when a venue reappears.
Data and fallback
Use the API, coordinates, response fields, and assets in the linked Wolt README. If the historical API is unavailable, create two local venue responses in your own project and alternate them. Keep the same IDs for shared venues.
Run the feature
See the venue list change when the coordinate advances.
Favorite a venue and confirm that its state persists when it reappears and after restart.
Follow-ups after the core
Use 30 minutes to finish the list and favorite action, then add timed updates.
Explain cancellation and stale results when location requests overlap.
You confirmed a 30-minute interview that involves creating a small app feature. The exact feature, starting project, and allowed libraries are unconfirmed. The schedule below is for rehearsal.
Six preparation priorities
UI state and ownership
Keep loading, data, and error state in the ViewModel. Describe the repository boundary before you start.
Give a short background and the Android work you want to discuss.
3–9 min
One feature
Explain Driver AI, your scope, a technical decision, and one follow-up.
9–20 min
Core concepts
Practice architecture/state, Flow/coroutines, lifecycle, and Compose effects. Keep the first answer short.
20–26 min
Failure and validation
Explain one race or production debugging example and how you would test it.
26–30 min
Questions and closing
Ask one or two questions about Android ownership and app/device reliability.
30-minute rehearsal: build a small app feature
Time
Focus
Rehearsal task
0 to 3 min
Clarify
Clarify the selected brief, required behavior, and scope for this attempt.
3 to 6 min
Model
Describe your models, UI state, and repository boundary before coding.
6 to 21 min
Implement
Create your models, state holder, and screen. Finish the core before follow-ups.
21 to 26 min
Run
Run the app and inspect the behavior listed in the brief. Explain unfinished requirements.
26 to 30 min
Explain
Explain state ownership, request handling, and one next improvement. Ask a question.
Timed practice problem
Choose Quora chat, the Robinhood catalog, or sleep history from the problem bank. Build the stated 30-minute core yourself, starting from an empty Kotlin and Compose project.
Implement the visible feature described in your chosen brief.
Handle user actions and the relevant loading or error states.
Run the feature and confirm its stated behavior.
Explain what you finished, one tradeoff, and what remains.
Original practice for a 30-minute app-feature interview. This is not Eight Sleep's actual assessment. The dates and sleep durations are sample data.
Complete SleepHistoryViewModel.kt and SleepHistoryScreen.kt in the new starter. Load recent nights through the supplied repository, show a dated list with durations, and handle loading, empty results, and error with retry.
The Activity, lifecycle collection, Gradle setup, models, and repository are supplied. Your work is the ViewModel loading function and the Compose screen.
Show a loading indicator, then rows with dates and formatted durations.
Use stable night IDs in the list.
Show an empty message when no nights return.
On failure, show an error and Retry. Clear the error when retrying and avoid duplicate active loads.
Open and run the project
Unzip the starter, open it in Android Studio, and sync with JDK 17 and SDK 35. Run it on an API 26 or newer emulator or phone before timing your attempt. No account, real Pod, or API key is needed.
The repository delays each call for 600 milliseconds. In MainActivity.kt, choose SampleResponse.DATA, ERROR, or EMPTY and restart the app. ERROR and EMPTY affect the first load; the next call returns data.
./gradlew :app:assembleDebug
The model and sample repository compiled with the local Kotlin compiler and cached coroutines. Full APK build and emulator execution remain unverified because Gradle startup is blocked by this session. No new tests were added.
What to say while coding
F01Clarify before coding
Say this
Before I start, can I confirm what is supplied and what you want me to finish? Is the repository already available? I'm planning to cover the list, loading, empty state, and an error with retry. Is that the right scope?
Answer aloud first, then show the script to compare.
F02Explain your plan
Say this
I'll keep the screen state in the ViewModel and expose it as StateFlow. The repository will provide the nights, and Compose will render the current state. I'll load once when the ViewModel starts and load again on retry, so recomposition won't start another request.
Answer aloud first, then show the script to compare.
F03Describe the implementation order
Say this
I'll get the successful load and list working first. Then I'll add loading feedback, failure with retry, and the empty result. I'll use the night ID as the list key and keep duration formatting in the row.
Answer aloud first, then show the script to compare.
F04Close with the tradeoff
Say this
The ViewModel owns the request and the UI state. The screen renders that state and sends the retry action back. I prevent another load while one is active. Rotation keeps the ViewModel, while process recreation reloads the data. If overlapping refreshes were required, I'd define which result should win and handle stale completions explicitly.
Answer aloud first, then show the script to compare.
Questions to explain afterward
Why StateFlow here?
The screen needs the latest loading/data/error state when it starts collecting. StateFlow represents that ongoing state. SharedFlow is not needed for this state model.
Why not fetch directly in the composable body?
The body can recompose. Keep loading in the ViewModel and use a controlled entry or user action to start work.
What happens when Retry is tapped twice?
This reference allows one load at a time. A second call while the Job is active does nothing. Explain a different policy if the product needs overlapping refreshes.
What survives rotation or process death?
The ViewModel survives an Activity configuration change. A new process creates a new ViewModel and fetches again. Persistence would need a separate requirement.
Official employer listing indexed in search; its direct page is JavaScript-only in this environment. Requirements were checked from the official indexed posting on October 7. Interview questions below are original practice, not an asserted Eight Sleep question bank.
Role emphasis
Your practice
Native Android and Kotlin
Explain lifecycle, state ownership, concurrency, and one shipped client feature.
Compose and custom UI
Build a state-driven screen; discuss effects, accessibility, and performance.
Coroutines, Flow, MVVM/MVI
Show a ViewModel/repository boundary and deterministic state transitions.
Testable apps and quality
Exercise delayed results, errors, retry, rotation, and stale state.
Collaboration with firmware/backend/ML
Explain contracts and distinguish a UI request from authoritative device state.
Hardware/health experience is a bonus
Use real hardware debugging as transferable experience and learn the actual device protocol.
I separate UI, state and decisions, and data access. A screen-level ViewModel exposes immutable UI state and accepts actions. A repository owns access to device or network data. MVVM describes those responsibilities; an MVI-style reducer can make transitions explicit. I add layers when they clarify ownership or reuse.
Answer aloud first, then show the script to compare.
I model the states the user can actually encounter, using immutable data or sealed alternatives. A device control can have a desired value, confirmed target, connection status, pending request, and an error. I expose read-only StateFlow and update state deliberately so the UI can render a coherent snapshot.
Answer aloud first, then show the script to compare.
Remember: One coherent state; explicit pending; immutable updates.
Watch the distinction: An optimistic desired value is not a confirmed device target.
StateFlow represents a current value and gives new collectors that value; equal updates are conflated. SharedFlow is a configurable broadcast stream, with replay and buffering choices. A cold flow runs its producer when collected. I choose based on state ownership and delivery needs, and model important UI outcomes as state rather than assuming a transient event will be received.
Answer aloud first, then show the script to compare.
Remember: Current state; broadcast stream; producer per collection.
Watch the distinction: A SharedFlow configuration does not automatically guarantee delivery across stopped collectors.
The scope defines the lifetime and parent-child relationship of work. viewModelScope is useful for screen business work that should end when the ViewModel is cleared. Dispatchers address execution: blocking I/O and CPU work have different needs. A suspend function is not automatically background work; I make blocking operations main-safe at the appropriate boundary.
Answer aloud first, then show the script to compare.
A05What does cancelling a request actually guarantee?
Coroutines · original practice question
Explain aloud
Cancellation is cooperative. I preserve CancellationException and clean up owned resources. Cancelling a local caller does not prove a remote side effect was cancelled. For a control flow, I keep request identity separate from authoritative device state and reconcile after uncertainty. In the exercise, request IDs manage pending UI work and device revisions determine current confirmed truth.
Answer aloud first, then show the script to compare.
I use collectAsStateWithLifecycle for Android UI state. For lifecycle-aware collection outside Compose, repeatOnLifecycle creates and cancels the collection at the chosen lifecycle boundary. Upstream lifetime is a separate decision: a hot stream or ViewModel task can keep running after the UI stops collecting, so I inspect the sharing policy and the operation’s owner.
Answer aloud first, then show the script to compare.
A07remember, rememberSaveable, or SavedStateHandle?
Compose · original practice question
Explain aloud
remember retains a value across recomposition while its composition entry remains. rememberSaveable can restore supported small UI values through saved state. SavedStateHandle helps a ViewModel restore small input state after system recreation. Durable data belongs in persistent storage. I save a draft or identifier, then refresh authoritative device or server state.
Answer aloud first, then show the script to compare.
LaunchedEffect runs coroutine work tied to its composition lifetime and restarts when its keys change. DisposableEffect is useful for registrations that need cleanup. rememberUpdatedState supplies an updated value without restarting an effect solely for that value. I choose keys that match the operation’s identity and keep I/O out of the composable body.
Answer aloud first, then show the script to compare.
A09How do you investigate excessive recomposition or jank?
Compose · original practice question
Explain aloud
I first reproduce the user-visible problem and measure it. Then I inspect changing state, expensive work, item identity, and the work performed during composition, layout, or drawing. Stable item keys and appropriate state reads can help. I use performance tooling to verify a change rather than assuming every recomposition is a defect.
Answer aloud first, then show the script to compare.
Remember: Measure; inspect state and work; verify.
Watch the distinction: Do not claim a performance improvement from a recomposition counter alone.
A10What should happen after rotation or process recreation?
Lifecycle · original practice question
Explain aloud
Rotation can preserve a ViewModel while recreating the UI. Process recreation requires saved inputs and persistent data to restore a useful screen. I restore the local draft, reconnect or reload, and make freshness visible. A cached device target remains last-known state until current authoritative information arrives.
Answer aloud first, then show the script to compare.
Remember: Restore inputs; refresh truth; show freshness.
Watch the distinction: Never treat a restored UI value as proof that a device currently has that value.
A11How would you prevent duplicate streamed chat messages?
Networking · original practice question
Explain aloud
In a proposed design, I use message identity for message upserts and event or sequence identity for incremental updates. I distinguish partial and final content and define ordering, replay, and reconnection behavior. Identical text is not enough to identify a duplicate. I would test replay, retries, out-of-order delivery, and finalization against the actual protocol.
Answer aloud first, then show the script to compare.
Remember: Identity; ordering; partial versus final.
Watch the distinction: These are design options, not the recovered root cause of your P000 fix.
A12How would you explain client transcription versus streaming?
Networking · original practice question
Explain aloud
Your initial Driver AI flow was speech-to-text on the client, transcript to backend and LLM, then text returned to the client. For a proposed streaming design, I would clarify which stage streams, what incremental feedback users gain, and the resource, timing, cancellation, and reconnect tradeoffs. I would compare experience and correctness alongside implementation cost.
Answer aloud first, then show the script to compare.
Remember: Name the stage; experience; resources and timing.
Watch the distinction: The exact streaming stage and the 40% usage measurement remain unconfirmed.
A13How do desired and confirmed control state differ?
Device · original practice question
Explain aloud
Desired state represents what the user requested. Confirmed state represents what the system reports as applied; an acknowledged request can still be pending. An applied target is also different from a sensed physical temperature. I model those meanings separately, expose uncertainty and failure, and keep the UI aligned with authoritative state.
Answer aloud first, then show the script to compare.
A14How would you design cloud and local device control?
Device · original practice question
Explain aloud
As a proposed design, I separate the user action and state model from transport and protocol details. I define how local and cloud state reconcile, which controls remain available offline, and how the UI explains limitations. Eight Sleep’s Backup Mode write-up describes local Bluetooth control during outages and deferred data processing; I would ask about today’s actual Android boundaries.
Answer aloud first, then show the script to compare.
Remember: Common state model; transport boundary; reconciliation.
Watch the distinction: Do not present the historical rollout or a proposed transport abstraction as their current internal architecture.
A15What Android Bluetooth permissions would you check?
Device · original practice question
Explain aloud
For Android 12 and above, I distinguish scanning, communication with paired devices, and advertising, and request the relevant Nearby devices runtime permissions. Older versions have different permission and location rules. I handle denial and unavailable Bluetooth in the UI and request only the capabilities required by the actual flow.
Answer aloud first, then show the script to compare.
Remember: Version; capability; denial state.
Watch the distinction: Bluetooth access and device authentication are different concerns.
I define reads and writes separately. Local persistent data can support offline reads with explicit freshness. Durable synchronization may fit WorkManager, but a live device control has different timing and authorization constraints. For a temperature draft, I would avoid blindly replaying an old action on reconnect and first establish current state and user intent.
Answer aloud first, then show the script to compare.
Remember: Read cache; write policy; freshness.
Watch the distinction: Offline-first does not mean every command should be replayed later.
I use runTest with test dispatchers sharing a scheduler so I can control virtual time and execution order. I inject boundaries and assert state and effects across delayed success, error, cancellation, and overlap. The supplied reducer harness covers deterministic transitions; coroutine integration tests would separately verify scheduling and collection behavior.
Answer aloud first, then show the script to compare.
I test what the user can observe and do using semantics: desired versus confirmed labels, pending state, disabled actions offline, errors, and retry. I test interactions and resulting state with appropriate synchronization. Smaller state and repository tests cover logic, while device tests cover the hardware and OS boundary.
Answer aloud first, then show the script to compare.
I reproduce with a representative device and build, then use traces and diagnostics to locate blocking, contention, CPU work, or an expensive render path. I keep blocking work off the main thread at the correct boundary and verify the user journey after the change. I distinguish a responsiveness problem, crash, and network latency before choosing the fix.
Answer aloud first, then show the script to compare.
Remember: Reproduce; trace; isolate; verify.
Watch the distinction: Moving everything to an I/O dispatcher is not a diagnosis.
A20How do you make a control screen accessible and trustworthy?
Product · original practice question
Explain aloud
I use understandable labels, semantic state, accessible control targets, and clear pending, error, and offline feedback. Non-text controls need meaningful descriptions; text needs to work with scaling and assistive navigation. For a device control, I make the difference between a draft and confirmed state visible so the user can understand what happened.
Answer aloud first, then show the script to compare.
Extended practice: reliable bed-temperature control
Original role-inspired exercise · simulated device · 45–60 minutes for the core + Android integration
Implement the reducer in the starter Android project. Keep the selected draft separate from the confirmed device target. Handle overlapping requests, old responses, current errors, disconnect, and explicit retry. A Compose UI, ViewModel, and fake repository are supplied.
Levels 0–10 and monotonic revisions are fictional exercise contracts, not Pod specifications.
Select changes desired state; it does not change confirmed state or send a command.
Submit records the latest pending request and emits a Send effect; offline submit emits no command.
Authoritative results update confirmed state only when their device revision is newer.
Request identity controls which pending operation completes or errors; it does not define authoritative device truth.
A current error preserves desired and confirmed values and permits retry.
Disconnect clears pending UI work; reconnect refreshes status and does not automatically replay a draft.
An older request can still carry newer authoritative device truth. Cancellation of a caller does not prove a remote command was undone.
Score your attempt
Area
Points
What to demonstrate
State correctness
30
Draft, pending, and confirmed target have distinct meanings.
Races and failures
30
Old results/errors cannot erase newer intent; newer device truth is preserved.
Android integration
20
Lifecycle-aware state collection, scoped work, and appropriate persistence.
UI and explanation
20
Clear feedback, usable controls, and a concise explanation of tradeoffs.
Senior-level extensions
Save the desired draft through process recreation, then refresh authoritative device status.
Inject a repository interface and add coroutine tests with one virtual-time scheduler.
Add Compose semantics tests for pending, offline, error, and retry states.
Explain a reconciliation approach for a device protocol without monotonic revisions.
Read the state and action contract
package dev.dchung.bedcontrol.domain
// Fictional exercise protocol: levels 0..10, monotonic device revisions.
data class Pending(val id: Long, val level: Int)
data class ControlState(
val desired: Int = 5,
val confirmed: Int = 5,
val connected: Boolean = true,
val revision: Long = 0,
val pending: Pending? = null,
val error: String? = null,
)
sealed interface Action {
data class Select(val level: Int) : Action
data class Submit(val id: Long) : Action
data class Applied(val id: Long, val level: Int, val revision: Long) : Action
data class Failed(val id: Long, val reason: String) : Action
data class Connection(val connected: Boolean) : Action
data class Snapshot(val level: Int, val revision: Long) : Action
}
sealed interface Effect {
data class Send(val id: Long, val level: Int) : Effect
}
data class Transition(val state: ControlState, val effects: List<Effect> = emptyList())
Reveal the reference reducer after your attempt
package dev.dchung.bedcontrol.domain
fun reduce(state: ControlState, action: Action): Transition = when (action) {
is Action.Select -> if (action.level in 0..10) {
Transition(state.copy(desired = action.level, error = null))
} else {
Transition(state.copy(error = "Choose a level from 0 to 10"))
}
is Action.Submit -> when {
!state.connected -> Transition(state.copy(error = "Offline: reconnect before applying"))
state.pending == null && state.desired == state.confirmed -> Transition(state)
else -> Transition(
state.copy(pending = Pending(action.id, state.desired), error = null),
listOf(Effect.Send(action.id, state.desired)),
)
}
is Action.Applied -> {
// Request identity controls UI completion; device revision controls device truth.
val actual = if (action.revision > state.revision) {
state.copy(confirmed = action.level, revision = action.revision)
} else state
Transition(if (state.pending?.id == action.id) {
actual.copy(pending = null, error = null)
} else actual)
}
is Action.Failed -> Transition(if (state.pending?.id == action.id) {
state.copy(pending = null, error = action.reason)
} else state)
is Action.Connection -> Transition(state.copy(
connected = action.connected,
pending = if (action.connected) state.pending else null,
error = if (action.connected) null else "Disconnected: device status may be stale",
))
is Action.Snapshot -> Transition(if (action.revision > state.revision) {
state.copy(confirmed = action.level, revision = action.revision)
} else state)
}
Compare request identity with device revision. An older request may still report newer device truth. This simulator’s revision contract is a teaching choice.
Run the earlier device example
For a fresh attempt, use Empty project setup. These steps apply to the earlier downloadable device project.
Download and unzip the starter. Open its project folder in Android Studio.
Use JDK 17 and install SDK Platform 35 / Build Tools 35.0.0. Gradle 8.13 and AGP 8.12.0 are pinned, with Kotlin/Compose compiler 2.1.20.
Let Gradle sync in your normal development environment. Dependencies may need downloading.
Run the app on an API 26+ phone or emulator. It uses a simulated repository and requires no account or API key.
Implement domain/Reducer.kt. Run bash run-core-tests.sh with the Kotlin CLI compiler to check the core independently of Android.
Try quick overlapping Apply actions, error/retry, disconnect, and rotation. Add process-restoration behavior as a stretch.
A published company exercise: build a vertical video feed and profile screen from supplied JSON. Kotlin is preferred for Android; the README suggests 2–3 hours and asks for documented decisions.
Useful for lifecycle, state ownership, media, navigation, and explaining tradeoffs. This is Streamlabs’ exercise, not Eight Sleep’s.
A published Kotlin task: show nearby venues, change the simulated location every ten seconds, animate the list, and persist favorites.
Use the 30-minute list and favorites version first. Add timed refresh and stale-request handling in a longer session. Historical API availability is unconfirmed.
An existing hands-on exercise for state, hoisting, and state holders.
Use before the custom device-control exercise if you want to refresh Compose fundamentals.
Sources and scope
Each recall card links to relevant primary documentation. The topic selection and proposed designs are a synthesis for this role. Your duplicate-message fix’s root cause is still unconfirmed; the design options in A11 are not claims about that fix.