Developing a Story Reader App using Jetpack Compose is a fun and educational project, perfect for beginners and intermediate Android developers. Below is a complete guide and project plan with key features, architecture, and code setup to help you build a clean and modern Story Reader App.
📖 What is a Story Reader App?
A Story Reader App is: An app that displays a collection of stories and allows users to read them or listen via text-to-speech.
A Story Reader App is a type of mobile application that allows users to read and/or listen to stories. It is often designed for children, learners, or casual readers and can include textual content, illustrations, and Text-to-Speech (TTS) support to make stories more interactive.
🎯 The Main Purpose of this App
- To entertain or educate users with short or long stories.
- To help children learn reading and improve vocabulary.
- To support multilingual reading (e.g., Hindi, English).
- To offer offline story access.
- To make stories accessible for visually impaired users using TTS.
👶 Example Use Cases
Audience | Usage |
---|---|
Children | Reading fairy tales, Panchatantra, bedtime stories |
Language Learners | Reading short stories in Hindi or English |
Parents | Reading aloud or using TTS to tell stories |
Bloggers | Sharing moral stories via mobile app |
✅ Common Features
Features | Description |
---|---|
📚 Story List | Browse a list of available stories |
📝 Story Detail | Read the full content of a story |
🔊 Text-to-Speech | Listen to the story being read aloud |
🌙 Dark Mode | Comfortable reading at night |
❤️ Favorites | Save/bookmark your favorite stories |
📶 Offline Mode | Read stories without internet |
📱 Real-World Examples
- Google Play Books (Kids section)
- Panchatantra Story apps
- Kindle Kids Edition
- Bal Kahani (Hindi Stories App)
🔧 In Development Context (Jetpack Compose)
When building a Story Reader App using Jetpack Compose, you typically:
- Show a list of stories using LazyColumn
- Use a ViewModel to manage story data
- Enable reading aloud with TextToSpeech
- Navigate between screens using NavHost
In this tutorial, we’ll build a simple and beautiful Story Reader Android App using Jetpack Compose. The app will display Hindi stories like Panchatantra, and let users listen to the stories using Text-to-Speech (TTS). We’ll also use modern UI components and MVVM architecture.
🚀 What You'll Build
- 📝 Story list UI
- 🔊 Text-to-Speech playback
- 🌙 Dark and Light Theme
- 🧠 MVVM with clean structure
🛠️ Tech Stack
Tool | Purpose |
---|---|
Jetpack Compose | Declarative UI |
TextToSpeech API | Read text aloud |
Hilt | Dependency Injection |
ViewModel | State Management |
Kotlin | Programming Language |
📦 Step 1: Setup Dependencies
Add these dependencies to your build.gradle (app-level):📸implementation ("androidx.navigation:navigation-compose:2.9.0")
implementation("io.coil-kt:coil-compose:2.6.0")
🧠 Step 2: Create Story Model and Sample Stories
package com.codingbihar.storyreaderapp
import java.io.Serializable
val storyList = listOf(
Story(
title = "\uD83C\uDF1F 1. The Monkey and the Crocodile",
content = "Once upon a time, a clever monkey lived on a big fruit tree near a river. The tree was full of sweet, juicy mangoes, and the monkey ate them every day happily.\n" +
"\n" +
"One day, a crocodile swam by and rested under the tree.\n" +
"\n" +
"“Hello, Crocodile!” the monkey called. “You look tired. Would you like some mangoes?”\n" +
"\n" +
"The crocodile smiled. “Yes, please! I’ve never tasted mangoes before.”\n" +
"\n" +
"The monkey plucked some and threw them down. The crocodile loved the sweet taste and thanked the monkey. From that day, they became good friends. Every day, the crocodile visited, and the monkey shared fruits with him.\n" +
"\n" +
"One day, the crocodile took some mangoes home to his wife. She tasted them and got a wicked idea.\n" +
"\n" +
"“These mangoes are so sweet,” she said. “If the monkey eats them every day, imagine how sweet his heart must be! Bring me the monkey’s heart!”\n" +
"\n" +
"The crocodile was shocked. “He is my friend! I can’t do that!”\n" +
"\n" +
"But his wife kept insisting. Finally, the crocodile gave in.\n" +
"\n" +
"The next day, the crocodile invited the monkey to come home for lunch.\n" +
"\n" +
"“I’ve never been across the river,” said the monkey. “How will I get there?”\n" +
"\n" +
"“Climb on my back,” said the crocodile.\n" +
"\n" +
"Halfway across the river, the crocodile said, “Friend, I must tell you the truth. My wife wants to eat your heart.”\n" +
"\n" +
"The clever monkey stayed calm.\n" +
"\n" +
"“Oh! You should have told me earlier,” he said. “I left my heart on the tree!”\n" +
"\n" +
"The crocodile was confused. “Really?”\n" +
"\n" +
"“Yes,” said the monkey. “Take me back. I’ll get it for you.”\n" +
"\n" +
"The crocodile swam back quickly. As soon as they reached the tree, the monkey jumped up.\n" +
"\n" +
"“You tricked me!” said the monkey. “A heart is never taken out and left somewhere. Go away and never come back!”\n" +
"\n" +
"The crocodile felt ashamed and swam away.",
moral = "Stay calm in tough situations and use your brain to solve problems.",
imageRes = R.drawable.one
),
Story(
title = "\uD83C\uDF1F 2. The Blue Jackal",
content = "",
moral = "",
imageRes = R.drawable.two
),
Story(
title = "\uD83C\uDF1F 3. The Tortoise and the Geese",
content = "",
moral = "",
imageRes = R.drawable.three
),
Story(
title = "\uD83C\uDF1F 4. The Lion and the Clever Rabbit",
content = "",
moral = "",
imageRes = R.drawable.four
),
Story(
title = "\uD83C\uDF1F 5. The Greedy Crow",
content = "",
moral = "",
imageRes = R.drawable.five
),
Story(
title = "\uD83C\uDF1F 6. The Mice and the Elephants",
content = "",
moral = "",
imageRes = R.drawable.six
),
Story(
title = "\uD83C\uDF1F 7. The Foolish Lion and the Smart Deer",
content = "",
moral = "",
imageRes = R.drawable.seven
),
Story(
title = "\uD83C\uDF1F 8. The Brahmin and the Goat",
content = "",
moral = "",
imageRes = R.drawable.eight
),
Story(
title = "\uD83C\uDF1F 9. The Loyal Mongoose",
content = "",
moral = "",
imageRes = R.drawable.nine
),
Story(
title = "\uD83C\uDF1F 10. The Turtle Who Couldn’t Stop Talking",
content = "",
moral = "",
imageRes = R.drawable.two
)
)
data class Story(
val title: String,
val content: String,
val moral: String,
val imageRes: Int
) : Serializable
🖼️ Step 3: Story List UI
📸
@Composable
fun StoryListScreen(onStoryClick: (Story) -> Unit) {
val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp
val columns = if (screenWidthDp > 600) 3 else 2 // Tablet vs phone
Column(Modifier.fillMaxSize().systemBarsPadding()) {
Text(
"Story Reader App",
style = MaterialTheme.typography.displayMedium
)
Text("By: www.codingbihar.com")
HorizontalDivider(
Modifier.fillMaxWidth()
.height(3.dp),
color = MaterialTheme.colorScheme.primary
)
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(storyList) { story ->
Card(
modifier = Modifier
// .systemBarsPadding()
.fillMaxWidth()
.aspectRatio(0.9f)
.clickable { onStoryClick(story) },
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = story.imageRes),
contentDescription = story.title,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(170.dp)
.clip(RoundedCornerShape(8.dp))
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = story.title,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}
}
}
}
}
🔊 Step 4: Story Detail
📸
@Composable
fun StoryDetailScreen(story: Story) {
val isPlaying = TTSManager.isPlaying
Column(Modifier.fillMaxSize().systemBarsPadding()) {
Row(Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween) {
// 🎧 Play/Pause Button
Text("Tap to Play>>>>>")
IconButton(onClick = {
if (isPlaying.value) {
TTSManager.stop()
} else {
TTSManager.speak(story.content)
}
}
) {
Icon(
imageVector = if (isPlaying.value) Icons.Default.Face else Icons.Default.PlayArrow,
contentDescription = if (isPlaying.value) "Pause" else "Play",
Modifier.size(76.dp)
)
}
}
Column(
modifier = Modifier
.padding(16.dp)
.systemBarsPadding()
.verticalScroll(rememberScrollState())
) {
Text(story.title, fontSize = 24.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(8.dp))
Text(
story.content,
fontSize = 18.sp,
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(onLongPress = {
TTSManager.speak(story.content)
})
}
)
Spacer(Modifier.height(16.dp))
Text(
"Moral: ${story.moral}",
fontWeight = FontWeight.SemiBold,
fontStyle = FontStyle.Italic
)
Spacer(Modifier.height(24.dp))
/*Button(onClick = {
if (isPlaying) stopSpeaking() else speakText()
}) {
Text(if (isPlaying) "Pause" else "Play")
}*/
}
}
}
🧭 Step 5: Navigation Setup
@Composable
fun StoryReaderApp() {
val navController = rememberNavController()
val selectedStory = remember { mutableStateOf<Story?>(null) }
NavHost(navController = navController, startDestination = "storyList") {
composable("storyList") {
StoryListScreen(onStoryClick = {
selectedStory.value = it
navController.navigate("storyDetail")
})
}
composable("storyDetail") {
selectedStory.value?.let { story ->
TTSManager.getTTS()?.let { tts ->
StoryDetailScreen(story)
}
}
}
}
}
🧠 Step 6: TTS Manager
package com.codingbihar.storyreaderapp
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import androidx.compose.runtime.mutableStateOf
import java.util.Locale
object TTSManager {
private var tts: TextToSpeech? = null
private val handler = Handler(Looper.getMainLooper())
// This state can be observed in Composables
val isPlaying = mutableStateOf(false)
fun initialize(context: Context) {
tts = TextToSpeech(context) {
if (it == TextToSpeech.SUCCESS) {
tts?.language = Locale.US
tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String?) {
handler.post {
isPlaying.value = true
}
}
override fun onDone(utteranceId: String?) {
handler.post {
isPlaying.value = false
}
}
override fun onError(utteranceId: String?) {
handler.post {
isPlaying.value = false
}
}
})
}
}
}
fun speak(text: String) {
val params = Bundle()
tts?.speak(text, TextToSpeech.QUEUE_FLUSH, params, "TTS_FINISHED")
}
fun stop() {
tts?.stop()
isPlaying.value = false
}
fun shutdown() {
tts?.stop()
tts?.shutdown()
}
fun getTTS(): TextToSpeech? = tts
}
🏁 Step 7: MainActivity
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.S)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// Initialize Text-to-Speech
TTSManager.initialize(this)
setContent {
StoryReaderAppTheme {
/* Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
StoryReaderApp()
}
}
}
override fun onDestroy() {
super.onDestroy()
TTSManager.shutdown()
}
}