Skip to content

Commit

Permalink
[Android]feat: 레시피 추천 목록 화면 구현 및 API 연동(#18) (#20)
Browse files Browse the repository at this point in the history
* feat: data 모듈 생성 및 의존성 정의

* feat(recipe): 레시피 추천 저장소 & api 구현

* feat(recipeRecommend): 레시피 추천 화면을 그린다

* feat(RecipeUiState): 레시피 추천 아이템을 정의한다

* feat(RecipeRecommend): 레시피 추천 VerticalPager 애니메이션 구현

* feat: Main Screen 을 start destination 으로 변경

* feat(data): 레시피 Response mapper 정의

* feat(designsystem): Color 추가

* feat(reciperecommend): 무한스크롤 추가

* feat(recipeDetail): 레시피 상세 조회 api 사용

* feat(recipeDetail): 레시피 상세 조회 네비게이팅 구현

* feat(RecipeRecommend): 레시피 추천 목록에서 좋아요와 재료 목록을 확인할 수 있다

* feat(core): setting Kotlin Jvm build
  • Loading branch information
SeongHoonC committed Jun 15, 2024
1 parent 896a692 commit 5f11142
Show file tree
Hide file tree
Showing 53 changed files with 1,082 additions and 267 deletions.
6 changes: 6 additions & 0 deletions Android/.idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Android/.idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Android/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ android {

dependencies {

// feature
// modules
implementation(project(":feature:main"))
implementation(project(":feature:reciperecommend"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
Expand Down
5 changes: 5 additions & 0 deletions Android/core/data-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.hilt)
alias(libs.plugins.org.jetbrains.kotlin.kapt)
}

android {
Expand Down Expand Up @@ -40,4 +42,7 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

implementation(libs.dagger.hilt.android)
kapt(libs.dagger.hilt.compiler)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package com.sundaegukbap.banchango.core.data.repository.api
import com.sundaegukbap.banchango.Recipe

interface RecipeRepository {
fun getRecipeRecommendation(): List<Recipe>
suspend fun getRecipeRecommendation(): Result<List<Recipe>>
suspend fun getRecipeDetail(id: Long): Result<Recipe>
}
File renamed without changes.
68 changes: 68 additions & 0 deletions Android/core/data/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.hilt)
alias(libs.plugins.org.jetbrains.kotlin.kapt)
alias(libs.plugins.kotlin.serialization)
}

android {
namespace = "com.sundaegukbap.banchango.core.data"
compileSdk = 34

defaultConfig {
minSdk = 28
buildConfigField("String", "BASE_URL", getSecretKey("base_url"))

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
buildConfig = true
}
}

dependencies {

// modules
implementation(project(":core:model"))
implementation(project(":core:data-api"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

// hilt
implementation(libs.dagger.hilt.android)
kapt(libs.dagger.hilt.compiler)

// okhttp3
implementation(libs.okhttp3.okhttp)
implementation(libs.retrofit2.kotlinx.serialization.converter)
implementation(libs.kotlinx.serialization.json)
}

fun getSecretKey(propertyKey: String): String {
return gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
}
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions Android/core/data/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.sundaegukbap.banchango.core.data.api

import com.sundaegukbap.banchango.core.data.api.model.RecipeRecommendResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path

internal interface RecipeApi {
@GET("api/recipe/recommand/{userId}")
suspend fun getRecipeRecommendation(
@Path("userId") userId: Long
): Response<List<RecipeRecommendResponse>>

@GET("api/recipe/{userId}/{recipeId}")
suspend fun getRecipeDetail(
@Path("userId") userId: Long,
@Path("recipeId") recipeId: Long
): Response<RecipeRecommendResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sundaegukbap.banchango.core.data.api.model

import kotlinx.serialization.Serializable

@Serializable
data class RecipeRecommendsResponse(
val recipeRecommends: List<RecipeRecommendResponse>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.sundaegukbap.banchango.core.data.api.model

import kotlinx.serialization.Serializable

@Serializable
data class RecipeRecommendResponse(
val id: Long,
val name: String,
val introduction: String,
val image: String,
val link: String,
val have: List<String>,
val need: List<String>,
val servings: Int,
val cookingTime: Int,
val isBookmarked: Boolean,
val difficulty: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.sundaegukbap.banchango.core.data.di

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.sundaegukbap.banchango.core.data.BuildConfig
import com.sundaegukbap.banchango.core.data.api.RecipeApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import javax.inject.Qualifier
import javax.inject.Singleton

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BaseUrlQualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DefaultRetrofitQualifier

@Module
@InstallIn(SingletonComponent::class)
internal object ApiModule {

@Provides
@Singleton
fun provideRetrofitConverterFactory(): retrofit2.Converter.Factory {
val json = Json {
ignoreUnknownKeys = true
}
return json.asConverterFactory("application/json".toMediaType())
}

@Provides
@Singleton
@DefaultRetrofitQualifier
fun providesDefaultRetrofit(
@BaseUrlQualifier baseUrl: String,
converterFactory: retrofit2.Converter.Factory,
): Retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(converterFactory)
.build()

@Provides
@Singleton
@BaseUrlQualifier
fun providesBaseUrl(): String = BuildConfig.BASE_URL

@Provides
@Singleton
fun providesRecipeRecommendApi(
@DefaultRetrofitQualifier retrofit: Retrofit
): RecipeApi = retrofit.create(RecipeApi::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.sundaegukbap.banchango.core.data.di

import com.sundaegukbap.banchango.core.data.repository.FakeRecipeRepository
import com.sundaegukbap.banchango.core.data.repository.api.RecipeRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@InstallIn(SingletonComponent::class)
@Module
internal abstract class RepositoryModule {

@Binds
abstract fun bindsRecipeRepository(recipeRepository: FakeRecipeRepository): RecipeRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.sundaegukbap.banchango.core.data.mapper

import com.sundaegukbap.banchango.Recipe
import com.sundaegukbap.banchango.core.data.api.model.RecipeRecommendResponse

internal fun List<RecipeRecommendResponse>.toData(): List<Recipe> = map { it.toData() }

internal fun RecipeRecommendResponse.toData(): Recipe {
return Recipe(
id = id,
name = name,
introduction = introduction,
image = image,
link = link,
have = have,
need = need,
servings = servings,
cookingTime = cookingTime,
isBookmarked = isBookmarked,
difficulty = difficulty,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.sundaegukbap.banchango.core.data.repository

import com.sundaegukbap.banchango.Recipe
import com.sundaegukbap.banchango.core.data.api.RecipeApi
import com.sundaegukbap.banchango.core.data.mapper.toData
import com.sundaegukbap.banchango.core.data.repository.api.RecipeRepository
import javax.inject.Inject

internal class DefaultRecipeRepository @Inject constructor(
private val recipeApi: RecipeApi,
) : RecipeRepository {
override suspend fun getRecipeRecommendation(): Result<List<Recipe>> {
return runCatching {
val response = recipeApi.getRecipeRecommendation(1)
if (response.isSuccessful) {
if (response.body() == null) {
throw IllegalStateException("Response body is null")
}
response.body()!!.toData()
} else {
throw IllegalStateException("Response is not successful")
}
}
}

override suspend fun getRecipeDetail(id: Long): Result<Recipe> {
return runCatching {
val response = recipeApi.getRecipeDetail(1, id.toLong())
if (response.isSuccessful) {
if (response.body() == null) {
throw IllegalStateException("Response body is null")
}
response.body()!!.toData()
} else {
throw IllegalStateException("Response is not successful")
}
}
}
}
Loading

0 comments on commit 5f11142

Please sign in to comment.