Android Compose 첫 도입 후기
View 만 사용하다가 Compose 첫 사용
먼저, 코드를 보면 전체적인 구조가 기존 View와는 상당히 다른 것을 알 수 있습니다.
대한민국 식약처가 제약사에 대해 회수/폐기와 행정 처분을 내린 목록을 표시하는 화면입니다.
예제 코드 클래스 구성
- NewsFragment
- 뉴스 화면을 표시할 메인 Fragment
- NewsScreen
- 위 Fragment에 나타나는 Compose 기반 뉴스 화면
- RecallSuspensionScreen
- 회수 폐기 목록 화면
- RecallSuspensionViewModel
- 위 화면에서 쓰이는 ViewModel
View위에서도 Compose를 사용할 수 있습니다!
How to use Compose in XML layout in Android
NewsScreen
/**
* 뉴스 타입
*/
enum class ChipType {
RECALLS_SUSPENSION, ADMIN_ACTION
}
/**
* 뉴스 화면
*/
@Preview
@Composable
fun NewsScreen() {
var selectedChip by remember { mutableStateOf(ChipType.RECALLS_SUSPENSION) }
Column {
Text(
text = stringResource(id = com.android.mediproject.core.ui.R.string.news),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 16.dp, start = 24.dp, end = 24.dp)
)
ChipGroup(selectedChip, onChipSelected = { chip ->
selectedChip = chip
})
if (selectedChip == ChipType.RECALLS_SUSPENSION) RecallDisposalScreen()
else Text(text = "AdminAction")
}
}
/**
* 뉴스 타입 선택
*
* @param selectedChip 선택된 뉴스 타입
* @param onChipSelected 뉴스 타입 선택 시 호출되는 콜백
*/
@Composable
fun ChipGroup(selectedChip: ChipType, onChipSelected: (ChipType) -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 16.dp, bottom = 8.dp, start = 24.dp, end = 24.dp)
) {
CustomFilterChip(
title = stringResource(id = R.string.recallSuspension),
isSelected = selectedChip == ChipType.RECALLS_SUSPENSION,
type = ChipType.RECALLS_SUSPENSION
) {
onChipSelected(if (selectedChip == ChipType.RECALLS_SUSPENSION) ChipType.RECALLS_SUSPENSION else ChipType.ADMIN_ACTION)
}
Spacer(Modifier.width(8.dp))
CustomFilterChip(
title = stringResource(id = R.string.adminAction),
isSelected = selectedChip == ChipType.ADMIN_ACTION,
type = ChipType.ADMIN_ACTION
) {
onChipSelected(if (selectedChip == ChipType.RECALLS_SUSPENSION) ChipType.RECALLS_SUSPENSION else ChipType.ADMIN_ACTION)
}
}
}
/**
* 뉴스 타입 Chip
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomFilterChip(type: ChipType, title: String, isSelected: Boolean, onClick: (ChipType) -> Unit) {
FilterChip(
selected = isSelected,
onClick = { onClick.invoke(type) },
label = { Text(title, fontSize = 13.sp) },
shape = RoundedCornerShape(36.dp),
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = Color.Blue,
selectedLabelColor = Color.White,
disabledContainerColor = Color.White,
disabledLabelColor = Color.Blue
),
)
}
NewsFragment
NewsFragment.kt
@AndroidEntryPoint
class NewsFragment : BaseFragment<FragmentNewsBinding, NewsViewModel>(FragmentNewsBinding::inflate) {
override val fragmentViewModel: NewsViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
lifecycleOwner = viewLifecycleOwner
composeView.setContent {
NewsScreen()
}
}
}
}
RecallSuspensionScreen
회수 폐기 chip클릭 시 나오는 화면
RecallSuspensionScreen.kt
/**
* 회수 폐기 목록 표시
*/
@Preview
@Composable
fun RecallDisposalScreen(viewModel: RecallSuspensionViewModel = hiltViewModel()) {
val list = viewModel.recallDisposalList.collectAsLazyPagingItems()
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(
count = list.itemCount, key = list.itemKey(), contentType = list.itemContentType(
)
) { index ->
list[index]?.let { ListItem(it) }
if (index < list.itemCount - 1) Divider(modifier = Modifier.padding(horizontal = 24.dp))
}
when (list.loadState.append) {
is LoadState.NotLoading -> Unit
is LoadState.Loading -> {
item {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
}
}
}
is LoadState.Error -> TODO()
else -> TODO()
}
}
}
/**
* 회수 폐기 목록 아이템
*
* @param recallSuspensionListItemDto 회수 폐기 목록 아이템
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ListItem(recallSuspensionListItemDto: RecallSuspensionListItemDto) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 9.dp),
shape = RectangleShape,
onClick = {
recallSuspensionListItemDto.onClick?.invoke(recallSuspensionListItemDto)
},
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = CenterVertically,
) {
Text(
text = recallSuspensionListItemDto.product,
style = MaterialTheme.typography.titleMedium,
fontSize = 14.sp,
color = Color.Black,
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f),
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
Text(
text = recallSuspensionListItemDto.let {
if (it.recallCommandDate != null) it.recallCommandDate
else it.rtrlCommandDt
}!!.toJavaLocalDate().format(dateFormat),
fontSize = 12.sp,
modifier = Modifier.align(Alignment.CenterVertically),
color = Color.Gray,
maxLines = 1,
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = recallSuspensionListItemDto.rtrvlResn, fontSize = 12.sp, color = Color.Gray, maxLines = 1
)
}
}
}
private val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd")
결과 화면
View, Compose 비교
ViewModel은 View, Compose 모두 같습니다.
Compose를 사용하면서 알게된 View와 Compose의 차이점 및 장단점 입니다.
- View의 장점
- 안드로이드 개발의 기존 방식으로, 다양한 자료와 커뮤니티 지원이 있습니다.
- 현재까지의 모든 안드로이드 버전과 호환됩니다.
- View의 단점
- 상태 관리와 레이아웃 업데이트를 수동으로 처리해야 합니다.
- XML을 사용하기 때문에 코드와 레이아웃 파일 간에 이동이 필요합니다.
- 코드가 깁니다.
- Compose의 장점
- 선언적 UI로 인해 상태 관리가 쉽고, 코드 가독성이 높습니다.
- 코틀린 DSL을 사용하여 코드 작성과 동시에 UI를 프리뷰할 수 있습니다.
- 커스텀 뷰를 쉽게 만들 수 있으며, 재사용이 용이합니다.
- Compose를 사용하면 UI 작성 시 더 성능이 좋습니다.
- Compose의 단점
- 최소 API 레벨 21(Android 5.0 Lollipop) 이상만 지원합니다.
- 상대적으로 새로운 기술로서, 자료와 커뮤니티 지원이 View에 비해 상대적으로 적습니다.
Compose의 주요 개념
- State
- @Composable 함수로 화면 레이아웃을 구성
- Modifier
- Row, Column
State(상태)
Compose는 상태에 따라 UI가 자동으로 갱신 됩니다.
상태를 관리하기 위해 remember와 mutableStateOf와 같은 함수를 사용하여 상태를 저장하고 변경할 수 있습니다.
상태가 변경되면, 관련된 Composable 함수가 자동으로 재구성되어 UI를 갱신 합니다.
State 와 ViewModel
State와 ViewModel은 서로 유사한 역할을 합니다.
ViewModel에서 LiveData, Flow등으로 관련 화면의 생명주기 동안 필요한 데이터를 저장하고 유지하는 것이 가능한 것 처럼,
Compose의 State도 이와 유사한 역할을 합니다.
ViewModel에서 데이터를 저장 유지 하는 기능에 특화되었다고 생각하면 됩니다.
var selectedChip by remember { mutableStateOf(ChipType.RECALLS_SUSPENSION) }
위 코드는 enum class 의 속성을 State로 관리하는 것을 의미합니다.
만약 selectedChip의 값이 바뀐다면 자동으로 selectedChip의 값을 사용하는 레이아웃을 갱신합니다.
@Composable 함수로 화면 레이아웃을 구성
Compose에서 UI는 작은 조각으로 나누어진 Composable 함수로 구성됩니다.
이 함수를 통해 레이아웃을 구성합니다.
Composable 함수는 다른 Composable 함수를 호출하여 계층적인 레이아웃을 구성하는 것이 가능합니다.
ViewGroup내에 ViewGroup을 선언하는 것과 같은 역할입니다.
@Composable
fun RecallDisposalScreen(viewModel: RecallSuspensionViewModel = hiltViewModel()) {
}
위와 같은 방식으로 코드를 작성합니다.
Compose에서 ViewModel을 사용하려면 매개변수에 위와 같이 viewmodel()을 하면됩니다.
Modifier
Composable 함수의 레이아웃 디자인에 관한 설정을 할 때 사용합니다.
여러 가지 Modifier를 사용하여 레이아웃, 패딩, 배경색 등의 속성을 설정할 수 있습니다.
modifier = Modifier.padding(top = 16.dp, start = 24.dp, end = 24.dp)
위 내용은, View에서 setPadding() 또는 XML내에서 android:padding 속성으로 지정하는 것을 의미합니다.
Row, Column
- Row
- 수평 방향으로 레이아웃을 배치할 수 있습니다.
- LinearLayout 속성을 Horizontal로 설정한 것과 유사합니다.
- Column
- 수직 방향으로 레이아웃을 배치할 수 있습니다.
- LinearLayout 속성을 Vertical로 설정한 것과 유사합니다.
위 두 개를 이용해서 List화면을 구성하는 것이 가능합니다.
그러나 모든 List의 Item을 로드하여 화면에 표시하기 때문에 오버헤드가 큰 문제점이 있습니다.
View에서 ListView를 사용하는 것과 같다고 생각하면 됩니다.
오버헤드를 줄이기 위해 LazyColumn, LazyRow를 사용하는 것이 좋습니다.
LazyColumn와 LazyRow 는 View의 RecyclerView와 같은 역할을 합니다.
정리
써보니 생각보다 어렵지 않고, 코드가 확실히 줄어서 편합니다.