Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.android.library) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
alias(libs.plugins.kotlin.kapt) apply false

}

Expand Down
2 changes: 1 addition & 1 deletion feature/category/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt)
kotlin("kapt")
alias(libs.plugins.kotlin.kapt)
}

android {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import com.chan.category.ui.model.CategoryModel
class CategoryContract {
sealed class Event : ViewEvent {
object CategoriesLoad : Event()
data class SelectCategory(val categoryId: Int): Event()
data class CategoryScrolledIndex (val firstVisibleItemIndex: Int): Event()
}

data class State(
val categoryList: List<CategoryModel> = emptyList(),
var selectedCategoryId: Int? = null,
val selectedCategoryId: Int? = null,
val headerPositions : List<Pair<Int, Int>> = emptyList(),
val isLoading: Boolean = false,
val isError: Boolean = false
) : ViewState

sealed class Effect : ViewEffect {
//추후 네비게이션 추가 예정
data class ShowError(val errorMessage: String) : Effect()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.lifecycle.viewModelScope
import com.chan.android.BaseViewModel
import com.chan.category.domian.CategoryRepository
import com.chan.category.ui.mapper.toPresentation
import com.chan.category.ui.model.CategoryModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
Expand All @@ -17,16 +18,59 @@ class CategoryViewModel @Inject constructor(

override fun handleEvent(event: CategoryContract.Event) {
when (event) {
CategoryContract.Event.CategoriesLoad -> getCategories()
is CategoryContract.Event.CategoriesLoad -> getCategories()
is CategoryContract.Event.SelectCategory -> updateSelectedCategoryId(event.categoryId)
is CategoryContract.Event.CategoryScrolledIndex -> {
val newId = viewState.value.headerPositions
.filter { it.first <= event.firstVisibleItemIndex }
.maxByOrNull { it.first }
?.second
?: viewState.value.selectedCategoryId

setState { copy(selectedCategoryId = newId) }
}
}
}

fun getCategories() {
viewModelScope.launch {
setState { copy(isLoading = true, isError = false) }

val categoryList = categoryRepository.getCategories().map { it.toPresentation() }
setState { copy(categoryList = categoryList, isLoading = false) }
try {
val categoryList = categoryRepository.getCategories().map { it.toPresentation() }
val firstId = categoryList.firstOrNull()?.id

val mappings = categoryHeaderMapping(categoryList)
setState {
copy(
categoryList = categoryList,
selectedCategoryId = firstId,
headerPositions = mappings,
isLoading = false
)
}
} catch (e: Exception) {
setState { copy(isLoading = false, isError = true) }
setEffect { CategoryContract.Effect.ShowError(e.message.toString()) }
}
}
}

private fun updateSelectedCategoryId(categoryId: Int) {
setState { copy(selectedCategoryId = categoryId) }
}

private fun categoryHeaderMapping(categories: List<CategoryModel>): List<Pair<Int, Int>> {
val list = mutableListOf<Pair<Int, Int>>()
var index = 0
categories.forEach { category ->
category.subCategoryItems.forEach { subCategory ->
//리스트 subCategoryItem.id
list += index to category.id
//헤더에 속한 CategoryId
index += 1 + subCategory.items.size
}
}
return list
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
package com.chan.category.ui.composables

import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.chan.category.ui.CategoryContract
import com.chan.category.ui.CategoryViewModel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlin.math.abs


@Composable
Expand All @@ -31,50 +40,135 @@ fun CategoryScreen(
) {

val state by categoryViewModel.viewState.collectAsState()
val effects = categoryViewModel.effect
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()

LaunchedEffect(Unit) {
categoryViewModel.setEvent(CategoryContract.Event.CategoriesLoad)
}
LaunchedEffect(effects) {
effects.collect { effect ->
when (effect) {
is CategoryContract.Effect.ShowError -> Log.d(
"CategoryScreen",
" Error : ${effect.errorMessage}"
)
}
}
}

LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
.map { visibleItems ->
//중앙에 가까운 Items
val start = listState.layoutInfo.viewportStartOffset
val end = listState.layoutInfo.viewportEndOffset
val centerY = (start + end) / 2

visibleItems.minByOrNull { info ->
val itemCenter = info.offset + info.size / 2
abs(itemCenter - centerY)
}?.index
}
.mapNotNull { itemIndex ->
// 헤더의 CategoryIndex 구함
itemIndex?.let {
state.headerPositions
.filter { it.first <= itemIndex }
.maxByOrNull { it.first }
?.second
}
}
.distinctUntilChanged()
.collect { newCatId ->
categoryViewModel.setEvent(
CategoryContract.Event.SelectCategory(newCatId)
)
}
}

Row(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
modifier = Modifier
.width(100.dp)
.fillMaxWidth(0.33f)
.fillMaxHeight()
.background(color = Color(0xFFF5F5F5))
.background(color = Color(0xFFF6F7F9))
) {
items(items = state.categoryList, key = { it.id }) { category ->
Text(
text = category.name,
val selected = category.id == state.selectedCategoryId
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = if (selected) Color.White else Color.Transparent,
)
.clickable {
val targetIndex = state.headerPositions
.first { it.second == category.id }
.first

scope.launch {
listState.scrollToItem(
index = targetIndex,
scrollOffset = 0
)
}
categoryViewModel.setEvent(
CategoryContract.Event.SelectCategory(
category.id
)
)
}
.padding(vertical = 12.dp, horizontal = 8.dp)
)
) {
Text(
text = category.name,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 8.dp),
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal
),
color = if (selected) Color.Black else Color.Gray
)
}
}
}



LazyColumn(
state = listState,
modifier = Modifier
.fillMaxHeight()
.weight(1f),
verticalArrangement = Arrangement.spacedBy(24.dp)
.fillMaxWidth()
.background(color = Color.White)
.padding(start = 20.dp)
) {
state.categoryList.forEach { category ->
item(key = "header-${category.id}") {
Text(
text = category.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color.Black,
modifier = Modifier.padding(vertical = 8.dp)
)
}
category.subCategoryItems.forEach { subCategory ->

item(key = "header-${subCategory.id}") {
Text(
text = subCategory.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color.Black,
modifier = Modifier.padding(top = 25.dp)
)
}
items(
items = subCategory.items,
key = { it.id }
) { subItem ->
Text(
text = subItem.name,
style = MaterialTheme.typography.titleSmall,
color = Color.DarkGray,
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp)
)
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ google-gson = { group = "com.google.code.gson", name = "gson", version.ref = "gs
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltVersion" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
Expand Down