DEV Community

Cover image for My process for creating Custom Jetpack compose Components
Tristan Elliott
Tristan Elliott

Posted on • Edited on

My process for creating Custom Jetpack compose Components

Introduction

  • Recently I created a new feature for my Android app and this is the process I took to create it. The process consists of 3 main steps, 1) Design, 2) Implement, 3) Refactor
  • The point of this blog post is to be short and give beginners a systemic way they can approach creating custom Jetpack Compose components. The focus will not be on the code but instead on the process as a whole

2024-04-30 update

  • HERE is the latest design wiki for how I am create custom Jetpack Compose components

GitHub code

Table of contents

  1. Design
  2. Implementation
  3. Refactor

1) Design

  • This step is where we design what the component will do and how it will work. When designing the feature I like to break down everything in terms of Jetpack Composes' standard layout elements, the Row(in blue) and Column(in red). This step will give us a design looking like this:

feature design

  • While it might look a little simple and silly now, the blue Rows and red Columns will not only help us build the component but also show us where to begin when we start refactoring

2) Implementation

  • This part in the process is all about taking the design and getting it to work. Throw all ideas about clean coding practices away and just get the dang thing working. This is where spaghetti code is allowed, matter of fact here is my spaghetti code for my most recent feature. You don't need to understand what this code does, just notice how hard it is to understand what it actually does: ```

fun VaccinationCheck(){
var vaccineText by remember { mutableStateOf("") }
var dateText by remember { mutableStateOf(Date().toString()) }
val vaccineList = remember { mutableStateListOf()}

val convertedDate = Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
val selectedDate = remember { mutableStateOf<LocalDate?>(convertedDate) }
val calendarState = rememberSheetState()

CalendarDialog(
    state = calendarState,
    config = CalendarConfig(
        monthSelection = true,
        yearSelection = true
    ),
    selection = CalendarSelection.Date(selectedDate = selectedDate.value){ newDate ->

        selectedDate.value = newDate
        dateText = newDate.toString()

    }
)
Column(){
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
    ) {
        //I NEED A TEXT INPUT AND A DATE INPUT
        OutlinedTextField(
            modifier = Modifier.weight(1.5f),
            singleLine = true,
            value = vaccineText,
            onValueChange = { vaccineText = it },
            textStyle = TextStyle(fontSize = 20.sp),
            placeholder = {
                Text(text = "Vaccination", fontSize = 20.sp)
            }

        )
        OutlinedTextField(
            modifier = Modifier
                .clickable { calendarState.show() }
                .weight(1f),
            enabled = false,
            value = selectedDate.value.toString(),
            onValueChange = { dateText = it },
            textStyle = TextStyle(fontSize = 20.sp),
            placeholder = {
                Text(text = dateText, fontSize = 20.sp)
            }

        )
    }
    //INSIDE THE COLUMN
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
    ){
        Spacer(Modifier.weight(1.5f))
        Button(
            enabled = vaccineText.length >1,
            onClick = {
                val text = "$vaccineText " + selectedDate.value
                val check = vaccineList.indexOf(text)
                if(check == -1){
                    vaccineList.add(text)
                }

            },
            modifier = Modifier.weight(1f)
        ) {
            Text(text = "Vaccinate")
        }
    }
    LazyColumn(modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 8.dp)
        .height(150.dp)

    ){
        itemsIndexed(
            items = vaccineList,
            key ={index:Int,item:String ->item.hashCode() + index}
        ){ index:Int, item:String ->
            /***********SETTING UP SWIPE TO DISMISS****************/
            val currentVaccine by rememberUpdatedState(item)//references the current vaccine
            val dismissState = rememberDismissState(
                confirmStateChange = {

                    vaccineList.remove(item)
                    true
                }
            )
            SwipeToDismiss(state = dismissState, background ={} ) {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 8.dp)
                        .border(
                            BorderStroke(2.dp, Color.LightGray),
                            shape = RoundedCornerShape(8)
                        )
                ){
                    Text(text = item, fontSize = 20.sp,modifier = Modifier.padding(8.dp))
                }

            }
        }



    }

}
Enter fullscreen mode Exit fullscreen mode

}



- As you can see from the code above, its a maze and almost impossible to understand what is going on. But guess what? IT WORKS!! which means we can move on to the next step

###3) Refactor <a name="refactor"></a>
- This is the step where we have to start implementing some good coding practices. For writing clean compose code I recommend these 2 resources:

**1)**[Twitter Compose Ruleset](https://twitter.github.io/compose-rules/)
**2)**[Always provide a Modifier parameter](https://chris.banes.me/posts/always-provide-a-modifier/)

- Using the resource above my compose function now looks like this:
Enter fullscreen mode Exit fullscreen mode

@OptIn(ExperimentalMaterialApi::class)
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun VaccinationCheck(){
var vaccineText by remember { mutableStateOf("") }
var dateText1 by remember { mutableStateOf(Date().toString()) }
val vaccineList = remember { mutableStateListOf()}

val convertedDate = Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
val selectedDate = remember { mutableStateOf<LocalDate?>(convertedDate) }
val calendarState = rememberSheetState()

CalendarView(
    calendarState,
    selectedDate = selectedDate,
    updateDateText = {
        newDate -> dateText1 = newDate
    }
)

Column(){
    VaccineRow(
        vaccineText =vaccineText,
        updateText = {text -> vaccineText = text},
        calendarState = calendarState,
        selectedDate = selectedDate,
        dateText = dateText1,
        updateDateText = {dateText ->dateText1 = dateText },
        modifier = Modifier.fillMaxWidth()
    )

    VaccineButtonRow(
        vaccineText = vaccineText,
        addToVaccineList ={
            val text = "$vaccineText " + selectedDate.value
            val check = vaccineList.indexOf(text)
            if(check == -1){
                vaccineList.add(text)
            }
        },
        modifier = Modifier.fillMaxWidth()

    )

    VaccineLazyColumn(
        vaccineList = vaccineList,
        removeItemFromList ={item -> vaccineList.remove(item)},
        modifier = Modifier.fillMaxWidth()
    )


}
Enter fullscreen mode Exit fullscreen mode

}

- Way better right? I used the Rows and Columns from our design stage to refactor our code. Taking each Row and Column and putting it into it's own component

- I hope this short blog post gave you a starting point for creating cleaner compose code.

###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](https://twitter.com/AndroidTristan).
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
theplebdev profile image
Tristan Elliott • Edited

@artesanoandroid This article is a little out of date. You can visit the design wiki I created, HERE, for a more update demonstration of creating custom components

  • Also, HERE is my article on modeling complex state in jetpack compose. It has been performing well in the metrics and has been featured in a few news letters