Android use cases
The examples on this page use Android, Compose, ViewModel, Flow, RecyclerView, or Intent classes. Plain Kotlin Playground does not include those dependencies. Each snippet is therefore labeled and explained as Android-only rather than linked to a program that would fail to compile.
Compose event handlers
Android/Compose-only example:
@Composable
fun ProfileScreen(
state: ProfileUiState,
onRetry: () -> Unit,
onUserClick: (UserId) -> Unit,
) {
if (state.error != null) {
ErrorContent(onRetry = onRetry)
} else {
UserList(users = state.users, onUserClick = onUserClick)
}
}
The composable displays state and reports events through function parameters. It does not need to know whether retrying calls a ViewModel, navigator, fake, or preview action.
Execution when the user taps a user row:
UserListinvokesonUserClickwith aUserId.ProfileScreendoes not decide what navigation means; it forwards the event supplied by its caller.- A production caller may navigate, while a preview or test may record the ID.
✕ UI directly creates its dependency
@Composable
fun ProfileScreen() {
val repository = RealUserRepository()
// UI now owns data construction and is difficult to preview or test.
}
✓ UI receives state and reports events
@Composable
fun ProfileScreen(
state: ProfileUiState,
onRetry: () -> Unit,
) { /* render state */ }
Why this is testable
A test or preview can pass a small lambda and record the event:
var clickedId: UserId? = null
ProfileScreen(
state = sampleState,
onRetry = {},
onUserClick = { id -> clickedId = id },
)
Do not pass every ViewModel function through many UI layers without thinking. When the list becomes difficult to understand, group events into one clear event lambda or interface.
Corner case: callbacks passed to a composable may capture old values during recomposition. For long-lived effects that need the latest callback, Compose APIs such as rememberUpdatedState can be appropriate. That behavior belongs in the advanced Compose chapter; do not solve it by making every callback global or mutable.
ViewModel state transformation
AndroidX ViewModel, coroutines, and Flow example:
val uiState: StateFlow<SearchUiState> = repository.results
.map { result -> result.toUiState() }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SearchUiState.Loading,
)
map is a higher-order function. It receives a conversion lambda and applies it to every repository result. Flow behavior, cancellation, and lifecycle belong to their own chapter; the important point here is that the transformation is passed as a value.
Practical scenario:
- The repository emits data-layer results.
mapconverts each result into a screen-specificSearchUiState.stateInkeeps the latest state in the ViewModel scope.- The UI observes one stable state type instead of understanding repository details.
Corner cases to decide explicitly:
- What initial state appears before the first result?
- What happens when mapping throws?
- Should upstream work continue when the UI is not observing?
- Are equal consecutive states expected to produce another UI update?
RecyclerView click callback
Android RecyclerView-only example:
class UserAdapter(
private val onUserClick: (UserId) -> Unit,
) : ListAdapter<UserUi, UserViewHolder>(UserDiff) {
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
holder.itemView.setOnClickListener {
onUserClick(item.id)
}
}
}
Passing only UserId keeps navigation outside the adapter. The screen decides what clicking means. Also ensure listeners and captured Views do not outlive the ViewHolder or screen.
✕ Adapter performs navigation itself
holder.itemView.setOnClickListener {
holder.itemView.context.startActivity(detailsIntent)
}
The adapter now owns a screen decision and needs Android navigation details.
✓ Adapter reports the item identity
holder.itemView.setOnClickListener {
onUserClick(item.id)
}
The Fragment or screen-level caller decides whether to navigate, show a dialog, or ignore the event.
Corner case: never trust an old adapter position captured earlier. Read or bind the current item appropriately, and account for list updates between binding and clicking.
Inline and reified Activity helper
Android Activity/Intent-only example:
inline fun <reified T : Activity> Context.openActivity(
configure: Intent.() -> Unit = {},
) {
startActivity(Intent(this, T::class.java).apply(configure))
}
context.openActivity<DetailsActivity> {
putExtra("userId", userId.value)
}
reified TprovidesDetailsActivity::class.java.- The receiver lambda configures the
Intent. - The default empty lambda makes configuration optional.
This improves syntax, but string extras are still not type-safe. A typed route model or destination-owned factory may be safer in a large application.
✕ Helper hides a mismatched extra key
putExtra("user_id", userId.value)
// Destination accidentally reads "userId" and receives null.
✓ Destination owns construction and parsing
object DetailsContract {
private const val EXTRA_USER_ID = "userId"
fun intent(context: Context, userId: UserId): Intent =
Intent(context, DetailsActivity::class.java)
.putExtra(EXTRA_USER_ID, userId.value)
}
The reified helper removes class-literal repetition; it does not validate extras, flags, exported-component rules, or whether a suitable Activity exists.
Choosing the right tool
| Situation | Good first choice | Why |
|---|---|---|
| Small synchronous transformation | Ordinary function | Clear and easy to test |
| UI click or event | Lambda parameter | Caller chooses the action |
| Repeated iteration with a changing rule | Higher-order function | Reuses the control flow |
| One asynchronous result | suspend function | Clear cancellation and error handling |
| Stream of values | Flow | Represents several asynchronous values |
| Runtime generic check in a small helper | inline reified | Actual type is available at the call site |
| Java-facing generic API | Explicit Class<T> or type token | Easier Java use and a clear API |
Function parameters reduce direct dependencies, but they do not automatically create good architecture. Keep ownership clear: UI reports events, ViewModels coordinate screen behavior, repositories handle data, and lifecycle-aware scopes own asynchronous work.