Material 3 Components
The compose-material3 module provides ready-to-use Material 3 components that work out of the box with RecorderState and PlayerState.
Add the dependency
implementation(libs.kodio.compose.material3)
implementation("space.kodio:compose-material3:0.1.1")
A styled recording button that shows the current state with appropriate icons and colors:
val recorderState = rememberRecorderState()
RecordAudioButton(
isRecording = recorderState.isRecording,
isProcessing = recorderState.isProcessing,
onClick = { recorderState.toggle() }
)
The button automatically displays:
đī¸ Microphone icon when idle
âšī¸ Stop icon while recording
âŗ Loading indicator while processing
A playback button that adapts to the player's current state:
val playerState = rememberPlayerState(recording)
PlayAudioButton(
isPlaying = playerState.isPlaying,
isPaused = playerState.isPaused,
isReady = playerState.isReady,
isFinished = playerState.isFinished,
onClick = { playerState.toggle() }
)
The button shows:
âļī¸ Play icon when ready or paused
â¸ī¸ Pause icon while playing
đ Replay icon when finished
A button for requesting microphone permission with appropriate messaging:
val recorderState = rememberRecorderState()
if (recorderState.needsPermission) {
AudioPermissionButton(
onClick = { recorderState.requestPermission() }
)
}
ErrorDialog
A Material 3 dialog for displaying audio errors:
recorderState.error?.let { error ->
ErrorDialog(
error = error,
onDismiss = { recorderState.clearError() }
)
}
The dialog automatically formats error messages based on the error type:
PermissionDenied: Suggests enabling microphone access
DeviceNotFound: Indicates no microphone available
NotInitialized: Reminds to call Kodio.initialize() on Android
Complete example
Here's a full recording UI using all Material 3 components:
@Composable
fun MaterialAudioUI() {
val recorderState = rememberRecorderState()
val playerState = rememberPlayerState()
// Load recording into player when available
LaunchedEffect(recorderState.recording) {
recorderState.recording?.let { playerState.load(it) }
}
Column(
modifier = Modifier.fillMaxWidth().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Permission request
if (recorderState.needsPermission) {
AudioPermissionButton(
onClick = { recorderState.requestPermission() }
)
} else {
// Waveform during recording
if (recorderState.isRecording) {
AudioWaveform(
amplitudes = recorderState.liveAmplitudes,
modifier = Modifier.fillMaxWidth().height(80.dp)
)
}
// Record button
RecordAudioButton(
isRecording = recorderState.isRecording,
isProcessing = recorderState.isProcessing,
onClick = { recorderState.toggle() }
)
// Play button (when recording available)
if (recorderState.hasRecording) {
PlayAudioButton(
isPlaying = playerState.isPlaying,
isPaused = playerState.isPaused,
isReady = playerState.isReady,
isFinished = playerState.isFinished,
onClick = { playerState.toggle() }
)
}
}
// Error handling
recorderState.error?.let { error ->
ErrorDialog(
error = error,
onDismiss = { recorderState.clearError() }
)
}
}
}
Component reference
Component | Purpose | Required Props |
|---|
RecordAudioButton
| Recording toggle | isRecording, onClick
|
PlayAudioButton
| Playback toggle | isPlaying, isReady, onClick
|
AudioPermissionButton
| Permission request | onClick
|
ErrorDialog
| Error display | error, onDismiss
|
Last modified: 13 January 2026