Last active
October 9, 2025 14:46
-
-
Save stevdza-san/05b564a93909bb9ae3dfdafaa2807048 to your computer and use it in GitHub Desktop.
Jetpack Compose - Day 2
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.animation.AnimatedContent | |
import androidx.compose.animation.AnimatedVisibility | |
import androidx.compose.animation.ExperimentalAnimationApi | |
import androidx.compose.animation.animateContentSize | |
import androidx.compose.animation.core.LinearEasing | |
import androidx.compose.animation.core.RepeatMode | |
import androidx.compose.animation.core.animateFloat | |
import androidx.compose.animation.core.animateFloatAsState | |
import androidx.compose.animation.core.infiniteRepeatable | |
import androidx.compose.animation.core.rememberInfiniteTransition | |
import androidx.compose.animation.core.tween | |
import androidx.compose.animation.fadeIn | |
import androidx.compose.animation.fadeOut | |
import androidx.compose.animation.slideInVertically | |
import androidx.compose.animation.slideOutVertically | |
import androidx.compose.animation.togetherWith | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.clickable | |
import androidx.compose.foundation.interaction.MutableInteractionSource | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.Column | |
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.sizeIn | |
import androidx.compose.material3.Button | |
import androidx.compose.material3.Text | |
import androidx.compose.runtime.Composable | |
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.draw.rotate | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.text.font.FontWeight | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
@Composable | |
fun AnimateVisibility() { | |
var visible by remember { mutableStateOf(true) } | |
Column( | |
modifier = Modifier | |
.fillMaxSize() | |
.animateContentSize(), | |
verticalArrangement = Arrangement.Center, | |
horizontalAlignment = Alignment.CenterHorizontally | |
) { | |
AnimatedVisibility( | |
visible = visible, | |
enter = fadeIn(), | |
exit = fadeOut() | |
) { | |
// Fade in/out the background and the foreground. | |
Box( | |
Modifier.background(Color.DarkGray) | |
) { | |
Box( | |
Modifier | |
.align(Alignment.Center) | |
.padding(all = 24.dp) | |
.animateEnterExit( | |
// Slide in/out the inner box. | |
enter = slideInVertically(), | |
exit = slideOutVertically() | |
) | |
.sizeIn(minWidth = 256.dp, minHeight = 64.dp) | |
.background(Color.Red) | |
) { | |
// Content of the notification… | |
} | |
} | |
} | |
Spacer(modifier = Modifier.height(12.dp)) | |
Button( | |
onClick = { visible = !visible } | |
) { | |
Text(text = if (visible) "Hide" else "Show") | |
} | |
} | |
} | |
@OptIn(ExperimentalAnimationApi::class) | |
@Composable | |
fun AnimatedContentExample() { | |
var count by remember { mutableStateOf(0) } | |
Column( | |
modifier = Modifier.fillMaxSize(), | |
verticalArrangement = Arrangement.Center, | |
horizontalAlignment = Alignment.CenterHorizontally | |
) { | |
Button(onClick = { count++ }) { | |
Text( | |
text = "Increase Count", | |
fontSize = 30.sp | |
) | |
} | |
AnimatedContent( | |
targetState = count, | |
transitionSpec = { | |
// Define how the old and new content animate | |
slideInVertically { height -> height } + fadeIn() togetherWith | |
slideOutVertically { height -> -height } + fadeOut() | |
} | |
) { targetCount -> | |
Text( | |
text = "$targetCount", | |
fontSize = 80.sp, | |
fontWeight = FontWeight.Bold | |
) | |
} | |
} | |
} | |
@Composable | |
fun AnimateContentSizeExample() { | |
var expanded by remember { mutableStateOf(false) } | |
Box( | |
modifier = Modifier.fillMaxSize(), | |
contentAlignment = Alignment.Center | |
) { | |
Box( | |
modifier = Modifier | |
.background(Color.Blue) | |
.animateContentSize() | |
.height(if (expanded) 400.dp else 200.dp) | |
.fillMaxWidth(0.5f) | |
.clickable( | |
interactionSource = remember { MutableInteractionSource() }, | |
indication = null | |
) { | |
expanded = !expanded | |
} | |
) {} | |
} | |
} | |
@Composable | |
fun ValueBasedAnimationExample() { | |
var enabled by remember { | |
mutableStateOf(true) | |
} | |
val rotation by animateFloatAsState( | |
targetValue = if (enabled) 0f else 180f, | |
label = "rotation", | |
animationSpec = tween(durationMillis = 500) | |
) | |
Box( | |
modifier = Modifier.fillMaxSize(), | |
contentAlignment = Alignment.Center | |
) { | |
Box( | |
Modifier | |
.size(100.dp) | |
.rotate(degrees = rotation) | |
.background(Color.Red) | |
.clickable { enabled = !enabled } | |
) | |
} | |
} | |
@Composable | |
fun RotatingBox() { | |
var enabled by remember { | |
mutableStateOf(true) | |
} | |
val rotation by animateFloatAsState( | |
targetValue = if (enabled) 0f else 180f, | |
label = "rotation" | |
) | |
Box( | |
modifier = Modifier | |
.fillMaxSize() | |
.size(150.dp), | |
contentAlignment = Alignment.Center | |
){ | |
Box( | |
Modifier | |
.size(100.dp) | |
.rotate(degrees = rotation) | |
.background(Color.Red) | |
.clickable { enabled = !enabled } | |
) | |
} | |
} | |
@Composable | |
fun InfiniteRotatingBox() { | |
// Infinite transition | |
val infiniteTransition = rememberInfiniteTransition() | |
val rotation by infiniteTransition.animateFloat( | |
initialValue = 0f, | |
targetValue = 360f, | |
animationSpec = infiniteRepeatable( | |
animation = tween(durationMillis = 2000, easing = LinearEasing), | |
repeatMode = RepeatMode.Restart | |
) | |
) | |
Box( | |
modifier = Modifier | |
.fillMaxSize() | |
.size(150.dp), | |
contentAlignment = Alignment.Center | |
) { | |
Box( | |
modifier = Modifier | |
.size(100.dp) | |
.rotate(rotation) | |
.background(Color.Red) | |
) | |
} | |
} |
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.Manifest | |
import android.content.Intent | |
import android.net.Uri | |
import android.provider.Settings | |
import androidx.compose.foundation.layout.* | |
import androidx.compose.foundation.rememberScrollState | |
import androidx.compose.foundation.verticalScroll | |
import androidx.compose.material3.* | |
import androidx.compose.runtime.* | |
import androidx.compose.runtime.saveable.rememberSaveable | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.ui.text.font.FontWeight | |
import androidx.compose.ui.text.style.TextAlign | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import com.google.accompanist.permissions.* | |
// https://google.github.io/accompanist/permissions/ | |
@OptIn(ExperimentalPermissionsApi::class) | |
@Composable | |
actual fun PermissionDemo() { | |
// Saveable states to track permission request history | |
var cameraPermissionRequested by rememberSaveable { mutableStateOf(false) } | |
// Camera permission | |
val cameraPermissionState = rememberPermissionState( | |
permission = Manifest.permission.CAMERA | |
) { | |
cameraPermissionRequested = true | |
} | |
Column( | |
modifier = Modifier | |
.safeDrawingPadding() | |
.fillMaxSize() | |
.padding(16.dp) | |
.verticalScroll(rememberScrollState()), | |
verticalArrangement = Arrangement.spacedBy(16.dp) | |
) { | |
Text( | |
text = "Runtime Permissions Demo", | |
fontSize = 24.sp, | |
fontWeight = FontWeight.Bold, | |
modifier = Modifier.fillMaxWidth(), | |
textAlign = TextAlign.Center | |
) | |
HorizontalDivider() | |
// Camera Permission Section | |
PermissionSection( | |
permissionState = cameraPermissionState, | |
permissionRequested = cameraPermissionRequested, | |
) | |
} | |
} | |
@OptIn(ExperimentalPermissionsApi::class) | |
@Composable | |
private fun PermissionSection( | |
title: String = "Camera Permission", | |
permissionState: PermissionState, | |
permissionDescription: String = "This permission allows the app to access your device camera to take photos and record videos.", | |
deniedMessage: String = "Camera permission is required to use camera features.", | |
permissionRequested: Boolean, | |
) { | |
val context = LocalContext.current | |
// More accurate permanently denied detection | |
val isPermanentlyDenied = permissionRequested && | |
!permissionState.status.isGranted && | |
!permissionState.status.shouldShowRationale | |
Card( | |
modifier = Modifier.fillMaxWidth(), | |
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) | |
) { | |
Column( | |
modifier = Modifier.padding(16.dp), | |
verticalArrangement = Arrangement.spacedBy(12.dp) | |
) { | |
Text( | |
text = title, | |
fontSize = 18.sp, | |
fontWeight = FontWeight.SemiBold | |
) | |
when { | |
permissionState.status.isGranted -> { | |
Text( | |
text = "✅ Permission Granted", | |
color = MaterialTheme.colorScheme.primary, | |
fontWeight = FontWeight.Medium | |
) | |
Text( | |
text = "The app now has access to this feature.", | |
style = MaterialTheme.typography.bodyMedium | |
) | |
} | |
isPermanentlyDenied -> { | |
Text( | |
text = "🚫 Permission Permanently Denied", | |
color = MaterialTheme.colorScheme.error, | |
fontWeight = FontWeight.Medium | |
) | |
Text( | |
text = "You have permanently denied this permission. Please enable it in the app settings.", | |
style = MaterialTheme.typography.bodyMedium | |
) | |
Button( | |
onClick = { | |
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) | |
intent.data = Uri.fromParts("package", context.packageName, null) | |
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | |
context.startActivity(intent) | |
}, | |
modifier = Modifier.fillMaxWidth() | |
) { | |
Text("Open App Settings") | |
} | |
} | |
permissionState.status.shouldShowRationale -> { | |
Text( | |
text = "🔒 Permission Denied", | |
color = MaterialTheme.colorScheme.error, | |
fontWeight = FontWeight.Medium | |
) | |
Text( | |
text = deniedMessage, | |
style = MaterialTheme.typography.bodyMedium | |
) | |
Text( | |
text = permissionDescription, | |
style = MaterialTheme.typography.bodySmall, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
Button( | |
onClick = { | |
permissionState.launchPermissionRequest() | |
}, | |
modifier = Modifier.fillMaxWidth() | |
) { | |
Text("Grant Permission") | |
} | |
} | |
else -> { | |
Text( | |
text = "⚠️ Permission Required", | |
color = MaterialTheme.colorScheme.secondary, | |
fontWeight = FontWeight.Medium | |
) | |
Text( | |
text = permissionDescription, | |
style = MaterialTheme.typography.bodyMedium | |
) | |
Button( | |
onClick = { | |
permissionState.launchPermissionRequest() | |
}, | |
modifier = Modifier.fillMaxWidth() | |
) { | |
Text("Request Permission") | |
} | |
} | |
} | |
} | |
} | |
} |
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.animation.AnimatedContent | |
import androidx.compose.animation.ContentTransform | |
import androidx.compose.animation.EnterTransition | |
import androidx.compose.animation.ExitTransition | |
import androidx.compose.animation.fadeIn | |
import androidx.compose.animation.fadeOut | |
import androidx.compose.animation.togetherWith | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.runtime.Composable | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.Color | |
sealed class RequestState<out T> { | |
data object Idle : RequestState<Nothing>() | |
data object Loading : RequestState<Nothing>() | |
data class Success<out T>(val data: T) : RequestState<T>() | |
data class Error(val message: String) : RequestState<Nothing>() | |
fun isLoading(): Boolean = this is Loading | |
fun isError(): Boolean = this is Error | |
fun isSuccess(): Boolean = this is Success | |
fun getSuccessData() = (this as Success).data | |
fun getSuccessDataOrNull() = if (this.isSuccess()) this.getSuccessData() else null | |
fun getErrorMessage(): String = (this as Error).message | |
fun <R> map(transform: (T) -> R): RequestState<R> = | |
when (this) { | |
is Success -> Success(transform(data)) | |
is Error -> this as RequestState<R> | |
is Loading -> this as RequestState<R> | |
is Idle -> this as RequestState<R> | |
} | |
} | |
@Composable | |
fun <T> RequestState<T>.DisplayResult( | |
modifier: Modifier = Modifier, | |
onIdle: (@Composable () -> Unit)? = null, | |
onLoading: (@Composable () -> Unit)? = null, | |
onError: (@Composable (String) -> Unit)? = null, | |
onSuccess: @Composable (T) -> Unit, | |
transitionSpec: ContentTransform? = fadeIn() togetherWith fadeOut(), | |
backgroundColor: Color? = null, | |
) { | |
AnimatedContent( | |
modifier = modifier | |
.background(color = backgroundColor ?: Color.Unspecified), | |
targetState = this, | |
transitionSpec = { | |
transitionSpec ?: (EnterTransition.None togetherWith ExitTransition.None) | |
}, | |
label = "Content Animation" | |
) { state -> | |
Box( | |
modifier = Modifier.fillMaxWidth(), | |
contentAlignment = Alignment.Center | |
) { | |
when (state) { | |
is RequestState.Idle -> { | |
onIdle?.invoke() | |
} | |
is RequestState.Loading -> { | |
onLoading?.invoke() | |
} | |
is RequestState.Error -> { | |
onError?.invoke(state.getErrorMessage()) | |
} | |
is RequestState.Success -> { | |
onSuccess(state.getSuccessData()) | |
} | |
} | |
} | |
} | |
} |
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.Arrangement | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.material3.Button | |
import androidx.compose.material3.Card | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.Text | |
import androidx.compose.material3.darkColorScheme | |
import androidx.compose.material3.lightColorScheme | |
import androidx.compose.runtime.Composable | |
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.text.font.FontWeight | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
private val LightColorScheme = lightColorScheme( | |
primary = Color(0xFF6200EE), | |
onPrimary = Color.White, | |
secondary = Color(0xFF03DAC6), | |
onSecondary = Color.Black, | |
background = Color(0xFFF5F5F5), | |
onBackground = Color.Black, | |
surface = Color.White, | |
onSurface = Color.Black | |
) | |
private val DarkColorScheme = darkColorScheme( | |
primary = Color(0xFFBB86FC), | |
onPrimary = Color.Black, | |
secondary = Color(0xFF03DAC6), | |
onSecondary = Color.Black, | |
background = Color(0xFF121212), | |
onBackground = Color.White, | |
surface = Color(0xFF1E1E1E), | |
onSurface = Color.White | |
) | |
@Composable | |
fun ThemeChange() { | |
var isDarkTheme by remember { mutableStateOf(false) } | |
val colorScheme = if (isDarkTheme) DarkColorScheme else LightColorScheme | |
MaterialTheme(colorScheme = colorScheme) { | |
Column( | |
modifier = Modifier | |
.fillMaxSize() | |
.background(MaterialTheme.colorScheme.background) | |
.padding(16.dp), | |
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), | |
horizontalAlignment = Alignment.CenterHorizontally | |
) { | |
Card( | |
modifier = Modifier.fillMaxWidth(), | |
) { | |
Column( | |
modifier = Modifier.padding(16.dp), | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
Text( | |
text = "Current Theme:", | |
fontSize = 18.sp, | |
fontWeight = FontWeight.Bold | |
) | |
Text( | |
text = if (isDarkTheme) "🌙 Dark Mode" else "☀️ Light Mode", | |
fontSize = 24.sp, | |
color = MaterialTheme.colorScheme.primary | |
) | |
} | |
} | |
Button( | |
onClick = { isDarkTheme = !isDarkTheme }, | |
modifier = Modifier.fillMaxWidth() | |
) { | |
Text( | |
text = if (isDarkTheme) "Switch to Light Theme" else "Switch to Dark Theme", | |
fontSize = 16.sp | |
) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment