Blog Infos
Author
Published
Topics
, , , ,
Published

Everyone builds the composables as they find them useful, convenient and readable. You may have noticed that basic Jetpack Compose components have quite similar parameter structures, naming conventions and behaviour.

It is because there is the guide for it, how to create your composables, so they are easily readable, reasonable and explicit for other programmers in your team or community.

I decided to give you a shorter glimpse and interpretation, what are the most useful ones with some context/practicality tips, because not all of them are intended to be used for application programming, but rather a library creation.

Simplicity over complexity

First of all, it is always better to have more single-purpose composables than massive behemoths with multiple functionalities. This is not about that you should not merge multiple composables into one screen.

The best way is to create your own modified versions of the buttons, checkboxes, textfields etc. and those components compose together to create specific blocs of the screen.

If you try to create complex composables, which tackle a lot of problems at the same time, you will spend more time fixing issues, which could have been isolated to certain components and easily reachable.

Firstly create low-level blocks, which are reusable across whole project and move on to higher-level blocks and screens afterwards. The higher you go, the more limited modifications should be as you would open ways to tarnish the UI’s consistency. Example:

// LOW-LEVEL BLOCS

// custom body text
@Composable
fun BodyText(...) {
}

// icon with custom modifier for whole app
@Composable
fun AppIcon(...) {
}

/// HIGHER-LEVEL BLOCS
@Composable
fun IconTextButton(@StringRes val title: Int, @DrawableRes val drawableId: Int, modifier: Modifier) =
    Surface(
        modifier = modifier,
    ) {
        Row() {
            AppIcon(
                painter = painterResource(id = item.drawableId),
                contentDescription = null
            )
            Spacer()
            BodyText(
                text = stringResource(id = title),
            )
        }
    }

/// SCREEN-LEVEL
@Composable
fun NavigationScreen() {
    Column {
        IconTextButton(...)
        IconTextButton(...)
        IconTextButton(...)
        IconTextButton(...)
    }
}

You will get:

  • better readability
  • less ambiguity and more confidence
  • more flexibility
  • easily extensible codebase
Consistency over customisations

This is highly personal, but I prefer more consistency over customisations. Because, if the app has consistent implementation, the maintenance is much simpler, faster and cheaper.

Of course, customers, managers, and UX people will bring to the table more ideas on how to make every single screen better for that special use case.

But I always recommend cracking down on it a tiny bit and getting everything as consistent as possible (here comes the importance of planning and discussing ahead).

Unfortunately, we do not live in perfect world and these things will happen.

Here is what you can do from a code perspective.

Do not attempt to create additional styling objects beyond MaterialTheme.

// DON'T
@Composable
fun Button(style: ButtonStyle, ...) {
    ...
}

You can create multiple themes, if it is required and scope them for specific screens.

Do create consistent MaterialTheme and create separated Composables to serve the specific purpose like PrimaryButtonBackButtonToggleButton or TitleTextSubtitleTextBodyText.

You can still wrap these buttons and make additional changes, but it should be more intentional than just out of spite or a special request.

These low level composables will create you after some time small library, which can be housed independently and used at other projects! If it is the same company, both apps will look consistent. I usually have one module or folder, which is dedicated for all such composables.

If you are about to create a modified version, prefer explicit inputs to implicit behaviour of the Composable. The easiest way is to put the default option into the definition of the function.

Avoid using edge case null parameters or inaccessible classes — rather define default behaviour with default Jetpack compose components.

// create your own versions of the text as independent composables
@Composable
fun TitleText(@StringRes id: int, ...) {
    Text(
        text = stringResource(id = id),
        style = MaterialTheme.typography.titleLarge
    )
}

@Composable
fun BodyText(@StringRes id: int, ...) {
    Text(
        text = stringResource(id = id),
        style = MaterialTheme.typography.bodyMedium
    )
}

// in need of modification, modify already existing one 
// without adding more styles - add more explicit inputs with defaults
@Composable
fun OtherTitleText(@StringRes id: int, textAlign = TextAlign.Center, ... ) {
    TitleText(id = id, textAlign = textAlign)
}
Input parameters
Modifier

Every composable should contain only one Modifier as an optional parameter in input, which modifies the surrounding visuals of your @Composable.

