Created
October 6, 2025 16:18
-
-
Save stevdza-san/e7349ff0f6e20a4d0d5d3ac623be22e6 to your computer and use it in GitHub Desktop.
Jetpack Compose - Day 1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.foundation.layout.* | |
import androidx.compose.material3.* | |
import androidx.compose.runtime.* | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.unit.dp | |
@Composable | |
fun DerivedVsRememberExample() { | |
Column( | |
modifier = Modifier | |
.fillMaxSize() | |
.safeDrawingPadding() | |
.padding(16.dp), | |
horizontalAlignment = Alignment.CenterHorizontally, | |
verticalArrangement = Arrangement.spacedBy(16.dp) | |
) { | |
Text( | |
text = "Derived State vs Remember", | |
style = MaterialTheme.typography.headlineMedium | |
) | |
// Correct Example - Using derivedStateOf | |
CorrectExample() | |
HorizontalDivider( | |
modifier = Modifier.padding(vertical = 16.dp), | |
thickness = DividerDefaults.Thickness, | |
color = DividerDefaults.color | |
) | |
// Wrong Example - Using remember with key | |
WrongExample() | |
} | |
} | |
@Composable | |
private fun CorrectExample() { | |
var text by remember { mutableStateOf("") } | |
// Using derivedStateOf | |
// This automatically recomposes when the state value changes | |
val isValid by remember { derivedStateOf { text.length > 3 } } | |
Column( | |
modifier = Modifier.fillMaxWidth(), | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
Text( | |
text = " Correct: Using derivedStateOf", | |
style = MaterialTheme.typography.titleMedium, | |
color = MaterialTheme.colorScheme.primary | |
) | |
OutlinedTextField( | |
value = text, | |
onValueChange = { text = it }, | |
label = { Text("Enter text (> 3 chars)") }, | |
modifier = Modifier.fillMaxWidth() | |
) | |
Text( | |
text = "Is valid: $isValid", | |
style = MaterialTheme.typography.bodyLarge, | |
color = if (isValid) { | |
MaterialTheme.colorScheme.primary | |
} else { | |
MaterialTheme.colorScheme.error | |
} | |
) | |
Text( | |
text = "This works correctly because derivedStateOf creates a derived state that automatically updates when text.value changes.", | |
style = MaterialTheme.typography.bodySmall, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
} | |
} | |
@Composable | |
private fun WrongExample() { | |
var text by remember { mutableStateOf("") } | |
// Using remember with key (wrong) | |
// This only updates when the text MutableState object reference changes, | |
// not when state value changes! | |
val isValid = remember(text) { mutableStateOf(text.length > 3) } | |
Column( | |
modifier = Modifier.fillMaxWidth(), | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
Text( | |
text = " Wrong: Using remember with key", | |
style = MaterialTheme.typography.titleMedium, | |
color = MaterialTheme.colorScheme.error | |
) | |
OutlinedTextField( | |
value = text, | |
onValueChange = { text = it }, | |
label = { Text("Enter text (> 3 chars)") }, | |
modifier = Modifier.fillMaxWidth() | |
) | |
Text( | |
text = "Is valid: ${isValid.value}", | |
style = MaterialTheme.typography.bodyLarge, | |
color = if (isValid.value) { | |
MaterialTheme.colorScheme.primary | |
} else { | |
MaterialTheme.colorScheme.error | |
} | |
) | |
Text( | |
text = "This doesn't work! The validation stays at initial value because remember(text) only triggers when the MutableState object changes, not its value property.", | |
style = MaterialTheme.typography.bodySmall, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.foundation.background | |
import androidx.compose.foundation.layout.* | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.rememberLazyListState | |
import androidx.compose.foundation.rememberScrollState | |
import androidx.compose.foundation.verticalScroll | |
import androidx.compose.material3.* | |
import androidx.compose.runtime.* | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.unit.dp | |
@Composable | |
fun DerivedVsRememberExample2() { | |
Column( | |
modifier = Modifier | |
.fillMaxSize() | |
.safeDrawingPadding() | |
.padding(16.dp), | |
verticalArrangement = Arrangement.spacedBy(24.dp) | |
) { | |
Text( | |
text = "derivedStateOf: The Recomposition Buffer", | |
style = MaterialTheme.typography.headlineMedium | |
) | |
Text( | |
text = "derivedStateOf buffers out unnecessary recompositions when state changes more frequently than your UI needs to update.", | |
style = MaterialTheme.typography.bodyMedium, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
HorizontalDivider() | |
ScrollButtonExample() | |
} | |
} | |
@Composable | |
private fun ScrollButtonExample() { | |
Column( | |
modifier = Modifier | |
.fillMaxWidth() | |
.verticalScroll(rememberScrollState()), | |
verticalArrangement = Arrangement.spacedBy(16.dp) | |
) { | |
Text( | |
text = "Example: Show button after scrolling", | |
style = MaterialTheme.typography.titleLarge, | |
color = MaterialTheme.colorScheme.primary | |
) | |
Text( | |
text = "Scroll the lists below. Watch the recomposition counters!", | |
style = MaterialTheme.typography.bodyMedium | |
) | |
Row( | |
modifier = Modifier.fillMaxWidth(), | |
horizontalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
// Without derivedStateOf | |
Column( | |
modifier = Modifier.weight(1f), | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
WithoutDerivedStateOf() | |
} | |
// With derivedStateOf | |
Column( | |
modifier = Modifier.weight(1f), | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
WithDerivedStateOf() | |
} | |
} | |
Card( | |
colors = CardDefaults.cardColors( | |
containerColor = MaterialTheme.colorScheme.tertiaryContainer | |
) | |
) { | |
Column( | |
modifier = Modifier.padding(16.dp), | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
Text( | |
text = "💡 Key Insight:", | |
style = MaterialTheme.typography.titleMedium, | |
color = MaterialTheme.colorScheme.tertiary | |
) | |
Text( | |
text = "• firstVisibleItemIndex changes: 0 → 1 → 2 → 3 → 4...\n" + | |
"• Without derivedStateOf: Recomposes EVERY time (hundreds of times)\n" + | |
"• With derivedStateOf: Recomposes ONLY when result changes (false → true)\n" + | |
"• derivedStateOf acts as a BUFFER, filtering out unnecessary changes", | |
style = MaterialTheme.typography.bodyMedium, | |
color = MaterialTheme.colorScheme.onTertiaryContainer | |
) | |
} | |
} | |
Text( | |
text = "🎯 When to use what:\n\n" + | |
"✅ derivedStateOf: When state changes MORE frequently than you need to update UI\n" + | |
" - Scroll position → boolean visibility\n" + | |
" - Text length (0,1,2,3...) → validation result (true/false)\n" + | |
" - Progress (0-100) → completion status\n\n" + | |
"✅ remember(key): When you want to update EVERY time the key changes\n" + | |
" - Recreating objects when dependencies change\n" + | |
" - No need to buffer state changes", | |
style = MaterialTheme.typography.bodyMedium, | |
color = MaterialTheme.colorScheme.onSurface | |
) | |
} | |
} | |
@Composable | |
private fun WithoutDerivedStateOf() { | |
val listState = rememberLazyListState() | |
var recompositions by remember { mutableStateOf(0) } | |
// ❌ Without derivedStateOf | |
// This recomposes EVERY time firstVisibleItemIndex changes (0→1→2→3...) | |
val showButton by remember(key1 = listState.firstVisibleItemIndex) { | |
mutableStateOf(listState.firstVisibleItemIndex > 0) | |
} | |
// Track recompositions - SideEffect runs on EVERY recomposition | |
SideEffect { | |
recompositions++ | |
} | |
Card( | |
colors = CardDefaults.cardColors( | |
containerColor = MaterialTheme.colorScheme.errorContainer | |
) | |
) { | |
Column(modifier = Modifier.padding(8.dp)) { | |
Text( | |
text = "❌ Without derivedStateOf", | |
style = MaterialTheme.typography.labelLarge, | |
color = MaterialTheme.colorScheme.error | |
) | |
Text( | |
text = "Recompositions: $recompositions", | |
style = MaterialTheme.typography.bodySmall, | |
color = MaterialTheme.colorScheme.onErrorContainer | |
) | |
} | |
} | |
Box(modifier = Modifier.fillMaxWidth()) { | |
LazyColumn( | |
state = listState, | |
modifier = Modifier | |
.fillMaxWidth() | |
.height(300.dp) | |
.background(MaterialTheme.colorScheme.surface) | |
) { | |
items(100) { index -> | |
Text( | |
text = "Item #$index", | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(16.dp) | |
) | |
HorizontalDivider() | |
} | |
} | |
if (showButton) { | |
Button( | |
onClick = { }, | |
modifier = Modifier | |
.align(Alignment.BottomEnd) | |
.padding(8.dp), | |
colors = ButtonDefaults.buttonColors( | |
containerColor = MaterialTheme.colorScheme.error | |
) | |
) { | |
Text("Up") | |
} | |
} | |
} | |
} | |
@Composable | |
private fun WithDerivedStateOf() { | |
val listState = rememberLazyListState() | |
var recompositions by remember { mutableStateOf(0) } | |
// ✅ With derivedStateOf | |
// This BUFFERS the changes and only recomposes when the boolean result changes | |
val showButton by remember { | |
derivedStateOf { listState.firstVisibleItemIndex > 0 } | |
} | |
LaunchedEffect(Unit) { | |
} | |
SideEffect { | |
recompositions++ | |
} | |
Card( | |
colors = CardDefaults.cardColors( | |
containerColor = MaterialTheme.colorScheme.primaryContainer | |
) | |
) { | |
Column(modifier = Modifier.padding(8.dp)) { | |
Text( | |
text = "✅ With derivedStateOf", | |
style = MaterialTheme.typography.labelLarge, | |
color = MaterialTheme.colorScheme.primary | |
) | |
Text( | |
text = "Recompositions: $recompositions", | |
style = MaterialTheme.typography.bodySmall, | |
color = MaterialTheme.colorScheme.onPrimaryContainer | |
) | |
} | |
} | |
Box(modifier = Modifier.fillMaxWidth()) { | |
LazyColumn( | |
state = listState, | |
modifier = Modifier | |
.fillMaxWidth() | |
.height(300.dp) | |
.background(MaterialTheme.colorScheme.surface) | |
) { | |
items(100) { index -> | |
Text( | |
text = "Item #$index", | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(16.dp) | |
) | |
HorizontalDivider() | |
} | |
} | |
if (showButton) { | |
Button( | |
onClick = { }, | |
modifier = Modifier | |
.align(Alignment.BottomEnd) | |
.padding(8.dp), | |
colors = ButtonDefaults.buttonColors( | |
containerColor = MaterialTheme.colorScheme.primary | |
) | |
) { | |
Text("Up") | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.foundation.clickable | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.Row | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.text.BasicText | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.SideEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableIntStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.unit.dp | |
/** | |
* Demo showing how recomposition behaves with inline layout content vs extracted composables. | |
* | |
* Key points: | |
* - Inline layout content (e.g., Row/Column content lambdas declared in the same scope) tends to | |
* recompose the whole content block when a state read inside that block changes, even if only one | |
* element actually reads the state. | |
* - Extracted composable boundaries allow Compose to skip recomposition of children whose parameters | |
* did not change. This makes unaffected parts of the UI cheaper to maintain. | |
* - Passing fresh/changing parameters (including creating a new Modifier in the parent on every pass) | |
* prevents skipping. Keep inputs stable for sections you want Compose to skip. | |
*/ | |
@Composable | |
fun RecompositionExample() { | |
Column( | |
modifier = Modifier | |
.fillMaxSize() | |
.padding(16.dp), | |
verticalArrangement = Arrangement.Center, | |
horizontalAlignment = Alignment.CenterHorizontally | |
) { | |
BasicText("Inline layouts (Row/Column) - state in first column") | |
InlineLayoutsDemo() | |
BasicText("\nExtracted layouts - only the affected column recomposes") | |
ExtractedLayoutsDemo() | |
} | |
} | |
/** | |
* Inline scenario: `clicks` is read inside the Row's content block. | |
* When `clicks` changes, the entire Row content is recomposed, so both columns log, | |
* even though only the first column depends on `clicks`. | |
*/ | |
@Composable | |
private fun InlineLayoutsDemo() { | |
var clicks by remember { mutableIntStateOf(0) } | |
Row( | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(vertical = 8.dp) | |
) { | |
// This prints on every recomposition of the Row's content scope | |
SideEffect { println("Direct Row recomposed") } | |
Column( | |
modifier = Modifier | |
.padding(8.dp) | |
.clickable { clicks++ } | |
) { | |
// First column depends on `clicks`, so it recomposes on click | |
SideEffect { println("Direct Column 1 recomposed") } | |
BasicText("Clicks: $clicks") | |
} | |
Column( | |
modifier = Modifier | |
.padding(8.dp) | |
) { | |
// Second column does NOT read `clicks`, but still recomposes in this inline example | |
SideEffect { println("Direct Column 2 recomposed") } | |
BasicText("Static content") | |
} | |
} | |
} | |
/** | |
* Extracted scenario: `FirstColumn(clicks)` receives the changing state, while `SecondColumn()` | |
* receives no changing parameters. Compose can skip `SecondColumn()` entirely. | |
* The Row still logs because this function (and thus its content lambda) recomposes when `clicks` changes. | |
* | |
* If you also want to avoid the Row log here, either: | |
* - Move the `clicks` state into `FirstColumn`, or | |
* - Wrap the Row into an extracted composable with stable parameters so it can be skipped. | |
*/ | |
@Composable | |
private fun ExtractedLayoutsDemo() { | |
var clicks by remember { mutableIntStateOf(0) } | |
Row( | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(vertical = 8.dp) | |
) { | |
SideEffect { println("Extracted Row recomposed") } | |
FirstColumn( | |
clicks = clicks, | |
onClick = { clicks++ } | |
) | |
SecondColumn() | |
} | |
} | |
/** | |
* Receives a changing parameter (`clicks`), so this composable will recompose on clicks. | |
*/ | |
@Composable | |
private fun FirstColumn( | |
clicks: Int, | |
onClick: () -> Unit, | |
) { | |
Column(modifier = Modifier.padding(8.dp).clickable { onClick() }) { | |
SideEffect { println("Extracted Column 1") } | |
BasicText("Clicks: $clicks") | |
} | |
} | |
/** | |
* Receives no changing parameters, so Compose can skip this subtree. | |
* Important: avoid passing a fresh `Modifier` from the parent every time, as that would | |
* create a changing input and prevent skipping. | |
*/ | |
@Composable | |
private fun SecondColumn() { | |
Column(modifier = Modifier.padding(8.dp)) { | |
SideEffect { println("Extracted Column 2") } | |
BasicText("Static content") | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.widget.Toast | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.rememberUpdatedState | |
import androidx.compose.ui.platform.LocalContext | |
import kotlinx.coroutines.delay | |
val MESSAGE_DELAY = 10000L | |
@Composable | |
actual fun ShowToast( | |
message: String, | |
) { | |
val context = LocalContext.current | |
val actualTrueMessage by rememberUpdatedState(message) | |
LaunchedEffect(Unit) { | |
delay(MESSAGE_DELAY) | |
Toast.makeText( | |
context, | |
actualTrueMessage, | |
Toast.LENGTH_SHORT | |
).show() | |
} | |
} | |
@Composable | |
fun App() { | |
var message by remember { | |
mutableStateOf("") | |
} | |
LaunchedEffect(Unit) { | |
message = "Hello" | |
delay(5000) | |
message = "Goodbye" | |
} | |
ShowToast(message = message) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment