Skip to content

Instantly share code, notes, and snippets.

@stevdza-san
Last active October 9, 2025 14:46
Show Gist options
  • Save stevdza-san/05b564a93909bb9ae3dfdafaa2807048 to your computer and use it in GitHub Desktop.
Save stevdza-san/05b564a93909bb9ae3dfdafaa2807048 to your computer and use it in GitHub Desktop.
Jetpack Compose - Day 2
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)
)
}
}
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")
}
}
}
}
}
}
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())
}
}
}
}
}
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