From 28643364438781623685fd57c27750d5d96384b1 Mon Sep 17 00:00:00 2001 From: Choi SeongHoon <108349655+SeongHoonC@users.noreply.github.com> Date: Tue, 28 May 2024 14:20:44 +0900 Subject: [PATCH] =?UTF-8?q?[Android]=20feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC?= =?UTF-8?q?=20=EC=B6=94=EC=B2=9C=20=EB=AA=A9=EB=A1=9D=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=86=A0=ED=83=80=EC=9E=85=20=EA=B5=AC=ED=98=84(#13)=20(#14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: Android Version Catalog 적용 * Feat: Hilt Application 설정 * Feat: Glide 의존성 추가 * Feat: Recipe ViewModel 임시 정의 * Feat: 레시피 추천 프로토 타입 구현 * Feat: Glide 를 사용해 NetworkImage 를 구현한다 * Refactor: Composable 분리 및 패키지 정리 * feat: ktlintCheck dependency true --- Android/app/build.gradle.kts | 17 +++ Android/app/src/main/AndroidManifest.xml | 7 +- .../banchango/BanchangoApplication.kt | 7 ++ .../sundaegukbap/banchango/MainActivity.kt | 47 ------- .../core/designsystem/NetworkImage.kt | 25 ++++ .../sundaegukbap/banchango/model/Recipe.kt | 14 +++ .../reciperecommend/MainActivity.kt | 33 +++++ .../reciperecommend/RecipeCard.kt | 116 ++++++++++++++++++ .../RecipeRecommendViewModel.kt | 33 +++++ .../reciperecommend/RecipesRecommendScreen.kt | 47 +++++++ .../banchango/repository/RecipeRepository.kt | 7 ++ Android/build.gradle.kts | 12 +- Android/gradle/libs.versions.toml | 27 +++- 13 files changed, 336 insertions(+), 56 deletions(-) create mode 100644 Android/app/src/main/java/com/sundaegukbap/banchango/BanchangoApplication.kt delete mode 100644 Android/app/src/main/java/com/sundaegukbap/banchango/MainActivity.kt create mode 100644 Android/app/src/main/java/com/sundaegukbap/banchango/core/designsystem/NetworkImage.kt create mode 100644 Android/app/src/main/java/com/sundaegukbap/banchango/model/Recipe.kt create mode 100644 Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/MainActivity.kt create mode 100644 Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipeCard.kt create mode 100644 Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipeRecommendViewModel.kt create mode 100644 Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipesRecommendScreen.kt create mode 100644 Android/app/src/main/java/com/sundaegukbap/banchango/repository/RecipeRepository.kt diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index 0812f90..056eee6 100644 --- a/Android/app/build.gradle.kts +++ b/Android/app/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.org.jetbrains.kotlin.kapt) + alias(libs.plugins.hilt) } android { @@ -59,6 +61,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.androidx.lifecycle.runtime.compose.android) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -66,4 +69,18 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + + implementation(libs.dagger.hilt.android) + kapt(libs.dagger.hilt.compiler) + implementation(libs.lifecycle.viewmodel.ktx) + + // navigation + implementation(libs.navigation.compose) + implementation(libs.hilt.navigation.compose) + + // status bar + implementation(libs.accompanist.systemuicontroller) + + // glide + implementation(libs.glide.compose) } diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml index 4ddc0a5..941264a 100644 --- a/Android/app/src/main/AndroidManifest.xml +++ b/Android/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + + tools:targetApi="34"> diff --git a/Android/app/src/main/java/com/sundaegukbap/banchango/BanchangoApplication.kt b/Android/app/src/main/java/com/sundaegukbap/banchango/BanchangoApplication.kt new file mode 100644 index 0000000..aa366d7 --- /dev/null +++ b/Android/app/src/main/java/com/sundaegukbap/banchango/BanchangoApplication.kt @@ -0,0 +1,7 @@ +package com.sundaegukbap.banchango + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class BanchangoApplication : Application() diff --git a/Android/app/src/main/java/com/sundaegukbap/banchango/MainActivity.kt b/Android/app/src/main/java/com/sundaegukbap/banchango/MainActivity.kt deleted file mode 100644 index 26d61ea..0000000 --- a/Android/app/src/main/java/com/sundaegukbap/banchango/MainActivity.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.sundaegukbap.banchango - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.sundaegukbap.banchango.ui.theme.BanchangoTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - BanchangoTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) - } - } - } - } -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - BanchangoTheme { - Greeting("Android") - } -} diff --git a/Android/app/src/main/java/com/sundaegukbap/banchango/core/designsystem/NetworkImage.kt b/Android/app/src/main/java/com/sundaegukbap/banchango/core/designsystem/NetworkImage.kt new file mode 100644 index 0000000..b2900ef --- /dev/null +++ b/Android/app/src/main/java/com/sundaegukbap/banchango/core/designsystem/NetworkImage.kt @@ -0,0 +1,25 @@ +package com.sundaegukbap.banchango.core.designsystem + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.layout.ContentScale +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.bumptech.glide.integration.compose.placeholder + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +fun NetworkImage(modifier: Modifier, url: String) { + GlideImage( + model = url, + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = modifier.fillMaxSize(), + loading = placeholder(ColorPainter(Color(0xD9FFFFFF))), + failure = placeholder(ColorPainter(Color(0xD9FFFFFF))), + ) +} + diff --git a/Android/app/src/main/java/com/sundaegukbap/banchango/model/Recipe.kt b/Android/app/src/main/java/com/sundaegukbap/banchango/model/Recipe.kt new file mode 100644 index 0000000..adb62e5 --- /dev/null +++ b/Android/app/src/main/java/com/sundaegukbap/banchango/model/Recipe.kt @@ -0,0 +1,14 @@ +package com.sundaegukbap.banchango.model + +data class Recipe( + val id: Long, + val name: String, + val introduction: String, + val image: String, + val link: String, + val cookingTime: Int, + val servings: Int, + val difficulty: String, + val have: List, + val need: List +) diff --git a/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/MainActivity.kt b/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/MainActivity.kt new file mode 100644 index 0000000..bfbfe82 --- /dev/null +++ b/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/MainActivity.kt @@ -0,0 +1,33 @@ +package com.sundaegukbap.banchango.presentation.reciperecommend + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.ui.graphics.Color +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.sundaegukbap.banchango.ui.theme.BanchangoTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + private val viewModel: RecipeRecommendViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + viewModel.getRecipeRecommendation() + setContent { + BanchangoTheme { + val systemUiController = rememberSystemUiController() + systemUiController.setSystemBarsColor(color = Color(0xBFFFFFFF), darkIcons = true) + systemUiController.setNavigationBarColor( + color = Color(0xFFFFFFFF) + ) + RecipesRecommendScreen() + } + } + } +} diff --git a/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipeCard.kt b/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipeCard.kt new file mode 100644 index 0000000..10f5e24 --- /dev/null +++ b/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipeCard.kt @@ -0,0 +1,116 @@ +package com.sundaegukbap.banchango.presentation.reciperecommend + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight.Companion.Bold +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.sundaegukbap.banchango.core.designsystem.NetworkImage +import com.sundaegukbap.banchango.model.Recipe +import com.sundaegukbap.banchango.ui.theme.BanchangoTheme + +@Composable +fun RecipeCard( + page: Int, + recipe: Recipe, + onHateClick: (page: Int) -> Unit = {}, + onLikeClick: (page: Int) -> Unit = {}, +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black), + contentAlignment = Alignment.Center, + ) { + NetworkImage( + modifier = Modifier + .fillMaxSize(), + url = recipe.image, + ) + RecipeInfo(recipe, page, onHateClick, onLikeClick) + } +} + +@Composable +private fun RecipeInfo( + recipe: Recipe, + page: Int, + onHateClick: (page: Int) -> Unit, + onLikeClick: (page: Int) -> Unit +) { + Box { + Column { + Text( + recipe.name, + color = Color.White, + fontSize = 24.sp, + style = TextStyle(fontWeight = Bold), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = page.toString(), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + color = Color.White, + fontSize = 60.sp + ) + Row( + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) { + Button( + modifier = Modifier.padding(end = 16.dp), + onClick = { + onHateClick(page + 1) + } + ) { + Text("싫어요") + } + Button( + onClick = { + onLikeClick(page + 1) + }, + ) { + Text("좋아요") + } + } + } + } +} + + +@Preview(showBackground = true) +@Composable +fun RecipeCardPreview() { + BanchangoTheme { + RecipeCard( + page = 1, recipe = Recipe( + id = 1, + name = "간장계란볶음밥", + introduction = "아주 간단하면서 맛있는 계란간장볶음밥으로 한끼식사 만들어보세요. 아이들이 더 좋아할거예요.", + image = "https://recipe1.ezmember.co.kr/cache/recipe/2018/05/26/d0c6701bc673ac5c18183b47212a58571.jpg", + link = "https://www.10000recipe.com/recipe/6889616", + cookingTime = 10, + servings = 2, + difficulty = "Easy", + have = listOf(1, 2, 3, 4, 5), + need = listOf(6, 7, 8, 9, 10), + ) + ) + } +} diff --git a/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipeRecommendViewModel.kt b/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipeRecommendViewModel.kt new file mode 100644 index 0000000..1e33384 --- /dev/null +++ b/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipeRecommendViewModel.kt @@ -0,0 +1,33 @@ +package com.sundaegukbap.banchango.presentation.reciperecommend + +import androidx.lifecycle.ViewModel +import com.sundaegukbap.banchango.model.Recipe +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class RecipeRecommendViewModel @Inject constructor() : ViewModel() { + + private val _recipes: MutableStateFlow> = MutableStateFlow(emptyList()) + val recipes: StateFlow> = _recipes.asStateFlow() + + fun getRecipeRecommendation() { + _recipes.value = List(10) { + Recipe( + id = (it + 1).toLong(), + name = "간장계란볶음밥", + introduction = "아주 간단하면서 맛있는 계란간장볶음밥으로 한끼식사 만들어보세요. 아이들이 더 좋아할거예요.", + image = "https://recipe1.ezmember.co.kr/cache/recipe/2018/05/26/d0c6701bc673ac5c18183b47212a58571.jpg", + link = "https://www.10000recipe.com/recipe/6889616", + cookingTime = 10, + servings = 2, + difficulty = "Easy", + have = listOf(1, 2, 3, 4, 5), + need = listOf(6, 7, 8, 9, 10) + ) + } + } +} diff --git a/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipesRecommendScreen.kt b/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipesRecommendScreen.kt new file mode 100644 index 0000000..9de4207 --- /dev/null +++ b/Android/app/src/main/java/com/sundaegukbap/banchango/presentation/reciperecommend/RecipesRecommendScreen.kt @@ -0,0 +1,47 @@ +package com.sundaegukbap.banchango.presentation.reciperecommend + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RecipesRecommendScreen( + modifier: Modifier = Modifier, + viewModel: RecipeRecommendViewModel = hiltViewModel(), +) { + val recipesUiState by viewModel.recipes.collectAsStateWithLifecycle() + + val pagerState = rememberPagerState( + pageCount = { + recipesUiState.size + } + ) + val coroutineScope = rememberCoroutineScope() + VerticalPager( + modifier = modifier, + state = pagerState, + contentPadding = PaddingValues(vertical = 200.dp, horizontal = 40.dp), + pageSpacing = 40.dp, + ) { page -> + RecipeCard( + page = page, + recipe = recipesUiState[page], + onLikeClick = { + coroutineScope.launch { pagerState.animateScrollToPage(it) } + }, + onHateClick = { + coroutineScope.launch { pagerState.animateScrollToPage(it) } + }, + ) + } +} diff --git a/Android/app/src/main/java/com/sundaegukbap/banchango/repository/RecipeRepository.kt b/Android/app/src/main/java/com/sundaegukbap/banchango/repository/RecipeRepository.kt new file mode 100644 index 0000000..c1769e4 --- /dev/null +++ b/Android/app/src/main/java/com/sundaegukbap/banchango/repository/RecipeRepository.kt @@ -0,0 +1,7 @@ +package com.sundaegukbap.banchango.repository + +import com.sundaegukbap.banchango.model.Recipe + +interface RecipeRepository { + fun getRecipeRecommendation(): List +} diff --git a/Android/build.gradle.kts b/Android/build.gradle.kts index 21f22d9..fe46b1c 100644 --- a/Android/build.gradle.kts +++ b/Android/build.gradle.kts @@ -2,9 +2,15 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false - id("org.jlleitschuh.gradle.ktlint") version "11.1.0" apply false + alias(libs.plugins.org.jetbrains.kotlin.kapt) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.ktlint) apply true } -allprojects { - apply(plugin = "org.jlleitschuh.gradle.ktlint") +buildscript { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } diff --git a/Android/gradle/libs.versions.toml b/Android/gradle/libs.versions.toml index 3ef25e6..9366f2d 100644 --- a/Android/gradle/libs.versions.toml +++ b/Android/gradle/libs.versions.toml @@ -1,13 +1,21 @@ [versions] -agp = "8.4.0" +agp = "8.4.1" kotlin = "1.9.0" coreKtx = "1.13.1" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" -lifecycleRuntimeKtx = "2.7.0" +lifecycleRuntimeKtx = "2.8.0" activityCompose = "1.9.0" -composeBom = "2023.08.00" +composeBom = "2024.05.00" +hilt = "2.44" +lifecycleViewModelKtx = "2.8.0" +ktlint = "11.1.0" +composeNavigation = "2.7.7" +composeHiltNavigation = "1.2.0" +acompanistSystemUiController = "0.27.0" +lifecycleRuntimeComposeAndroid = "2.8.0" +glide = "1.0.0-beta01" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -17,15 +25,26 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui = { group = "androidx.compose.ui", name = "ui", version = "1.7.0-beta01" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +dagger-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } +lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewModelKtx" } +navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation" } +hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "composeHiltNavigation" } +accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "acompanistSystemUiController" } +androidx-lifecycle-runtime-compose-android = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose-android", version.ref = "lifecycleRuntimeComposeAndroid" } +glide-compose = { group = "com.github.bumptech.glide", name = "compose", version.ref = "glide" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +org-jetbrains-kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }