Skip to content

🔀 :: (#797) build flavors 적용#801

Merged
uson1004 merged 9 commits intodevelopfrom
refactor/797-build-flavors-적용
Dec 1, 2025

Hidden character warning

The head ref may contain hidden characters: "refactor/797-build-flavors-\uc801\uc6a9"
Merged

🔀 :: (#797) build flavors 적용#801
uson1004 merged 9 commits intodevelopfrom
refactor/797-build-flavors-적용

Conversation

@uson1004
Copy link
Member

@uson1004 uson1004 commented Nov 30, 2025

개요

build flavors를 적용하여 기존 코드와 새로 작업할 코드를 삭제 없이 유지하였습니다

작업사항

  • 파일 전체 전부 분리

추가 로 할 말

Summary by CodeRabbit

  • New Features

    • Environment-specific build flavors (dev/prod) with separate app variants and resources.
    • Push notifications via Firebase with automatic device token handling.
    • In-app immediate update flow with improved failure signaling.
    • Edge-to-edge UI support and updated app navigation UI (basic two-screen demo).
  • Bug Fixes / Stability

    • Injection and serialization support added; network and serialization configuration provided.

✏️ Tip: You can customize this high-level summary in your review settings.

@uson1004 uson1004 self-assigned this Nov 30, 2025
@uson1004 uson1004 added the docs 문서가 추가되거나 변경되는 경우문 label Nov 30, 2025
@uson1004 uson1004 linked an issue Nov 30, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Nov 30, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds dev/prod product flavors with flavor-specific source sets and dependency routing; introduces dev-only app wiring (DevApplication, dev MainActivity, messaging service, UI, ViewModel) and prod-only Hilt DI modules (network, serialization, app config); enables serialization Gradle plugin.

Changes

Cohort / File(s) Summary
Build Configuration
app/build.gradle.kts
Added serialization plugin; introduced flavorDimensions + productFlavors (dev, prod) with BuildConfig fields and id/version suffixes; added flavor-specific sourceSets; rerouted many dependencies to prodImplementation/devImplementation and configured kspProd.
Dev Manifest & App Setup
app/src/dev/AndroidManifest.xml, app/src/dev/kotlin/team/aliens/dms/android/app/DevApplication.kt
New dev manifest declaring DevApplication and launcher MainActivity; DevApplication implements Configuration.Provider and wires HiltWorkerFactory for WorkManager.
Dev UI & Activity
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt, app/src/dev/kotlin/team/aliens/dms/android/app/MainActivity.kt, app/src/dev/kotlin/team/aliens/dms/android/app/EdgeToEdgeConfig.kt
Added DmsApp composable with NavKey-based ScreenA/ScreenB and navigation; dev MainActivity integrates MainActivityViewModel, update-result handling, and passes viewModel to DmsApp; added setEdgeToEdgeConfig() extension.
Dev Messaging Service
app/src/dev/kotlin/team/aliens/dms/android/app/service/DmsMessagingService.kt
New DmsMessagingService (FirebaseMessagingService) with DI DeviceTokenManager, persists tokens on onNewToken and shows notifications on onMessageReceived.
Dev ViewModel
app/src/dev/kotlin/team/aliens/dms/android/app/MainActivityViewModel.kt
New Hilt ViewModel exposing autoSignInAvailable: StateFlow<Boolean>, isUpdateFailed StateFlow, and methods onUpdateFailed() / consumeUpdateFailed().
Prod Manifest & Activity
app/src/prod/AndroidManifest.xml, app/src/prod/kotlin/team/aliens/dms/android/app/MainActivity.kt
New prod manifest declaring DmsApplication; prod MainActivity with Hilt-injected @IsJwtAvailable StateFlow and DeviceTokenManager, Play Core update-check flow, edge-to-edge setup, and Compose DmsApp rendering.
Prod ViewModel
app/src/prod/kotlin/team/aliens/dms/android/app/MainActivityViewModel.kt
Added update-failure state (_isUpdateFailed / isUpdateFailed) and methods onUpdateFailed() / consumeUpdateFailed().
Prod DI Modules
app/src/prod/kotlin/team/aliens/dms/android/app/di/...
Added Hilt modules: AppConfigModule (provides ZoneOffset.UTC), NetworkConfigModule (provides BaseUrl, token/feature URLs, interceptors, ignore-requests wiring with qualifiers), and SerializationModule (provides Moshi with Kotlin adapter).
Prod Navigation (cosmetic)
app/src/prod/kotlin/team/aliens/dms/android/app/navigation/...
Minor import reorderings in navigation files — no behavioral changes.

Sequence Diagram(s)

sequenceDiagram
  participant Activity as MainActivity
  participant PlayCore as Play Core (AppUpdateManager)
  participant ViewModel as MainActivityViewModel

  Activity->>PlayCore: requestAppUpdateInfo()
  PlayCore-->>Activity: updateAvailability(result)
  alt update available
    Activity->>PlayCore: startImmediateUpdate(intentSender)
    PlayCore-->>Activity: activityResult(success/failure)
    alt failure
      Activity->>ViewModel: onUpdateFailed()
    end
  else no update
    Activity-->>Activity: no-op (early return)
  end
Loading
sequenceDiagram
  participant FCM as Firebase
  participant Service as DmsMessagingService
  participant TokenMgr as DeviceTokenManager
  participant Notif as NotificationManager

  FCM->>Service: onNewToken(token)
  Service->>TokenMgr: saveDeviceToken(token) (IO)
  FCM->>Service: onMessageReceived(remotemsg)
  Service->>Notif: build/send notification(title, body)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • Gradle flavor dependency routing, KSP kspProd and correctness of prodImplementation/devImplementation declarations.
    • Hilt module qualifiers, provider wiring, and singleton scoping in NetworkConfigModule.
    • WorkManager Hilt integration in DevApplication (Configuration.Provider usage).
    • Threading and lifecycle in DmsMessagingService (token persistence on IO dispatcher).
    • Play Core update flow and cross-variant ViewModel update-failure signaling.

Possibly related PRs

Suggested labels

setting

Suggested reviewers

  • parkuiery

Poem

🐰
I hopped through flavors, code in tow,
Dev to test and Prod to show,
Hilt seeds sown, notifications sing,
Gradle split — a build-time spring,
A rabbit cheers: happy CI-go!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references build flavors (build flavors 적용 = applying build flavors) which aligns with the main changeset adding dev/prod product flavors and environment-specific configurations. However, it uses emoji and Korean text making it less clear to non-Korean speakers, and doesn't convey the primary change concisely in English.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/797-build-flavors-적용

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0040cb0 and 1089680.

📒 Files selected for processing (1)
  • app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/build.gradle.kts (1)

66-71: Duplicate aaptOptions.cruncherEnabled = false setting.

Line 68 and line 70 both set aaptOptions.cruncherEnabled = false. Remove the duplicate.

         debug {
             splits.abi.isEnable = false
             aaptOptions.cruncherEnabled = false
             splits.density.isEnable = false
-            aaptOptions.cruncherEnabled = false
         }
🧹 Nitpick comments (6)
app/src/dev/kotlin/team/aliens/dms/android/app/EdgeToEdgeConfig.kt (1)

1-1: Align package name with directory structure (or vice versa).

The file lives under team/aliens/dms/android/app but the package is team.aliens.dms.android. Consider either updating the package to team.aliens.dms.android.app or moving the file so directory and package stay in sync for easier navigation and refactoring.

app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (2)

25-27: Unused parameter: windowSizeClass

The windowSizeClass parameter is declared but not used in the function body. Consider either using it for responsive layouts or removing it if not needed for this dev flavor scaffold.


45-48: Consider using the design system Button for consistency.

This uses androidx.compose.material3.Button (line 6) rather than the custom Button from team.aliens.dms.android.core.designsystem.Buttons. Since this is a dev flavor scaffold for experimentation, this may be intentional, but if consistency with the design system is desired, consider switching to the design system component.

-import androidx.compose.material3.Button
+import team.aliens.dms.android.core.designsystem.ContainedButton
-                    Button(onClick = { backStack.add(ScreenB) }) {
-                        Text("Go to Screen B")
-                    }
+                    ContainedButton(onClick = { backStack.add(ScreenB) }) {
+                        Text("Go to Screen B")
+                    }
app/src/dev/kotlin/team/aliens/dms/android/app/MainActivity.kt (3)

34-40: Complete the update cancellation/failure handling.

The TODO comment indicates that forced update modal handling is not yet implemented. When users cancel or fail the update, the app should handle this appropriately.

Do you want me to generate an implementation for the forced update modal or open a new issue to track this task?


46-49: Consider replacing deprecated FLAG_LAYOUT_NO_LIMITS.

The FLAG_LAYOUT_NO_LIMITS flag is deprecated and may cause issues on newer Android versions (especially with display cutouts and notches). Consider using WindowInsetsController with systemBarsBehavior instead for better compatibility.

Apply this diff to modernize the window configuration:

-        window.setFlags(
-            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
-            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
-        )
+        WindowCompat.getInsetsController(window, window.decorView).apply {
+            systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+        }

Note: Add the import androidx.core.view.WindowInsetsControllerCompat if you apply this change.


54-54: Complete adaptive layout implementation.

The TODO comment indicates that adaptive layout support is pending. This is an important feature for better tablet and foldable device support.

Do you want me to help implement adaptive layout handling based on the windowSizeClass or open a new issue to track this task?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebfff20 and 91de011.

📒 Files selected for processing (14)
  • app/build.gradle.kts (4 hunks)
  • app/src/dev/AndroidManifest.xml (1 hunks)
  • app/src/dev/kotlin/team/aliens/dms/android/app/DevApplication.kt (1 hunks)
  • app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1 hunks)
  • app/src/dev/kotlin/team/aliens/dms/android/app/EdgeToEdgeConfig.kt (1 hunks)
  • app/src/dev/kotlin/team/aliens/dms/android/app/MainActivity.kt (1 hunks)
  • app/src/dev/kotlin/team/aliens/dms/android/app/service/DmsMessagingService.kt (1 hunks)
  • app/src/prod/AndroidManifest.xml (1 hunks)
  • app/src/prod/kotlin/team/aliens/dms/android/app/di/app/AppConfigModule.kt (1 hunks)
  • app/src/prod/kotlin/team/aliens/dms/android/app/di/network/NetworkConfigModule.kt (1 hunks)
  • app/src/prod/kotlin/team/aliens/dms/android/app/di/serialization/SerializationModule.kt (1 hunks)
  • app/src/prod/kotlin/team/aliens/dms/android/app/navigation/DmsNavigator.kt (1 hunks)
  • app/src/prod/kotlin/team/aliens/dms/android/app/navigation/authorized/AuthorizedNavGraph.kt (1 hunks)
  • app/src/prod/kotlin/team/aliens/dms/android/app/navigation/authorized/AuthorizedNavigator.kt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1)
core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/Buttons.kt (1)
  • Button (42-92)
app/src/dev/kotlin/team/aliens/dms/android/app/MainActivity.kt (2)
app/src/prod/kotlin/team/aliens/dms/android/app/MainActivity.kt (1)
  • checkAppUpdate (68-84)
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1)
  • DmsApp (25-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: build
🔇 Additional comments (16)
app/src/prod/kotlin/team/aliens/dms/android/app/navigation/authorized/AuthorizedNavigator.kt (1)

14-14: LGTM for prod flavor navigation setup.

The Volunteers feature is properly wired into the authorized navigation interface. The import and interface inheritance are correctly positioned and follow the established patterns.

Note: Ensure a corresponding dev flavor variant exists at app/src/dev/kotlin/team/aliens/dms/android/app/navigation/authorized/AuthorizedNavigator.kt with appropriate feature set for the dev environment.

Also applies to: 28-28

app/src/prod/kotlin/team/aliens/dms/android/app/navigation/DmsNavigator.kt (1)

48-48: Import correctly enables existing openVolunteer() implementation.

The added import on Line 48 properly backs the openVolunteer() method at Line 104, which was already present. This import addition resolves a missing dependency.

app/src/prod/kotlin/team/aliens/dms/android/app/navigation/authorized/AuthorizedNavGraph.kt (1)

21-21: Navigation graph correctly incorporates Volunteers feature.

The nested nav graph and destination screen additions follow the established patterns and properly integrate the Volunteers feature into the authorized navigation structure. Internal consistency verified across imports and graph composition.

Also applies to: 26-26, 37-37, 56-56

app/src/dev/kotlin/team/aliens/dms/android/app/EdgeToEdgeConfig.kt (1)

7-13: Edge‑to‑edge setup and API guard look good.

The ComponentActivity extension cleanly centralizes edge‑to‑edge setup, and the SDK_INT >= Q guard around isNavigationBarContrastEnforced is correct and avoids API‑level crashes.

app/src/prod/kotlin/team/aliens/dms/android/app/di/network/NetworkConfigModule.kt (5)

1-24: LGTM!

The module setup is correct with proper Hilt annotations and well-organized imports. The BuildConfig alias is a good practice to avoid ambiguity in multi-module projects.


26-40: LGTM!

URL providers are correctly implemented with proper singleton scoping and qualifier annotations.


117-125: LGTM!

Default interceptors provider is correctly implemented.


127-135: LGTM!

Global interceptors provider is correctly implemented with the JWT interceptor for authenticated request handling.


74-103: > Likely an incorrect or invalid review comment.

app/src/prod/kotlin/team/aliens/dms/android/app/di/serialization/SerializationModule.kt (1)

11-13: LGTM!

The module is correctly configured as a Hilt singleton component module.

app/build.gradle.kts (2)

30-55: LGTM! Well-structured flavor configuration.

The environment-based flavor setup correctly separates dev and prod configurations with appropriate source sets, application ID suffix, and version name suffix for dev builds. The BuildConfig field ENVIRONMENT provides runtime differentiation.


122-131: Different navigation systems per flavor - verify this is intentional.

  • dev uses navigation3.runtime and navigation3.ui (experimental Navigation 3)
  • prod uses navigation.compose and hilt.navigation.compose (stable Navigation Compose) with Compose Destinations

This divergence means dev and prod have fundamentally different navigation architectures. Ensure this is intentional for prototyping Navigation 3 in dev while maintaining stable navigation in prod.

app/src/prod/AndroidManifest.xml (1)

1-8: LGTM! The manifest correctly uses tools:replace="android:name" to override the application class for the prod flavor. DmsApplication is properly defined in the prod source set.

app/src/prod/kotlin/team/aliens/dms/android/app/di/app/AppConfigModule.kt (1)

10-17: LGTM. The module correctly provides a singleton ZoneOffset.UTC for consistent timezone handling. Both prod and dev flavors have identical AppConfigModule implementations, so there are no flavor-specific inconsistencies.

app/src/dev/kotlin/team/aliens/dms/android/app/DevApplication.kt (1)

9-19: LGTM! Standard Hilt + WorkManager integration.

The implementation correctly:

  • Uses @HiltAndroidApp for Hilt component generation
  • Implements Configuration.Provider for custom WorkManager configuration
  • Injects HiltWorkerFactory for DI-enabled Workers

The prod DmsApplication has equivalent WorkManager setup with identical implementation, and Workers are used in production (MealWorker in the core/widget module).

app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1)

