DEV Community

Cover image for Organizing large Jetpack Compose code bases. My best version yet
Tristan Elliott
Tristan Elliott

Posted on • Updated on

Organizing large Jetpack Compose code bases. My best version yet

Table of contents

  1. Too long didn't read. Give me the code!!!
  2. Resources
  3. The most basic of organizations
  4. Does this work with Recomposition?
  5. Scoping to enforce Compose types
  6. Defining the Scopes

My app on the Google play store

Resources

Too long didn't read (TLDR)

  • Essentially, we are trying to create a design system that we can implement across our compose code base
  • step 1: organize your composables by wrapping them like this:
object SharedComponents {
@Composable
    fun TooLongDidNotRead(){
        Text("No time to read! I need the code now!")
    }

}

Enter fullscreen mode Exit fullscreen mode
  • step 2: Implement Scoping to enforce what type of composables go where

GitHub code

The most basic of organizations | Grouping

  • If you find your Jetpack Compose code confusing to understand and copy and pasting is faster than reusing, then you might need to do some reorganization.
  • The most basic of Jetpack compose organization is what I call grouping. Essentially, grouping takes place when you use Kotlin's object declaration to group similar jetpack compose components together. Like so,
/**
 * SharedComponents represents the most used and most stable versions of components used throughout this application
 * **/
object SharedComponents {

    /**
     * very fancy text*/
    @Composable
    fun FancyText(){

    }

    /**Less fancy text*/
    @Composable
    fun NormalText(){

    }

}
// how to use
SharedComponents.FancyText()


Enter fullscreen mode Exit fullscreen mode
  • While this reorganization seems trivial at first glance is really does give you a better understanding of what composables are from where.

Does this work with Recomposition?

  • So you are wondering if this will still allow compose to do recomposition even without the @Stable annotation. The answer is.... I think so. I tested it with this code:
var timesClicked by remember { mutableStateOf(0) }
    Testing.Combined(
        timesClicked.toString(),
        onClick = {timesClicked++}
    )

object Testing{
    @Composable
    fun Combined(
        timesClicked:String,
        onClick:()->Unit,
    ){

        Column() {
            RecomposeEveryTime(
                timesClicked,
                onClick = {onClick()}
            )
            DontReCompose()
        }
    }

    @Composable
    fun RecomposeEveryTime(
        timesClicked:String,
        onClick:()->Unit,
    ){
        Log.d("TESTINGRECOMPOSE","RecomposeEveryTime")

        Button(onClick ={onClick()}) {
            Text(
                timesClicked,
                fontSize = 30.sp,
                color = Color.Red
            )
        }
    }
    @Composable
    fun DontReCompose(){
        Log.d("TESTINGRECOMPOSE","DontReCompose ")
        Text(
            "NO RECOMPOSITION",
            fontSize = 30.sp,
            color = Color.Red
        )
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Judging from the log statements, only the RecomposeEveryTime() is recomposing (which is the desired effect) and DontReCompose() is not. So from these tests I can proudly say, I think recomposition works as intended

Scoping to enforce Compose types


    @Composable
    fun NoDrawerScaffold(
        topBar:@Composable ScaffoldTopBarScope.() -> Unit,
        bottomBar:@Composable ScaffoldBottomBarScope.() -> Unit,
        content:@Composable (contentPadding: PaddingValues,) -> Unit,

        ) {
        val topBarScaffoldScope = remember(){ScaffoldTopBarScope(35.dp)}
        val bottomBarScaffoldScope = remember(){ScaffoldBottomBarScope(25.dp)}

        Scaffold(
            containerColor = MaterialTheme.colorScheme.primary,
            topBar = {
                Row(modifier = Modifier
                    .fillMaxWidth()
                    .background(MaterialTheme.colorScheme.primary)){
                    with(topBarScaffoldScope){
                        topBar()
                    }
                }

            },
            bottomBar = {
                with(bottomBarScaffoldScope){
                    bottomBar()
                }
            },

            ) { contentPadding ->
            content(contentPadding)

        }
    }

Enter fullscreen mode Exit fullscreen mode
  • The composable function above uses ScaffoldTopBarScope and ScaffoldBottomBarScope to only allow composables of those instances to be allowed.
  • If you are wondering why we have to define an instance of the scope to first use it, like so:
with(topBarScaffoldScope){
                        topBar()
                    }

Enter fullscreen mode Exit fullscreen mode
  • It is because of the way that extention functions work in kotlin. ScaffoldTopBarScope.() -> Unit defines a extension function on an instance of ScaffoldTopBarScope. So to be able to properly use topBar() we need an instance of ScaffoldTopBarScope

Defining the Scopes

  • When defining scopes we need to do so in a very particular way:
@Stable
class ScaffoldBottomBarScope(
    private val iconSize: Dp, 
){
@Composable
    fun DualButtonNavigationBottomBarRow(){
         // composable function 
            Icon(
            imageVector = imageVector,
            contentDescription = contentDescription,
            tint = color,
            modifier = Modifier.size(iconSize)
        )
}

}

Enter fullscreen mode Exit fullscreen mode
  • Notice the @Stable annotation. This is done because of how Jetpack compose defines Stability. Long story short, @Stable allows recomposition to work as it is intended to

Please read the original Bumble tech post

  • If you are really looking to find a better organization stragety for your jetpack compose code, then please read the original Bumble tech article on this topic

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.

Top comments (0)