Skip to content

mtrakal/kmp-compose-cheatsheet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 

Repository files navigation

Kotlin Multiplatform + Compose cheatsheet

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.

What is Compose?

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 and remember 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).

Useful tools / links

Guidelines

Basic links

Codelabs

Interesting/sample projects

Design

K2

Testing

DI

Code style / quality / linters

Helpers

Navigation

  implementation("androidx.navigation:navigation-compose:$nav_version")

Networking

Database

Resources / L10n / I18n

UI

  • Image loader: coil (Coil 3 is KMP ready)

Logging

Security

iOS setup

Wear OS

Dependencies

Gradle configuration

Setting project

.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

Preview

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..

Components

Surface

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.

Scaffold

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)) {
        ...
    }

Column

Used to create a vertical layout.

Row

Used to create a horizontal layout.

LazyColumn

We can save scroll position / state by using rememberLazyListState().

@Composable
fun LazyColumnList() {
    val state: LazyListState = rememberLazyListState()

    LazyColumn(state = state) {
        items(items)
    }
}

Usage

Composable as a parameter to reduce parameters

Thinking in Compose

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()
    }
}

Remember state

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.

State

Should be stored as close as possible to the composable function that uses it. Or use hoisting.

  • For simple states, we can use remember or mutableStateOf.
  • 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.

Animated graph / chart

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)

Adaptive UI

@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
    ...
}

About

Kotlin Multiplatform (KMM) with Compose cheatsheet

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published