Skip to content

Commit bf7cbe3

Browse files
committed
> Added type-safe navigation
> Added a universally accessible snackbar
1 parent 9002385 commit bf7cbe3

File tree

4 files changed

+309
-16
lines changed

4 files changed

+309
-16
lines changed

app/build.gradle.kts

+11-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ plugins {
44

55
id("kotlin-kapt")
66
alias(libs.plugins.kotlin.compose)
7+
8+
// serialization
9+
// kotlin("jvm") version "2.1.10"
10+
kotlin("plugin.serialization") version "2.1.10"
711
}
812

913
android {
@@ -86,13 +90,10 @@ dependencies {
8690

8791
// Room
8892
val room_version = "2.6.1"
89-
9093
implementation("androidx.room:room-runtime:$room_version")
91-
9294
// If this project uses any Kotlin source, use Kotlin Symbol Processing (KSP)
9395
// See Add the kapt plugin to your project
9496
kapt("androidx.room:room-compiler:$room_version")
95-
9697
// optional - Kotlin Extensions and Coroutines support for Room
9798
implementation("androidx.room:room-ktx:$room_version")
9899

@@ -103,4 +104,11 @@ dependencies {
103104
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0")
104105
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") // Latest version
105106

107+
// serialization
108+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
109+
110+
// navigation
111+
implementation("androidx.navigation:navigation-compose:2.8.8")
112+
113+
106114
}

app/src/main/java/com/sujith/kotlin/koinnew/MainActivity.kt

+234-13
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,77 @@
11
package com.sujith.kotlin.koinnew
22

3+
import android.app.Application
4+
import android.content.Context
35
import android.os.Bundle
6+
import android.widget.Toast
47
import androidx.activity.ComponentActivity
58
import androidx.activity.compose.setContent
69
import androidx.activity.enableEdgeToEdge
10+
import androidx.compose.animation.AnimatedVisibility
11+
import androidx.compose.animation.core.Spring
12+
import androidx.compose.animation.core.spring
13+
import androidx.compose.animation.fadeIn
14+
import androidx.compose.animation.slideInVertically
15+
import androidx.compose.animation.slideOutVertically
716
import androidx.compose.foundation.background
817
import androidx.compose.foundation.border
918
import androidx.compose.foundation.layout.Arrangement
19+
import androidx.compose.foundation.layout.Box
1020
import androidx.compose.foundation.layout.Column
1121
import androidx.compose.foundation.layout.PaddingValues
22+
import androidx.compose.foundation.layout.Row
1223
import androidx.compose.foundation.layout.Spacer
1324
import androidx.compose.foundation.layout.fillMaxSize
1425
import androidx.compose.foundation.layout.fillMaxWidth
26+
import androidx.compose.foundation.layout.height
1527
import androidx.compose.foundation.layout.padding
1628
import androidx.compose.foundation.lazy.LazyColumn
1729
import androidx.compose.foundation.lazy.items
30+
import androidx.compose.foundation.shape.RoundedCornerShape
1831
import androidx.compose.material3.Button
32+
import androidx.compose.material3.ButtonDefaults
1933
import androidx.compose.material3.Card
2034
import androidx.compose.material3.CardDefaults
35+
import androidx.compose.material3.MaterialTheme
2136
import androidx.compose.material3.OutlinedTextField
2237
import androidx.compose.material3.Scaffold
38+
import androidx.compose.material3.Snackbar
39+
import androidx.compose.material3.SnackbarHost
40+
import androidx.compose.material3.SnackbarHostState
41+
import androidx.compose.material3.SnackbarResult
2342
import androidx.compose.material3.Text
43+
import androidx.compose.material3.TextButton
2444
import androidx.compose.runtime.Composable
25-
import androidx.compose.runtime.collectAsState
45+
import androidx.compose.runtime.LaunchedEffect
2646
import androidx.compose.runtime.getValue
47+
import androidx.compose.runtime.mutableStateOf
2748
import androidx.compose.runtime.remember
49+
import androidx.compose.runtime.rememberCoroutineScope
2850
import androidx.compose.ui.Alignment
2951
import androidx.compose.ui.Modifier
3052
import androidx.compose.ui.graphics.Color
3153
import androidx.compose.ui.graphics.RectangleShape
54+
import androidx.compose.ui.text.font.FontWeight
3255
import androidx.compose.ui.tooling.preview.Preview
3356
import androidx.compose.ui.unit.dp
3457
import androidx.lifecycle.compose.collectAsStateWithLifecycle
58+
import androidx.navigation.NavHostController
59+
import androidx.navigation.compose.NavHost
60+
import androidx.navigation.compose.composable
61+
import androidx.navigation.compose.rememberNavController
3562
import com.sujith.kotlin.koinnew.model.Tweet
3663
import com.sujith.kotlin.koinnew.ui.theme.KoinNewTheme
3764
import com.sujith.kotlin.koinnew.viewmodel.MainViewModel
65+
import com.sujith.kotlin.koinnew.viewmodel.ObserveAsEvents
66+
import com.sujith.kotlin.koinnew.viewmodel.SnackBarAction
67+
import com.sujith.kotlin.koinnew.viewmodel.SnackBarController
68+
import com.sujith.kotlin.koinnew.viewmodel.SnackBarEvent
69+
import kotlinx.coroutines.CoroutineScope
3870
import kotlinx.coroutines.flow.MutableStateFlow
3971
import kotlinx.coroutines.flow.StateFlow
72+
import kotlinx.coroutines.flow.firstOrNull
73+
import kotlinx.coroutines.launch
74+
import kotlinx.serialization.Serializable
4075
import org.koin.androidx.viewmodel.ext.android.viewModel
4176

4277
class MainActivity : ComponentActivity() {
@@ -57,36 +92,159 @@ class MainActivity : ComponentActivity() {
5792
}*/
5893
setContent {
5994
KoinNewTheme {
60-
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
61-
val tweets by viewModel.tweetsFlow.collectAsState()/*collectAsStateWithLifecycle()*/
62-
AddTweet(
63-
innerPadding,
64-
viewModel::addTweet,
65-
tweets,
66-
viewModel.tweetText,
67-
viewModel.category,
68-
viewModel
69-
)
95+
val scope = rememberCoroutineScope()
96+
val snackbarHostState = remember { SnackbarHostState() }
97+
val currentEvent = remember { mutableStateOf<SnackBarEvent?>(null) }
98+
99+
ObserveAsEvents(flow = SnackBarController.events) { events ->
100+
currentEvent.value = events // Store the latest event
101+
102+
scope.launch {
103+
snackbarHostState.currentSnackbarData?.dismiss()
104+
val result = snackbarHostState.showSnackbar(
105+
message = events.message,
106+
actionLabel = events.action?.label,
107+
duration = events.duration,
108+
withDismissAction = events.withDismissAction
109+
)
110+
when (result) {
111+
SnackbarResult.ActionPerformed -> events.action?.action?.invoke()
112+
SnackbarResult.Dismissed -> events.action?.dismissAction?.invoke()
113+
}
114+
}
115+
}
116+
Scaffold(
117+
modifier = Modifier.fillMaxSize(),
118+
snackbarHost = {
119+
SnackbarHost(hostState = snackbarHostState) { data ->
120+
/*AnimatedVisibility( // Animation for Snackbar entry
121+
visible = snackbarHostState.currentSnackbarData != null,
122+
enter = fadeIn(animationSpec = spring(stiffness = Spring.DampingRatioHighBouncy)), // Slide up animation
123+
exit = slideOutVertically(targetOffsetY = { it }) // Slide down when dismissed
124+
) {
125+
126+
}*/
127+
Snackbar(
128+
modifier = Modifier
129+
.padding(16.dp)
130+
.background(
131+
currentEvent.value?.backgroundColor ?: MaterialTheme.colorScheme.primary,
132+
shape = RoundedCornerShape(16.dp)
133+
),
134+
action = {
135+
data.visuals.actionLabel?.let { actionLabel ->
136+
TextButton(
137+
onClick = { data.performAction() },
138+
colors = ButtonDefaults.textButtonColors( // Custom background color
139+
containerColor = Color.Yellow, // Change action button bg color
140+
contentColor = Color.Black // Change text color
141+
)
142+
) {
143+
Text(
144+
text = actionLabel,
145+
fontWeight = FontWeight.Bold
146+
)
147+
}
148+
}
149+
},
150+
151+
dismissAction = if (data.visuals.withDismissAction) {
152+
{
153+
TextButton(
154+
onClick = { data.dismiss() },
155+
colors = ButtonDefaults.textButtonColors( // Custom dismiss bg color
156+
containerColor = Color.Red, // Change dismiss button bg color
157+
contentColor = Color.White // Change text color
158+
)
159+
) {
160+
Text(text = "Dismiss", fontWeight = FontWeight.Bold)
161+
}
162+
}
163+
} else null,
164+
shape = RoundedCornerShape(16.dp),
165+
containerColor = MaterialTheme.colorScheme.secondary,
166+
contentColor = Color.White,
167+
actionContentColor = Color.Red,
168+
dismissActionContentColor = Color.Green,
169+
) {
170+
Column {
171+
Text(
172+
text = data.visuals.message,
173+
style = MaterialTheme.typography.bodyLarge
174+
)
175+
Spacer(modifier = Modifier.height(4.dp)) // Add spacing
176+
177+
// Custom Dismiss Button (Optional)
178+
/* TextButton(
179+
onClick = { data.dismiss() } // Explicitly dismiss snackbar
180+
) {
181+
Text(
182+
text = "Dismiss",
183+
color = Color.Green,
184+
fontWeight = FontWeight.Bold
185+
)
186+
}*/
187+
}
188+
}
189+
}
190+
},
191+
) { innerPadding ->
192+
val navController = rememberNavController()
193+
NavHost(navController = navController, startDestination = Screens.AddTweet) {
194+
composable<Screens.AddTweet>() {
195+
val tweets by viewModel.tweetsFlow./*collectAsState()*/collectAsStateWithLifecycle()
196+
AddTweet(
197+
this@MainActivity,
198+
innerPadding,
199+
viewModel::addTweet,
200+
tweets,
201+
viewModel.tweetText,
202+
viewModel.category,
203+
viewModel,
204+
snackbarHostState,
205+
scope,
206+
navController = navController
207+
)
208+
}
209+
composable<Screens.ScreenB>() {
210+
ScreenB()
211+
}
212+
}
70213
}
71214
}
72215
}
73216
}
74217
}
75218

219+
220+
sealed class Screens {
221+
@Serializable
222+
data object AddTweet : Screens()
223+
224+
@Serializable
225+
data object ScreenB : Screens()
226+
}
227+
228+
76229
@Composable
77230
fun AddTweet(
231+
context: Context,
78232
paddingValues: PaddingValues,
79233
addOnclick: (Tweet) -> Unit,
80234
tweets: List<Tweet>,
81235
tweet: StateFlow<String>,
82236
category: StateFlow<String>,
83237
viewModel: MainViewModel,
238+
snackbarHostState: SnackbarHostState,
239+
scope: CoroutineScope = rememberCoroutineScope(),
240+
navController: NavHostController,
84241
) {
85242

86243
// val category by category.collectAsStateWithLifecycle()
87244
// val tweetText by tweet.collectAsStateWithLifecycle()
88245
val category by remember { viewModel.category }.collectAsStateWithLifecycle()
89246
val tweetText by remember { viewModel.tweetText }.collectAsStateWithLifecycle()
247+
90248
Column(
91249
modifier = Modifier
92250
.padding(paddingValues)
@@ -108,10 +266,59 @@ fun AddTweet(
108266
addOnclick(Tweet(category, tweetText))
109267
viewModel.resetFields()
110268
}
269+
else {
270+
scope.launch {
271+
SnackBarController.sendEvent(
272+
event = SnackBarEvent(
273+
message = "Deleted the data",
274+
withDismissAction = false,
275+
backgroundColor = Color.White,
276+
action = SnackBarAction(
277+
label = "Undo",
278+
action = {
279+
println("Action performed")
280+
SnackBarController.sendEvent(
281+
event = SnackBarEvent(
282+
message = "Undo deleted data",
283+
backgroundColor = Color.Red
284+
)
285+
)
286+
},
287+
dismissAction = {
288+
println("Dismissed")
289+
// viewModel.resetFields()
290+
SnackBarController.sendEvent(
291+
event = SnackBarEvent(
292+
message = "Successfully deleted",
293+
backgroundColor = Color.Green,
294+
)
295+
)
296+
}
297+
)
298+
)
299+
)
300+
}
301+
}
111302
}
112303
) {
113304
Text(text = "Add")
114305
}
306+
Button(onClick = {
307+
// navController.navigate(Screens.ScreenB)
308+
// Toast.makeText(context, "nav b", Toast.LENGTH_SHORT).show()
309+
scope.launch {
310+
SnackBarController.sendEvent(
311+
event = SnackBarEvent(
312+
message = "Custom Background Color!",
313+
backgroundColor = Color(0xFF00FF00) // Explicit green color
314+
)
315+
)
316+
}
317+
318+
319+
}) {
320+
Text("nav b")
321+
}
115322
Spacer(
116323
modifier = Modifier
117324
.padding(horizontal = 10.dp)
@@ -121,6 +328,16 @@ fun AddTweet(
121328
}
122329
}
123330

331+
@Composable
332+
fun ScreenB() {
333+
Box(
334+
modifier = Modifier.fillMaxSize(),
335+
contentAlignment = Alignment.Center
336+
) {
337+
Text(text = "Screen B")
338+
}
339+
}
340+
124341
@Composable
125342
fun CustomOutlinedTextField(value: String, labelText: String, onValueChange: (String) -> Unit) {
126343
OutlinedTextField(
@@ -171,14 +388,18 @@ fun AddTweetPreview() {
171388
val dummyCategory = MutableStateFlow("Sample Category")
172389
val dummyTweetText = MutableStateFlow("Sample Tweet")
173390
val fakeViewModel = MainViewModel() // Temporary ViewModel for Preview
174-
391+
val context = Application()
175392
AddTweet(
393+
context = context,
176394
paddingValues = PaddingValues(10.dp),
177395
addOnclick = {},
178396
tweets = listOf(Tweet("test", "tweet")),
179397
tweet = dummyTweetText,
180398
category = dummyCategory,
181-
viewModel = fakeViewModel
399+
viewModel = fakeViewModel,
400+
snackbarHostState = SnackbarHostState(),
401+
navController = rememberNavController()
402+
182403
)
183404
}
184405

0 commit comments

Comments
 (0)