DEV Community

Cover image for The rules I am using to organize and document my Jetpack Compose code
Tristan Elliott
Tristan Elliott

Posted on

The rules I am using to organize and document my Jetpack Compose code

Table of contents

  1. Introduction
  2. The Rules
  3. Identify the features
  4. Reworking the code
  5. Break everything into Parts
  6. Look for Builders
  7. Create the Builders
  8. Create the Implementations

My app on the Google play store

Disclaimer

  • I haven't checked any of this code for Jank or performance bottle necks, so there is a real possibility that this could not scale. However, if you do see any glaring mistakes or have any other questions. Please leave a comment down below.

GitHub code

Introduction

  • So in my previous post I talked about the architecture system I was building to better organize my Jetpack compose code. Now that I have reworked most of my code base into the BPI (Builder, Parts, Implementations) architecture. I am going to walk both of us through the rules that I have created and the approaches we should take when trying to organize old code into this style.

The rules

  • live list, HERE
  • If you click the link above you can see that I have created a live list of rules that I will be constantly updating as I run into new problems. Feel free to comment your own rules if you feel the need necessary .

Identify the features

  • So as your UI becomes more complex you are going to notice sections of your code that are only used in specific areas. It is important that we identify theses areas and label them. Here is a UI demonstration of what I am talking about

chat breakdown into parts

  • Notice how I have broken my chat functionality into 3 distinct parts, where AutoScrollChatWithTextBox contains AutoScrollingChat and EnterChatTextBox. In my actual code base, these parts are broke down even further into their own distinct parts. This sort of high level break down will allow you to get a better understanding of where the organization should occur.

  • While it may not initially seem impressive. This break down of code has allowed me to organize 2000 plus lines of Jetpack compose code into this single 36 line compose function. This is something that I am very proud of:

  • GitHub code

 @Composable
        fun ScrollableChat(
            noChatMode: Boolean,
            determineScrollState:@Composable () -> Unit,
            autoScrollingChat:@Composable () -> Unit,
            enterChat:@Composable (modifier:Modifier) -> Unit,
            scrollToBottom:@Composable (modifier:Modifier) -> Unit,
            draggableButton:@Composable () -> Unit,
        ){
            determineScrollState()
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Red)
            ) {
                autoScrollingChat()
                enterChat(
                    Modifier
                        .align(Alignment.BottomCenter)
                        .fillMaxWidth(),
                )
                scrollToBottom(
                    modifier = Modifier
                        .align(Alignment.BottomCenter)
                        .padding(bottom = 77.dp)
                )
                draggableButton()
                if(noChatMode){
                    Text(
                        "You are in no chat mode",
                        modifier = Modifier.align(Alignment.Center),
                        color = MaterialTheme.colorScheme.onPrimary,
                        fontSize = 20.sp
                    )
                }
            }
        }

Enter fullscreen mode Exit fullscreen mode
  • The rework its self took me 4 days. However, because of this rework I was able to seamlessly add the draggableButton() functionality in 20mins. This organization has also given me confidence in my UI code. I am confident that it will work and if it does not. I am even more confident that I will be able to find the problem
  • You can see this draggable button in action on my Twitter, HERE

Reworking the code

  • for an example of what the final product should look like, you can see my GitHub code HERE
  • Now we can talk about how we should approach reworking code. To make the rework easier on ourselves, we should follow these 4 steps in order:

1) Break everything into Parts
2) Look for Builders
3) Create the Builders
4) Create the Implementations

1) Break everything into Parts

  • When we code in compose, everything can usually be broken down into layout components, like a Row, or a Column. We can use this feature to break down our code into parts. We can start with a code like this:
@Composable
fun TestingParts(){
    Column() {
        Row() {
            Text("This is a string")
        }
        Row() {
            Button(onClick = { /*TODO*/ }) {
                Text(text ="Click me")
            }
        }

    }
}

Enter fullscreen mode Exit fullscreen mode
  • We can then break the two Rows down into their own Parts and we are left with this:
