π What is a Story Reader App?
π― 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)
- Show a list of stories using LazyColumn
- Use a ViewModel to manage story data
- Enable reading aloud with TextToSpeech
- Navigate between screens using NavHost
π 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(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()
}
}