Donts and Dos:

  • do put the modifier behind the required parameters
  • don’t pass other styles — do define material design or specific composable
  • don’t put multiple modifiers as input — do separate it into another composable
  • don’t modify the inner parts of the composable — do modify the surroundings of the composable with the modifier
Order
  1. Required parameters
  2. modifier (only one modifier)
  3. optional parameter
  4. composable trailing

It feels much more intuitive as most of the default composables have the same structure and it should become your habit to write such composables.

Here is an example:

@Composable
fun AppButton(
    // required might not be even needed, 
    // because modifier has most of the required parameters 
    // such as clickable {}
    
    // modifier
    modifier: Modifier = Modifier,
    // optional
    enabled: Boolean = true,
    // composable trailing
    text: @Composable () -> Unit,
    ) {}
// usage
AppButton(
    modifier = Modifier.padding(bottom = 30.dp).clickable {},
) {
    Text(...)
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

With the advent of Android 15, edge-to-edge design has become the default configuration. Consequently, applications must be capable of accommodating window insets, including the system status bar and navigation bar, as well as supporting drawing…
Watch Video

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android developer
Seven Principles Mobility GmbH

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android develop ...
Seven Principles Mob ...

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android developer
Seven Principles Mobility ...

Jobs

State

Do not post channels or mutable states into the composables. The state should be handled within the ViewModel. UI should not be aware of any logic handling, only reacting to the current state and passing UI events between the ViewModel and the actual user.

The Composable input should only contain an immutable state, which the ViewModel can rebuild.

There are 2 ways of passing values, which I prefer.

  • The first one is passing pure kotlin data class as a result of the composable.
  • Second one is the passing function intended for reading the value — this delays the reading of the value to the points, which are needed.
data class SomeData(val text: String) 

// pass the data directly to the composable
@Composable
fun TitleText(text: SomeData) {
    Text(text, ...)
}

// delay getter by passing function
@Composable
fun MainScreen(text: () -> String){
}

// invocation with function
MainScreen({ someData.text })
Slot inputs

Slot input is another set of composables, which are used in the hierarchy of the composable. For example, column or row use your composables as slot input.

Creating simple composables as it was described on the top is one way of dealing with a lot of variants of the text, button, switch, etc.

But me and others are always tempted to write something like this:

@Composable
fun TextButton(
    text: String,
){
    ...
}

If you go with this solution, this is not a sign you are a bad programmer, but there is another way how to make this composable a bit more flexible.

You can pass in Text composable directly and take advantage of patterns for simple composables and start to compose them together.

Here is an example:

@Composable
fun BoldBodyText(
    text: String
) {
    ...
}

@Composable
fun PrimaryButton(
    content: @Composable () -> Unit
) {
    ...
    content()
    ...
}
// usage / composable which could be extracted as individual component
PrimaryButton {
    BoldBodyText("My text")
}
Semantics

Semantics are an inseparable part of the Jetpack Compose as they are used for testing and for accessibility purposes for people with special needs.

If you create higher-level composables, you can benefit from Modifier.semantics(mergeDescendants = true), where all the semantics are merged as one node and can be perceived as one item from a testing and accessibility perspective. clickable and toggleable are doing it by default.

To learn more about end-to-end testing and the usage of semantics in testing, I recommend reading my other article about it here:

End-To-End Testing With Robot Pattern And Jetpack Compose
Make your end-to-end tests more explicit and readable for everyone
proandroiddev.com

To learn more about accessibility and removal of testTags from the codebase, you can read my other article about it here:

https://proandroiddev.com/stop-using-test-tags-in-the-jetpack-compose-production-code-b98e2679221f?source=post_page—–0602e558b722——————————–

Conclusion

All of these tips and conventions act as a guide on how to make your codebase better, but they are only silver bullet for some of the issues.

You will find situations, where it is just better to break the convention or amend it in a way, that is beneficial for your project and colleagues. There may be a better way for your purpose.

But, if you build a general-purpose library for multiple projects or open-source solutions, then it is essential to keep them intact.

Thanks for reading and do not forget to follow for more!

Resources

https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-component-api-guidelines.md?source=post_page—–0602e558b722——————————–

For more about Android development:

https://tomas-repcik.medium.com/list/android-development-3a15a240889a?source=post_page—–0602e558b722——————————–

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu