From c2d6928e3f1ee7068e6067eff9d8ebd8503bda67 Mon Sep 17 00:00:00 2001 From: LEE SEOK GYU <54509842+likppi10@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:27:01 +0900 Subject: [PATCH] cd build setting (#120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ruby 세팅 수정 (#93) * ruby 세팅 수정 (#95) * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update Gemfile.lock * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update android-cd.yml * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * api request 가 특정 시점 이후 호출되지 않는 이슈 해결 (#117) * uiState 변경 방식 update 로 통일 * TODO 제거 이거 예전에 지웠는데 왜 남아있지 * Ktor 를 Retrofit 으로 migration * GuestLogin 관련 함수를 BandalartService 내에서 분리 GuestLoginService interface 로 따로 선언 * style check success * 사용하지 않는 클래스 주석처리 * GuestLoginService Provider 추가 * KtorClient, RetrofitClient 를 별도의 networkModule 로 분리 networkModule 을 생성함에 따라 모듈의 순환 구조를 방지하기 위해 datastore 모듈 추가 * style check success * style check success * fix run build fail * 스플래시 뷰모델 api 호출 이후 0.5 초 이내에 응답을 받을 경우 Loading Screen 을 띄우지 않는 로직 적용 update 로 uiState 변경 로직 통일 * chore: 코드 정리 viewModel 에서 수집한 error 는 이미 toAlertMessage 함수에 의해 custom 된 exception message 이므로 로그를 확인할 필요가 없으므로 제거 * 하드코딩된 텍스트 stringResource 로 변경 * Update android-cd.yml --------- Co-authored-by: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> --- .github/workflows/android-cd.yml | 21 +- app/build.gradle.kts | 6 + app/proguard-debug.pro | 9 + app/proguard-rules.pro | 9 + build.gradle.kts | 5 +- core/data/build.gradle.kts | 9 +- .../android/core/data/di/RepositoryModule.kt | 12 +- .../android/core/data/di/ServiceModule.kt | 27 ++ .../CompletedBandalartKeyDataStoreImpl.kt | 2 +- .../GuestLoginLocalDataSourceImpl.kt | 2 +- .../RecentBandalartKeyDataSourceImpl.kt | 2 +- .../BandalartRemoteDataSourceImpl.kt | 111 +++--- .../GuestLoginRemoteDataSourceImpl.kt | 14 +- .../ServerHealthCheckRepositoryImpl.kt | 32 +- .../core/data/service/BandalartService.kt | 85 +++++ .../core/data/service/GuestLoginService.kt | 10 + .../core/data/util/extension/Exception.kt | 16 +- .../core/data/util/extension/HttpClient.kt | 58 ++- core/datastore/build.gradle.kts | 30 ++ .../core/datastore}/DataStoreProvider.kt | 4 +- .../core/datastore/di/DataStoreModule.kt} | 10 +- .../repository/ServerHealthCheckRepository.kt | 20 +- .../usecase/CheckServerHealthUseCase.kt | 24 +- core/network/build.gradle.kts | 39 ++ .../android/core/network}/di/NetworkModule.kt | 56 ++- .../feature/home/BandalartBottomSheet.kt | 22 +- .../home/BandalartBottomSheetViewModel.kt | 198 +++++------ .../android/feature/home/HomeScreen.kt | 1 - .../android/feature/home/HomeViewModel.kt | 336 +++++++++--------- .../android/feature/splash/SplashScreen.kt | 5 +- .../android/feature/splash/SplashViewModel.kt | 27 +- gradle/libs.versions.toml | 25 +- settings.gradle.kts | 2 + 33 files changed, 784 insertions(+), 445 deletions(-) create mode 100644 core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/ServiceModule.kt create mode 100644 core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/service/BandalartService.kt create mode 100644 core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/service/GuestLoginService.kt create mode 100644 core/datastore/build.gradle.kts rename core/{data/src/main/kotlin/com/nexters/bandalart/android/core/data/local => datastore/src/main/kotlin/com/nexters/bandalart/android/core/datastore}/DataStoreProvider.kt (97%) rename core/{data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/DataStoreProviderModule.kt => datastore/src/main/kotlin/com/nexters/bandalart/android/core/datastore/di/DataStoreModule.kt} (71%) create mode 100644 core/network/build.gradle.kts rename core/{data/src/main/kotlin/com/nexters/bandalart/android/core/data => network/src/main/kotlin/com/nexters/bandalart/android/core/network}/di/NetworkModule.kt (51%) diff --git a/.github/workflows/android-cd.yml b/.github/workflows/android-cd.yml index c8cb6d96..97fdd4ce 100644 --- a/.github/workflows/android-cd.yml +++ b/.github/workflows/android-cd.yml @@ -81,13 +81,20 @@ jobs: run: | echo "SERVER_BASE_URL=${{ secrets.SERVER_BASE_URL }}" >> secrets.properties - - name: Decode Keystore - id: decode_keystore - uses: timheuer/base64-to-file@v1 - with: - fileName: '/app/bandalart.jks' - encodedString: ${{secrets.APP_RELEASE_KEY_STORE_BASE_64}} - + # - name: Decode Keystore + # id: decode_keystore + # uses: timheuer/base64-to-file@v1 + # with: + # fileName: '/app/bandalart.jks' + # encodedString: ${{secrets.APP_RELEASE_KEY_STORE_BASE_64}} + + - name: Generate Keystore file from Github Secrets + run: | + echo "$KEYSTORE" > ./app/bandalart.b64 + base64 -d -i ./app/bandalart.b64 > ./app/bandalart.jks + env: + KEYSTORE: ${{ secrets.APP_RELEASE_KEY_STORE_BASE_64 }} + - name: Generate keystore.properties run: | echo "STORE_FILE=${{ secrets.STORE_FILE }}" >> keystore.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7bee4d64..f399bebf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,6 +26,10 @@ android { buildTypes { getByName("debug") { + proguardFiles( + getDefaultProguardFile("proguard-android.txt"), + "proguard-debug.pro" + ) applicationIdSuffix = ".dev" manifestPlaceholders += mapOf( "appName" to "@string/app_name_dev" @@ -59,8 +63,10 @@ dependencies { coreLibraryDesugaring(libs.desugar.jdk) implementations( projects.core.data, + projects.core.datastore, projects.core.designsystem, projects.core.domain, + projects.core.network, projects.core.ui, projects.feature.complete, projects.feature.home, diff --git a/app/proguard-debug.pro b/app/proguard-debug.pro index 18d6340c..a81766c2 100644 --- a/app/proguard-debug.pro +++ b/app/proguard-debug.pro @@ -3,3 +3,12 @@ -dontobfuscate #난독화를 수행하지 않도록 함 -keepattributes SoureFile,LineNumberTable #소스파일, 라인 정보 유지 # End: Debug ProGuard rules + + # Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). + -keep,allowobfuscation,allowshrinking interface retrofit2.Call + -keep,allowobfuscation,allowshrinking class retrofit2.Response + + # With R8 full mode generic signatures are stripped for classes that are not + # kept. Suspend functions are wrapped in continuations where the type argument + # is used. + -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 18e131eb..1e871007 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,12 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + + # Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). + -keep,allowobfuscation,allowshrinking interface retrofit2.Call + -keep,allowobfuscation,allowshrinking class retrofit2.Response + + # With R8 full mode generic signatures are stripped for classes that are not + # kept. Suspend functions are wrapped in continuations where the type argument + # is used. + -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index d6aa778f..cebdc701 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -73,8 +73,9 @@ tasks.register("clean", type = Delete::class) { } tasks.register("bundleRelease", type = Exec::class) { - commandLine(project.rootDir.resolve("gradlew"), "bundle") - workingDir = project.rootDir + commandLine(project.rootDir.resolve("gradlew"), "bundle") + workingDir = project.rootDir + } tasks.register("release") { diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index bc233707..0d494014 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -5,7 +5,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { bandalart("android-library") bandalart("android-hilt") - alias(libs.plugins.google.secrets) alias(libs.plugins.kotlinx.serialization) } @@ -20,9 +19,13 @@ android { dependencies { implementations( projects.core.domain, + projects.core.datastore, + projects.core.network, libs.kotlinx.serialization.json, libs.androidx.datastore.preferences, libs.bundles.ktor.client, + libs.bundles.retrofit, + libs.bundles.okhttp, libs.timber, ) } @@ -32,7 +35,3 @@ tasks.withType { freeCompilerArgs = freeCompilerArgs + listOf("-opt-in=kotlin.ExperimentalStdlibApi") } } - -secrets { - defaultPropertiesFileName = "secrets.properties" -} diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/RepositoryModule.kt index 0d5e86f9..d34a5e61 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/RepositoryModule.kt @@ -2,10 +2,8 @@ package com.nexters.bandalart.android.core.data.di import com.nexters.bandalart.android.core.data.repository.BandalartRepositoryImpl import com.nexters.bandalart.android.core.data.repository.GuestLoginTokenRepositoryImpl -import com.nexters.bandalart.android.core.data.repository.ServerHealthCheckRepositoryImpl import com.nexters.bandalart.android.core.domain.repository.BandalartRepository import com.nexters.bandalart.android.core.domain.repository.GuestLoginTokenRepository -import com.nexters.bandalart.android.core.domain.repository.ServerHealthCheckRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -22,11 +20,11 @@ internal abstract class RepositoryModule { guestLoginTokenRepositoryImpl: GuestLoginTokenRepositoryImpl, ): GuestLoginTokenRepository - @Binds - @Singleton - abstract fun bindServerHealthCheckRepository( - serverHealthCheckRepositoryImpl: ServerHealthCheckRepositoryImpl, - ): ServerHealthCheckRepository +// @Binds +// @Singleton +// abstract fun bindServerHealthCheckRepository( +// serverHealthCheckRepositoryImpl: ServerHealthCheckRepositoryImpl, +// ): ServerHealthCheckRepository @Binds @Singleton diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/ServiceModule.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/ServiceModule.kt new file mode 100644 index 00000000..73e981e0 --- /dev/null +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/ServiceModule.kt @@ -0,0 +1,27 @@ +package com.nexters.bandalart.android.core.data.di + +import com.nexters.bandalart.android.core.data.service.BandalartService +import com.nexters.bandalart.android.core.data.service.GuestLoginService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import retrofit2.Retrofit + +@Module +@InstallIn(SingletonComponent::class) +internal object ServiceModule { + + @Singleton + @Provides + internal fun provideBandalartService(retrofit: Retrofit): BandalartService { + return retrofit.create(BandalartService::class.java) + } + + @Singleton + @Provides + internal fun provideGuestLoginService(retrofit: Retrofit): GuestLoginService { + return retrofit.create(GuestLoginService::class.java) + } +} diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/CompletedBandalartKeyDataStoreImpl.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/CompletedBandalartKeyDataStoreImpl.kt index 9c92d560..08453e97 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/CompletedBandalartKeyDataStoreImpl.kt +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/CompletedBandalartKeyDataStoreImpl.kt @@ -1,7 +1,7 @@ package com.nexters.bandalart.android.core.data.local.datasource import com.nexters.bandalart.android.core.data.datasource.CompletedBandalartKeyDataSource -import com.nexters.bandalart.android.core.data.local.DataStoreProvider +import com.nexters.bandalart.android.core.datastore.DataStoreProvider import javax.inject.Inject internal class CompletedBandalartKeyDataStoreImpl @Inject constructor( diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/GuestLoginLocalDataSourceImpl.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/GuestLoginLocalDataSourceImpl.kt index 288eb322..75be08bc 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/GuestLoginLocalDataSourceImpl.kt +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/GuestLoginLocalDataSourceImpl.kt @@ -1,7 +1,7 @@ package com.nexters.bandalart.android.core.data.local.datasource import com.nexters.bandalart.android.core.data.datasource.GuestLoginLocalDataSource -import com.nexters.bandalart.android.core.data.local.DataStoreProvider +import com.nexters.bandalart.android.core.datastore.DataStoreProvider import javax.inject.Inject internal class GuestLoginLocalDataSourceImpl @Inject constructor( diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/RecentBandalartKeyDataSourceImpl.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/RecentBandalartKeyDataSourceImpl.kt index e20f57ce..18f5546b 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/RecentBandalartKeyDataSourceImpl.kt +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/datasource/RecentBandalartKeyDataSourceImpl.kt @@ -1,7 +1,7 @@ package com.nexters.bandalart.android.core.data.local.datasource import com.nexters.bandalart.android.core.data.datasource.RecentBandalartKeyDataSource -import com.nexters.bandalart.android.core.data.local.DataStoreProvider +import com.nexters.bandalart.android.core.datastore.DataStoreProvider import javax.inject.Inject internal class RecentBandalartKeyDataSourceImpl @Inject constructor( diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/remote/datasource/BandalartRemoteDataSourceImpl.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/remote/datasource/BandalartRemoteDataSourceImpl.kt index cc97b458..51120a4b 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/remote/datasource/BandalartRemoteDataSourceImpl.kt +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/remote/datasource/BandalartRemoteDataSourceImpl.kt @@ -9,52 +9,65 @@ import com.nexters.bandalart.android.core.data.model.bandalart.UpdateBandalartEm import com.nexters.bandalart.android.core.data.model.bandalart.UpdateBandalartMainCellRequest import com.nexters.bandalart.android.core.data.model.bandalart.UpdateBandalartSubCellRequest import com.nexters.bandalart.android.core.data.model.bandalart.UpdateBandalartTaskCellRequest +import com.nexters.bandalart.android.core.data.service.BandalartService import com.nexters.bandalart.android.core.data.util.extension.safeRequest -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.request.delete -import io.ktor.client.request.get -import io.ktor.client.request.patch -import io.ktor.client.request.post -import io.ktor.client.request.setBody import javax.inject.Inject internal class BandalartRemoteDataSourceImpl @Inject constructor( - private val client: HttpClient, + // private val client: HttpClient, + private val service: BandalartService, ) : BandalartRemoteDataSource { override suspend fun createBandalart(): BandalartResponse? { - return client.safeRequest { - post("v1/bandalarts").body() +// return client.safeRequest { +// post("v1/bandalarts").body() +// } + return safeRequest { + service.createBandalart() } } override suspend fun getBandalartList(): List? { - return client.safeRequest { - get("v1/bandalarts").body() +// return client.safeRequest { +// get("v1/bandalarts").body() +// } + return safeRequest { + service.getBandalartList() } } override suspend fun getBandalartDetail(bandalartKey: String): BandalartDetailResponse? { - return client.safeRequest { - get("v1/bandalarts/$bandalartKey").body() +// return client.safeRequest { +// get("v1/bandalarts/$bandalartKey").body() +// } + return safeRequest { + service.getBandalartDetail(bandalartKey) } } override suspend fun deleteBandalart(bandalartKey: String) { - client.safeRequest { - delete("v1/bandalarts/$bandalartKey") +// client.safeRequest { +// delete("v1/bandalarts/$bandalartKey") +// } + safeRequest { + service.deleteBandalart(bandalartKey) } } override suspend fun getBandalartMainCell(bandalartKey: String): BandalartCellResponse? { - return client.safeRequest { - get("v1/bandalarts/$bandalartKey/cells").body() +// return client.safeRequest { +// get("v1/bandalarts/$bandalartKey/cells").body() +// } + return safeRequest { + service.getBandalartMainCell(bandalartKey) } } override suspend fun getBandalartCell(bandalartKey: String, cellKey: String): BandalartCellResponse? { - return client.safeRequest { - get("v1/bandalarts/$bandalartKey/cells/$cellKey").body() +// return client.safeRequest { +// get("v1/bandalarts/$bandalartKey/cells/$cellKey").body() +// } + return safeRequest { + service.getBandalartCell(bandalartKey, cellKey) } } @@ -63,10 +76,13 @@ internal class BandalartRemoteDataSourceImpl @Inject constructor( cellKey: String, updateBandalartMainCellRequest: UpdateBandalartMainCellRequest, ) { - client.safeRequest { - patch("v1/bandalarts/$bandalartKey/cells/$cellKey") { - setBody(updateBandalartMainCellRequest) - } +// client.safeRequest { +// patch("v1/bandalarts/$bandalartKey/cells/$cellKey") { +// setBody(updateBandalartMainCellRequest) +// } +// } + safeRequest { + service.updateBandalartMainCell(bandalartKey, cellKey, updateBandalartMainCellRequest) } } @@ -75,10 +91,13 @@ internal class BandalartRemoteDataSourceImpl @Inject constructor( cellKey: String, updateBandalartSubCellRequest: UpdateBandalartSubCellRequest, ) { - client.safeRequest { - patch("v1/bandalarts/$bandalartKey/cells/$cellKey") { - setBody(updateBandalartSubCellRequest) - } +// client.safeRequest { +// patch("v1/bandalarts/$bandalartKey/cells/$cellKey") { +// setBody(updateBandalartSubCellRequest) +// } +// } + safeRequest { + service.updateBandalartSubCell(bandalartKey, cellKey, updateBandalartSubCellRequest) } } @@ -87,10 +106,13 @@ internal class BandalartRemoteDataSourceImpl @Inject constructor( cellKey: String, updateBandalartTaskCellRequest: UpdateBandalartTaskCellRequest, ) { - client.safeRequest { - patch("v1/bandalarts/$bandalartKey/cells/$cellKey") { - setBody(updateBandalartTaskCellRequest) - } +// client.safeRequest { +// patch("v1/bandalarts/$bandalartKey/cells/$cellKey") { +// setBody(updateBandalartTaskCellRequest) +// } +// } + safeRequest { + service.updateBandalartTaskCell(bandalartKey, cellKey, updateBandalartTaskCellRequest) } } @@ -99,22 +121,31 @@ internal class BandalartRemoteDataSourceImpl @Inject constructor( cellKey: String, updateBandalartEmojiRequest: UpdateBandalartEmojiRequest, ) { - client.safeRequest { - patch("v1/bandalarts/$bandalartKey/cells/$cellKey") { - setBody(updateBandalartEmojiRequest) - } +// client.safeRequest { +// patch("v1/bandalarts/$bandalartKey/cells/$cellKey") { +// setBody(updateBandalartEmojiRequest) +// } +// } + safeRequest { + service.updateBandalartEmoji(bandalartKey, cellKey, updateBandalartEmojiRequest) } } override suspend fun deleteBandalartCell(bandalartKey: String, cellKey: String) { - client.safeRequest { - delete("v1/bandalarts/$bandalartKey/cells/$cellKey") +// client.safeRequest { +// delete("v1/bandalarts/$bandalartKey/cells/$cellKey") +// } + safeRequest { + service.deleteBandalartCell(bandalartKey, cellKey) } } override suspend fun shareBandalart(bandalartKey: String): BandalartShareResponse? { - return client.safeRequest { - post("v1/bandalarts/$bandalartKey/shares").body() +// return client.safeRequest { +// post("v1/bandalarts/$bandalartKey/shares").body() +// } + return safeRequest { + service.shareBandalart(bandalartKey) } } } diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/remote/datasource/GuestLoginRemoteDataSourceImpl.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/remote/datasource/GuestLoginRemoteDataSourceImpl.kt index 0e0c5002..252b571d 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/remote/datasource/GuestLoginRemoteDataSourceImpl.kt +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/remote/datasource/GuestLoginRemoteDataSourceImpl.kt @@ -2,18 +2,20 @@ package com.nexters.bandalart.android.core.data.remote.datasource import com.nexters.bandalart.android.core.data.datasource.GuestLoginRemoteDataSource import com.nexters.bandalart.android.core.data.model.GuestLoginTokenResponse +import com.nexters.bandalart.android.core.data.service.GuestLoginService import com.nexters.bandalart.android.core.data.util.extension.safeRequest -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.request.post import javax.inject.Inject internal class GuestLoginRemoteDataSourceImpl @Inject constructor( - private val client: HttpClient, + private val service: GuestLoginService, + // private val client: HttpClient, ) : GuestLoginRemoteDataSource { override suspend fun createGuestLoginToken(): GuestLoginTokenResponse? { - return client.safeRequest { - post("v1/users/guests").body() +// return client.safeRequest { +// post("v1/users/guests").body() +// } + return safeRequest { + service.createGuestLoginToken() } } } diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/repository/ServerHealthCheckRepositoryImpl.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/repository/ServerHealthCheckRepositoryImpl.kt index 46f1db2f..bc822687 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/repository/ServerHealthCheckRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/repository/ServerHealthCheckRepositoryImpl.kt @@ -1,18 +1,18 @@ package com.nexters.bandalart.android.core.data.repository -import com.nexters.bandalart.android.core.data.util.extension.safeRequest -import com.nexters.bandalart.android.core.domain.repository.ServerHealthCheckRepository -import io.ktor.client.HttpClient -import io.ktor.client.request.get -import javax.inject.Inject - -@Suppress("TooGenericExceptionCaught") -internal class ServerHealthCheckRepositoryImpl @Inject constructor( - private val client: HttpClient, -) : ServerHealthCheckRepository { - override suspend fun checkServerHealth() { - client.safeRequest { - get("/health-check") - } - } -} +// import com.nexters.bandalart.android.core.data.util.extension.safeRequest +// import com.nexters.bandalart.android.core.domain.repository.ServerHealthCheckRepository +// import io.ktor.client.HttpClient +// import io.ktor.client.request.get +// import javax.inject.Inject +// +// @Suppress("TooGenericExceptionCaught") +// internal class ServerHealthCheckRepositoryImpl @Inject constructor( +// private val client: HttpClient, +// ) : ServerHealthCheckRepository { +// override suspend fun checkServerHealth() { +// client.safeRequest { +// get("/health-check") +// } +// } +// } diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/service/BandalartService.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/service/BandalartService.kt new file mode 100644 index 00000000..4818e02a --- /dev/null +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/service/BandalartService.kt @@ -0,0 +1,85 @@ +package com.nexters.bandalart.android.core.data.service + +import com.nexters.bandalart.android.core.data.model.bandalart.BandalartCellResponse +import com.nexters.bandalart.android.core.data.model.bandalart.BandalartDetailResponse +import com.nexters.bandalart.android.core.data.model.bandalart.BandalartResponse +import com.nexters.bandalart.android.core.data.model.bandalart.BandalartShareResponse +import com.nexters.bandalart.android.core.data.model.bandalart.UpdateBandalartEmojiRequest +import com.nexters.bandalart.android.core.data.model.bandalart.UpdateBandalartMainCellRequest +import com.nexters.bandalart.android.core.data.model.bandalart.UpdateBandalartSubCellRequest +import com.nexters.bandalart.android.core.data.model.bandalart.UpdateBandalartTaskCellRequest +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path + +interface BandalartService { + @POST("v1/bandalarts") + suspend fun createBandalart(): Response + + @GET("v1/bandalarts") + suspend fun getBandalartList(): Response> + + @GET("v1/bandalarts/{bandalartKey}") + suspend fun getBandalartDetail( + @Path("bandalartKey") bandalartKey: String, + ): Response + + @DELETE("v1/bandalarts/{bandalartKey}") + suspend fun deleteBandalart( + @Path("bandalartKey") bandalartKey: String, + ): Response + + @GET("v1/bandalarts/{bandalartKey}/cells") + suspend fun getBandalartMainCell( + @Path("bandalartKey") bandalartKey: String, + ): Response + + @GET("v1/bandalarts/{bandalartKey}/cells/{cellKey}") + suspend fun getBandalartCell( + @Path("bandalartKey") bandalartKey: String, + @Path("cellKey") cellKey: String, + ): Response + + @PATCH("v1/bandalarts/{bandalartKey}/cells/{cellKey}") + suspend fun updateBandalartMainCell( + @Path("bandalartKey") bandalartKey: String, + @Path("cellKey") cellKey: String, + @Body updateBandalartMainCellRequest: UpdateBandalartMainCellRequest, + ): Response + + @PATCH("v1/bandalarts/{bandalartKey}/cells/{cellKey}") + suspend fun updateBandalartSubCell( + @Path("bandalartKey") bandalartKey: String, + @Path("cellKey") cellKey: String, + @Body updateBandalartSubCellRequest: UpdateBandalartSubCellRequest, + ): Response + + @PATCH("v1/bandalarts/{bandalartKey}/cells/{cellKey}") + suspend fun updateBandalartTaskCell( + @Path("bandalartKey") bandalartKey: String, + @Path("cellKey") cellKey: String, + @Body updateBandalartTaskCellRequest: UpdateBandalartTaskCellRequest, + ): Response + + @PATCH("v1/bandalarts/{bandalartKey}/cells/{cellKey}") + suspend fun updateBandalartEmoji( + @Path("bandalartKey") bandalartKey: String, + @Path("cellKey") cellKey: String, + @Body updateBandalartEmojiRequest: UpdateBandalartEmojiRequest, + ): Response + + @DELETE("v1/bandalarts/{bandalartKey}/cells/{cellKey}") + suspend fun deleteBandalartCell( + @Path("bandalartKey") bandalartKey: String, + @Path("cellKey") cellKey: String, + ): Response + + @POST("v1/bandalarts/{bandalartKey}/shares") + suspend fun shareBandalart( + @Path("bandalartKey") bandalartKey: String, + ): Response +} diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/service/GuestLoginService.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/service/GuestLoginService.kt new file mode 100644 index 00000000..b8b0debf --- /dev/null +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/service/GuestLoginService.kt @@ -0,0 +1,10 @@ +package com.nexters.bandalart.android.core.data.service + +import com.nexters.bandalart.android.core.data.model.GuestLoginTokenResponse +import retrofit2.Response +import retrofit2.http.POST + +interface GuestLoginService { + @POST("v1/users/guests") + suspend fun createGuestLoginToken(): Response +} diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/util/extension/Exception.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/util/extension/Exception.kt index 6045067d..1818351a 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/util/extension/Exception.kt +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/util/extension/Exception.kt @@ -1,12 +1,20 @@ package com.nexters.bandalart.android.core.data.util.extension -import io.ktor.client.plugins.ResponseException -import java.nio.channels.UnresolvedAddressException +import java.net.UnknownHostException +import retrofit2.HttpException + +// internal fun Exception.toAlertMessage(): String { +// return when (this) { +// is UnresolvedAddressException -> "네트워크 연결을 확인해주세요." +// is ResponseException -> "서버에 문제가 발생했어요. 잠시 후 다시 시도해주세요." +// else -> "예기치 못한 오류가 발생했어요. 잠시 후 다시 시도해주세요." +// } +// } internal fun Exception.toAlertMessage(): String { return when (this) { - is UnresolvedAddressException -> "네트워크 연결을 확인해주세요." - is ResponseException -> "서버에 문제가 발생했어요. 잠시 후 다시 시도해주세요." + is HttpException -> "서버에 문제가 발생했어요. 잠시 후 다시 시도해주세요." + is UnknownHostException -> "네트워크 연결을 확인해주세요." else -> "예기치 못한 오류가 발생했어요. 잠시 후 다시 시도해주세요." } } diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/util/extension/HttpClient.kt b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/util/extension/HttpClient.kt index a7aea85a..d461b61b 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/util/extension/HttpClient.kt +++ b/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/util/extension/HttpClient.kt @@ -1,25 +1,57 @@ package com.nexters.bandalart.android.core.data.util.extension import com.nexters.bandalart.android.core.data.util.ExceptionWrapper -import io.ktor.client.HttpClient -import io.ktor.client.plugins.ResponseException -import java.nio.channels.UnresolvedAddressException +import java.net.UnknownHostException +import retrofit2.HttpException +import retrofit2.Response +import timber.log.Timber + +// @Suppress("TooGenericExceptionCaught") +// internal suspend fun HttpClient.safeRequest( +// request: suspend HttpClient.() -> T, +// ): T { +// return try { +// this.request() +// } catch (exception: UnresolvedAddressException) { +// throw ExceptionWrapper(message = exception.toAlertMessage(), cause = exception) +// } catch (exception: ResponseException) { +// throw ExceptionWrapper( +// statusCode = exception.response.status.value, +// message = exception.toAlertMessage(), +// cause = exception, +// ) +// } catch (exception: Exception) { +// throw ExceptionWrapper(message = exception.toAlertMessage(), cause = exception) +// } +// } @Suppress("TooGenericExceptionCaught") -internal suspend fun HttpClient.safeRequest( - request: suspend HttpClient.() -> T, -): T { - return try { - this.request() - } catch (exception: UnresolvedAddressException) { - throw ExceptionWrapper(message = exception.toAlertMessage(), cause = exception) - } catch (exception: ResponseException) { +internal suspend fun safeRequest(request: suspend () -> Response): T? { + try { + val response = request() + if (response.isSuccessful) { + return response.body() + } else { + val errorBody = response.errorBody()?.string() ?: "Unknown error" + Timber.d(Exception(errorBody)) + throw ExceptionWrapper( + statusCode = response.code(), + message = Exception(errorBody).toAlertMessage(), + cause = Exception(errorBody), + ) + } + } catch (exception: HttpException) { + Timber.d(exception) throw ExceptionWrapper( - statusCode = exception.response.status.value, - message = exception.toAlertMessage(), + statusCode = exception.code(), + message = exception.response()?.errorBody()?.string() ?: exception.message(), cause = exception, ) + } catch (exception: UnknownHostException) { + Timber.d(exception) + throw ExceptionWrapper(message = exception.toAlertMessage(), cause = exception) } catch (exception: Exception) { + Timber.d(exception) throw ExceptionWrapper(message = exception.toAlertMessage(), cause = exception) } } diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts new file mode 100644 index 00000000..013b8b93 --- /dev/null +++ b/core/datastore/build.gradle.kts @@ -0,0 +1,30 @@ +@file:Suppress("INLINE_FROM_HIGHER_PLATFORM", "UnstableApiUsage") + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + bandalart("android-library") + bandalart("android-hilt") + alias(libs.plugins.kotlinx.serialization) +} + +android { + namespace = "com.nexters.bandalart.android.core.datastore" + + buildFeatures { + buildConfig = true + } +} + +dependencies { + implementations( + libs.kotlinx.serialization.json, + libs.androidx.datastore.preferences, + ) +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + listOf("-opt-in=kotlin.ExperimentalStdlibApi") + } +} diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/DataStoreProvider.kt b/core/datastore/src/main/kotlin/com/nexters/bandalart/android/core/datastore/DataStoreProvider.kt similarity index 97% rename from core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/DataStoreProvider.kt rename to core/datastore/src/main/kotlin/com/nexters/bandalart/android/core/datastore/DataStoreProvider.kt index 63e3a06a..d8e2c7c7 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/local/DataStoreProvider.kt +++ b/core/datastore/src/main/kotlin/com/nexters/bandalart/android/core/datastore/DataStoreProvider.kt @@ -1,4 +1,4 @@ -package com.nexters.bandalart.android.core.data.local +package com.nexters.bandalart.android.core.datastore import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.first import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -internal class DataStoreProvider @Inject constructor( +class DataStoreProvider @Inject constructor( private val dataStore: DataStore, ) { diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/DataStoreProviderModule.kt b/core/datastore/src/main/kotlin/com/nexters/bandalart/android/core/datastore/di/DataStoreModule.kt similarity index 71% rename from core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/DataStoreProviderModule.kt rename to core/datastore/src/main/kotlin/com/nexters/bandalart/android/core/datastore/di/DataStoreModule.kt index 56fc7d4f..97f8362b 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/DataStoreProviderModule.kt +++ b/core/datastore/src/main/kotlin/com/nexters/bandalart/android/core/datastore/di/DataStoreModule.kt @@ -1,10 +1,10 @@ -package com.nexters.bandalart.android.core.data.di +package com.nexters.bandalart.android.core.datastore.di import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore -import com.nexters.bandalart.android.core.data.local.DataStoreProvider +import com.nexters.bandalart.android.core.datastore.DataStoreProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,14 +17,12 @@ private val Context.bandalartDataStore: DataStore by preferencesDat @Module @InstallIn(SingletonComponent::class) -internal object DataStoreProviderModule { +internal object DataStoreModule { @Provides @Singleton fun providePreferencesDataStore(@ApplicationContext context: Context) = context.bandalartDataStore @Provides - @Singleton - fun provideDataStoreProvider(dataStore: DataStore): DataStoreProvider = - DataStoreProvider(dataStore) + fun provideDataStore(dataStore: DataStore) = DataStoreProvider(dataStore) } diff --git a/core/domain/src/main/kotlin/com/nexters/bandalart/android/core/domain/repository/ServerHealthCheckRepository.kt b/core/domain/src/main/kotlin/com/nexters/bandalart/android/core/domain/repository/ServerHealthCheckRepository.kt index fa9ce277..70200d95 100644 --- a/core/domain/src/main/kotlin/com/nexters/bandalart/android/core/domain/repository/ServerHealthCheckRepository.kt +++ b/core/domain/src/main/kotlin/com/nexters/bandalart/android/core/domain/repository/ServerHealthCheckRepository.kt @@ -1,12 +1,12 @@ package com.nexters.bandalart.android.core.domain.repository -/** 서버 Health Check API */ - -interface ServerHealthCheckRepository { - - /** - * 서버 Health Check - */ - - suspend fun checkServerHealth() -} +// /** 서버 Health Check API */ +// +// interface ServerHealthCheckRepository { +// +// /** +// * 서버 Health Check +// */ +// +// suspend fun checkServerHealth() +// } diff --git a/core/domain/src/main/kotlin/com/nexters/bandalart/android/core/domain/usecase/CheckServerHealthUseCase.kt b/core/domain/src/main/kotlin/com/nexters/bandalart/android/core/domain/usecase/CheckServerHealthUseCase.kt index 0e79abac..8135469d 100644 --- a/core/domain/src/main/kotlin/com/nexters/bandalart/android/core/domain/usecase/CheckServerHealthUseCase.kt +++ b/core/domain/src/main/kotlin/com/nexters/bandalart/android/core/domain/usecase/CheckServerHealthUseCase.kt @@ -1,14 +1,14 @@ package com.nexters.bandalart.android.core.domain.usecase -import com.nexters.bandalart.android.core.domain.repository.ServerHealthCheckRepository -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class CheckServerHealthUseCase @Inject constructor( - private val repository: ServerHealthCheckRepository, -) { - suspend operator fun invoke() { - return repository.checkServerHealth() - } -} +// import com.nexters.bandalart.android.core.domain.repository.ServerHealthCheckRepository +// import javax.inject.Inject +// import javax.inject.Singleton +// +// @Singleton +// class CheckServerHealthUseCase @Inject constructor( +// private val repository: ServerHealthCheckRepository, +// ) { +// suspend operator fun invoke() { +// return repository.checkServerHealth() +// } +// } diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts new file mode 100644 index 00000000..6292df2e --- /dev/null +++ b/core/network/build.gradle.kts @@ -0,0 +1,39 @@ +@file:Suppress("INLINE_FROM_HIGHER_PLATFORM", "UnstableApiUsage") + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + bandalart("android-library") + bandalart("android-hilt") + alias(libs.plugins.google.secrets) + alias(libs.plugins.kotlinx.serialization) +} + +android { + namespace = "com.nexters.bandalart.android.core.network" + + buildFeatures { + buildConfig = true + } +} + +dependencies { + implementations( + projects.core.datastore, + libs.kotlinx.serialization.json, + libs.bundles.ktor.client, + libs.bundles.retrofit, + libs.bundles.okhttp, + libs.timber, + ) +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + listOf("-opt-in=kotlin.ExperimentalStdlibApi") + } +} + +secrets { + defaultPropertiesFileName = "secrets.properties" +} diff --git a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/NetworkModule.kt b/core/network/src/main/kotlin/com/nexters/bandalart/android/core/network/di/NetworkModule.kt similarity index 51% rename from core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/NetworkModule.kt rename to core/network/src/main/kotlin/com/nexters/bandalart/android/core/network/di/NetworkModule.kt index d56288ab..df78e489 100644 --- a/core/data/src/main/kotlin/com/nexters/bandalart/android/core/data/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/com/nexters/bandalart/android/core/network/di/NetworkModule.kt @@ -1,7 +1,8 @@ -package com.nexters.bandalart.android.core.data.di +package com.nexters.bandalart.android.core.network.di -import com.nexters.bandalart.android.core.data.BuildConfig -import com.nexters.bandalart.android.core.data.local.DataStoreProvider +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.nexters.bandalart.android.core.datastore.DataStoreProvider +import com.nexters.bandalart.android.core.network.BuildConfig import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,14 +19,29 @@ import io.ktor.client.request.header import io.ktor.http.ContentType import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json +import java.util.concurrent.TimeUnit import javax.inject.Singleton import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit import timber.log.Timber private const val MaxTimeoutMillis = 3000L private const val MaxRetryCount = 3 +private val json = Json { + encodeDefaults = true + ignoreUnknownKeys = true + prettyPrint = true + isLenient = true +} + +private val jsonRule = Json { json } + @Module @InstallIn(SingletonComponent::class) internal object NetworkModule { @@ -49,12 +65,7 @@ internal object NetworkModule { header("X-GUEST-KEY", guestLoginToken) } install(ContentNegotiation) { - json(Json { - encodeDefaults = true - ignoreUnknownKeys = true - prettyPrint = true - isLenient = true - }) + json(jsonRule) } install(Logging) { logger = object : Logger { @@ -66,4 +77,31 @@ internal object NetworkModule { } } } + + @Singleton + @Provides + fun provideRetrofitHttpClient(dataStoreProvider: DataStoreProvider): Retrofit { + val contentType = "application/json".toMediaType() + val httpClient = OkHttpClient.Builder() + .connectTimeout(MaxTimeoutMillis, TimeUnit.MILLISECONDS) + .addInterceptor { chain: Interceptor.Chain -> + val request = chain.request().newBuilder() + .addHeader("Content-Type", "application/json") + .addHeader("X-GUEST-KEY", runBlocking { dataStoreProvider.getGuestLoginToken() }) + .build() + chain.proceed(request) + } + .addInterceptor( + HttpLoggingInterceptor { message -> + Timber.tag("HttpClient").d(message) + }.setLevel(HttpLoggingInterceptor.Level.BODY), + ) + .build() + + return Retrofit.Builder() + .baseUrl(BuildConfig.SERVER_BASE_URL) + .client(httpClient) + .addConverterFactory(jsonRule.asConverterFactory(contentType)) + .build() + } } diff --git a/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/BandalartBottomSheet.kt b/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/BandalartBottomSheet.kt index 73bfc8e0..0feb51a6 100644 --- a/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/BandalartBottomSheet.kt +++ b/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/BandalartBottomSheet.kt @@ -106,7 +106,7 @@ fun BandalartBottomSheet( ) -> Unit, viewModel: BottomSheetViewModel = hiltViewModel(), ) { - val uiState by viewModel.bottomSheetState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current val scope = rememberCoroutineScope() val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -166,7 +166,7 @@ fun BandalartBottomSheet( bandalartKey = bandalartKey, cellKey = uiState.cellData.key, ) - viewModel.openDeleteCellDialog(deleteCellDialogState = false) + viewModel.openDeleteCellDialog(flag = false) bottomSheetState.hide() }.invokeOnCompletion { if (!bottomSheetState.isVisible) { @@ -175,7 +175,7 @@ fun BandalartBottomSheet( } } }, - onCancelClicked = { viewModel.openDeleteCellDialog(deleteCellDialogState = false) }, + onCancelClicked = { viewModel.openDeleteCellDialog(flag = false) }, ) } @@ -226,8 +226,8 @@ fun BandalartBottomSheet( .aspectRatio(1f) .background(Gray100) .clickable { - viewModel.openEmojiPicker(emojiPickerState = !uiState.isEmojiPickerOpened) - if (uiState.isDatePickerOpened) viewModel.openDatePicker(datePickerState = false) + viewModel.openEmojiPicker(flag = !uiState.isEmojiPickerOpened) + if (uiState.isDatePickerOpened) viewModel.openDatePicker(flag = false) }, contentAlignment = Alignment.Center, ) { @@ -306,7 +306,7 @@ fun BandalartBottomSheet( isBottomSheet = false, onResult = { currentEmojiResult, openEmojiPushResult -> viewModel.emojiSelected(profileEmoji = currentEmojiResult) - viewModel.openEmojiPicker(emojiPickerState = openEmojiPushResult) + viewModel.openEmojiPicker(flag = openEmojiPushResult) }, emojiPickerScope = rememberCoroutineScope(), emojiPickerState = rememberModalBottomSheetState(skipPartiallyExpanded = true), @@ -339,8 +339,8 @@ fun BandalartBottomSheet( .fillMaxWidth() .height(18.dp) .clickable { - viewModel.openDatePicker(datePickerState = !uiState.isDatePickerOpened) - if (uiState.isEmojiPickerOpened) viewModel.openEmojiPicker(emojiPickerState = false) + viewModel.openDatePicker(flag = !uiState.isDatePickerOpened) + if (uiState.isEmojiPickerOpened) viewModel.openEmojiPicker(flag = false) }, ) { if (uiState.cellData.dueDate.isNullOrEmpty()) { @@ -367,7 +367,7 @@ fun BandalartBottomSheet( BandalartDatePicker( onResult = { dueDateResult, openDatePickerPushResult -> viewModel.dueDateChanged(dueDate = dueDateResult.toString()) - viewModel.openDatePicker(datePickerState = openDatePickerPushResult) + viewModel.openDatePicker(flag = openDatePickerPushResult) }, datePickerScope = rememberCoroutineScope(), datePickerState = rememberModalBottomSheetState(skipPartiallyExpanded = true), @@ -422,7 +422,7 @@ fun BandalartBottomSheet( ) Switch( checked = uiState.cellData.isCompleted, - onCheckedChange = { switchOn -> viewModel.isCompletedChanged(isCompleted = switchOn) }, + onCheckedChange = { switchOn -> viewModel.isCompletedChanged(flag = switchOn) }, colors = SwitchDefaults.colors( uncheckedThumbColor = White, uncheckedTrackColor = Gray300, @@ -450,7 +450,7 @@ fun BandalartBottomSheet( if (!isBlankCell) { BottomSheetDeleteButton( modifier = Modifier.weight(1f), - onClick = { viewModel.openDeleteCellDialog(deleteCellDialogState = !uiState.isDeleteCellDialogOpened) }, + onClick = { viewModel.openDeleteCellDialog(flag = !uiState.isDeleteCellDialogOpened) }, ) Spacer(modifier = Modifier.width(9.dp)) } diff --git a/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/BandalartBottomSheetViewModel.kt b/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/BandalartBottomSheetViewModel.kt index 35b9cbae..9eb71e7a 100644 --- a/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/BandalartBottomSheetViewModel.kt +++ b/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/BandalartBottomSheetViewModel.kt @@ -64,14 +64,14 @@ class BottomSheetViewModel @Inject constructor( private val deleteBandalartCellUseCase: DeleteBandalartCellUseCase, ) : ViewModel() { - private val _bottomSheetState = MutableStateFlow(BottomSheetUiState()) - val bottomSheetState: StateFlow = this._bottomSheetState.asStateFlow() + private val _uiState = MutableStateFlow(BottomSheetUiState()) + val uiState: StateFlow = _uiState.asStateFlow() private val _eventFlow = MutableSharedFlow() val eventFlow: SharedFlow = _eventFlow.asSharedFlow() fun copyCellData(cellData: BandalartCellUiModel) { - _bottomSheetState.update { + _uiState.update { it.copy( cellData = cellData, cellDataForCheck = cellData.copy(), @@ -85,28 +85,29 @@ class BottomSheetViewModel @Inject constructor( cellKey: String, updateBandalartMainCellModel: UpdateBandalartMainCellModel, ) { - if (!_bottomSheetState.value.isNetworking) { - _bottomSheetState.value = _bottomSheetState.value.copy(isNetworking = true) - viewModelScope.launch { - val result = updateBandalartMainCellUseCase(bandalartKey, cellKey, updateBandalartMainCellModel.toEntity()) - when { - result.isSuccess && result.getOrNull() != null -> { - _bottomSheetState.value = _bottomSheetState.value.copy(isCellUpdated = true) - } - result.isSuccess && result.getOrNull() == null -> { - Timber.e("Request succeeded but data validation failed") - } - result.isFailure -> { - val exception = result.exceptionOrNull()!! - _bottomSheetState.value = _bottomSheetState.value.copy( - error = exception, - isNetworking = false, - ) - _eventFlow.emit(BottomSheetUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) - Timber.e(exception.message) + if (_uiState.value.isNetworking) + return + + _uiState.update { it.copy(isNetworking = true) } + viewModelScope.launch { + val result = updateBandalartMainCellUseCase(bandalartKey, cellKey, updateBandalartMainCellModel.toEntity()) + when { + result.isSuccess && result.getOrNull() != null -> { + _uiState.update { + it.copy(isCellUpdated = true) } } + result.isSuccess && result.getOrNull() == null -> { + Timber.e("Request succeeded but data validation failed") + } + result.isFailure -> { + val exception = result.exceptionOrNull()!! + _uiState.update { it.copy(error = exception) } + _eventFlow.emit(BottomSheetUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) + Timber.e(exception.message) + } } + _uiState.update { it.copy(isNetworking = false) } } } @@ -115,28 +116,27 @@ class BottomSheetViewModel @Inject constructor( cellKey: String, updateBandalartSubCellModel: UpdateBandalartSubCellModel, ) { - if (!_bottomSheetState.value.isNetworking) { - _bottomSheetState.value = _bottomSheetState.value.copy(isNetworking = true) - viewModelScope.launch { - val result = updateBandalartSubCellUseCase(bandalartKey, cellKey, updateBandalartSubCellModel.toEntity()) - when { - result.isSuccess && result.getOrNull() != null -> { - _bottomSheetState.value = _bottomSheetState.value.copy(isCellUpdated = true) - } - result.isSuccess && result.getOrNull() == null -> { - Timber.e("Request succeeded but data validation failed") - } - result.isFailure -> { - val exception = result.exceptionOrNull()!! - _bottomSheetState.value = _bottomSheetState.value.copy( - error = exception, - isNetworking = false, - ) - _eventFlow.emit(BottomSheetUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) - Timber.e(exception.message) - } + if (_uiState.value.isNetworking) + return + + _uiState.update { it.copy(isNetworking = true) } + viewModelScope.launch { + val result = updateBandalartSubCellUseCase(bandalartKey, cellKey, updateBandalartSubCellModel.toEntity()) + when { + result.isSuccess && result.getOrNull() != null -> { + _uiState.update { it.copy(isCellUpdated = true) } + } + result.isSuccess && result.getOrNull() == null -> { + Timber.e("Request succeeded but data validation failed") + } + result.isFailure -> { + val exception = result.exceptionOrNull()!! + _uiState.update { it.copy(error = exception) } + _eventFlow.emit(BottomSheetUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) + Timber.e(exception.message) } } + _uiState.update { it.copy(isNetworking = false) } } } @@ -145,79 +145,73 @@ class BottomSheetViewModel @Inject constructor( cellKey: String, updateBandalartTaskCellModel: UpdateBandalartTaskCellModel, ) { - if (!_bottomSheetState.value.isNetworking) { - _bottomSheetState.value = _bottomSheetState.value.copy(isNetworking = true) - viewModelScope.launch { - val result = updateBandalartTaskCellUseCase(bandalartKey, cellKey, updateBandalartTaskCellModel.toEntity()) - when { - result.isSuccess && result.getOrNull() != null -> { - _bottomSheetState.value = _bottomSheetState.value.copy(isCellUpdated = true) - } - result.isSuccess && result.getOrNull() == null -> { - Timber.e("Request succeeded but data validation failed") - } - result.isFailure -> { - val exception = result.exceptionOrNull()!! - _bottomSheetState.value = _bottomSheetState.value.copy( - error = exception, - isNetworking = false, - ) - _eventFlow.emit(BottomSheetUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) - Timber.e(exception.message) - } + if (_uiState.value.isNetworking) + return + + _uiState.update { it.copy(isNetworking = true) } + viewModelScope.launch { + val result = updateBandalartTaskCellUseCase(bandalartKey, cellKey, updateBandalartTaskCellModel.toEntity()) + when { + result.isSuccess && result.getOrNull() != null -> { + _uiState.update { it.copy(isCellUpdated = true) } + } + result.isSuccess && result.getOrNull() == null -> { + Timber.e("Request succeeded but data validation failed") + } + result.isFailure -> { + val exception = result.exceptionOrNull()!! + _uiState.update { it.copy(error = exception) } + _eventFlow.emit(BottomSheetUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) + Timber.e(exception.message) } } + _uiState.update { it.copy(isNetworking = false) } } } fun deleteBandalartCell(bandalartKey: String, cellKey: String) { - if (!_bottomSheetState.value.isNetworking) { - _bottomSheetState.value = _bottomSheetState.value.copy(isNetworking = true) - viewModelScope.launch { - val result = deleteBandalartCellUseCase(bandalartKey, cellKey) - when { - result.isSuccess && result.getOrNull() != null -> { } - result.isSuccess && result.getOrNull() == null -> { - Timber.e("Request succeeded but data validation failed") - } - result.isFailure -> { - val exception = result.exceptionOrNull()!! - _bottomSheetState.value = _bottomSheetState.value.copy( + if (_uiState.value.isNetworking) + return + + _uiState.update { it.copy(isNetworking = true) } + viewModelScope.launch { + val result = deleteBandalartCellUseCase(bandalartKey, cellKey) + when { + result.isSuccess && result.getOrNull() != null -> {} + result.isSuccess && result.getOrNull() == null -> { + Timber.e("Request succeeded but data validation failed") + } + result.isFailure -> { + val exception = result.exceptionOrNull()!! + _uiState.update { + it.copy( isCellDeleted = false, error = exception, - isNetworking = false, ) - openDeleteCellDialog(false) - _eventFlow.emit(BottomSheetUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) - Timber.e(exception.message) } + openDeleteCellDialog(false) + _eventFlow.emit(BottomSheetUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) + Timber.e(exception.message) } } + _uiState.update { it.copy(isNetworking = false) } } } - fun openDeleteCellDialog(deleteCellDialogState: Boolean) { - viewModelScope.launch { - _bottomSheetState.update { - it.copy(isDeleteCellDialogOpened = deleteCellDialogState) - } - } + fun openDeleteCellDialog(flag: Boolean) { + _uiState.update { it.copy(isDeleteCellDialogOpened = flag) } } - fun openDatePicker(datePickerState: Boolean) { - _bottomSheetState.update { - it.copy(isDatePickerOpened = datePickerState) - } + fun openDatePicker(flag: Boolean) { + _uiState.update { it.copy(isDatePickerOpened = flag) } } - fun openEmojiPicker(emojiPickerState: Boolean) { - _bottomSheetState.update { - it.copy(isEmojiPickerOpened = emojiPickerState) - } + fun openEmojiPicker(flag: Boolean) { + _uiState.update { it.copy(isEmojiPickerOpened = flag) } } fun emojiSelected(profileEmoji: String?) { - _bottomSheetState.update { + _uiState.update { it.copy( cellData = it.cellData.copy( profileEmoji = profileEmoji, @@ -227,7 +221,7 @@ class BottomSheetViewModel @Inject constructor( } fun titleChanged(title: String) { - _bottomSheetState.update { + _uiState.update { it.copy( cellData = it.cellData.copy( title = title, @@ -237,7 +231,7 @@ class BottomSheetViewModel @Inject constructor( } fun colorChanged(mainColor: String, subColor: String) { - _bottomSheetState.update { + _uiState.update { it.copy( cellData = it.cellData.copy( mainColor = mainColor, @@ -248,7 +242,7 @@ class BottomSheetViewModel @Inject constructor( } fun dueDateChanged(dueDate: String?) { - _bottomSheetState.update { + _uiState.update { it.copy( cellData = it.cellData.copy( dueDate = dueDate, @@ -258,7 +252,7 @@ class BottomSheetViewModel @Inject constructor( } fun descriptionChanged(description: String?) { - _bottomSheetState.update { + _uiState.update { it.copy( cellData = it.cellData.copy( description = description, @@ -267,19 +261,17 @@ class BottomSheetViewModel @Inject constructor( } } - fun isCompletedChanged(isCompleted: Boolean) { - _bottomSheetState.update { + fun isCompletedChanged(flag: Boolean) { + _uiState.update { it.copy( cellData = it.cellData.copy( - isCompleted = isCompleted, + isCompleted = flag, ), ) } } fun bottomSheetClosed() { - _bottomSheetState.update { - BottomSheetUiState() - } + _uiState.update { BottomSheetUiState() } } } diff --git a/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/HomeScreen.kt b/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/HomeScreen.kt index 15c6888e..8a7531da 100644 --- a/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/HomeScreen.kt +++ b/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/HomeScreen.kt @@ -162,7 +162,6 @@ internal fun HomeScreen( ) { val context = LocalContext.current - // TODO null 를 파라미터로 넣어줘야 하는 이유 학습 LaunchedEffect(key1 = Unit) { getBandalartList(null) } diff --git a/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/HomeViewModel.kt b/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/HomeViewModel.kt index 4eda89e8..d0596040 100644 --- a/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/kotlin/com/nexters/bandalart/android/feature/home/HomeViewModel.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -107,9 +108,7 @@ class HomeViewModel @Inject constructor( val eventFlow: SharedFlow = _eventFlow.asSharedFlow() init { - _uiState.value = _uiState.value.copy( - isShowSkeleton = true, - ) + _uiState.update { it.copy(isShowSkeleton = true) } } fun getBandalartList(bandalartKey: String? = null) { @@ -118,11 +117,12 @@ class HomeViewModel @Inject constructor( when { result.isSuccess && result.getOrNull() != null -> { val bandalartList = result.getOrNull()!!.map { it.toUiModel() } - _uiState.value = _uiState.value.copy( - bandalartList = bandalartList, - error = null, - ) - + _uiState.update { + it.copy( + bandalartList = bandalartList, + error = null, + ) + } // 이전 반다라트 목록 상태 조회 val prevBandalartList = getPrevBandalartListUseCase() @@ -145,26 +145,27 @@ class HomeViewModel @Inject constructor( // 생성한 반다라트 표를 화면에 띄우는 경우 if (bandalartKey != null) { - _uiState.value = _uiState.value.copy(isShowSkeleton = true) + _uiState.update { it.copy(isShowSkeleton = true) } getBandalartDetail(bandalartKey) return@launch } // 반다라트 목록이 존재하지 않을 경우, 새로운 반다라트를 생성 if (bandalartList.isEmpty()) { - _uiState.value = _uiState.value.copy(isNetworking = false) + _uiState.update { it.copy(isNetworking = false) } createBandalart() + return@launch } // 반다라트 목록이 존재할 경우 else { // 가장 최근에 확인한 반다라트 표를 화면에 띄우는 경우 val recentBandalartkey = getRecentBandalartKey() - // 가장 최근에 확인한 반다라트 표가 존재하는 경우 + // 가장 최근에 확인한 반다라트 표가 존재 하는 경우 if (bandalartList.any { it.key == recentBandalartkey }) { getBandalartDetail(recentBandalartkey) } - // 가장 최근에 확인한 반다라트 표가 존재하지 않을 경우 + // 가장 최근에 확인한 반다라트 표가 존재 하지 않을 경우 else { - _uiState.value = _uiState.value.copy(isShowSkeleton = true) + _uiState.update { it.copy(isShowSkeleton = true) } // 목록에 가장 첫번째 표를 화면에 띄움 getBandalartDetail(bandalartList[0].key) } @@ -175,15 +176,17 @@ class HomeViewModel @Inject constructor( } result.isFailure -> { val exception = result.exceptionOrNull()!! - _uiState.value = _uiState.value.copy( - isLoading = false, - isShowSkeleton = false, - isNetworkErrorAlertDialogOpened = true, - error = exception, - ) - Timber.e(exception.message) + _uiState.update { + it.copy( + isLoading = false, + isShowSkeleton = false, + isNetworkErrorAlertDialogOpened = true, + error = exception, + ) + } } } + _uiState.update { it.copy(isNetworking = false) } } } @@ -193,12 +196,14 @@ class HomeViewModel @Inject constructor( when { result.isSuccess && result.getOrNull() != null -> { val bandalartDetailData = result.getOrNull()!!.toUiModel() - _uiState.value = _uiState.value.copy( - bandalartDetailData = bandalartDetailData, - isBandalartListBottomSheetOpened = false, - isBandalartCompleted = isBandalartCompleted, - error = null, - ) + _uiState.update { + it.copy( + bandalartDetailData = bandalartDetailData, + isBandalartListBottomSheetOpened = false, + isBandalartCompleted = isBandalartCompleted, + error = null, + ) + } getBandalartMainCell(bandalartKey) } result.isSuccess && result.getOrNull() == null -> { @@ -206,16 +211,18 @@ class HomeViewModel @Inject constructor( } result.isFailure -> { val exception = result.exceptionOrNull()!! - _uiState.value = _uiState.value.copy( - isLoading = false, - isShowSkeleton = false, - bandalartCellData = null, - isNetworkErrorAlertDialogOpened = true, - error = exception, - ) - Timber.e(exception.message) + _uiState.update { + it.copy( + isLoading = false, + isShowSkeleton = false, + bandalartCellData = null, + isNetworkErrorAlertDialogOpened = true, + error = exception, + ) + } } } + _uiState.update { it.copy(isNetworking = false) } } } @@ -224,14 +231,14 @@ class HomeViewModel @Inject constructor( val result = getBandalartMainCellUseCase(bandalartKey) when { result.isSuccess && result.getOrNull() != null -> { - _uiState.value = _uiState.value.copy( - isLoading = false, - isShowSkeleton = false, - bandalartCellData = result.getOrNull()!!.toUiModel(), - error = null, - isNetworking = false, - ) - bottomSheetDataChanged(isBottomSheetDataChangedState = false) + _uiState.update { + it.copy( + isShowSkeleton = false, + bandalartCellData = result.getOrNull()!!.toUiModel(), + error = null, + ) + } + bottomSheetDataChanged(flag = false) openNetworkErrorAlertDialog(false) } result.isSuccess && result.getOrNull() == null -> { @@ -239,97 +246,111 @@ class HomeViewModel @Inject constructor( } result.isFailure -> { val exception = result.exceptionOrNull()!! - _uiState.value = _uiState.value.copy( - bandalartCellData = null, - isNetworkErrorAlertDialogOpened = true, - isLoading = false, - isShowSkeleton = false, - error = exception, - ) - Timber.e(exception.message) + _uiState.update { + it.copy( + bandalartCellData = null, + isNetworkErrorAlertDialogOpened = true, + isShowSkeleton = false, + error = exception, + ) + } } } + _uiState.update { + it.copy( + isNetworking = false, + isLoading = false, + ) + } } } fun createBandalart() { - if (!_uiState.value.isNetworking) { - _uiState.value = _uiState.value.copy(isNetworking = true) - viewModelScope.launch { - if (_uiState.value.bandalartList.size + 1 > 5) { - _eventFlow.emit(HomeUiEvent.ShowToast(UiText.StringResource(R.string.limit_create_bandalart))) - return@launch - } - _uiState.value = _uiState.value.copy(isShowSkeleton = true) - val result = createBandalartUseCase() - when { - result.isSuccess && result.getOrNull() != null -> { - val bandalart = result.getOrNull()!! - _uiState.value = _uiState.value.copy( + if (_uiState.value.isNetworking) + return + + _uiState.update { it.copy(isNetworking = true) } + viewModelScope.launch { + if (_uiState.value.bandalartList.size + 1 > 5) { + _eventFlow.emit(HomeUiEvent.ShowToast(UiText.StringResource(R.string.limit_create_bandalart))) + return@launch + } + _uiState.update { it.copy(isShowSkeleton = true) } + val result = createBandalartUseCase() + when { + result.isSuccess && result.getOrNull() != null -> { + val bandalart = result.getOrNull()!! + _uiState.update { + it.copy( isBandalartListBottomSheetOpened = false, error = null, ) - // 새로운 반다라트를 생성하면 화면에 생성된 반다라트 표를 보여주도록 key 를 전달 - getBandalartList(bandalart.key) - // 새로운 반다라트의 키를 최근에 확인한 반다라트로 저장 - setRecentBandalartKey(bandalart.key) - // 새로운 반다라트를 로컬에 저장 - upsertBandalartKey(bandalart.key) - _eventFlow.emit(HomeUiEvent.ShowSnackbar(UiText.StringResource(R.string.create_bandalart))) } - result.isSuccess && result.getOrNull() == null -> { - Timber.e("Request succeeded but data validation failed") - } - result.isFailure -> { - val exception = result.exceptionOrNull()!! - _uiState.value = _uiState.value.copy( + // 새로운 반다라트를 생성하면 화면에 생성된 반다라트 표를 보여주도록 key 를 전달 + getBandalartList(bandalart.key) + // 새로운 반다라트의 키를 최근에 확인한 반다라트로 저장 + setRecentBandalartKey(bandalart.key) + // 새로운 반다라트를 로컬에 저장 + upsertBandalartKey(bandalart.key) + _eventFlow.emit(HomeUiEvent.ShowSnackbar(UiText.StringResource(R.string.create_bandalart))) + } + result.isSuccess && result.getOrNull() == null -> { + Timber.e("Request succeeded but data validation failed") + } + result.isFailure -> { + val exception = result.exceptionOrNull()!! + _uiState.update { + it.copy( isLoading = false, isShowSkeleton = false, error = exception, - isNetworking = false, ) - _eventFlow.emit(HomeUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) - Timber.e(exception.message) } + _eventFlow.emit(HomeUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) } } + _uiState.update { it.copy(isNetworking = false) } } } fun deleteBandalart(bandalartKey: String) { - if (!_uiState.value.isNetworking) { - _uiState.value = _uiState.value.copy(isNetworking = true) - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isShowSkeleton = true) - val result = deleteBandalartUseCase(bandalartKey) - when { - result.isSuccess && result.getOrNull() != null -> { - _uiState.value = _uiState.value.copy( + if (_uiState.value.isNetworking) + return + + _uiState.update { it.copy(isNetworking = true) } + viewModelScope.launch { + _uiState.update { it.copy(isShowSkeleton = true) } + val result = deleteBandalartUseCase(bandalartKey) + when { + result.isSuccess && result.getOrNull() != null -> { + _uiState.update { + it.copy( isBandalartDeleted = true, error = null, ) - openBandalartDeleteAlertDialog(false) - getBandalartList() - deleteBandalartKey(bandalartKey) - _eventFlow.emit(HomeUiEvent.ShowSnackbar(UiText.StringResource(R.string.delete_bandalart))) - } - result.isSuccess && result.getOrNull() == null -> { - Timber.e("Request succeeded but data validation failed") } - result.isFailure -> { - val exception = result.exceptionOrNull()!! - _uiState.value = _uiState.value.copy( + openBandalartDeleteAlertDialog(false) + getBandalartList() + deleteBandalartKey(bandalartKey) + _eventFlow.emit(HomeUiEvent.ShowSnackbar(UiText.StringResource(R.string.delete_bandalart))) + } + result.isSuccess && result.getOrNull() == null -> { + Timber.e("Request succeeded but data validation failed") + } + result.isFailure -> { + val exception = result.exceptionOrNull()!! + _uiState.update { + it.copy( isLoading = false, isShowSkeleton = false, isBandalartDeleted = false, error = exception, - isNetworking = false, ) - _eventFlow.emit(HomeUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) - Timber.e(exception.message) } + _eventFlow.emit(HomeUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) } } + _uiState.update { it.copy(isNetworking = false) } } } @@ -338,31 +359,33 @@ class HomeViewModel @Inject constructor( cellKey: String, updateBandalartEmojiModel: UpdateBandalartEmojiModel, ) { - if (!_uiState.value.isNetworking) { - _uiState.value = _uiState.value.copy(isNetworking = true) - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoading = true) - val result = updateBandalartEmojiUseCase(bandalartKey, cellKey, updateBandalartEmojiModel.toEntity()) - when { - result.isSuccess && result.getOrNull() != null -> { - getBandalartList(bandalartKey) - } - result.isSuccess && result.getOrNull() == null -> { - Timber.e("Request succeeded but data validation failed") - } - result.isFailure -> { - val exception = result.exceptionOrNull()!! - _uiState.value = _uiState.value.copy( + if (_uiState.value.isNetworking) + return + + _uiState.update { it.copy(isNetworking = true) } + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + val result = updateBandalartEmojiUseCase(bandalartKey, cellKey, updateBandalartEmojiModel.toEntity()) + when { + result.isSuccess && result.getOrNull() != null -> { + getBandalartList(bandalartKey) + } + result.isSuccess && result.getOrNull() == null -> { + Timber.e("Request succeeded but data validation failed") + } + result.isFailure -> { + val exception = result.exceptionOrNull()!! + _uiState.update { + it.copy( isLoading = false, isShowSkeleton = false, error = exception, - isNetworking = false, ) - _eventFlow.emit(HomeUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) - Timber.e(exception.message) } + _eventFlow.emit(HomeUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) } } + _uiState.update { it.copy(isNetworking = false) } } } @@ -370,84 +393,65 @@ class HomeViewModel @Inject constructor( if (uiState.value.isNetworking || _uiState.value.shareUrl.isNotEmpty()) return - _uiState.value = _uiState.value.copy(isNetworking = true) + _uiState.update { it.copy(isNetworking = true) } viewModelScope.launch { val result = shareBandalartUseCase(bandalartKey) when { result.isSuccess && result.getOrNull() != null -> { - _uiState.value = _uiState.value.copy( - shareUrl = result.getOrNull()!!.shareUrl, - error = null, - ) + _uiState.update { + it.copy( + shareUrl = result.getOrNull()!!.shareUrl, + error = null, + ) + } } result.isSuccess && result.getOrNull() == null -> { Timber.e("Request succeeded but data validation failed") } result.isFailure -> { val exception = result.exceptionOrNull()!! - _uiState.value = _uiState.value.copy( - error = exception, - ) + _uiState.update { it.copy(error = exception) } _eventFlow.emit(HomeUiEvent.ShowToast(UiText.DirectString(exception.message.toString()))) - Timber.e(exception.message) } } - _uiState.value = _uiState.value.copy(isNetworking = false) + _uiState.update { it.copy(isNetworking = false) } } } - fun openDropDownMenu(state: Boolean) { - _uiState.value = _uiState.value.copy( - isDropDownMenuOpened = state, - ) + fun openDropDownMenu(flag: Boolean) { + _uiState.update { it.copy(isDropDownMenuOpened = flag) } } - fun openBandalartDeleteAlertDialog(state: Boolean) { - _uiState.value = _uiState.value.copy( - isBandalartDeleteAlertDialogOpened = state, - ) + fun openBandalartDeleteAlertDialog(flag: Boolean) { + _uiState.update { it.copy(isBandalartDeleteAlertDialogOpened = flag) } } - fun openEmojiBottomSheet(state: Boolean) { - _uiState.value = _uiState.value.copy( - isEmojiBottomSheetOpened = state, - ) + fun openEmojiBottomSheet(flag: Boolean) { + _uiState.update { it.copy(isEmojiBottomSheetOpened = flag) } } - fun openCellBottomSheet(state: Boolean) { - _uiState.value = _uiState.value.copy( - isCellBottomSheetOpened = state, - ) + fun openCellBottomSheet(flag: Boolean) { + _uiState.update { it.copy(isCellBottomSheetOpened = flag) } } - fun bottomSheetDataChanged(isBottomSheetDataChangedState: Boolean) { - _uiState.value = _uiState.value.copy( - isBottomSheetDataChanged = isBottomSheetDataChangedState, - ) + fun bottomSheetDataChanged(flag: Boolean) { + _uiState.update { it.copy(isBottomSheetDataChanged = flag) } } - fun loadingChanged(isLoadingChanged: Boolean) { - _uiState.value = _uiState.value.copy( - isLoading = isLoadingChanged, - ) + fun loadingChanged(flag: Boolean) { + _uiState.update { it.copy(isLoading = flag) } } - fun showSkeletonChanged(isShowSkeletonChanged: Boolean) { - _uiState.value = _uiState.value.copy( - isShowSkeleton = isShowSkeletonChanged, - ) + fun showSkeletonChanged(flag: Boolean) { + _uiState.update { it.copy(isShowSkeleton = flag) } } - fun openNetworkErrorAlertDialog(state: Boolean) { - _uiState.value = _uiState.value.copy( - isNetworkErrorAlertDialogOpened = state, - ) + fun openNetworkErrorAlertDialog(flag: Boolean) { + _uiState.update { it.copy(isNetworkErrorAlertDialogOpened = flag) } } - fun openBandalartListBottomSheet(state: Boolean) { - _uiState.value = _uiState.value.copy( - isBandalartListBottomSheetOpened = state, - ) + fun openBandalartListBottomSheet(flag: Boolean) { + _uiState.update { it.copy(isBandalartListBottomSheetOpened = flag) } } private suspend fun getRecentBandalartKey(): String { @@ -463,9 +467,7 @@ class HomeViewModel @Inject constructor( } fun initShareUrl() { - _uiState.value = _uiState.value.copy( - shareUrl = "", - ) + _uiState.update { it.copy(shareUrl = "") } } private fun upsertBandalartKey(bandalartKey: String, isCompleted: Boolean = false) { diff --git a/feature/splash/src/main/kotlin/com/nexters/bandalart/android/feature/splash/SplashScreen.kt b/feature/splash/src/main/kotlin/com/nexters/bandalart/android/feature/splash/SplashScreen.kt index 13c768e7..edb23e85 100644 --- a/feature/splash/src/main/kotlin/com/nexters/bandalart/android/feature/splash/SplashScreen.kt +++ b/feature/splash/src/main/kotlin/com/nexters/bandalart/android/feature/splash/SplashScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavOptions @@ -57,8 +58,8 @@ fun SplashScreen( } uiState.isNetworkErrorAlertDialogOpened -> { NetworkErrorAlertDialog( - title = "네트워크 문제로 표를\n불러오지 못했어요", - message = "다시 시도해주시기 바랍니다.", + title = stringResource(R.string.network_error_dialog_title), + message = stringResource(R.string.network_error_dialog_message), onConfirmClick = { openNetworkErrorAlertDialog(false) createGuestLoginToken() diff --git a/feature/splash/src/main/kotlin/com/nexters/bandalart/android/feature/splash/SplashViewModel.kt b/feature/splash/src/main/kotlin/com/nexters/bandalart/android/feature/splash/SplashViewModel.kt index 1957b01e..ad57fe6a 100644 --- a/feature/splash/src/main/kotlin/com/nexters/bandalart/android/feature/splash/SplashViewModel.kt +++ b/feature/splash/src/main/kotlin/com/nexters/bandalart/android/feature/splash/SplashViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -66,37 +67,31 @@ class SplashViewModel @Inject constructor( fun createGuestLoginToken() { viewModelScope.launch { + val delayJob = launch { + delay(500) + _uiState.update { it.copy(isLoading = true) } + } val result = createGuestLoginTokenUseCase() + delayJob.cancel() when { result.isSuccess && result.getOrNull() != null -> { val newGuestLoginToken = result.getOrNull()!! setGuestLoginTokenUseCase(newGuestLoginToken.key) - _uiState.value = _uiState.value.copy( - isLoggedIn = false, - isLoading = false, - ) + _uiState.update { it.copy(isLoggedIn = false) } createBandalartUseCase() } result.isSuccess && result.getOrNull() == null -> { Timber.e("Request succeeded but data validation failed") } result.isFailure -> { - val exception = result.exceptionOrNull() - _uiState.value = _uiState.value.copy( - isNetworkErrorAlertDialogOpened = true, - isLoading = false, - ) - Timber.e(exception) + _uiState.update { it.copy(isNetworkErrorAlertDialogOpened = true) } } } + _uiState.update { it.copy(isLoading = false) } } } - fun openNetworkErrorAlertDialog(state: Boolean) { - viewModelScope.launch { - _uiState.value = _uiState.value.copy( - isNetworkErrorAlertDialogOpened = state, - ) - } + fun openNetworkErrorAlertDialog(flag: Boolean) { + _uiState.update { it.copy(isNetworkErrorAlertDialogOpened = flag) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5b18e43..a069c11b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ kotlin-ktlint-source = "0.48.2" kotlinx-coroutines = "1.7.3" kotlinx-datetime = "0.4.0" kotlinx-serialization = "1.5.1" +kotlinx-serialization-converter = "1.0.0" android-hilt = "2.47" @@ -25,15 +26,17 @@ androidx-startup = "1.1.1" androidx-activity-compose = "1.7.2" androidx-compose-compiler = "1.4.8" androidx-compose-bom = "2023.08.00" -androidx-compose-material3 = "1.2.0-alpha05" -androidx-compose-runtime-tracing = "1.0.0-alpha03" +androidx-compose-material3 = "1.2.0-alpha06" +androidx-compose-runtime-tracing = "1.0.0-alpha04" androidx-compose-constraintlayout = "1.0.1" -androidx-compose-navigation = "2.7.0" +androidx-compose-navigation = "2.7.1" androidx-hilt-navigation-compose = "1.0.0" desugar-jdk = "2.0.3" javax-inject = "1" ktor-client = "2.3.3" +retrofit = "2.9.0" +okhttp = "5.0.0-alpha.11" lottie-compose = "5.2.0" timber = "5.0.1" facebook-shimmer = "0.5.0" @@ -85,6 +88,12 @@ ktor-client-json = { group = "io.ktor", name = "ktor-client-json", version.ref = ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor-client" } ktor-serialization = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor-client" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "kotlinx-serialization-converter" } + +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } + desugar-jdk = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar-jdk" } detekt-plugin-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "kotlin-detekt" } javax-inject = { group = "javax.inject", name = "javax.inject", version.ref = "javax-inject" } @@ -146,3 +155,13 @@ ktor-client = [ "ktor-client-logging", "ktor-serialization", ] + +retrofit = [ + "retrofit", + "retrofit-kotlinx-serialization-converter", +] + +okhttp = [ + "okhttp", + "okhttp-logging-interceptor", +] diff --git a/settings.gradle.kts b/settings.gradle.kts index e5703aa6..b7802173 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,8 +23,10 @@ buildCache { include( ":app", ":core:data", + ":core:datastore", ":core:designsystem", ":core:domain", + ":core:network", ":core:ui", ":feature:complete", ":feature:home",