Compose 함수는 어떤 조건으로 나누는것이 좋을까?(Stateful, stateless)



개인 광고 영역

Compose 함수는 어떠한 조건을 사용해 나누는 것이 좋을까? 고려하면 좋을 것은 무엇일까?

  • 스크린으로 나눈다?
  • 컴포넌트로 나눈다?
  • 무조건 많이 나눈다?

답은 없지만 최소한 어떤 규칙을 가지고 나눌 수 있을지 고민하고 적용한 부분을 정리해 본다.


이 글에서 알아볼 내용

  • 코드의 분리 관점을 무엇으로 볼지를 고민해 본 글이다.
  • 글에 오류가 있을 수 있으니 참고만 하길


발표 자료


풀 코드를 통해 함수를 분리해 보자

작성한 코드는 @Composalbe MainScreen 함수 하나로 작성하였다.

이 코드는 총 311줄로 아래와 같은 단점이 생긴다.

  • 함수를 분리하지 않는 경우 새로운 Screen이 추가되면 기존 코드에 이어질 수 있다.
  • 기존 코드의 수정도 어렵다.

all

Github - MainActivityBackup.kt 전체 코드

이 코드는 어디까지나 분리 없이, 설명하기 위한 방식을 선택하기 위해 작성한 코드이다.

class MainActivityBackup : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyApplicationTheme {
                MainScreen(
                    list = listOf(
                        NavigationSample(
                            title = "HOME",
                            icon = R.drawable.baseline_home_24,
                            trigger = NavigationSample.Trigger.HOME,
                        ),
                        NavigationSample(
                            title = "WEB",
                            icon = R.drawable.baseline_web_24,
                            trigger = NavigationSample.Trigger.WEB,
                        ),
                    ),
                )
            }
        }
    }
}

/**
  * MainScreen의 전체 코드
  **/
