This cheat sheet is primarily focused on the Jetpack Compose part of Android plus on the Kotlin Multiplatform projects. So, not all described can be used for multiplatform projects, but most of them can be used in Android projects based on KMP + Compose. It's a good start for multiplatform projects when we migrate existing Android projects to KMP.
Compose is a modern toolkit for building native Android UI. It simplifies and accelerates UI development.
- It's
functions
(CamelCase style!). - Composable accept parameters.
- Use
MutableState
andremember
to manage state. - It can run parallel, run frequently, and run in any order.
- Can be skipped (if not visible, don't need to be recomposed).
- Compose guidelines
- API Guidelines for Jetpack Compose
- API Guidelines for
@Composable
components in Jetpack Compose - Kotlin for Compose
- JetBrains KMP samples
- TV maniac
- Clean architecture for Android
- Pokedex Compose
- Android showcase
- Compose playground - web
- Test toolkit: kotest - How to use
- Marathon - Cross platform test runner
- konsist - samples
- detekt
- ktlint
- ktlint-compose-rules
- danger
- kotlinx-kover Kotlin code coverage tool like JaCoco
- acompanist - Android only? - permissions, navigation, adaptive screens
implementation("androidx.navigation:navigation-compose:$nav_version")
- sqldelight
- Room from 2.7.0-alpha01 /
- DataStore from 1.1.0
- JetBrains Compose resources
- Moko (resources, geo, permissions, etc)
- lyricist resources L10n
- Image loader: coil (Coil 3 is KMP ready)
- Horologist bundle of libraries for Wear OS
.editorconfig
# We can use global .editorconfig file for personal (all projects) settings.
# https://blog.danskingdom.com/Reasons-to-use-both-a-local-and-global-editorconfig-file/
root = false
[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{kt,kts}]
max_line_length = 140
indent_size = 4
# Don't allow any wildcard imports
ij_kotlin_packages_to_use_import_on_demand = unset
# Prevent wildcard imports
ij_kotlin_name_count_to_use_star_import = 99
ij_kotlin_name_count_to_use_star_import_for_members = 99
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ktlint_code_style = ktlint_official
ktlint_standard = enabled
# Compose
ktlint_function_naming_ignore_when_annotated_with=Composable, Test
[*.md]
trim_trailing_whitespace = false
max_line_length = unset
Use annotation class
to create a preview.
@Preview(
name = "Light Mode",
group = "Light / dark mode",
uiMode = Configuration.UI_MODE_NIGHT_NO
)
@Preview(
name = "Dark Mode",
group = "Light / dark mode",
uiMode = Configuration.UI_MODE_NIGHT_YES
)
annotation class PreviewLightDarkMode
Usage:
@PreviewFotnScale
@PreviewLightDarkMode
@Composable
fun SomePreview() {
...
}
Will create both preview just by annotating the function. We can create same for font size, etc..
Used to apply a background color, shape, border, elevation, etc. to a composable function.
Text color will be used as on[Surface]
color when we use MaterialTheme.colorScheme.surface
.
Used to create a layout with a top bar, bottom bar, and a body. Solve issues with drawing under status bar, navigation bar, etc. Just provide padding to child composable.
Scaffold(modifier) { paddingValues ->
Surface(modifier = modifier.padding(paddingValues)) {
...
}
Used to create a vertical layout.
Used to create a horizontal layout.
We can save scroll position / state by using rememberLazyListState()
.
@Composable
fun LazyColumnList() {
val state: LazyListState = rememberLazyListState()
LazyColumn(state = state) {
items(items)
}
}
We can pass composable lambda function as a parameter to split the code into smaller parts.
@Composable
fun SomeComposable(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Surface(modifier = modifier) {
content()
}
}
remembered
is used to remember the state of a composable function.
It is used to store the state of a composable function and recompose the composable function when the state changes.
@Composable
fun ViewName(item: Item) {
val checkedState: MutableState<Item?> = remembered {
mutableStateOf(null)
}
Checkbox(checked = { checkedState.value == item })
}
Don't use =
, use by
keyword instead (not need call it with .value
).
@Composable
fun ViewName(item: Item) {
var checkedState: MutableState<Item?> by remembered {
mutableStateOf(null)
}
Checkbox(
checked = { checkedState == item },
onCheckedChange = { checkedState = it }
)
}
Use remember
to retain the state across recompositions.
We can go with the rememberSaveable
to retain the state across configuration changes, but due to the limitation with the data type compatibility (which can be solved with some boilerplate code),
better to use ViewModel.
@Composable
fun ViewName(item: Item) {
val checkedState: Item? by rememberSaveable { mutableStateOf(null) }
Checkbox(checked = { checkedState == item })
}
Remember can store lists as well. But don't store it by adding items to the list, use mutableStateOf
instead.
In case, that we call list.addAll()
on remembered list, it can duplicate items or call recomposition multiple times and have performance issues.
val list = remember {
mutableStateListOf<Item>().apply { addAll(getAllTasks()) }
}
Hoist state to at least the lowest common ancestor of the composable functions that need to access it.
The lowest common ancestor can also be outside of the Composition. For example, when hoisting state in a ViewModel
because business logic is involved.
Should be stored as close as possible to the composable function that uses it. Or use hoisting.
- For simple states, we can use
remember
ormutableStateOf
. - For more complex states we can use State Holders (simple kotlin class).
- For states that are shared between multiple composable functions, we use
ViewModel
. - For states that require business logic, we use
ViewModel
.
We can transform Flow
to State
by using collectAsState()
to keeps track of state changes.
class ScreenViewModel {
val flow: Flow<String> = flowOf("Some text")
}
@Composable
fun Screen(viewModel: ScreenViewModel) {
val state by viewModel.flow.collectAsState()
}
Stability - Annotation @Stable
and @Immutable
Annotations are used to optimize the performance of Compose. It don't need recompose in case that compose function know, that data will not change.
When we mark class as @Immutable
we create an agreement, that data will never change in that class. If we need to change something, we need to create a new instance of that class (using copy
for
example...)
Be careful with @Immutable
annotation, because it is preferred before checking what happen with data inside class. We can violate the contract and get unexpected behavior.
For example we change data in List
or Map
inside @Immutable
class! This can't happen.
@Immutable
class Data {
val isVariableImmutable = true
}
When we mark class as @Stable
we create an agreement, that we will notify Compose function about data changes using mutableStateOf()
.
@Stable
class Data {
var isVariableStable by mutableStateOf(false)
}
How to choose proper animation type.
Use animate[Dp|Size|Offset|*]AsState
.Animation is interruptible (can be cancelled by new animation).
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp
)
Expand / collapse animation for show items
AnimatedVisibility(
visible = expanded
) {
// content
TextView()
}
Expand / collapse same item (for example maxLines for Text item)
var expanded = remember { mutableStateOf(false) }
Text(
text = "Some\nmultiline\ntext",
maxLines = if (expanded) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.animateContentSize()
.clickable { expanded = !expanded }
)
Animate content changes
AnimatedContent(
targetState = content,
transitionSpec = {
fadeIn(animationSpec = tween(300))
slideIntoContainer(animationSpec = ..., towards = Up).with(slideOutOfContainer(..., towards = Down))
}
) { targetState ->
when (targetState) {
State.Screen1 -> Screen1()
State.Screen2 -> Screen2()
}
}
Animate progress bar
val progress by animateFloatAsState(
targetValue = currentProgress / totalProgress.toFloat(),
)
LinearProgressIndicator(progress = progress)
Animate rotation (like background around image)
val infiniteTransition = rememberInfiniteTransition()
val rotateAnimation = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
easing = LinearEasing
)
)
Image(
modifier = Modifier
.drawBehind {
rotate(rotateAnimation.value) {
drawCircle(rainbowColorBrush, style = Stroke(borderWidth))
}
}
.padding(borderWidth)
.clip(CircleShape)
)
modifier = modifier
.background(MaterialTheme.colorScheme.background, shape = CircleShape) // for search bars, icons, etc... circle whole item
.background(MaterialTheme.colorScheme.background, shape = MaterialTheme.shapes.medium) // rounded corners
)
Gradient cut border shape for inputs, etc...
val Gradient = listOf(...)
Modifier
.border(
border = BorderStroke(
width = 2.dp,
brush = Brush.linearGradient(Gradient)
),
shape = CutCornerShape(8.dp)
)
Gradient for cursor while typing in input field.
val Gradient = listOf(...)
BasicTextField(
cursorBrush = Brush.linearGradient(Gradient)
@Composable
fun MyApp(
windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
// Perform logic on the size class to decide whether to show the top app bar.
val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT
...
}