DEV Community

Cover image for The architectural patterns I am using to better organize my Jetpack Compose code
Tristan Elliott
Tristan Elliott

Posted on • Updated on

The architectural patterns I am using to better organize my Jetpack Compose code

Table of contents

  1. The problem I have
  2. Parts
  3. Builder
  4. Implementations
  5. Bonus documentation
  6. Reworking old code (updated:2023-12-05)
  7. Making the API cleaner (updated:2023-12-06)

My app on the Google play store

Video version

  • I would recommend watching the video as it is more up to date then this article

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

The problem I have

  • Ok, I have more these two dialogs, which as you can see are very similar:

Ban Dialog

Timeout Dialog

  • But I have two separate code bases for them and I am not sharing any code between them. Which is quite a problem when I want to make more dialogs. Also, a time consumer when I want to implement new features or changes to the both of them. So to organize the code and hopefully cut down on development time, I have developed a little organization strategy:

(PBI architecture for short)
1) Parts :
2) Builder:
3) Implementations:

1) Parts

  • First thing to do is to create an entire new file called Dialogs and fill it with this object expression:
object DialogParts{

}

Enter fullscreen mode Exit fullscreen mode
  • Then we want to add all the composables that could be used inside a dialog and add it to our DialogParts:
  • Checkout my full parts code on GitHub, HERE

object DialogParts{
@Composable
    fun SubHeader(
        secondary: Color,
        onPrimary: Color,
        subTitleText:String
    ){
        Divider(color = secondary, thickness = 1.dp, modifier = Modifier.fillMaxWidth())
        Text(subTitleText,color = onPrimary, fontSize = 20.sp)
    }
// and so on

}

Enter fullscreen mode Exit fullscreen mode
  • Organizing compose code with an object expression is not new, I actually read about it HERE. Then to use this composable we simply go: DialogParts.SubHeader. I have read that some people do not like the object expression because it is so verbose. However, when dealing with a large code base simply seeing DialogParts.SubHeader tells us two things. 1)It is part of a dialog and 2) it is a subheader(smaller than a main header). It is actually because of it verboseness that I like it. We can even add documentation(which you can checkout on my GitHub) and give the reader even more understandings on what is going on.

2) Builder

  • In the same Dialog file we are going to use the object expression technique again but with the name DialogBuilder:
object DialogBuilder {



}
Enter fullscreen mode Exit fullscreen mode
  • Inside of this builder, we want to make the most generic possible version of our Dialogs. It is this generic version of our Dialog that will be the template to create actual Dialog implementation. Then we are going to use the slot based layouts to add our differentiation to the Dialog. Using this pattern, our generic Dialog would look like this:
  • Full GitHub code here
object DialogBuilder {
@Composable
    fun RadioButtonDialog(
        dialogHeaderContent:@Composable () -> Unit,
        dialogSubHeaderContent:@Composable () -> Unit,
        dialogRadioButtonsContent:@Composable () -> Unit,
        dialogTextFieldContent:@Composable () -> Unit,
        dialogConfirmCancelContent:@Composable () -> Unit,
        onDismissRequest: () -> Unit,
        primary: Color,
        secondary: Color
    ){
        Dialog(onDismissRequest = { onDismissRequest() }) {
            Card(
                modifier = Modifier
                    .fillMaxWidth(),
                shape = RoundedCornerShape(16.dp),
                backgroundColor = primary,
                border = BorderStroke(2.dp, secondary)
            ) {
                Column(
                    modifier = Modifier
                        .padding(10.dp)
                        .background(primary)
                ) {
                    dialogHeaderContent()
                    dialogSubHeaderContent()
                    dialogRadioButtonsContent()
                    dialogTextFieldContent()
                    dialogConfirmCancelContent()
                }
            }
        }

    }
}

Enter fullscreen mode Exit fullscreen mode
  • Again, with this code we are trying to make a generic Dialog. Where someone could look at this RadioButtonDialog and just know what it needs to be built(Header, SubHeader...). Ultimately, the goal is to be able to tell someone, hey, I need you to create a Dialog. Inside the DialogBuilder I want you to implement a new RadioButtonDialog and they would have a easy understanding of what you want.

3) Implementations

  • Now the implementations, as you can imagine, are a little messy but I believe I have gotten the complexity down to a level that is understandable. With this implementation, I want to be able to look at the code and know two things: 1) what is each individual part doing and 2) what are we building with this code. So the implementation code would look like this:
  • Full GitHub code here