@SuppressLint("SetJavaScriptEnabled")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MainScreen(
    list: List<NavigationSample>,
    navController: NavHostController = rememberNavController(),
) {
    val navBackStackEntry by navController.currentBackStackEntryAsState() // Navigation 활용을 위한 코드
    val currentDestination = navBackStackEntry?.destination
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = currentDestination?.route ?: "") },
            )
        },
        bottomBar = {
            NavigationBar {
                list.forEach { screen ->
                    NavigationBarItem(
                        label = { Text(text = screen.title) },
                        selected = currentDestination?.hierarchy?.any { it.route == screen.trigger.name } == true,
                        icon = { Icon(painter = painterResource(id = screen.icon), contentDescription = screen.title) },
                        onClick = {
                            navController.navigate(screen.trigger.name) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        },
        modifier = Modifier.fillMaxSize()
    ) {
        Box(
            modifier = Modifier.padding(it)
        ) {
            val context = LocalContext.current
            CompositionLocalProvider(LocalWebOwner provides WebView(context)) {
                NavHost(
                    navController = navController,
                    startDestination = NavigationSample.Trigger.HOME.name,
                    enterTransition = { EnterTransition.None },
                    exitTransition = { ExitTransition.None },
                ) {
                    composable(route = NavigationSample.Trigger.HOME.name) {
                        var listItem by remember { mutableStateOf(ListItem(emptyList())) }
                        Column {
                            LazyColumn(
                                contentPadding = PaddingValues(20.dp),
                                verticalArrangement = Arrangement.spacedBy(10.dp),
                                modifier = Modifier.fillMaxWidth().weight(1f)
                            ) {
                                items(listItem.items) { item ->
                                    Surface(shape = MaterialTheme.shapes.small) {
                                        if (item.editMode) {
                                            Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
                                                var changeItem by remember { mutableStateOf(item) }
                                                TextField(
                                                    value = changeItem.text,
                                                    onValueChange = { new -> changeItem = changeItem.copy(text = new) },
                                                    modifier = Modifier.fillMaxWidth()
                                                )

                                                Row {
                                                    Button(
                                                        onClick = {
                                                            listItem = listItem.copy(
                                                                items = listItem.items.map { listItem ->
                                                                    if (listItem.index == item.index) {
                                                                        changeItem.copy(editMode = false)
                                                                    } else {
                                                                        listItem
                                                                    }
                                                                },
                                                            )
                                                        },
                                                        modifier = Modifier.weight(1f)
                                                    ) { Text(text = "Save") }

                                                    Button(
                                                        onClick = { listItem = listItem.copy(items = listItem.items.toMutableList().also { newList -> newList.remove(item) }) },
                                                        modifier = Modifier.weight(1f).padding(start = 10.dp)
                                                    ) { Text(text = "X") }
                                                }
                                            }
                                        } else {
                                            Column(modifier = Modifier.fillMaxWidth().background(color = Color.Gray.copy(0.3f))) {
                                                Row {
                                                    Text(
                                                        text = item.text,
                                                        modifier = Modifier.weight(1f).padding(horizontal = 16.dp).padding(top = 16.dp)
                                                    )

                                                    IconButton(onClick = { listItem = listItem.copy(items = listItem.items.toMutableList().also { newList -> newList.remove(item) }) }) {
                                                        Icon(painter = painterResource(id = R.drawable.baseline_close_24), contentDescription = "remove")
                                                    }
                                                }

                                                Button(
                                                    onClick = {
                                                        listItem = listItem.copy(
                                                            items = listItem.items.map { listItem ->
                                                                if (listItem == item) {
                                                                    listItem.copy(editMode = true)
                                                                } else {
                                                                    listItem
                                                                }
                                                            },
                                                        )
                                                    },
                                                    modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).padding(top = 10.dp, bottom = 16.dp)
                                                ) {
                                                    Text(text = "edit")
                                                }
                                            }
                                        }
                                    }
                                }
                            }

                            Button(
                                onClick = {
                                    listItem = listItem.copy(items = listItem.items.toMutableList().also { list -> list.add(ListItem.Item(index = list.size, text = "", editMode = true)) })
                                },
                                modifier = Modifier.padding(20.dp)
                            ) {
                                Text(text = "New")
                            }
                        }
                    }

                    composable(route = NavigationSample.Trigger.WEB.name) {
                        val chromeClient = CustomWebChromeClient()
                        val client = CustomWebViewClient()

                        val webView = LocalWebOwner.current

                        var url by remember { mutableStateOf("https://thdev.tech/") }

                        Column(modifier = Modifier.fillMaxSize()) {
                            TextField(
                                value = url,
                                onValueChange = { new -> url = new },
                                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
                                keyboardActions = KeyboardActions(onSearch = { webView?.loadUrl(url) }),
                                modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).padding(bottom = 8.dp)
                            )

                            BoxWithConstraints(modifier = Modifier.weight(1f)) {
                                val width = if (constraints.hasFixedWidth) { ViewGroup.LayoutParams.MATCH_PARENT } else { ViewGroup.LayoutParams.WRAP_CONTENT }
                                val height = if (constraints.hasFixedHeight) { ViewGroup.LayoutParams.MATCH_PARENT } else { ViewGroup.LayoutParams.WRAP_CONTENT }
                                val layoutParams = FrameLayout.LayoutParams(width, height)

                                LaunchedEffect(key1 = Unit) { webView?.loadUrl(url) }

                                BackHandler {
                                    if (webView?.canGoBack() == true) {
                                        webView.goBack()
                                    }
                                }

                                AndroidView(
                                    factory = {
                                        val parentLayout = FrameLayout(context).apply {
                                            val web = webView ?: WebView(context)

                                            web.apply {
                                                this.layoutParams = layoutParams
                                                settings.run {
                                                    javaScriptEnabled = true
                                                    defaultTextEncodingName = "UTF-8"
                                                    loadWithOverviewMode = true
                                                    useWideViewPort = true
                                                    setSupportZoom(true)

                                                    mixedContentMode =
                                                        WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
                                                    setNetworkAvailable(true)
                                                    cacheMode = WebSettings.LOAD_DEFAULT

                                                    setSupportMultipleWindows(true)
                                                }

                                                webChromeClient = chromeClient
                                                webViewClient = client
                                            }
                                            addView(web)
                                        }
                                        parentLayout
                                    },
                                    onRelease = { parentFrame ->
                                        (parentFrame.children.first() as? WebView)?.let { web ->
                                            parentFrame.removeView(web)
                                        }
                                    }
                                )
                            }
                        }
                    }
                }
            }
        }
    }
}


어떤 식으로 분리할까?

함수를 분리하는 룰은 다양할 수 있다.

  • 클린 코드 방식을 적용하여 함수는 하나의 역할과 맡는 이름을 가지도록 한다
  • 선언형 UI에서 이야기하는 Stateful, Stateless를 이해한 분리

클린 코드 방식으론 최대한 많이 나누고, 적절한 함수명을 써주는 것이 맞다. 하지만 선언형 UI를 작성하는 컴포즈이니 우린 선언형 UI 방식을 따라 Preview 가능한 형태로 만들어보도록 한다.


