Build Your Own Voice Notes App in Android with Jetpack Compose
What will you learn?
Let’s dive in step by step.
π ️ Step 1: Set Up Permissions
<uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />π¨ Step 2: Designing the UI with Compose
- A Live Transcript section to show the recognized speech.
- A Saved Notes section that lists everything we’ve stored.
- A row of buttons at the bottom: Start Listening, Stop, Save, and Delete All.
π€ Step 3: Adding Speech Recognition
- You create a SpeechRecognizer instance.
- You configure an Intent with RecognizerIntent.ACTION_RECOGNIZE_SPEECH.
- You set a RecognitionListener to receive results (final and partial).
- Start listening, and the recognizer will return text once you stop speaking.
- This gives us the live transcript we’ll display in the app.
πΎ Step 4: Saving Notes
π Full Code Example
package com.codingbihar.voicenotesapp
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Environment
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import java.io.File
import java.util.Locale
@Composable
fun VoiceNoteApp() {
val context = LocalContext.current
// Launcher for microphone permission
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { /* no-op */ }
// Ask permission when screen appears
LaunchedEffect(Unit) {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
}
Box(Modifier.fillMaxSize()) {
VoiceNotesScreen()
}
}
@Composable
fun VoiceNotesScreen() {
val context = LocalContext.current
var notesList by remember { mutableStateOf(readFromFile().lines().filter { it.isNotBlank() }) }
var isRecording by remember { mutableStateOf(false) }
var transcript by remember { mutableStateOf("") }
// recognizer
val speechRecognizer = remember {
SpeechRecognizer.createSpeechRecognizer(context)
}
val recognizerIntent = remember {
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
}
}
DisposableEffect(Unit) {
val listener = object : RecognitionListener {
override fun onReadyForSpeech(params: Bundle?) {
isRecording = true
}
override fun onEndOfSpeech() {
isRecording = false
// restart for continuous listening
speechRecognizer.startListening(recognizerIntent)
}
override fun onError(error: Int) {
isRecording = false
// restart after error
speechRecognizer.startListening(recognizerIntent)
}
override fun onResults(results: Bundle?) {
val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
if (!matches.isNullOrEmpty()) {
// instead of replacing, append
transcript += " " + matches[0]
}
// restart listening
speechRecognizer.startListening(recognizerIntent)
}
override fun onPartialResults(partialResults: Bundle?) {
val matches =
partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
if (!matches.isNullOrEmpty()) {
// show live words while speaking (but don’t save)
// temporary live text can be displayed separately if needed
transcript = transcript.trimEnd() + " " + matches[0]
}
}
override fun onBeginningOfSpeech() {}
override fun onBufferReceived(buffer: ByteArray?) {}
override fun onEvent(eventType: Int, params: Bundle?) {}
override fun onRmsChanged(rmsdB: Float) {}
}
speechRecognizer.setRecognitionListener(listener)
// start immediately
speechRecognizer.startListening(recognizerIntent)
onDispose {
speechRecognizer.destroy()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.padding(16.dp)
) {
Text(
text = "π€ Listening...",
color = Color.Red,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(8.dp)
)
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
) {
OutlinedTextField(
value = transcript,
onValueChange = { transcript = it },
label = { Text("Transcript") },
textStyle = LocalTextStyle.current.copy(fontSize = 18.sp),
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
)
}
Spacer(Modifier.height(12.dp))
Row {
Button(onClick = {
if (transcript.isNotBlank()) {
appendToFile(transcript)
notesList = readFromFile().lines().filter { it.isNotBlank() }
transcript = ""
Toast.makeText(context, "Note saved!", Toast.LENGTH_SHORT).show()
}
}) {
Text("Save Note")
}
Spacer(Modifier.width(8.dp))
Button(onClick = { transcript = "" }) {
Text("Clear")
}
}
Spacer(Modifier.height(16.dp))
Text("Saved Notes:", fontWeight = FontWeight.Bold)
LazyColumn(
modifier = Modifier
.height(200.dp)
.padding(top = 8.dp)
) {
items(notesList) { note ->
Text("• $note")
}
}
Spacer(Modifier.height(16.dp))
Button(onClick = {
val folder = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"VoiceNotes"
)
val file = File(folder, "voice_notes.txt")
if (file.exists()) file.delete()
notesList = emptyList()
}) {
Text("Delete All")
}
}
}
}File helpers for saving notes:
// save to custom folder (Downloads/VoiceNotes)
fun appendToFile(text: String) {
val folder = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"VoiceNotes"
)
if (!folder.exists()) folder.mkdirs()
val file = File(folder, "voice_notes.txt")
file.appendText("$text\n")
}
// read from custom folder
fun readFromFile(): String {
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"VoiceNotes/voice_notes.txt"
)
return if (file.exists()) file.readText() else ""
}π Enhancements You Can Try
- List UI: Instead of showing notes as plain text, use LazyColumn so each note appears in its own card.
- Timestamps: Add a date and time to every note when saving.
- Share Option: Let users share notes via WhatsApp, Gmail, or Google Drive.
- Multi-language Support: By setting RecognizerIntent.EXTRA_LANGUAGE, you can capture speech in different languages.
- AI Integration: Later, you could connect this with a free AI API (like Hugging Face or OpenAI trial) to automatically summarize notes.
✅ Final Thoughts
- Work with SpeechRecognizer for speech-to-text.
- Design a Compose UI with buttons and text sections.
- Save, display, and delete notes locally.


