From f5c10fd8635d7706d2611b3d9f18cab1906c2355 Mon Sep 17 00:00:00 2001 From: SeongHoonC Date: Sun, 22 Sep 2024 14:57:11 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20orbit=204.4.0=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Android/feature/home/build.gradle.kts | 5 +++++ Android/gradle/libs.versions.toml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/Android/feature/home/build.gradle.kts b/Android/feature/home/build.gradle.kts index 5316d0c..c83b16d 100644 --- a/Android/feature/home/build.gradle.kts +++ b/Android/feature/home/build.gradle.kts @@ -70,6 +70,11 @@ dependencies { implementation(libs.navigation.compose) implementation(libs.hilt.navigation.compose) + implementation(libs.orbit.core) + implementation(libs.orbit.viewmodel) + implementation(libs.orbit.compose) + testImplementation(libs.orbit.test) + // status bar implementation(libs.accompanist.systemuicontroller) diff --git a/Android/gradle/libs.versions.toml b/Android/gradle/libs.versions.toml index 7ddf4da..e97e9be 100644 --- a/Android/gradle/libs.versions.toml +++ b/Android/gradle/libs.versions.toml @@ -20,6 +20,7 @@ jetbrainsKotlinJvm = "1.9.0" appcompat = "1.6.1" material = "1.12.0" material3Android = "1.2.1" +orbit = "4.4.0" serializationConverter = "1.0.0" serialization = "1.6.3" kotlinxImmutable = "0.3.7" @@ -54,6 +55,10 @@ navigation-compose = { module = "androidx.navigation:navigation-compose", versio kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } kotlinx-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" } okhttp3-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "squareupOkhttp" } +orbit-compose = { module = "org.orbit-mvi:orbit-compose", version.ref = "orbit" } +orbit-core = { module = "org.orbit-mvi:orbit-core", version.ref = "orbit" } +orbit-test = { module = "org.orbit-mvi:orbit-test", version.ref = "orbit" } +orbit-viewmodel = { module = "org.orbit-mvi:orbit-viewmodel", version.ref = "orbit" } retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "serializationConverter" } From 0d27bc04495f03e47aea85c8199e5a826a09c648 Mon Sep 17 00:00:00 2001 From: SeongHoonC Date: Sun, 22 Sep 2024 15:42:19 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20Retrofit=20=EC=9D=84=20Result=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=B0=9B=EC=9D=84=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/api/IngredientRepository.kt | 1 + Android/core/data/build.gradle.kts | 1 + .../banchango/core/data/api/IngredientApi.kt | 15 +++++++++--- .../model/AddIngredientContainerRequest.kt | 8 +++++++ .../banchango/core/data/di/ApiModule.kt | 2 ++ .../core/data/di/RepositoryModule.kt | 6 ++--- .../repository/DefaultIngredientRepository.kt | 23 ++++++------------- .../repository/FakeIngredientRepository.kt | 3 +++ Android/gradle/libs.versions.toml | 4 ++-- 9 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/api/model/AddIngredientContainerRequest.kt diff --git a/Android/core/data-api/src/main/java/com/sundaegukbap/banchango/core/data/repository/api/IngredientRepository.kt b/Android/core/data-api/src/main/java/com/sundaegukbap/banchango/core/data/repository/api/IngredientRepository.kt index dfc0ae7..0e4a549 100644 --- a/Android/core/data-api/src/main/java/com/sundaegukbap/banchango/core/data/repository/api/IngredientRepository.kt +++ b/Android/core/data-api/src/main/java/com/sundaegukbap/banchango/core/data/repository/api/IngredientRepository.kt @@ -4,4 +4,5 @@ import com.sundaegukbap.banchango.ContainerIngredient interface IngredientRepository { suspend fun getIngredientContainers(): Result> + suspend fun addIngredientContainer(containerName: String): Result } \ No newline at end of file diff --git a/Android/core/data/build.gradle.kts b/Android/core/data/build.gradle.kts index d182a89..1d2371c 100644 --- a/Android/core/data/build.gradle.kts +++ b/Android/core/data/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(libs.okhttp3.okhttp) implementation(libs.retrofit2.kotlinx.serialization.converter) implementation(libs.kotlinx.serialization.json) + implementation(libs.retrofit.adapters.result) } fun getSecretKey(propertyKey: String): String { diff --git a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/api/IngredientApi.kt b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/api/IngredientApi.kt index ac63973..f83368b 100644 --- a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/api/IngredientApi.kt +++ b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/api/IngredientApi.kt @@ -1,13 +1,22 @@ package com.sundaegukbap.banchango.core.data.api +import com.sundaegukbap.banchango.core.data.api.model.AddIngredientContainerRequest import com.sundaegukbap.banchango.core.data.api.model.ContainerIngredientDtos -import retrofit2.Response +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path internal interface IngredientApi { @GET("api/ingredients/main/list/{userid}") suspend fun getIngredients( @Path("userid") userId: Long, - ): Response -} \ No newline at end of file + ): Result + + @POST("/api/container/{userId}") + suspend fun addIngredientContainer( + @Path("userId") userId: Long, + @Body addIngredientContainerRequest: AddIngredientContainerRequest, + ): Result +} + diff --git a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/api/model/AddIngredientContainerRequest.kt b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/api/model/AddIngredientContainerRequest.kt new file mode 100644 index 0000000..68af7d8 --- /dev/null +++ b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/api/model/AddIngredientContainerRequest.kt @@ -0,0 +1,8 @@ +package com.sundaegukbap.banchango.core.data.api.model + +import kotlinx.serialization.Serializable + +@Serializable +data class AddIngredientContainerRequest( + val containerName: String, +) \ No newline at end of file diff --git a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/di/ApiModule.kt b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/di/ApiModule.kt index ca0ed81..0b245d7 100644 --- a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/di/ApiModule.kt +++ b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/di/ApiModule.kt @@ -1,6 +1,7 @@ package com.sundaegukbap.banchango.core.data.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.skydoves.retrofit.adapters.result.ResultCallAdapterFactory import com.sundaegukbap.banchango.core.data.BuildConfig import com.sundaegukbap.banchango.core.data.api.IngredientApi import com.sundaegukbap.banchango.core.data.api.RecipeApi @@ -44,6 +45,7 @@ internal object ApiModule { ): Retrofit = Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(converterFactory) + .addCallAdapterFactory(ResultCallAdapterFactory.create()) .build() @Provides diff --git a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/di/RepositoryModule.kt b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/di/RepositoryModule.kt index fdeb08e..b072d0d 100644 --- a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/di/RepositoryModule.kt +++ b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/di/RepositoryModule.kt @@ -2,8 +2,6 @@ package com.sundaegukbap.banchango.core.data.di import com.sundaegukbap.banchango.core.data.repository.DefaultIngredientRepository import com.sundaegukbap.banchango.core.data.repository.DefaultRecipeRepository -import com.sundaegukbap.banchango.core.data.repository.FakeIngredientRepository -import com.sundaegukbap.banchango.core.data.repository.FakeRecipeRepository import com.sundaegukbap.banchango.core.data.repository.api.IngredientRepository import com.sundaegukbap.banchango.core.data.repository.api.RecipeRepository import dagger.Binds @@ -17,9 +15,9 @@ import javax.inject.Singleton internal interface RepositoryModule { @Singleton @Binds - fun bindsRecipeRepository(recipeRepository: FakeRecipeRepository): RecipeRepository + fun bindsRecipeRepository(recipeRepository: DefaultRecipeRepository): RecipeRepository @Singleton @Binds - fun bindsIngredientRepository(ingredientRepository: FakeIngredientRepository): IngredientRepository + fun bindsIngredientRepository(ingredientRepository: DefaultIngredientRepository): IngredientRepository } diff --git a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/repository/DefaultIngredientRepository.kt b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/repository/DefaultIngredientRepository.kt index 21dc54e..db77c51 100644 --- a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/repository/DefaultIngredientRepository.kt +++ b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/repository/DefaultIngredientRepository.kt @@ -1,31 +1,22 @@ package com.sundaegukbap.banchango.core.data.repository -import android.util.Log -import com.sundaegukbap.banchango.Container import com.sundaegukbap.banchango.ContainerIngredient -import com.sundaegukbap.banchango.Ingredient -import com.sundaegukbap.banchango.IngredientKind import com.sundaegukbap.banchango.core.data.api.IngredientApi +import com.sundaegukbap.banchango.core.data.api.model.AddIngredientContainerRequest import com.sundaegukbap.banchango.core.data.mapper.toData import com.sundaegukbap.banchango.core.data.repository.api.IngredientRepository -import java.time.LocalDateTime import javax.inject.Inject internal class DefaultIngredientRepository @Inject constructor( private val ingredientApi: IngredientApi, ) : IngredientRepository { override suspend fun getIngredientContainers(): Result> { - return runCatching { - val response = ingredientApi.getIngredients(1) - Log.d("asdf", "response: $response") - if (response.isSuccessful) { - if (response.body() == null) { - throw IllegalStateException("Response body is null") - } - response.body()!!.containerIngredientDtos.map { it.toData() } - } else { - throw IllegalStateException("Response is not successful") - } + return ingredientApi.getIngredients(1).mapCatching { dto -> + dto.containerIngredientDtos.map { it.toData() } } } + + override suspend fun addIngredientContainer(containerName: String): Result { + return ingredientApi.addIngredientContainer(1, AddIngredientContainerRequest(containerName)) + } } diff --git a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/repository/FakeIngredientRepository.kt b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/repository/FakeIngredientRepository.kt index 4f0200f..bdc57dc 100644 --- a/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/repository/FakeIngredientRepository.kt +++ b/Android/core/data/src/main/java/com/sundaegukbap/banchango/core/data/repository/FakeIngredientRepository.kt @@ -51,4 +51,7 @@ internal class FakeIngredientRepository @Inject constructor() : IngredientReposi ) } + override suspend fun addIngredientContainer(containerName: String): Result { + return Result.success(Unit) + } } \ No newline at end of file diff --git a/Android/gradle/libs.versions.toml b/Android/gradle/libs.versions.toml index e97e9be..3c7c1b7 100644 --- a/Android/gradle/libs.versions.toml +++ b/Android/gradle/libs.versions.toml @@ -25,6 +25,7 @@ serializationConverter = "1.0.0" serialization = "1.6.3" kotlinxImmutable = "0.3.7" squareupOkhttp = "4.11.0" +retrofitAdaptersResult = "1.0.12" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -60,8 +61,7 @@ orbit-core = { module = "org.orbit-mvi:orbit-core", version.ref = "orbit" } orbit-test = { module = "org.orbit-mvi:orbit-test", version.ref = "orbit" } orbit-viewmodel = { module = "org.orbit-mvi:orbit-viewmodel", version.ref = "orbit" } retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "serializationConverter" } - - +retrofit-adapters-result = { module = "com.github.skydoves:retrofit-adapters-result", version.ref = "retrofitAdaptersResult" } [plugins] From 23a2794ec6dc3d69071d035f968dd67e33b46546 Mon Sep 17 00:00:00 2001 From: SeongHoonC Date: Sun, 22 Sep 2024 15:44:57 +0900 Subject: [PATCH 3/4] feat: HomeScreen MVVM to MVI --- .../banchango/IngredientContainers.kt | 33 +++- .../banchango/feature/home/HomeScreen.kt | 165 +++++++++++++++--- .../banchango/feature/home/HomeState.kt | 11 ++ .../banchango/feature/home/HomeViewModel.kt | 101 ++++++++--- 4 files changed, 252 insertions(+), 58 deletions(-) create mode 100644 Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeState.kt diff --git a/Android/core/model/src/main/java/com/sundaegukbap/banchango/IngredientContainers.kt b/Android/core/model/src/main/java/com/sundaegukbap/banchango/IngredientContainers.kt index 69627c5..2b15ebc 100644 --- a/Android/core/model/src/main/java/com/sundaegukbap/banchango/IngredientContainers.kt +++ b/Android/core/model/src/main/java/com/sundaegukbap/banchango/IngredientContainers.kt @@ -3,17 +3,32 @@ package com.sundaegukbap.banchango class ContainerIngredients( containerIngredients: List ) { - private val _value: MutableList = containerIngredients.toMutableList() - val value: List get() = _value.toList() + private val _value: MutableMap = containerIngredients + .groupBy { it.container } + .mapValues { (container, containerIngredients) -> + IngredientContainer( + container = container, + kindIngredientContainers = containerIngredients.toKindIngredientContainers() + ) + }.toMutableMap() + + val value: Map get() = _value.toMap() + + fun getKindIngredientContainerDetail( + container: Container, + kind: IngredientKind + ): KindIngredientContainer { + return _value[container]?.kindIngredientContainers?.find { it.kind == kind }!! + } + fun getIngredientContainers(): List { - return _value.groupBy { it.container } - .map { (container, containerIngredients) -> - IngredientContainer( - container = container, - kindIngredientContainers = containerIngredients.toKindIngredientContainers() - ) - } + return value.map { container -> + IngredientContainer( + container = container.key, + kindIngredientContainers = container.value.kindIngredientContainers + ) + } } private fun List.toKindIngredientContainers(): List { diff --git a/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeScreen.kt b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeScreen.kt index 40fed40..fb391d4 100644 --- a/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeScreen.kt +++ b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeScreen.kt @@ -1,7 +1,11 @@ package com.sundaegukbap.banchango.feature.home +import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -11,40 +15,36 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.Label import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Paint -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.res.colorResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope import com.sundaegukbap.banchango.Container import com.sundaegukbap.banchango.ContainerIngredient import com.sundaegukbap.banchango.Ingredient @@ -56,7 +56,8 @@ import com.sundaegukbap.banchango.core.designsystem.theme.Gray import com.sundaegukbap.banchango.core.designsystem.theme.LightOrange import com.sundaegukbap.banchango.core.designsystem.theme.Orange import com.sundaegukbap.banchango.core.designsystem.theme.White -import com.sundaegukbap.banchango.core.designsystem.theme.lightGray +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import java.time.LocalDateTime import java.time.temporal.ChronoUnit @@ -66,12 +67,114 @@ fun HomeRoute( onChangeStatusBarColor: (color: Color, darkIcons: Boolean) -> Unit, viewModel: HomeViewModel = hiltViewModel() ) { - val ingredientContainers by viewModel.ingredientContainers.collectAsStateWithLifecycle() + val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() + val localContext = LocalContext.current + + LaunchedEffect(true) { + viewModel.container.sideEffectFlow.onEach { + Toast.makeText(localContext, it, Toast.LENGTH_SHORT).show() + }.launchIn(this) + } HomeScreen( padding = padding, - ingredientContainers = ingredientContainers, - onChangeStatusBarColor = onChangeStatusBarColor + ingredientContainers = state.ingredientContainers, + onChangeStatusBarColor = onChangeStatusBarColor, + onContainerAddClicked = viewModel::addContainer, + onKindContainerClicked = viewModel::getKindIngredientContainerDetail, + ) + + if (state.isDetailShowing && state.kindIngredientContainerDetail != null) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + KindIngredientContainerDetailScreen( + padding = padding, + kindIngredientContainer = state.kindIngredientContainerDetail!!, + onBackClicked = viewModel::closeDetail, + ) + } + } + + if (state.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } +} + +@Composable +private fun KindIngredientContainerDetailScreen( + padding: PaddingValues, + kindIngredientContainer: KindIngredientContainer, + onBackClicked: () -> Unit +) { + BackHandler(onBack = onBackClicked) + Column( + modifier = Modifier + .fillMaxSize() + .background(White) + .padding(padding) + .padding(16.dp), + ) { + Text( + text = kindIngredientContainer.kind.label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(kindIngredientContainer.ingredients) { ingredient -> + val dDay = ChronoUnit.DAYS.between( + LocalDateTime.now(), + ingredient.expirationDate, + ) + Card(modifier = Modifier.height(200.dp)) { + Text( + text = ingredient.ingredient.name, + style = MaterialTheme.typography.bodyMedium, + fontSize = 16.sp + ) + Spacer(modifier = Modifier.width(10.dp)) + Text(text = "D - $dDay", style = MaterialTheme.typography.bodyMedium) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewKindIngredientContainerDetailScreen() { + KindIngredientContainerDetailScreen( + PaddingValues(16.dp), + KindIngredientContainer( + IngredientKind.VEGETABLE, + listOf( + ContainerIngredient( + 1, + Container(1, "냉장 1"), + Ingredient(2, "상추", IngredientKind.VEGETABLE, ""), + LocalDateTime.now(), + LocalDateTime.now() + ), + ContainerIngredient( + 1, + Container(1, "냉장 1"), + Ingredient(3, "배추", IngredientKind.VEGETABLE, ""), + LocalDateTime.now(), + LocalDateTime.now() + ) + ) + ), + onBackClicked = {} ) } @@ -79,6 +182,8 @@ fun HomeRoute( private fun HomeScreen( padding: PaddingValues, ingredientContainers: List, + onContainerAddClicked: (name: String) -> Unit, + onKindContainerClicked: (container: Container, kind: IngredientKind) -> Unit, onChangeStatusBarColor: (color: Color, darkIcons: Boolean) -> Unit ) { LazyColumn( @@ -122,9 +227,10 @@ private fun HomeScreen( } val kindIngredients = ingredientContainer.kindIngredientContainers val totalIngredients = kindIngredients.size + val onItemClicked: (kind: IngredientKind) -> Unit = { kind -> + onKindContainerClicked(ingredientContainer.container, kind) + } - - // Creating rows of two IngredientItems for (index in 0 until totalIngredients step 2) { Row( modifier = Modifier @@ -137,6 +243,7 @@ private fun HomeScreen( containerColor = itemColor, kindIngredientContainer = kindIngredients[index], buttonColor = buttonColor, + onIngredientItemClicked = onItemClicked, modifier = Modifier.weight(0.4f) // Fixed width for consistent size ) Spacer(modifier = Modifier.width(20.dp)) @@ -146,6 +253,7 @@ private fun HomeScreen( kindIngredientContainer = kindIngredients[index + 1], modifier = Modifier.weight(0.4f), // Fixed width for consistent size containerColor = itemColor, + onIngredientItemClicked = onItemClicked, buttonColor = buttonColor, ) } else { @@ -157,17 +265,23 @@ private fun HomeScreen( } Spacer(modifier = Modifier.height(20.dp)) } - item { AddContainerButton(if (ingredientContainers.size % 2 == 0) LightOrange else Gray) } + item { + AddContainerButton( + if (ingredientContainers.size % 2 == 0) LightOrange else Gray, + onAddClick = onContainerAddClicked + ) + } } } @Composable private fun AddContainerButton( containerColor: Color, + onAddClick: (name: String) -> Unit, ) { ElevatedCard( colors = CardDefaults.elevatedCardColors(containerColor = containerColor), - modifier = Modifier + modifier = Modifier.clickable(onClick = { onAddClick("냉장고") }) ) { Column( modifier = Modifier @@ -188,12 +302,14 @@ private fun IngredientItem( containerColor: Color, buttonColor: Color, kindIngredientContainer: KindIngredientContainer, + onIngredientItemClicked: (kind: IngredientKind) -> Unit, modifier: Modifier = Modifier ) { Card( colors = CardDefaults.cardColors(containerColor = containerColor), modifier = modifier .height(106.dp) + .clickable(onClick = { onIngredientItemClicked(kindIngredientContainer.kind) }) ) { Column(Modifier.padding(8.dp)) { val ingredients = kindIngredientContainer.ingredients @@ -217,8 +333,6 @@ private fun IngredientItem( color = Orange ) } - - // 2개까지만 표시하고, 2개 이상일 경우 ... 표시 ingredients.subList(0, minOf(ingredients.size, 2)).forEach { ingredient -> val dDay = ChronoUnit.DAYS.between( LocalDateTime.now(), @@ -253,6 +367,7 @@ private fun PreviewIngredientItem() { IngredientItem( containerColor = White, buttonColor = Gray, + onIngredientItemClicked = { _ -> }, kindIngredientContainer = KindIngredientContainer( IngredientKind.VEGETABLE, listOf( @@ -323,7 +438,9 @@ private fun PreviewHomeScreen() { ) ) ) - ) + ), + onContainerAddClicked = {}, + onKindContainerClicked = { _, _ -> } ) } } diff --git a/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeState.kt b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeState.kt new file mode 100644 index 0000000..f4d6032 --- /dev/null +++ b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeState.kt @@ -0,0 +1,11 @@ +package com.sundaegukbap.banchango.feature.home + +import com.sundaegukbap.banchango.IngredientContainer +import com.sundaegukbap.banchango.KindIngredientContainer + +data class HomeState( + val ingredientContainers: List = emptyList(), + val kindIngredientContainerDetail: KindIngredientContainer? = null, + val isLoading: Boolean = false, + val isDetailShowing: Boolean = false +) \ No newline at end of file diff --git a/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeViewModel.kt b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeViewModel.kt index 823696f..8136e24 100644 --- a/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeViewModel.kt +++ b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeViewModel.kt @@ -1,45 +1,96 @@ package com.sundaegukbap.banchango.feature.home -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sundaegukbap.banchango.Container import com.sundaegukbap.banchango.ContainerIngredients -import com.sundaegukbap.banchango.IngredientContainer +import com.sundaegukbap.banchango.IngredientKind import com.sundaegukbap.banchango.core.data.repository.api.IngredientRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val ingredientRepository: IngredientRepository -) : ViewModel() { +) : ViewModel(), ContainerHost { private var containerIngredients: ContainerIngredients = ContainerIngredients(emptyList()) - - private val _ingredientContainers: MutableStateFlow> = - MutableStateFlow(emptyList()) - val ingredientContainers: StateFlow> = - _ingredientContainers.asStateFlow() + override val container = container(HomeState()) init { - viewModelScope.launch { - ingredientRepository.getIngredientContainers() - .onSuccess { - containerIngredients = ContainerIngredients(it) - _ingredientContainers.value = containerIngredients.getIngredientContainers() - Log.d( - "asdf", - "containerIngredients: ${containerIngredients.getIngredientContainers()}" - ) - }.onFailure { - Log.e("asdf", "Failed to get ingredient containers", it) - } + intent { + viewModelScope.launch { + reduce { state.copy(isLoading = true) } + ingredientRepository.getIngredientContainers() + .onSuccess { + containerIngredients = ContainerIngredients(it) + reduce { + state.copy( + ingredientContainers = containerIngredients.getIngredientContainers(), + isLoading = false + ) + } + }.onFailure { + postSideEffect("Failed to get ingredient containers") + reduce { state.copy(isLoading = false) } + } + } + } + } + + fun addContainer(containerName: String) { + intent { + viewModelScope.launch { + reduce { state.copy(isLoading = true) } + ingredientRepository.addIngredientContainer(containerName) + .onSuccess { + ingredientRepository.getIngredientContainers() + .onSuccess { + containerIngredients = ContainerIngredients(it) + reduce { + state.copy( + ingredientContainers = containerIngredients.getIngredientContainers(), + isLoading = false + ) + } + }.onFailure { + reduce { state.copy(isLoading = false) } + postSideEffect("Failed to get ingredient containers") + } + }.onFailure { + reduce { state.copy(isLoading = false) } + postSideEffect("Failed to add ingredient container") + } + } } } -} + fun getKindIngredientContainerDetail(container: Container, kind: IngredientKind) { + intent { + reduce { + state.copy( + kindIngredientContainerDetail = containerIngredients.getKindIngredientContainerDetail( + container, + kind + ), + isDetailShowing = true + ) + } + } + containerIngredients.getKindIngredientContainerDetail(container, kind) + } + + fun closeDetail() { + intent { + reduce { + state.copy(kindIngredientContainerDetail = null, isDetailShowing = false) + } + } + } +} From 7f94ae919faafdaccd3ab6313a1cef6e41f49eeb Mon Sep 17 00:00:00 2001 From: SeongHoonC Date: Sun, 22 Sep 2024 16:44:17 +0900 Subject: [PATCH 4/4] feat: Add Ingredient Item Design --- .../designsystem/component/NetworkImage.kt | 2 +- .../banchango/feature/home/HomeScreen.kt | 28 ++--- .../feature/home/component/IngredientItem.kt | 116 ++++++++++++++++++ 3 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/component/IngredientItem.kt diff --git a/Android/core/designsystem/src/main/java/com/sundaegukbap/banchango/core/designsystem/component/NetworkImage.kt b/Android/core/designsystem/src/main/java/com/sundaegukbap/banchango/core/designsystem/component/NetworkImage.kt index 1fa5959..8c721ed 100644 --- a/Android/core/designsystem/src/main/java/com/sundaegukbap/banchango/core/designsystem/component/NetworkImage.kt +++ b/Android/core/designsystem/src/main/java/com/sundaegukbap/banchango/core/designsystem/component/NetworkImage.kt @@ -14,7 +14,7 @@ import com.sundaegukbap.banchango.core.designsystem.theme.BanchangoTheme @OptIn(ExperimentalGlideComposeApi::class) @Composable -fun NetworkImage(modifier: Modifier, url: String) { +fun NetworkImage(url: String, modifier: Modifier = Modifier) { GlideImage( model = url, contentScale = ContentScale.Crop, diff --git a/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeScreen.kt b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeScreen.kt index fb391d4..06f50d1 100644 --- a/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeScreen.kt +++ b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/HomeScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items @@ -42,9 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.lifecycleScope import com.sundaegukbap.banchango.Container import com.sundaegukbap.banchango.ContainerIngredient import com.sundaegukbap.banchango.Ingredient @@ -56,6 +53,7 @@ import com.sundaegukbap.banchango.core.designsystem.theme.Gray import com.sundaegukbap.banchango.core.designsystem.theme.LightOrange import com.sundaegukbap.banchango.core.designsystem.theme.Orange import com.sundaegukbap.banchango.core.designsystem.theme.White +import com.sundaegukbap.banchango.feature.home.component.IngredientItem import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import java.time.LocalDateTime @@ -132,19 +130,11 @@ private fun KindIngredientContainerDetailScreen( verticalArrangement = Arrangement.spacedBy(16.dp) ) { items(kindIngredientContainer.ingredients) { ingredient -> - val dDay = ChronoUnit.DAYS.between( - LocalDateTime.now(), - ingredient.expirationDate, + IngredientItem( + ingredient = ingredient.ingredient, + expirationDate = ingredient.expirationDate, + createdAt = ingredient.createdAt ) - Card(modifier = Modifier.height(200.dp)) { - Text( - text = ingredient.ingredient.name, - style = MaterialTheme.typography.bodyMedium, - fontSize = 16.sp - ) - Spacer(modifier = Modifier.width(10.dp)) - Text(text = "D - $dDay", style = MaterialTheme.typography.bodyMedium) - } } } } @@ -239,7 +229,7 @@ private fun HomeScreen( horizontalArrangement = Arrangement.SpaceBetween // Distributes items evenly ) { // First IngredientItem - IngredientItem( + KindIngredientContainerItem( containerColor = itemColor, kindIngredientContainer = kindIngredients[index], buttonColor = buttonColor, @@ -249,7 +239,7 @@ private fun HomeScreen( Spacer(modifier = Modifier.width(20.dp)) // Check for the second item if (index + 1 < totalIngredients) { - IngredientItem( + KindIngredientContainerItem( kindIngredientContainer = kindIngredients[index + 1], modifier = Modifier.weight(0.4f), // Fixed width for consistent size containerColor = itemColor, @@ -298,7 +288,7 @@ private fun AddContainerButton( } @Composable -private fun IngredientItem( +private fun KindIngredientContainerItem( containerColor: Color, buttonColor: Color, kindIngredientContainer: KindIngredientContainer, @@ -364,7 +354,7 @@ private fun IngredientItem( @Composable private fun PreviewIngredientItem() { BanchangoTheme { - IngredientItem( + KindIngredientContainerItem( containerColor = White, buttonColor = Gray, onIngredientItemClicked = { _ -> }, diff --git a/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/component/IngredientItem.kt b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/component/IngredientItem.kt new file mode 100644 index 0000000..56ac82c --- /dev/null +++ b/Android/feature/home/src/main/java/com/sundaegukbap/banchango/feature/home/component/IngredientItem.kt @@ -0,0 +1,116 @@ +package com.sundaegukbap.banchango.feature.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.BottomStart +import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.TopEnd +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.sundaegukbap.banchango.Ingredient +import com.sundaegukbap.banchango.IngredientKind +import com.sundaegukbap.banchango.core.designsystem.component.NetworkImage +import com.sundaegukbap.banchango.core.designsystem.theme.BanchangoTheme +import com.sundaegukbap.banchango.core.designsystem.theme.Orange +import com.sundaegukbap.banchango.core.designsystem.theme.White +import com.sundaegukbap.banchango.core.designsystem.theme.lightGray +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit + +@Composable +fun IngredientItem( + ingredient: Ingredient, + createdAt: LocalDateTime, + expirationDate: LocalDateTime, + modifier: Modifier = Modifier, +) { + val dDay = ChronoUnit.DAYS.between( + LocalDateTime.now(), + expirationDate, + ) + Card( + modifier = modifier + .fillMaxWidth() + .height(150.dp), + colors = CardDefaults.cardColors().copy(containerColor = lightGray) + ) { + Spacer(modifier = Modifier.height(8.dp)) + NetworkImage( + url = ingredient.image, + Modifier + .size(70.dp) + .clip(CircleShape) + .align(CenterHorizontally) + ) + Card( + colors = CardDefaults.cardColors().copy(containerColor = White), + modifier = Modifier + .fillMaxWidth() + .height(70.dp) + .padding(8.dp) + .background(Color.White, RoundedCornerShape(20.dp)) + ) { + Box(Modifier.fillMaxSize().padding(vertical = 4.dp, horizontal = 12.dp)) { + Text( + text = ingredient.name, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + ) + Text( + text = "D - $dDay", + fontSize = 12.sp, + color = Orange, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(TopEnd) + ) + Text( + modifier = Modifier.align(BottomStart), + text = "등록일 - ${createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))}", + fontSize = 10.sp, + lineHeight = 10.sp, + ) + } + + + } + } +} + +@Preview +@Composable +fun IngredientItemPreview() { + BanchangoTheme { + IngredientItem( + modifier = Modifier.width(200.dp), + ingredient = Ingredient( + id = 1, + name = "Ingredient", + kind = IngredientKind.VEGETABLE, + image = "", + ), + createdAt = LocalDateTime.now(), + expirationDate = LocalDateTime.now(), + ) + } +} \ No newline at end of file