위 코드를 몇 가지로 나눠보자

Navigation을 활용하고 있으니 크게 몇 가지로 나눌 수 있는데, 필자가 일반적으로 분리하는 방식은 아래와 같다.

  • Screen : 화면 단위로 나눈다
  • Components : Screen에 맞는 Components
  • Design Components : 일반적인 디자인 시스템의 Components

Screen은 화면인데 화면도 여러 개일 수 있다. Navigation을 포함하는 화면도 Screen이고, Navigation에 포함하는 Composable 역시 Screen이다.

Screen에 포함하는 단위 몇 가지는 Components 단위를 합쳐서 만든 UI로 분리할 수 있다.

그리고 마지막으로 Design Components는 Material design을 감싸 내부 UI를 구현하는 부분이다. 이 부분을 잘 나누면 팩토리처럼 합쳐서 하나의 화면 구성하는 것이 가능하다.


HomeScreen 분리

MainScreen에서 HomeScreen을 우선 분리해보았다.

@Composable
internal fun HomeScreen() {
    var listItem by remember { mutableStateOf(ListItem(emptyList())) }

   Column {
       LazyColumn {
           items(listItem.items) { item ->
               Surface(
                   shape = MaterialTheme.shapes.small,
               ) {
                   if (item.editMode) {
                       Column() { /* 생략 */ }
                   } else {
                       Column() { /* 생략 */ }
                   }
               }
           }
       }

       Button(
           onClick = { /* 생략 */ },
           modifier = Modifier
               .padding(20.dp)
       ) {
           Text(text = "New")
       }
   }
}

@Preview(showBackground = true)
@Composable
private fun PreviewHomeScreen() {
   HomeScreen()
}

이 코드에 대한 Preview는 아래와 같은데, 적합하지 않은 Preview인데, 그냥 흰 화면에 하단 버튼 하나만 보이는 정도이다.

step_01

이제 이 코드를 기반으로 Preview 가능하도록 수정하는 방법을 이야기해 본다.


Stateful versus stateless

Compose에서 제공하는 공식 문서에서 Stateful과 Stateless에 대한 설명을 포함하고 있다.

State and Jetpack Compose - 공식 문서

Stateful과 Stateless를 통해 코드를 분리할 수 있는데,

  • Stateful
    • remember를 사용해 객체를 저장하는 Composable 포함
  • stateless
    • 외부에서 값을 변경하거나 이벤트로 전달할 수 있는 형태

stateful은 remember를 포함하여 상태를 갖는 Composable 구성하기에 테스트하기 어렵다. 그래서 stateless 형태를 만들고, 외부에서 값을 전달하고, 필요한 event를 외부로 전달 받아 사용할 수 있도록 작업하는 것이다.

그래서 보통은 3가지로 나누는 게 가능하다.

  • Stateful : ViewModel을 통해 값을 받거나, remember {}를 통해 값을 임시 저장하는 부분
  • Stateless : 외부에서 주입하는 값에 따라 화면이 그려지고, 이벤트를 외부로 보낼 수 있는 함수
  • Preview : Stateless를 이용한 Preview를 처리


Stateful

stateful은 외부에서 viewModel을 통해 값을 remember로 감싸거나, 상태를 기억하도록 만드는게 주 목적이다.

@Composable
internal fun HomeScreen() {
   val listItem by remember { mutableStateOf(ListItem(emptyList())) }

   HomeScreen(listItem) {
    // Event...
   }
}


Stateless

stateless는 외부 값의 주입을 받을 수 있도록 작성한다.

이렇게 해야 Preview를 처리할 수 있고, 재사용 가능한 형태도 만들 수 있다.

@Composable
internal fun HomeScreen(listItem: ListItem, onClick: () -> Unit) {
   LazyColumn {
       items(listItem.items) { item ->
           Text(
               text = item.text,
               style = MaterialTheme.typography.bodyMedium,
               modifier = Modifier
                    .clickable(onClick = onClick)
           )
       }
   }
}

이를 도식화하면 아래와 같다.

udf-hello-screen

stateless로 코드를 작성한다는 것은 아래와 같은 장점을 가진다.