12-15: Navigation3 library is now at release-candidate status, not alpha.

androidx.navigation3 APIs have progressed to version 1.0.0-rc01 (as of November 2025) and are no longer experimental or alpha. For the dev flavor, this RC dependency is acceptable, though the library has not yet reached a final 1.0.0 stable release.

Comment on lines 63 to 65
lifecycleScope.launch {
deviceTokenManager.fetchDeviceToken()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for device token fetching.

The device token fetch operation has no error handling. If fetchDeviceToken() throws an exception, it could crash the app or fail silently. Consider adding try-catch or collecting the result to handle failures appropriately.

Apply this diff to add error handling:

         lifecycleScope.launch {
-            deviceTokenManager.fetchDeviceToken()
+            runCatching {
+                deviceTokenManager.fetchDeviceToken()
+            }.onFailure { exception ->
+                // TODO: Log error or show user notification
+            }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lifecycleScope.launch {
deviceTokenManager.fetchDeviceToken()
}
lifecycleScope.launch {
runCatching {
deviceTokenManager.fetchDeviceToken()
}.onFailure { exception ->
// TODO: Log error or show user notification
}
}
🤖 Prompt for AI Agents
In app/src/dev/kotlin/team/aliens/dms/android/app/MainActivity.kt around lines
63 to 65, the coroutine calling deviceTokenManager.fetchDeviceToken() lacks
error handling; wrap the call in a try-catch inside the lifecycleScope.launch or
handle the result returned by fetchDeviceToken(), catching exceptions and
logging them and/or reporting to your error telemetry, and optionally retrying
or surfacing a user-friendly fallback; ensure exceptions are not allowed to
propagate and crash the app.

Comment on lines 19 to 21
private val notificationManager: NotificationManager by lazy {
NotificationManager(context = this)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Inject NotificationManager instead of manual instantiation.

The NotificationManager is manually instantiated with lazy, while DeviceTokenManager is properly injected. This breaks the DI pattern and makes the service harder to test and maintain.

Apply this diff to inject NotificationManager:

+    @Inject
+    lateinit var notificationManager: NotificationManager
+
-    private val notificationManager: NotificationManager by lazy {
-        NotificationManager(context = this)
-    }

Ensure that NotificationManager has a Hilt binding configured in your DI modules.

🤖 Prompt for AI Agents
In app/src/dev/kotlin/team/aliens/dms/android/app/service/DmsMessagingService.kt
around lines 19 to 21, the NotificationManager is being manually instantiated
with a lazy delegate which breaks DI and testability; replace the lazy/manual
instantiation with a Hilt-injected property (e.g., add an @Inject lateinit var
notificationManager: NotificationManager to the service), remove the lazy block,
ensure the service is annotated with @AndroidEntryPoint (or otherwise
participates in Hilt), and confirm NotificationManager is provided in your Hilt
module/bindings so Hilt can inject it.

Comment on lines 25 to 27
CoroutineScope(Dispatchers.IO).launch {
deviceTokenManager.saveDeviceToken(deviceToken = deviceToken)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use structured concurrency or goAsync() to ensure token persistence.

Creating an unmanaged CoroutineScope(Dispatchers.IO) is risky because the coroutine may be cancelled if the service is destroyed before saveDeviceToken completes, resulting in the token not being saved.

Apply this diff to ensure the work completes using goAsync():

     override fun onNewToken(deviceToken: String) {
         super.onNewToken(deviceToken)
-        CoroutineScope(Dispatchers.IO).launch {
+        val pendingResult = goAsync()
+        CoroutineScope(Dispatchers.IO).launch {
-            deviceTokenManager.saveDeviceToken(deviceToken = deviceToken)
+            try {
+                deviceTokenManager.saveDeviceToken(deviceToken = deviceToken)
+            } catch (e: Exception) {
+                // Log error
+            } finally {
+                pendingResult.finish()
+            }
         }
     }

The goAsync() method extends the service's lifetime, ensuring the coroutine can complete even if the service is being destroyed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CoroutineScope(Dispatchers.IO).launch {
deviceTokenManager.saveDeviceToken(deviceToken = deviceToken)
}
val pendingResult = goAsync()
CoroutineScope(Dispatchers.IO).launch {
try {
deviceTokenManager.saveDeviceToken(deviceToken = deviceToken)
} catch (e: Exception) {
// Log error
} finally {
pendingResult.finish()
}
}
🤖 Prompt for AI Agents
In app/src/dev/kotlin/team/aliens/dms/android/app/service/DmsMessagingService.kt
around lines 25-27, replace the unmanaged CoroutineScope(Dispatchers.IO).launch
call with a pattern that uses goAsync() to extend the service lifetime: call
goAsync() to obtain a PendingResult, start a coroutine on Dispatchers.IO to call
deviceTokenManager.saveDeviceToken(deviceToken), and ensure you always call
pendingResult.finish() in a finally block (and catch/log exceptions) so the work
completes even if the service is destroyed.

Comment on lines 30 to 39
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
message.notification?.run {
notificationManager.setNotificationContent(
title = title,
body = body,
)
}
notificationManager.sendNotification()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix notification logic to prevent sending with stale content.

The code calls sendNotification() unconditionally on line 38, even when message.notification is null. This means a notification could be sent with stale or uninitialized content.

Apply this diff to only send notifications when content is present:

     override fun onMessageReceived(message: RemoteMessage) {
         super.onMessageReceived(message)
         message.notification?.run {
             notificationManager.setNotificationContent(
                 title = title,
                 body = body,
             )
+            notificationManager.sendNotification()
         }
-        notificationManager.sendNotification()
     }

Alternatively, if you need to handle data messages differently, add explicit logic:

     override fun onMessageReceived(message: RemoteMessage) {
         super.onMessageReceived(message)
-        message.notification?.run {
+        val notification = message.notification
+        if (notification != null) {
-            notificationManager.setNotificationContent(
-                title = title,
-                body = body,
-            )
+            notificationManager.setNotificationContent(
+                title = notification.title,
+                body = notification.body,
+            )
+            notificationManager.sendNotification()
+        } else {
+            // Handle data-only messages if needed
         }
-        notificationManager.sendNotification()
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
message.notification?.run {
notificationManager.setNotificationContent(
title = title,
body = body,
)
}
notificationManager.sendNotification()
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
message.notification?.run {
notificationManager.setNotificationContent(
title = title,
body = body,
)
notificationManager.sendNotification()
}
}
Suggested change
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
message.notification?.run {
notificationManager.setNotificationContent(
title = title,
body = body,
)
}
notificationManager.sendNotification()
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
val notification = message.notification
if (notification != null) {
notificationManager.setNotificationContent(
title = notification.title,
body = notification.body,
)
notificationManager.sendNotification()
} else {
// Handle data-only messages if needed
}
}
🤖 Prompt for AI Agents
In app/src/dev/kotlin/team/aliens/dms/android/app/service/DmsMessagingService.kt
around lines 30 to 39, the code always calls
notificationManager.sendNotification() even when message.notification is null
causing possible stale/uninitialized notification content; fix by only calling
sendNotification() when you have non-null notification content (move the
sendNotification() call inside the message.notification?.run { ... } block) or,
if data messages must trigger notifications, add an explicit else branch to
build/clear notification content (or return) before calling sendNotification().

Comment on lines +15 to +17
@Provides
@Singleton
fun provideMoshi(): Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine build configuration files
fd -E '.git' -t f \( -name "build.gradle" -o -name "build.gradle.kts" \) | head -20

Repository: team-aliens/DMS-Android

Length of output: 297


🏁 Script executed:

# Check for Moshi-related dependencies
rg "moshi" --type gradle -i

Repository: team-aliens/DMS-Android

Length of output: 215


🏁 Script executed:

# Check for ProGuard/R8 configuration files
fd -E '.git' -t f \( -name "proguard*.pro" -o -name "*.pro" -o -name "consumer-rules.pro" \)

Repository: team-aliens/DMS-Android

Length of output: 297


🏁 Script executed:

# Examine the SerializationModule.kt file in detail
cat -n app/src/prod/kotlin/team/aliens/dms/android/app/di/serialization/SerializationModule.kt

Repository: team-aliens/DMS-Android

Length of output: 692


🏁 Script executed:

# Find build.gradle files (simpler approach)
find . -type f -name "build.gradle*" | grep -v ".git" | head -20

Repository: team-aliens/DMS-Android

Length of output: 656


🏁 Script executed:

# Search for moshi in build files
find . -type f \( -name "build.gradle" -o -name "build.gradle.kts" \) -exec grep -l -i moshi {} \;

Repository: team-aliens/DMS-Android

Length of output: 150


🏁 Script executed:

# Find all ProGuard configuration files
find . -type f -name "*.pro" | grep -v ".git"

Repository: team-aliens/DMS-Android

Length of output: 1339


🏁 Script executed:

# Search for moshi dependencies across gradle files
grep -r "moshi" . --include="build.gradle" --include="build.gradle.kts" 2>/dev/null | head -30

Repository: team-aliens/DMS-Android

Length of output: 422


🏁 Script executed:

# Check app/build.gradle.kts for complete Moshi setup
cat app/build.gradle.kts | grep -A 5 -B 5 "moshi"

Repository: team-aliens/DMS-Android

Length of output: 345


🏁 Script executed:

# Check ProGuard rules in app/proguard-rules.pro for Moshi
cat app/proguard-rules.pro | grep -i "moshi"

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

# Search for all Moshi builder usages in the codebase
grep -r "Moshi\.Builder" . --include="*.kt" 2>/dev/null

Repository: team-aliens/DMS-Android

Length of output: 421


🏁 Script executed:

# Check if codegen is actually being used with generated adapters
grep -r "JsonAdapter" . --include="*.kt" 2>/dev/null | grep -i "generated" | head -10

Repository: team-aliens/DMS-Android

Length of output: 49


Replace reflection-based KotlinJsonAdapterFactory with Moshi code generation.

The project has moshi-kotlin-codegen configured with KSP as a dependency, but the SerializationModule uses the reflection-based KotlinJsonAdapterFactory instead. This creates a configuration mismatch and misses the benefits of compile-time code generation: better runtime performance, smaller APK size, and better R8 compatibility.

Additionally, no ProGuard/R8 rules are configured for Moshi, which is required for the reflection-based approach to work correctly after minification.

Switch to code-generated adapters: generate adapters with @JsonClass(generateAdapter = true) on data classes and use them directly in the Moshi builder, or remove the unused moshi-kotlin-codegen dependency if reflection is intentional.

🤖 Prompt for AI Agents
In
app/src/prod/kotlin/team/aliens/dms/android/app/di/serialization/SerializationModule.kt
around lines 15-17, the module currently registers the reflection-based
KotlinJsonAdapterFactory while the project is configured to use
moshi-kotlin-codegen; remove the KotlinJsonAdapterFactory from the Moshi builder
and switch to code-generated adapters by annotating all serializable data
classes with @JsonClass(generateAdapter = true) (or if you intentionally want
reflection, remove the moshi-kotlin-codegen dependency and add the required
ProGuard/R8 keep rules for Moshi); ensure DI provides a Moshi instance that does
not rely on KotlinJsonAdapterFactory when codegen is used so generated adapters
are used at runtime.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements build flavors (dev and prod) to separate the existing production code from new development work. It splits the app module into two distinct flavors, each with its own source sets, dependencies, and configurations, allowing parallel development without code deletion.

Key Changes:

  • Configured Gradle build flavors with separate dev and prod product flavors
  • Split dependencies to use flavor-specific implementations (e.g., prodImplementation for Compose Destinations, devImplementation for experimental navigation3)
  • Separated all app-level files into flavor-specific source directories

Reviewed changes

Copilot reviewed 14 out of 26 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
app/build.gradle.kts Configures build flavors with dimension, source sets, and flavor-specific dependencies
app/src/prod/AndroidManifest.xml Production flavor manifest specifying DmsApplication
app/src/prod/kotlin/team/aliens/dms/android/app/* Production app implementation with full navigation and feature integration
app/src/dev/AndroidManifest.xml Development flavor manifest with DevApplication and MainActivity configuration
app/src/dev/kotlin/team/aliens/dms/android/app/* Development app implementation using experimental navigation3 library with simplified setup

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

}
notificationManager.sendNotification()
}
} No newline at end of file
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing trailing newline at the end of the file. The dev flavor's DmsMessagingService is missing the trailing newline that exists in the prod version (line 41).

Copilot uses AI. Check for mistakes.
Comment on lines 27 to 32
windowSizeClass: WindowSizeClass,
// displayFeatures: List<DisplayFeature>,
// isJwtAvailable: StateFlow<Boolean>,
// appState: DmsAppState = rememberDmsAppState(
// isJwtAvailable = isJwtAvailable,
// ),
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commented-out parameters and functionality suggest incomplete implementation. These commented lines (displayFeatures, isJwtAvailable, appState) should either be removed if not needed, or uncommented and properly implemented if they are intended for future use. Consider adding a TODO comment explaining why this is a simplified version compared to the prod flavor.

Copilot uses AI. Check for mistakes.

DmsTheme {
DmsApp(
windowSizeClass = windowSizeClass,
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DmsApp composable is called without the required isJwtAvailable parameter that is injected in the MainActivity. In the prod version, this parameter is passed to DmsApp. Either the parameter should be passed here, or the dev DmsApp signature should be updated to not expect it (which appears to be the case based on the commented-out parameters).

Suggested change
windowSizeClass = windowSizeClass,
windowSizeClass = windowSizeClass,
isJwtAvailable = isJwtAvailable,

Copilot uses AI. Check for mistakes.
tools:replace="android:name">

<activity
android:name=".android.MainActivity"
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activity name path is incorrect. It should be team.aliens.dms.android.app.MainActivity instead of .android.MainActivity. The current path would resolve to team.aliens.dms.android.MainActivity which doesn't match the actual package location of the MainActivity class (team.aliens.dms.android.app).

Suggested change
android:name=".android.MainActivity"
android:name="team.aliens.dms.android.app.MainActivity"

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/src/dev/kotlin/team/aliens/dms/android/app/MainActivity.kt (1)

68-70: (Previously noted) Add error handling around fetchDeviceToken().

This coroutine still calls deviceTokenManager.fetchDeviceToken() without handling exceptions. If that call can fail, consider wrapping it and logging/reporting the error so a transient issue doesn’t crash the dev build.

         lifecycleScope.launch {
-            deviceTokenManager.fetchDeviceToken()
+            runCatching {
+                deviceTokenManager.fetchDeviceToken()
+            }.onFailure { throwable ->
+                // TODO: log or report token fetch failure
+            }
         }
🧹 Nitpick comments (3)
app/src/dev/kotlin/team/aliens/dms/android/app/EdgeToEdgeConfig.kt (1)

7-13: Edge-to-edge helper looks correct; consider reusing across variants.

The version-gated isNavigationBarContrastEnforced change is appropriate for Q+ and the extension cleanly encapsulates the window setup. If you need the same behavior in prod activities, consider moving this helper to a common source set (or invoking it from MainActivity) so the edge-to-edge configuration isn’t duplicated per flavor.

app/src/prod/kotlin/team/aliens/dms/android/app/MainActivity.kt (2)

68-70: Handle failures when fetching the device token.

fetchDeviceToken() is invoked in lifecycleScope.launch without any error handling. If this call can throw (network/Play Services issues, etc.), it may crash the activity. Wrapping it defensively and logging/reporting failures would make this path more robust.

-        lifecycleScope.launch {
-            deviceTokenManager.fetchDeviceToken()
-        }
+        lifecycleScope.launch {
+            runCatching {
+                deviceTokenManager.fetchDeviceToken()
+            }.onFailure { throwable ->
+                // TODO: log or report token fetch failure
+            }
+        }

73-95: Consider also checking whether IMMEDIATE updates are allowed before starting the flow.

You already guard on update availability and route failures to mainViewModel.onUpdateFailed(). For in-app updates it’s also common to check that the requested update type is allowed before starting the flow, to avoid immediately hitting the failure callback.

For example:

-        appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
-            if (appUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
-                return@addOnSuccessListener
-            }
+        appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
+            val isUpdateAvailable =
+                appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
+            val isImmediateAllowed =
+                appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
+            if (!isUpdateAvailable || !isImmediateAllowed) {
+                return@addOnSuccessListener
+            }

Not required for correctness (you already handle failures), but it can reduce unnecessary attempts.

Please double-check the latest Play Core in-app update guidelines to confirm this pattern matches current recommendations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91de011 and 0040cb0.

📒 Files selected for processing (7)
  • app/src/dev/AndroidManifest.xml (1 hunks)
  • app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1 hunks)
  • app/src/dev/kotlin/team/aliens/dms/android/app/EdgeToEdgeConfig.kt (1 hunks)
  • app/src/dev/kotlin/team/aliens/dms/android/app/MainActivity.kt (6 hunks)
  • app/src/dev/kotlin/team/aliens/dms/android/app/MainActivityViewModel.kt (1 hunks)
  • app/src/prod/kotlin/team/aliens/dms/android/app/MainActivity.kt (1 hunks)
  • app/src/prod/kotlin/team/aliens/dms/android/app/MainActivityViewModel.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/dev/AndroidManifest.xml
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1)
core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/Buttons.kt (1)
  • Button (42-92)
app/src/prod/kotlin/team/aliens/dms/android/app/MainActivity.kt (3)
app/src/dev/kotlin/team/aliens/dms/android/app/MainActivity.kt (1)
  • checkAppUpdate (73-95)
core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/Theme.kt (1)
  • DmsTheme (11-35)
app/src/prod/kotlin/team/aliens/dms/android/app/DmsApp.kt (1)
  • DmsApp (34-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
app/src/prod/kotlin/team/aliens/dms/android/app/MainActivity.kt (1)

28-66: MainActivity wiring and DI setup look solid.

The ViewModel injection, JWT availability flow, update launcher, and composition of DmsApp are all consistent and keep the prod flavor aligned with the dev setup. No functional issues spotted in this section.

app/src/dev/kotlin/team/aliens/dms/android/app/MainActivity.kt (2)

59-64: DmsApp call in dev flavor is correctly wired.

Passing both isJwtAvailable and mainViewModel keeps the dev MainActivity aligned with the prod flavor and ensures the dev DmsApp has everything it needs for navigation/auth and update-failure UI.


77-94: Update availability guard and failure routing look good.

The added early return when no update is available plus the failure listener that delegates to mainViewModel.onUpdateFailed() provide a clean, observable contract for the UI without unnecessary update attempts.

app/src/dev/kotlin/team/aliens/dms/android/app/MainActivityViewModel.kt (1)

11-26: Update-failure ViewModel state is cleanly modeled.

Using a private backing flag with onUpdateFailed() and consumeUpdateFailed() provides a straightforward way for the UI to react once to an update failure and then reset the state. This keeps the dev ViewModel aligned with the prod counterpart.

app/src/prod/kotlin/team/aliens/dms/android/app/MainActivityViewModel.kt (1)

15-24: Prod ViewModel’s update-failure API matches dev and looks good.

The added _isUpdateFailed flag, public isUpdateFailed, and the onUpdateFailed()/consumeUpdateFailed() pair provide a clear contract for the UI to observe and then clear update-failure events, keeping parity with the dev flavor.

Comment on lines 40 to 49
val isUpdateFailed by mainViewModel.isUpdateFailed.collectAsState()
val toast = LocalToast.current

if (isUpdateFailed) {
LaunchedEffect(Unit) {
toast.showErrorToast(
message = "업데이트 정보를 불러올 수 없습니다",
)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find the DmsApp.kt file and read the full context
fd -t f "DmsApp.kt" | head -5

Repository: team-aliens/DMS-Android

Length of output: 181


🏁 Script executed:

# Read the file to understand the full context
head -80 "app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt"

Repository: team-aliens/DMS-Android

Length of output: 2497


🏁 Script executed:

# Search for mainViewModel type and consumeUpdateFailed method
rg "consumeUpdateFailed" -A 3 -B 3

Repository: team-aliens/DMS-Android

Length of output: 1328


🏁 Script executed:

# Find the mainViewModel definition and its type
rg "mainViewModel.*=" "app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt" -A 2

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

# Search for isUpdateFailed definition in the codebase
rg "isUpdateFailed" -B 2 -A 2 | head -100

Repository: team-aliens/DMS-Android

Length of output: 3368


🏁 Script executed:

# Search for similar LaunchedEffect patterns with conditionals in Compose code
rg "LaunchedEffect\(Unit\)" -B 3 -A 3 | head -60

Repository: team-aliens/DMS-Android

Length of output: 6548


🏁 Script executed:

# Search for other event consumption patterns to see if this pattern is used elsewhere
rg "consume" -t kt --max-count 20

Repository: team-aliens/DMS-Android

Length of output: 93


isUpdateFailed flag is never consumed, preventing subsequent failures from being handled.

The current code collects isUpdateFailed and shows a toast inside LaunchedEffect(Unit) within an if block. Because LaunchedEffect(Unit) uses a constant key, it runs only once when the condition first becomes true. Since consumeUpdateFailed() is never called, the flag remains true indefinitely, and subsequent failures cannot trigger new toasts.

To handle each failure as a distinct event, react to flag changes and consume immediately:

-    val isUpdateFailed by mainViewModel.isUpdateFailed.collectAsState()
-    val toast = LocalToast.current
-
-    if (isUpdateFailed) {
-        LaunchedEffect(Unit) {
-            toast.showErrorToast(
-                message = "업데이트 정보를 불러올 수 없습니다",
-            )
-        }
-    }
+    val isUpdateFailed by mainViewModel.isUpdateFailed.collectAsState()
+    val toast = LocalToast.current
+
+    LaunchedEffect(isUpdateFailed) {
+        if (isUpdateFailed) {
+            toast.showErrorToast(
+                message = "업데이트 정보를 불러올 수 없습니다",
+            )
+            mainViewModel.consumeUpdateFailed()
+        }
+    }

This way each failure triggers exactly one toast and then resets the flag so later failures can be detected and handled again.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val isUpdateFailed by mainViewModel.isUpdateFailed.collectAsState()
val toast = LocalToast.current
if (isUpdateFailed) {
LaunchedEffect(Unit) {
toast.showErrorToast(
message = "업데이트 정보를 불러올 수 없습니다",
)
}
}
val isUpdateFailed by mainViewModel.isUpdateFailed.collectAsState()
val toast = LocalToast.current
LaunchedEffect(isUpdateFailed) {
if (isUpdateFailed) {
toast.showErrorToast(
message = "업데이트 정보를 불러올 수 없습니다",
)
mainViewModel.consumeUpdateFailed()
}
}
🤖 Prompt for AI Agents
In app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt around lines 40 to
49, the isUpdateFailed flag is collected but never consumed and the
LaunchedEffect uses a constant key so the toast only triggers once; change the
effect to react to the flag value (e.g., LaunchedEffect(isUpdateFailed) or
collectAsState changes) and immediately call the ViewModel method to
consume/reset the flag (e.g., mainViewModel.consumeUpdateFailed()) after showing
the toast so each subsequent failure becomes a new event and will display its
own toast.

@uson1004 uson1004 merged commit 61238f1 into develop Dec 1, 2025
1 of 3 checks passed
@uson1004 uson1004 deleted the refactor/797-build-flavors-적용 branch December 1, 2025 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs 문서가 추가되거나 변경되는 경우문

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build Flavors 적용

2 participants