@Composable
    fun BanDialog(
        onDismissRequest: () -> Unit,
        username: String,
        banDuration: Int,
        banReason: String,
        changeBanDuration: (Int) -> Unit,
        changeBanReason: (String) -> Unit,
        banUser: (BanUser) -> Unit,
        clickedUserId: String,
        closeDialog: () -> Unit,
        closeBottomModal: () -> Unit
    ) {
DialogBuilder.RadioButtonDialog(
            dialogHeaderContent = {DialogParts.DialogHeader(username,stringResource(R.string.ban),onPrimary) },
       dialogSubHeaderContent = {
         DialogParts.SubHeader(
             secondary = secondary,
             onPrimary = onPrimary,
             subTitleText = stringResource(R.string.duration_text)
                )
            },
dialogRadioButtonsContent = {},
dialogTextFieldContent = {},
dialogConfirmCancelContent = {}
onDismissRequest = { onDismissRequest() },
primary =primary ,
secondary = secondary,

}

Enter fullscreen mode Exit fullscreen mode
  • I like this code a lot, mainly because I can know literally nothing about this code base and by simply looking at DialogBuilder.RadioButtonDialog I know that we are building a Dialog and its core feature is probably going to be radio buttons.

  • Overall I like this combination of using the Slots layout and object expressions. I think it makes the code very readable and explicit. However, don't think this explicit code does not need documentation!!! We 100000% need documentation and you can see the documentation I created, HERE

Bonus documentation

  • If we want to get real fancy (I know I do) we can use GitHub pages to implement a make shift style guide to let people reading our documentation have a visual demonstration of what we are trying to build
  • A simple version(1.0) of a style guide that I have created can be found, HERE
  • It can also be seen in my documentation, HERE

Reworking old code (updated:2023-12-04)

  • Undocumented GitHub code
  • So I was working through my old code and trying to rework it into this pattern. However, I have found out that all these sloting layouts are really ugly and can be even more confusing if we have a large amount of them. So, I recommend that we still follow the P.B.I architecture but we try to hide the sloting layouts as much as possible. Here is the example I am working through with my chat layout:

1) Builders

object TwitchIRCSystemNotificationsBuilder{
    @Composable
    fun SystemChat(
        messageHeader:@Composable () -> Unit,
        messageText:@Composable () -> Unit,
    ){
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.Black.copy(alpha = 0.6f))
        ) {
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(15.dp)
            ) {
                messageHeader()
                messageText()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • The code above (builder section) has the sloting layout, which is fine and we are going to keep it that way. However, we want to hid that from out pretty UI code as much as possible

2) Parts:

object ChatMessagesParts{
    @Composable
    fun MessageHeader(
        contentDescription:String,
        iconImageVector: ImageVector
    ){
        Icons.Default.Lock
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.Start,
            modifier = Modifier.fillMaxWidth()
        ) {
            Icon(
                imageVector = iconImageVector,
                contentDescription =contentDescription,
                modifier = Modifier
                    .size(30.dp),
                tint = Color.White
            )
            Text(stringResource(R.string.sub), color = Color.White, fontSize = 20.sp)
        }
    }
// and so on....
}

Enter fullscreen mode Exit fullscreen mode
  • The code above is the Parts section, and nothing has changed from the code that parts section we made previously. The interesting part is next:

3) Implementations :

  • So this is the part that is a little different and is what I have learned from reworking old code
object SystemChats {

    @Composable
    fun ResubMessage(
        message: String?,
        systemMessage: String?
    ) {
        TwitchIRCSystemNotificationsBuilder.SystemChat(
                    messageHeader = {
                        ChatMessagesParts.MessageHeader(
                            contentDescription = stringResource(R.string.re_sub),
                            iconImageVector =Icons.Default.Star
                        )
                    },
                    messageText = {
                        ChatMessagesParts.MessageText(
                            message =message,
                            systemMessage =systemMessage
                        )
                    }
      }

Enter fullscreen mode Exit fullscreen mode
  • So our implementations now exist in their own object expression. Now, to create a clean API to use them we do this:
SystemChats.ResubMessage(
           message = "Twitch user message",
           systemMessage = "Twitch system message"
           )
Enter fullscreen mode Exit fullscreen mode
  • This makes it so the slot layouts are now hid from public view and we have created a really clean API

Making API cleaner (updated:2023-12-04)

  • Undocumented GitHub code example
  • So last night I came to the realization that we can make this design API even cleaner by only allowing individuals to access the implementations. We can do this with nested object expressions:
object SystemChats {
      // implementations go here
     private object TwitchIRCSystemNotificationsBuilder{
            // builders go here
     }

     private object ChatMessagesParts{
             // chat parts go here
     }

}

Enter fullscreen mode Exit fullscreen mode
  • Now outside users of this code, only have access to implementations and NOT TwitchIRCSystemNotificationsBuilder and ChatMessagesParts composables. Meaning they can only use the code we want them to!!! Slowly but surely we are developing an actuall design system

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)