이 내용은 구글 공식 문서에 제공하는 내용으로 원 문서를 참고하면 좋다.

  • 단일 정보 소스: 상태를 복제하는 대신 옮겼기 때문에 정보 소스가 하나만 있습니다. 버그 방지에 도움이 됩니다.
  • 캡슐화됨: 스테이트풀(Stateful) 컴포저블 만 상태를 수정할 수 있습니다. 철저히 내부적 속성입니다.
  • 공유 가능함: 호이스팅한 상태를 여러 컴포저블과 공유할 수 있습니다. 다른 컴포저블에서 name을 읽으려는 경우 호이스팅을 통해 그렇게 할 수 있습니다.
  • 가로채기 가능함: 스테이트리스(Stateless) 컴포저블의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정할 수 있습니다.
  • 분리됨: 스테이트리스(Stateless) 컴포저블의 상태가 저장될 수 있습니다. 액세스할 수 있습니다 예를 들어 이제 name를 ViewModel로 이동할 수 있습니다.


다시 돌아와서

이전에 작성한 HomeScreen 함수에 대해 아래와 같이 Stateful을 작성한다.

@Composable
internal fun HomeScreen() {
   var listItem by remember { mutableStateOf(ListItem(emptyList())) }

   HomeScreen(
       listItem = listItem,
       onEvent = {
           listItem = it
       }
   )
}

이전에 HomeScreen에서는 remember {} 부분을 지우고 외부에서 주입받을 수 있도록 parameters로 변경한다.

@Composable
private fun HomeScreen(
   listItem: ListItem,
   onEvent: (listItem: ListItem) -> Unit,
) {
    // 생략
}

이렇게 수정하면서 Preview가 가능해졌고, 좀 더 유연하게 사용할 수 있게 되었다.

@Preview(
   showBackground = true,
)
@Composable
private fun PreviewHomeScreen() {
   var listItem by remember {
       mutableStateOf(
           ListItem(
               listOf(
                   ListItem.Item(
                       index = 0,
                       text = "message",
                       editMode = false,
                   ),
                   ListItem.Item(
                       index = 1,
                       text = "",
                       editMode = true,
                   ),
               )
           )
       )
   }

   HomeScreen(
       listItem = listItem,
       onEvent = {
           listItem = it
       }
   )
}

이 코드에 대한 Preview는 아래와 같다.

한 번에 editMode와 viewMode를 확인할 수 있다.

step_02


Preview의 장점

Preview를 사용함으로써 다양한 기기 사이즈의 Preview를 바로 확인할 수 있다.

  • 테블릿, 폴더블, 일반 기기, 작은 기기 등

코드 상황에 따라 빠른 테스트도 가능하다.


Screen에 맞는 Components 분리

Screen을 분리하더라도 여전히 코드 양이 많다. Screen 용 Components를 분리하면 상황에 맞는 코드를 좀 더 빠르게 확인할 수 있다.

여기서는 크게 2개를 나눌 수 있다.

EditMode ViewMode
값 입력 및 삭제 수정 및 View
step_03_01 step_03_02


EditMode

분리하는 건 이전과 동일한데, 여기선 stateful이 필요하진 않아 stateless를 기준으로 분리한다.

외부 주입 기준이 HomeScreen에서 값을 저장하고, event를 처리하고 있기 때문에 HomeItemEdit에서는 별도로 처리할 부분이 없다.

@Composable
internal fun HomeItemEdit(
   item: ListItem.Item,
   onEditModeOff: (changeItem: ListItem.Item) -> Unit,
   onCancel: () -> Unit,
) {
   Column(
       modifier = Modifier.fillMaxWidth().padding(16.dp)
   ) {
       var changeItem by remember(item) { mutableStateOf(item) }
       TextField(
           value = changeItem.text,
           onValueChange = { new -> changeItem = changeItem.copy(text = new) },
           modifier = Modifier.fillMaxWidth()
       )
       Row {
           Button(
               onClick = { onEditModeOff(changeItem) },
               modifier = Modifier.weight(1f)
           ) { Text(text = "Save") }

           Button(
               onClick = { onCancel() },
               modifier = Modifier.weight(1f).padding(start = 10.dp)
           ) { Text(text = "X") }
       }
   }
}


EditMode Preview

EditMode에 대해 Stateless로 작업하였기에 아래와 같이 바로 테스트 가능한 형태를 만들 수 있다.

@Preview(showBackground = true)
@Composable
private fun PreviewHomeItemEdit() {
   var item by remember {
       mutableStateOf(ListItem.Item.NEW)
   }
   HomeItemEdit(
       item = item,
       onEditModeOff = { changeItem ->
           item = changeItem.copy(
               editMode = false,
           )
       },
       onCancel = {
           item = ListItem.Item.NEW
       },
   )
}