@Composable
fun TestingParts(){
    Column() {
        RowText()
        RowButton()
    }
}
@Composable
fun RowText(){
    Row() {
        Text("This is a string")
    }
}
@Composable
fun RowButton(){
    Row() {
        Button(onClick = { /*TODO*/ }) {
            Text(text ="Click me")
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Rules for Parts

  • 1) Parts can contain other parts
  • 2) If there are multiple parts inside another part. Consider making it a builder

Rules for documenting Parts

  • 1) Since parts can contain multiple parts, the first sentence should contain a declaration of how many parts it contains:
  • CustomTopBar contains 0 other parts or - CustomTopBar contains 2 other parts, [CustomText] and [CustomRow]
  • If their are multiple instances of the same part. They may be counted as 1 part and do not have to be listed twics
  • Here is an example:
 /**
         * - Contains 2 extra parts:
         * 1) [ImageWithViewCount]
         * 2) [StreamTitleWithInfo]
 * */


Enter fullscreen mode Exit fullscreen mode

2) Look for Builders

  • In this system I have defined Builders as, Builders represents the most generic sections of our code and should be thought of as UI layout guides used by the implementations.
  • A good way to identify a builder or at least a potential builder is to look for the composable with the most Parts:
@Composable
fun PossibleBuilder(){
    Column() {
        RowText()
        RowButton()
        RowDoubleButton()
        RowLargeButton()
        RowSmallButton()
    }
}
Enter fullscreen mode Exit fullscreen mode
  • So if you have a composable function that contains multiple parts like the code above. Then we have a possible Builder function

3) Create the Builders

  • Once we have identified the possible Builder, it is time to actually create it. We can do this with the help of Slot-based layouts and we transform our potential builder into an actual builder:
@Composable
fun PossibleBuilder(
    rowOne:@Composable () -> Unit,
    rowTwo:@Composable () -> Unit,
    rowThree:@Composable () -> Unit,
    rowFour:@Composable () -> Unit,
    RowFive:@Composable () -> Unit,
){
    Column() {
        rowOne()
        rowTwo()
        rowThree()
        rowFour()
        RowFive()
    }
}

Enter fullscreen mode Exit fullscreen mode

Rules for documenting Builders

  • The Rule for documenting Builders is this: first state where the builder is being used (what implementation). Followed by a short description and then the parameters
  • HERE is my example on GitHub

Rules for creating Builders

  • 1) Anything that uses a slot layout is automatically considered a Builder and should be labeled as such
  • 2) Avoid nesting Builder classes. If you are nesting multiple builders you may need to keep one builder as multiple parts and add it to another builder

4) Create the Implementations

  • Once you have created a Builder then creating the Implementation is a simple next step. A implementation is just a simple wrapper class around the builder, So our implementation would look like this:
@Composable
fun ImplementationButtons(){
    PossibleBuilder(
        rowOne={
               Row(){}
        },
        rowTwo={
            Row(){}
        },
        rowThree = {
            Row(){}
        },
        rowFour={
            Row(){}
        },
        rowFive={
            Row(){}
        }
    )
}

Enter fullscreen mode Exit fullscreen mode
  • As you can see, in its simplest form it really is just a wrapper class around the Builder class. Why? Because I really don't like the way the slot layout looks and I am using the wrapper implementation class to hide it.

Rules for creating Implementations

  • 1) if it does not contain a slot layout and is used elsewhere in the code base, then it goes at the top level and is considered an implementation
  • 2) the implementations should contain no business logic. It should be strictly a wrapper class that is exposed to the public

  • 3) Implementations can contain other implementations

  • 4) A implementation is the only code allowed to see other public sections of the code base.

Rules for documenting Implementations

  • 1) When documenting implementations, the first sentence should tell the user what builder they are using:
  • MainScaffoldComponent is the implementation of [Builder.ScaffoldBuilder]. Followed by a short description and then the parameters

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)