카카오뱅크 클론코딩으로 화면 구성 중 중요했던 Chip 컴포넌트와 Retrofit 연결에 대해 정리해보았습니다.📝
Chip 컴포넌트
칩(Chip)이란?
- 칩(Chip)은 작은 UI 요소로, 주로 선택지 제공, 필터링, 태그 지정 또는 입력된 데이터를 표시하는 데 사용
- 칩은 라벨, 아이콘, 선택 상태 등을 포함 가능
- 칩은 사용자가 클릭하여 선택하거나 선택 해제할 수 있는 대화형 요소로 사용
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LoanCategoryChips() {
val chipItems = listOf(
"신용대출", "중신용대출", "비상금대출", "신용대출 갈아타기", "개인사업자 신용대출",
"개인사업자 보증서대출", "중고차 구매대출", "저당대출", "전월세보증금 대출",
"전월세보증금 대출 갈아타기", "담보대출", "주택담보대출", "주택담보대출 갈아타기"
)
val selectedChips = remember { mutableStateListOf<String>() }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "대출",
style = H6_B,
color = Black,
modifier = Modifier.padding(bottom = 16.dp)
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(18.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.fillMaxWidth()
) {
chipItems.forEach { chip ->
SelectableChip(
text = chip,
isSelected = selectedChips.contains(chip),
onClick = {
if (selectedChips.contains(chip)) {
selectedChips.remove(chip)
} else {
selectedChips.add(chip)
}
}
)
}
}
}
}
@Composable
fun SelectableChip(
text: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val backgroundColor = if (isSelected) White else Light_Gray
val textColor = if (isSelected) Main_Yellow else Dark_Gray
val borderColor = if (isSelected) Main_Yellow else Light_Gray
Box(
modifier = Modifier
.background(backgroundColor, shape = RoundedCornerShape(16.dp))
.clickable(
interactionSource = NoRippleInteractionSource,
indication = null,
onClick = {
onClick()
}
)
.border(
BorderStroke(1.dp, borderColor),
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = text,
color = textColor,
style = B4_R
)
}
}
LoanCategoryChips Composable
LoanCategoryChips는 대출 카테고리 칩 목록을 표시하는 메인 컴포저블
- chipItems 리스트:
- 대출 카테고리의 이름을 포함한 문자열 리스트
- "신용대출", "중고차 구매대출", "담보대출" 등
- selectedChips 상태 관리:
- 사용자가 선택한 칩을 관리하기 위해 mutableStateListOf를 사용
- Jetpack Compose의 상태 관리 기능(remember)을 사용하여 UI와 동기화
- UI 구성:
- Column: 세로로 UI를 정렬
- Text: "대출"이라는 제목을 표시
- FlowRow: 칩들을 행렬 형태로 정렬
- 가로와 세로 간격(spacedBy)을 설정하여 깔끔하게 배치
- 칩 동작:
- chipItems 리스트의 각 항목에 대해 SelectableChip을 호출하여 칩 생성
- 클릭 이벤트(onClick)로 선택 상태를 토글(추가/삭제)
SelectableChip Composable
이 컴포저블은 각각의 칩 요소를 정의
- 입력 매개변수:
- text: 칩의 텍스트 라벨
- isSelected: 칩이 현재 선택 상태인지 여부
- onClick: 칩이 클릭될 때 호출되는 람다 함수
- 스타일링:
- 배경색:
- 선택된 경우(isSelected == true): White
- 선택되지 않은 경우: Light_Gray
- 텍스트 색상:
- 선택된 경우: Main_Yellow
- 선택되지 않은 경우: Dark_Gray
- 테두리 색상:
- 선택된 경우: Main_Yellow
- 선택되지 않은 경우: Light_Gray
- 배경색:
- 레이아웃:
- Box를 사용해 칩의 모양과 클릭 동작을 정의
- RoundedCornerShape(16.dp)로 모서리를 둥글게 처리
- clickable로 클릭 이벤트 등록
- border와 padding을 통해 칩의 외형 조정
- 텍스트:
- Text 컴포저블을 사용해 칩 내부에 텍스트를 렌더링
동작 과정
- LoanCategoryChips는 대출 카테고리 리스트(chipItems)를 반복하면서 SelectableChip을 생성합니다.
- 각 칩은 클릭 가능한 상태로, 클릭 시 selectedChips 리스트에 해당 카테고리를 추가하거나 제거합니다.
- 선택된 칩은 isSelected가 true로 설정되어 다른 색상과 스타일로 표시됩니다.
Jetpack Compose와 Retrofit을 활용한 사용자 리스트 화면 구현하기
1. Retrofit을 이용한 API 호출 설정
우선, REST API를 호출하기 위해 Retrofit 라이브러리를 사용합니다.
UserService는 사용자 정보를 받아오는 인터페이스입니다.
interface UserService {
@GET("api/users")
suspend fun getUsers(
@Query("page") page: Int,
): ResponseUserDto
}
ServicePool과 ApiFactory
Retrofit 인스턴스는 ApiFactory 객체에서 생성됩니다. 이를 통해 다양한 서비스 인터페이스를 쉽게 생성할 수 있습니다.
object ApiFactory {
val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.build()
}
inline fun <reified T> create(): T = retrofit.create(T::class.java)
}
object ServicePool {
val userService = ApiFactory.create<UserService>()
}
포인트
- baseUrl: API의 기본 URL을 설정합니다.
- Json.asConverterFactory: JSON 데이터를 Kotlin 객체로 변환하기 위해 사용됩니다.
2. 데이터 모델링
API에서 받은 JSON 데이터를 Kotlin 객체로 매핑하기 위해 @Serializable과 @SerialName을 사용했습니다.
ResponseUserDto는 API 응답의 전체 구조를 정의하며, 내부에 User와 Support 모델을 포함합니다.
@Serializable
data class ResponseUserDto (
@SerialName("page") val page: Int,
@SerialName("per_page") val perPage: Int,
@SerialName("total") val total: Int,
@SerialName("total_pages") val totalPages: Int,
@SerialName("data") val data: List<User>,
@SerialName("support") val support: Support
)
@Serializable
data class User (
@SerialName("id") val id: Int,
@SerialName("email") val email: String,
@SerialName("first_name") val firstName: String,
@SerialName("last_name") val lastName: String,
@SerialName("avatar") val avatar: String
)
@Serializable
data class Support (
@SerialName("url") val url: String,
@SerialName("text") val text: String
)
3. ViewModel을 통한 데이터 관리
Jetpack의 ViewModel과 LiveData를 사용하여 데이터 상태를 관리합니다.
MainViewModel은 사용자 데이터를 API로부터 가져오고, 성공 또는 실패를 처리합니다.
class MainViewModel : ViewModel() {
private val _users = MutableLiveData<List<User>>() // 내부에서 수정 가능한 데이터
val users: LiveData<List<User>> get() = _users // 외부에서 읽기만 가능한 데이터
fun getUsers() {
viewModelScope.launch {
runCatching { ServicePool.userService.getUsers(page = 2) }
.onSuccess {
_users.value = it.data
Log.d("MainViewModel", "getUsers: ${it.data}")
}
.onFailure {
_users.value = emptyList()
Log.e("MainViewModel", "getUsers: ${it.message}")
}
}
}
}
포인트
- viewModelScope.launch: Coroutine을 통해 비동기 작업을 수행합니다.
- runCatching: 네트워크 요청 성공과 실패를 처리하기 위한 간단한 방식입니다.
- _users: MutableLiveData를 통해 데이터를 업데이트하고, 외부에서는 읽기 전용으로 사용합니다.
4. Compose를 활용한 UI 구성
Compose로 UI를 구성하며, API에서 가져온 데이터를 LazyVerticalGrid를 사용해 표시합니다.
AccountScreen은 메인 화면을 정의하며, UserItem은 사용자 정보를 표시하는 개별 아이템을 나타냅니다.
메인 화면 (AccountScreen)
@Composable
fun AccountScreen(navController: NavController) {
val mainViewModel: MainViewModel = viewModel()
val users by mainViewModel.users.observeAsState(emptyList())
LaunchedEffect(Unit) {
mainViewModel.getUsers()
}
Column(
modifier = Modifier
.fillMaxSize()
.background(White)
) {
AccountCard()
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "전문가와 상담하기",
color = Black,
style = H6_B,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
items(users) { user ->
UserItem(user)
}
}
}
}
사용자 카드 UI (UserItem)
@Composable
fun UserItem(user: User) {
Column(
modifier = Modifier
.padding(8.dp)
.clip(RoundedCornerShape(8.dp))
.background(White)
.border(1.dp, Light_Gray, RoundedCornerShape(8.dp))
.fillMaxWidth()
.height(160.dp)
) {
Column (
modifier = Modifier.padding(top = 20.dp, start = 20.dp)
) {
Text(
text = user.firstName,
color = Black,
style = B3_B
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = user.email,
color = Black,
style = B4_R
)
}
Spacer(modifier = Modifier.height(10.dp))
AsyncImage(
model = user.avatar,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(top = 20.dp, start = 100.dp)
.size(60.dp)
.clip(RoundedCornerShape(30.dp))
)
}
}
포인트
- LazyVerticalGrid: 사용자 리스트를 격자 형태로 구성합니다.
- AsyncImage: URL에서 이미지를 로드하는 데 사용됩니다.
- LaunchedEffect: Compose의 생명주기에 따라 ViewModel의 데이터를 가져옵니다.
'Group Study (2024-2025) > Android' 카테고리의 다른 글
[Android] 카카오뱅크 클론 코딩 (4) (4) | 2024.12.18 |
---|---|
[Android] 카카오뱅크 클론 코딩 (2) (0) | 2024.11.27 |
[Android] 카카오뱅크 클론 코딩 (0) | 2024.11.20 |
[Android] 스타일 가이드와 디자인 패턴 (3) | 2024.11.13 |
[Android] Preferences Datastore, Room을 활용한 로컬 데이터 저장 (3) | 2024.11.03 |