Preview는 아래와 같다. 이때의 장점은 마우스를 통해 런타임 체크도 가능하다는 점이다.

step_03_03


ViewMode

ViewMode 역시 동일한 방식으로 Stateless로 작성한다.

@Composable
internal fun HomeItemView(
   item: ListItem.Item,
   onRemove: () -> Unit,
   onEditMode: () -> Unit,
) {
   Column(
       modifier = Modifier.fillMaxWidth().background(color = Color.Gray.copy(0.3f))
   ) {
       Row {
           Text(
               text = item.text,
               modifier = Modifier.weight(1f).padding(horizontal = 16.dp).padding(top = 16.dp)
           )

           IconButton(
               onClick = { onRemove() },
           ) {
               Icon(painter = painterResource(id = R.drawable.baseline_close_24), contentDescription = "remove")
           }
       }

       Button(
           onClick = { onEditMode() },
           modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).padding(top = 10.dp, bottom = 16.dp)
       ) {
           Text(text = "edit")
       }
   }
}


ViewMode - Preview

@Preview(showBackground = true)
@Composable
private fun PreviewHomeItemView() {
   var item by remember {
       mutableStateOf(
  ListItem.Item.NEW.copy(text = "message~!!!!\naaaa")
   )
   }

   HomeItemView(
       item = item,
       onRemove = {
           // Do nothing
       },
       onEditMode = {
           item = item.copy(
               editMode = true,
           )
       }
   )
}

View에서는 event 처리에 대한 코드가 별거 없으니 가볍게 넘어갈 수 있다.

step_03_04


Tip

Stateless에서 Stateful 코드를 부르면 결국 stateless로 작성하는 것이 크게 의미가 없단 소리다.

당연히 이렇게 작성할 일은 없고, stateless에서는 stateless로 작성한 코드를 부르는 것이 당연하지만 맞는 것이다.


몇 가지 알아두면 좋을 것

함수는 어디까지 나누는 게 맞을까?

아래와 같이 무작정 많이 나누는 것이 올바를 것인가?

/**
* Button의 재사용이 불가, Save 전용
* 함수를 나눈다는 장점은 있지만 필요할까?
*/
@Composable
private fun SaveButton(
   changeItem: ListItem.Item,
   modifier: Modifier,
   onEditModeOff: (changeItem: ListItem.Item) -> Unit,
) {
   Button(
       onClick = { onEditModeOff(changeItem) },
       modifier = modifier
   ) {
       SaveText()
   }
}

/**
* Text 하나 뿐인데 굳이...?
*/
@Composable
private fun SaveText() {
   Text(text = "Save")
}

이 코드에서는 Button에 포함하는 SaveText까지 함수로 나누었다. Button에는 TextStyle과 TextColor까지 포함하고 있다. 별도의 스타일을 지정하지 않으면 상위룰을 따라가게 된다.

그래서 굳이 이렇게까지 나눌 필요는 없다.

이보다는 아래와 같이 나누는 것이 더 좋다.

/**
* Button을 재사용할 수 있도록 수정
*/
@Composable
private fun HomeButton(
   onClick: () -> Unit,
   modifier: Modifier = Modifier,
   text: String,
) {
   Button(
       onClick = onClick,
       modifier = modifier
   ) {
       Text(text = text)
   }
}

/**
* 사용하는 경우
*/
Row {
   HomeButton(
       onClick = { },
       text = "Save",
       modifier = Modifier
           .weight(1f)
   )
   HomeButton(
       onClick = { },
       text = "x",
       modifier = Modifier
           .weight(1f)
           .padding(start = 10.dp)
   )
}

Text만을 가지는 Button을 기본으로 만들고, 이를 활용한 Preview 2가지를 해보면 아래와 같다.

step_04_01


Modifier의 위치

Modifier는 권장으론 필수 값 바로 다음에 Modifier가 오도록 적용해야 한다.

아래의 코드는 옵션 값 바로 다음에 Modifier를 적용했고, 필수 값은 맨 아래에 두었다. 권장하는 순서와 하나도 맞지 않다.

@Composable
private fun HomeButton(
   onClick: () -> Unit = {},
   modifier: Modifier = Modifier,
   text: String,
) {
   Button(
       onClick = onClick,
       modifier = modifier
   ) {
       Text(text = text)
   }
}

이를 권장 가이드대로 수정하면 아래와 같이 수정되어야 한다.

@Composable
private fun HomeButton(
   text: String,
   modifier: Modifier = Modifier,
   onClick: () -> Unit = {},
) {
   Button(
       onClick = onClick,
       modifier = modifier
   ) {
       Text(text = text)
   }
}


Modifier를 적용할 때는

Modifier를 사용할 때는 함수의 파라미터 순서 그대로 사용할 수도 있지만 개인적으론 Modifier를 최 하단으로 이동시켜 사용하고 있다.

@Preview(showBackground = true)
@Composable
private fun PreviewHomeButton() {
   Row {
       /* 필자가 추천하는 위치 */
       HomeButton(
           onClick = {},
           text = "Save",
           modifier = Modifier
               .weight(1f)
       )

       /* 일반적인 위치? */
       HomeButton(
           onClick = {},
           modifier = Modifier
               .weight(1f)
               .padding(start = 10.dp),
           text = "x",
       )
   }
}

개인적으론 Modifier 관련 설정이 계속 길어지다 보니 하단으로 내려 사용하고 있다.


Container에 따른 Modifier 위치는?

외부에서 바라볼 때는 당연하게도 container에 modifier가 적용될 것으로 기대할 수 있다. 하지만 아래와 같은 코드라면?

@Composable
private fun HomeButton(
   text: String,
   modifier: Modifier = Modifier,
   onClick: () -> Unit = {},
) {
   Button(
       onClick = onClick,
   ) {
       Text(
           text = text,
           modifier = modifier
       )
   }
}

이 코드는 적합하지 않다. 어느 누구도 Text에 modifier가 적용될 거라고 생각하지 못하기 때문이다.


디자인 컴포넌트

디자인 컴포넌트는 꼭 필수는 아니지만 있는 것이 좋다. 회사마다 디자인 시스템을 가지고 있는 경우도 있겠지만, 아닌 경우도 있을 것이다. 아닌 경우라고 하더라도 공통화를 해두면 나중에 수정하기 좋아진다.

  • Text
    • Text를 앱 내의 공통 Components로 작성할 경우 TextStyle 등을 한 번에 적용하는 것이 가능
  • Button
    • 디자인 가이드에 따라 Button을 공통화하고, 디자인을 적용할 수 있다(사용의 편리)
  • TextField
    • 디자인 가이드에 따라 TextField을 공통화

와 같은 정도로 분리할 수 있을 것이다.

여기서는 위에 Tip으로 작성한 Button 부분을 디자인 컴포넌트로 분리하고 이를 적용하는 방법을 적어본다.


Button Components

공용 버튼 컴포넌트는 아래와 같이 작성할 수 있다.

@Composable
fun ExampleButton(
   text: String,
   onClick: () -> Unit,
   modifier: Modifier = Modifier,
   enabled: Boolean = true,
   contentPadding: PaddingValues = ExampleButtonDefaults.ContentPadding,
   textStyle: TextStyle = ExampleButtonDefaults.defaultTextStyle,
   colors: ExampleButtonColors = ExampleButtonDefaults.filledButtonColors(),
) {
   Button(
       onClick = onClick,
       colors = ButtonDefaults.buttonColors(
           containerColor = colors.containerColor().value,
           contentColor = colors.contentColor().value,
       ),
       modifier = modifier
           .defaultMinSize(minHeight = ExampleButtonDefaults.MinHeight)
   ) {
       Text(
           text = text,
           style = textStyle,
           color = colors.contentColor().value,
           modifier = Modifier
               .padding(contentPadding)
       )
   }
}


Parameters 종류

Parameters는 크게 2개로 나뉘는데

  • 필수 값 : 함수에 꼭 주입해야 할 정보
  • 옵션 값 : 코틀린에 기본 값을 정의한다.

이 옵션 값에도 2 가지로 나뉜다.

  • 옵션 - inline : 누가 보아도 바로 확인할 수 있는 true/false 와 같은 정보
  • 옵션 - Defaults : 옵션 Defaults는 내부에서 정의한 값, 높이나 스타일이나 padding 등의 정보

이를 잘 반영해 디자인 시스템을 분리할 수 있다.


마무리

이 글에서는 MainScreen에서 작성한 코드를 HomeScreen > HomeScreen에 대한 Components > 디자인 Components까지 분리하는 과정을 담았다.

꼭 필요한 부분을 포함하여 작성하였고, 다음 글에서는 CompositionLocal과 관련한 글을 이어서 작성한다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments