-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 설정화면 Compose 마이그레이션 #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
앱 설정 화면(`SettingScreen`)을 Jetpack Compose로 신규 구현했습니다. 사용자가 대학 소식 알림을 설정하고, 앱 정보(버전, 약관, 문의하기)를 확인할 수 있는 UI를 구성했습니다.
- **`SettingScreen.kt` 추가:**
- `Scaffold`와 공통 `Header` 컴포넌트를 사용하여 전체 화면 레이아웃을 구성했습니다.
- `SubscriptionContent` 컴포저블을 통해 대학 이름 표시 및 알림 구독 상태를 제어하는 `Switch` UI를 구현했습니다.
- `AppInfoContent` 컴포저블을 통해 앱 버전 정보 표시, 이용 약관 및 문의하기 버튼을 리스트 형태로 구현했습니다.
- `HorizontalDivider`를 사용하여 콘텐츠 섹션을 시각적으로 구분하고, `FestabookTheme` 디자인 시스템을 적용했습니다.
설정 화면의 데이터를 `FestivalUiState`로 관리하도록 변경하고, 구독 스위치의 활성화 제어 기능을 추가했습니다. 또한 상단 앱바 컴포넌트를 교체하여 UI 일관성을 맞췄습니다.
- **`SettingScreen.kt` 수정:**
- 컴포저블 파라미터를 `universityName` 문자열 대신 `FestivalUiState`로 변경하여, 상태(`Success`, `Error`)에 따라 콘텐츠 표시 및 에러 처리를 수행하도록 구조를 개편했습니다.
- `LaunchedEffect`를 추가하여 `UiState.Error` 발생 시 `onError` 콜백이 호출되도록 로직을 추가했습니다.
- 기존 `Header` 컴포넌트를 `FestabookTopAppBar`로 교체하고, 전체 화면 배경색을 `White`로 지정했습니다.
- `SubscriptionContent`에 `isSubscribeEnabled` 파라미터를 추가하여 스위치의 `enabled` 속성을 제어하고, 비활성화 상태일 때의 트랙 색상을 정의했습니다.
`SettingViewModel`의 데이터 흐름을 개선하기 위해 기존 `LiveData` 기반의 상태 관리를 Coroutines Flow API로 마이그레이션했습니다.
- **`SettingViewModel.kt` 수정:**
- UI 상태(`isAllowed`, `isLoading`)를 관리하던 `MutableLiveData`를 `MutableStateFlow`로 교체했습니다.
- 일회성 이벤트(`permissionCheckEvent`, `error`, `success`) 처리를 위해 사용하던 `SingleLiveData`를 `MutableSharedFlow`로 변경했습니다.
- 데이터 발행 방식을 `setValue`에서 `tryEmit`으로 수정하고, `Boolean?` 타입 체크 로직을 Flow의 `value` 접근 방식으로 간소화했습니다.
기존 XML 및 DataBinding 기반의 설정 화면을 Jetpack Compose로 마이그레이션했습니다.
- **`SettingFragment.kt` 리팩토링:**
- `onCreateView`에서 `ComposeView`를 반환하도록 변경하고, `SettingScreen` 컴포저블을 통해 UI를 렌더링하도록 수정했습니다.
- `DataBinding` 및 `View` 관련 로직을 제거하고, `collectAsStateWithLifecycle`을 사용하여 `ViewModel`의 상태를 구독하도록 변경했습니다.
- 권한 요청 및 URL 이동 등의 사이드 이펙트를 처리하기 위해 `ObserveAsEvents`를 적용했습니다.
- **`ObserveAsEvents.kt` 추가:**
- Compose 환경에서 `Flow` 기반의 이벤트를 생명주기에 맞춰 안전하게 수집할 수 있는 유틸리티 함수를 추가했습니다.
`SettingViewModel` 테스트 코드에서 `getOrAwaitValue` 확장 함수 의존성을 제거하고, `StateFlow` 및 `SharedFlow` 특성에 맞는 검증 로직으로 변경했습니다.
- **`SettingViewModelTest.kt` 수정:**
- 불필요해진 `getOrAwaitValue` import 구문을 제거했습니다.
- `permissionCheckEvent` 검증 시 `value` 대신 `replayCache.first()`를 사용하여 이벤트를 확인하도록 수정했습니다.
- `isAllowed` 상태 검증 시 `getOrAwaitValue()` 호출을 `value` 프로퍼티 접근으로 대체했습니다.
- 일부 코드의 줄바꿈 포맷팅을 수정했습니다.
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 📝 WalkthroughWalkthroughThe pull request migrates the SettingFragment UI from a binding-based Fragment setup to a Compose-based implementation, introducing a lifecycle-aware Flow observation utility, converting SettingViewModel's reactive properties from LiveData to Flow/StateFlow variants, and adding a new Compose component for the settings screen. Changes
Sequence Diagram(s)sequenceDiagram
participant SF as SettingFragment
participant VM as SettingViewModel
participant CS as SettingScreen
participant Flow as Flow/StateFlow
SF->>SF: onCreateView() creates ComposeView
SF->>VM: collectAsStateWithLifecycle(permissionCheckEvent)
SF->>VM: collectAsStateWithLifecycle(isAllowed)
SF->>VM: collectAsStateWithLifecycle(error)
SF->>VM: collectAsStateWithLifecycle(isLoading)
SF->>VM: collectAsStateWithLifecycle(successFlow)
VM->>Flow: StateFlow emits current state
Flow->>SF: State collected as Compose State
SF->>CS: Pass state and callbacks to SettingScreen
CS->>CS: Render with ObserveAsEvents for events
Note over CS: User interaction (subscribe toggle, policy click)
CS->>VM: Call callback (onSubscribeClick, onPolicyClick)
VM->>Flow: Emit new state/event
Flow->>SF: Observe lifecycle & collect on STARTED
SF->>CS: Recompose with new state
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
There was a problem hiding this 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
🤖 Fix all issues with AI agents
In
@app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt:
- Around line 86-88: Remove the redundant super.onCreateView(...) invocation
inside SettingFragment (it inflates fragment_setting.xml in BaseFragment and its
result is unused) and update the class generic: change the class declaration to
extend BaseFragment<FragmentSettingBinding>() if the base requires a type
parameter, or remove the generic from BaseFragment usage entirely if not needed;
ensure no other code in SettingFragment references the discarded binding and
keep the ComposeView creation and setViewCompositionStrategy call intact.
🧹 Nitpick comments (7)
app/src/test/java/com/daedan/festabook/setting/SettingViewModelTest.kt (1)
24-25: Consider removingInstantTaskExecutorRuleif no longer needed.Since the ViewModel properties have been migrated from LiveData to StateFlow/SharedFlow, this rule may no longer be necessary. The only remaining LiveData is
success(viaasLiveData()), but it's not tested directly in these tests.app/src/main/java/com/daedan/festabook/presentation/common/component/ObserveAsEvents.kt (1)
20-26: ThewithContext(Dispatchers.Main.immediate)may be redundant.
LaunchedEffectalready runs in the composition's coroutine context (Main dispatcher), andrepeatOnLifecyclepreserves this context. The explicitwithContextcall adds unnecessary overhead.♻️ Simplified implementation
LaunchedEffect(flow, lifecycleOwner.lifecycle) { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - withContext(Dispatchers.Main.immediate) { - flow.collect(onEvent) - } + flow.collect(onEvent) } }app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt (1)
70-70: Consider usingrequireView()instead ofview!!.Using
view!!can throw an unhandledNullPointerException. While this callback occurs after view creation, usingrequireView()provides a clearer error message if called at an unexpected time.♻️ Safer alternative
- showNotificationDeniedSnackbar(view!!, requireContext()) + showNotificationDeniedSnackbar(requireView(), requireContext())app/src/main/java/com/daedan/festabook/presentation/setting/component/SettingScreen.kt (4)
59-64: Extract duplicatedscreenWidthDpcalculation.The screen width calculation is duplicated in
SettingScreen(lines 59-64) andAppInfoContent(lines 191-196). Consider extracting this to a local composable helper or passing the value as a parameter.♻️ Pass screenWidthDp as parameter to AppInfoContent
@Composable private fun AppInfoContent( appVersion: String, onPolicyClick: () -> Unit, onContactUsClick: () -> Unit, + screenWidthDp: Dp, modifier: Modifier = Modifier, ) { - val windowInfo = LocalWindowInfo.current - val density = LocalDensity.current - val screenWidthDp = - with(density) { - windowInfo.containerSize.width.toDp() - }Also applies to: 191-196
165-179: Consider making disabled state visually distinct.The disabled track colors (
disabledCheckedTrackColor = FestabookColor.black,disabledUncheckedTrackColor = FestabookColor.gray200) match the enabled colors. Users may not realize the switch is disabled during loading. Consider using a lighter/muted color for the disabled state.
224-272: Consider extracting a reusableClickableInfoRowcomposable.The Policy and Contact Us rows share identical structure. Extracting a reusable component would reduce duplication.
♻️ Extracted component example
@Composable private fun ClickableInfoRow( text: String, screenWidthDp: Dp, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = modifier .requiredWidth(screenWidthDp) .clickable(onClick = onClick) .padding( horizontal = festabookSpacing.paddingScreenGutter, vertical = festabookSpacing.paddingBody3, ), ) { Text(text = text, style = FestabookTypography.titleMedium) Icon( painter = painterResource(R.drawable.ic_arrow_forward_right), contentDescription = stringResource(R.string.move), tint = Color.Unspecified, ) } }
90-101: Consider adding a loading indicator for non-Success states.When
festivalUiStateisLoading, theSubscriptionContentarea is empty. Consider showing a shimmer/skeleton placeholder for better UX during data loading.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
app/src/main/java/com/daedan/festabook/presentation/common/component/ObserveAsEvents.ktapp/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.ktapp/src/main/java/com/daedan/festabook/presentation/setting/SettingViewModel.ktapp/src/main/java/com/daedan/festabook/presentation/setting/component/SettingScreen.ktapp/src/test/java/com/daedan/festabook/setting/SettingViewModelTest.kt
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/daedan/festabook/presentation/setting/component/SettingScreen.kt (2)
app/src/main/java/com/daedan/festabook/presentation/common/component/FestabookTopAppBar.kt (1)
FestabookTopAppBar(13-31)app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt (1)
FestabookTheme(15-34)
app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt (4)
app/src/main/java/com/daedan/festabook/presentation/common/PermissionUtil.kt (1)
showNotificationDeniedSnackbar(13-32)app/src/main/java/com/daedan/festabook/presentation/common/component/ObserveAsEvents.kt (1)
ObserveAsEvents(14-27)app/src/main/java/com/daedan/festabook/presentation/common/FragmentUtil.kt (3)
showErrorSnackBar(54-56)showErrorSnackBar(58-75)showErrorSnackBar(77-109)app/src/main/java/com/daedan/festabook/presentation/setting/component/SettingScreen.kt (1)
SettingScreen(47-120)
⏰ 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: Run-PR-Test
🔇 Additional comments (10)
app/src/test/java/com/daedan/festabook/setting/SettingViewModelTest.kt (2)
45-60: LGTM!The test correctly uses
replayCache.first()to access the emitted event from theSharedFlowwithreplay = 1, which is the appropriate pattern for testing one-shot events.
62-118: LGTM!The remaining tests correctly use
StateFlow.valueto access state and properly verify the rollback behavior when server operations fail.app/src/main/java/com/daedan/festabook/presentation/common/component/ObserveAsEvents.kt (1)
12-13: Acknowledge duplication note.The comment appropriately documents the intentional duplication with PR #24. Ensure proper conflict resolution during merge.
app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt (1)
89-131: LGTM!The Compose migration is well-structured:
- State collection uses
collectAsStateWithLifecyclefor lifecycle awarenessObserveAsEventscorrectly handles one-shot events (permissions, success, ViewModel errors)SettingScreencallbacks are properly wired- Error handling distinguishes between
FestivalUiState.Error(UI state) and ViewModel operation errorsapp/src/main/java/com/daedan/festabook/presentation/setting/component/SettingScreen.kt (1)
65-72: LGTM!The use of
rememberUpdatedStateforonErrorcorrectly prevents stale closure issues inLaunchedEffect, ensuring the latest callback is always invoked.app/src/main/java/com/daedan/festabook/presentation/setting/SettingViewModel.kt (5)
75-93: Verify the intended UX for optimistic success notification.The success event is emitted (line 78) before the server call completes. If the server call fails, users see the success snackbar followed by an error snackbar. Consider emitting success only after server confirmation, or clarifying this is the intended behavior.
27-31: Be aware of event replay on resubscription.
MutableSharedFlow(replay = 1)caches the last emitted event, which replays to new subscribers (e.g., on configuration change). This could cause duplicate snackbars or permission requests. Consider usingresetReplayCache()after consumption if this becomes problematic.Also applies to: 39-43, 48-51
55-61: LGTM!The
notificationAllowClickfunction correctly checks the current allowed state before deciding to request permission or delete the notification.
96-118: LGTM!The
deleteNotificationIdfunction correctly implements the optimistic UI pattern with proper rollback on failure.
48-53: No action needed. Bothsuccess(LiveData) andsuccessFlow(SharedFlow) are actively used in different parts of the codebase:MainActivity.ktobserves the LiveData version whileSettingFragment.ktuses the SharedFlow viaObserveAsEvents. The dual exposure is intentional to support different consumer patterns.Likely an incorrect or invalid review comment.
| ComposeView(requireContext()).apply { | ||
| super.onCreateView(inflater, container, savedInstanceState) | ||
| setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.kt" | xargs grep -l "class BaseFragment" | head -5Repository: festabook/android
Length of output: 137
🏁 Script executed:
cat -n app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt | sed -n '1,50p'Repository: festabook/android
Length of output: 2673
🏁 Script executed:
cat -n app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt | sed -n '80,110p'Repository: festabook/android
Length of output: 1721
🏁 Script executed:
cat -n ./app/src/main/java/com/daedan/festabook/presentation/common/BaseFragment.ktRepository: festabook/android
Length of output: 2326
🏁 Script executed:
cat -n app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt | sed -n '80,132p'Repository: festabook/android
Length of output: 2794
🏁 Script executed:
# Check if FragmentSettingBinding is used anywhere in SettingFragment
rg "FragmentSettingBinding" app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt -nRepository: festabook/android
Length of output: 173
🏁 Script executed:
# Check if SettingFragment accesses binding anywhere
rg "binding\." app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt -nRepository: festabook/android
Length of output: 126
🏁 Script executed:
# Check if binding is accessed in SettingFragment
rg "this\.binding" app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt -nRepository: festabook/android
Length of output: 43
🏁 Script executed:
# Verify the complete SettingFragment file to ensure nothing uses binding
wc -l app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.ktRepository: festabook/android
Length of output: 143
Remove unnecessary super.onCreateView() call and unused binding type parameter.
The super.onCreateView() call at line 87 inflates the XML layout (fragment_setting.xml) via DataBindingUtil.inflate() in BaseFragment, but the created binding and return value are immediately discarded since SettingFragment returns a ComposeView instead. This wastes resources during initialization. Additionally, the FragmentSettingBinding type parameter is never accessed in this fragment.
Remove the super.onCreateView() call and change the class declaration to BaseFragment<FragmentSettingBinding>() if the base class type parameter is required by the framework, or refactor to remove the type parameter entirely if possible.
🤖 Prompt for AI Agents
In
@app/src/main/java/com/daedan/festabook/presentation/setting/SettingFragment.kt
around lines 86 - 88, Remove the redundant super.onCreateView(...) invocation
inside SettingFragment (it inflates fragment_setting.xml in BaseFragment and its
result is unused) and update the class generic: change the class declaration to
extend BaseFragment<FragmentSettingBinding>() if the base requires a type
parameter, or remove the generic from BaseFragment usage entirely if not needed;
ensure no other code in SettingFragment references the discarded binding and
keep the ComposeView creation and setViewCompositionStrategy call intact.
etama123
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
밀러 고생하셨습니다 ~~
| ) { | ||
| val windowInfo = LocalWindowInfo.current | ||
| val density = LocalDensity.current | ||
| val screenWidthDp = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 값을 직접 계산해서 사용하기 보다는, 패딩이 필요한 컨텐츠에 한해서 따로 패딩을 주는 건 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
screenWidthDp는 단 두 군데에서만 사용이 되고 있습니다.
그 두 군데를 위해 나머지 컨텐츠에 각각 패딩을 주는 것보다는 requiredWidth을 사용하는게 가독성이 더 좋다고 생각했어요.
대신 remember를 사용하여 매번 값을 계산하지 않도록 변경했습니다 !
설정 화면 컴포저블 분리 및 최적화
| .clickable { | ||
| onPolicyClick() | ||
| }.padding( | ||
| horizontal = festabookSpacing.paddingScreenGutter, | ||
| vertical = festabookSpacing.paddingBody3, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
패딩을 주기 전 부분만 클릭 속성이 적용되어 있는데, 의도된 부분일까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.requiredWidth(screenWidthDp)
로 클릭 영역을 주어서 리플 효과가 스크린 전체로 퍼지도록 하는게 더 이쁘더라구요 !
| Row( | ||
| horizontalArrangement = Arrangement.SpaceBetween, | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| modifier = | ||
| Modifier | ||
| .requiredWidth(screenWidthDp) | ||
| .clickable { | ||
| onContactUsClick() | ||
| }.padding( | ||
| horizontal = festabookSpacing.paddingScreenGutter, | ||
| vertical = festabookSpacing.paddingBody3, | ||
| ), | ||
| ) { | ||
| Text( | ||
| text = stringResource(R.string.setting_contact_us), | ||
| style = FestabookTypography.titleMedium, | ||
| ) | ||
|
|
||
| Icon( | ||
| painter = painterResource(R.drawable.ic_arrow_forward_right), | ||
| contentDescription = stringResource(R.string.move), | ||
| tint = Color.Unspecified, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 메뉴 아이템을 하나의 컴포저블로 구성할 수 있을 거 같아요~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다 !
설정 화면 컴포저블 분리 및 최적화
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun AppInfoContent( | ||
| appVersion: String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
스위치도 앱 내에서 동일하게 사용하기 위해 하나의 공통 컴포저블로 만드는 것은 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 생각인 것 같아요 ! 반영했습니다 !
refactor(Setting): 공통 스위치 컴포넌트(FestabookSwitch) 적용
parkjiminnnn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
마이그레이션 제가 한다고 큰소리 쳤는데 못해서 미안합니다 🙏
고생 많으셨어요 밀러 알림은 잘 오는 듯 해요!
코멘트 몇 개만 확인 부탁드려요~
| @Composable | ||
| fun <T> ObserveAsEvents( | ||
| flow: Flow<T>, | ||
| onEvent: suspend (T) -> Unit, | ||
| ) { | ||
| val lifecycleOwner = LocalLifecycleOwner.current | ||
| LaunchedEffect(flow, lifecycleOwner.lifecycle) { | ||
| lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| withContext(Dispatchers.Main.immediate) { | ||
| flow.collect(onEvent) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은데요?
| } else { | ||
| Timber.d("Notification permission denied") | ||
| showNotificationDeniedSnackbar(binding.root, requireContext()) | ||
| showNotificationDeniedSnackbar(view!!, requireContext()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에디터가 requireView()를 추천하길래 알아봤더니 view!!나 requireView() 둘 다 뷰가 생성된 시점에 호출하면 둘 다 view객체를 반환한다고하네요.
하지만 예외 발생 측면에서 봤을 때 NPE 보다는 IllegalStateException으로 생명주기 위반을 좀 더 명확하게 알 수 있는 점에서 requireView()는 어떠신가용?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다 !
fix(Setting): Null-safe 뷰 접근 로직으로 수정
| MutableSharedFlow( | ||
| replay = 1, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희 fragment가 싱글톤이고 화면 회전도 없어서 불필요한 이벤트 발생은 없을 것 같네요 😮
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 replay = 0을 주고 했을 때는 이벤트를 못받더라구요 ?!
그래서 임시로 이벤트를 담아둘 수 있는 버퍼를 만들어서 해결해보려고 했는데, 오히려 저희 앱 상황에선 헷갈릴 수 있는 부분인 것 같아요.
tryEmit -> emit으로 변경하여 해결했습니다 !
| val success: LiveData<Unit> = _success.asLiveData() | ||
| val successFlow = _success.asSharedFlow() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요친구만 LiveData로 하신 이유를 알 수 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 MainActivity에서도 사용되고 있어 상호 운용성을 위해 LiveData로 두었습니다 !
| appVersion = appVersion, | ||
| onPolicyClick = onPolicyClick, | ||
| onContactUsClick = onContactUsClick, | ||
| modifier = Modifier.background(color = FestabookColor.white), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
background를 따로 주신 이유가 궁금합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preview에서 저걸 안주니까 Material 기본 색상이 뜨길래 주었습니다만...앱에서는 하얗게 뜨네요. 제거했습니다 !
fix(Setting): 설정 화면 불필요한 배경색 코드 제거
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 코드에 전체적으로 assertAll이 있으면 좋을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()라이브데이터가 사라져서 요친구도 이제 없어도 될 듯해요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다 !
test(Setting): 테스트 코드의 Flow 이벤트 및 상태 검증 방식 개선
FlowExtension이라는 친구를 사용중인데 이는 MVI PR에서도 사용중이니 해당 PR이 머지되면 해당 적용하도록 하겠습니다 !
`SettingScreen`의 `AppInfoContent`를 `AppVersionInfo`와 `AppInfoButton` 컴포저블로 분리하여 재사용성을 높이고 코드 구조를 개선했습니다. 또한, 불필요한 재계산을 방지하기 위해 `screenWidthDp` 값을 `remember`로 감싸 성능을 최적화했습니다.
- **`SettingScreen.kt` 수정:**
- **컴포저블 분리:** 기존 `AppInfoContent` 내부의 앱 버전 표시 로직을 `AppVersionInfo` 컴포저블로, 이용약관 및 문의하기 버튼 로직을 `AppInfoButton` 컴포저블로 각각 추출했습니다.
- **성능 최적화:** `screenWidthDp` 값을 계산하는 로직을 `remember` 블록으로 감싸, 리컴포지션 시 불필요한 재계산을 방지하도록 수정했습니다.
설정 화면(`SettingScreen`)에서 사용되던 `Switch` 컴포저블을 공통 UI 컴포넌트인 `FestabookSwitch`로 교체하여 코드의 일관성과 재사용성을 높였습니다.
- **`SettingScreen.kt` 수정:**
- 기존 `material3.Switch`를 `FestabookSwitch`로 대체했습니다.
- `SwitchDefaults`를 통해 직접 지정하던 색상 관련 로직을 제거하고, 공통 컴포넌트에 위임하여 코드를 간소화했습니다.
- 불필요해진 `Switch` 및 `SwitchDefaults` import 구문을 삭제했습니다.
`SettingFragment`에서 `Snackbar`를 표시할 때 발생할 수 있는 잠재적인 `NullPointerException`을 방지하기 위해, `view!!`를 통한 강제 언래핑 대신 `requireView()`를 사용하도록 수정했습니다. 이 변경으로 인해 뷰가 null일 경우 안전하게 예외를 발생시켜 문제를 조기에 발견할 수 있습니다.
- **`SettingFragment.kt` 수정:**
- `showNotificationDeniedSnackbar` 호출 시 인자로 `view!!` 대신 `requireView()`를 전달하도록 변경했습니다.
`SettingViewModel` 내 `SharedFlow`의 `replay` 캐시를 제거하고, 이벤트 발행 방식을 `tryEmit`에서 `emit`으로 변경하여 이벤트 처리의 안정성을 개선했습니다.
- **`SettingViewModel.kt` 수정:**
- `_permissionCheckEvent`, `_error`, `_success` `MutableSharedFlow` 초기화 시 `replay = 1` 옵션을 제거했습니다.
- `tryEmit`을 사용하던 이벤트 발행 로직을 `viewModelScope.launch` 내에서 `emit`을 호출하는 방식으로 변경하여, 구독자가 이벤트를 놓치지 않도록 수정했습니다. (예: `notificationAllowClick`, `saveNotificationId`, `deleteNotificationId`)
`SettingScreen`의 `AppInfoContent` 컴포저블에 적용되었던 불필요한 배경색(`Modifier.background`) 코드를 제거했습니다.
- **`SettingScreen.kt` 수정:**
- `AppInfoContent`에 중복으로 적용되어 있던 `background` Modifier를 삭제하여 코드를 정리했습니다.
`SettingViewModelTest`에서 `SharedFlow`로 관리되는 일회성 이벤트를 보다 안정적으로 검증하기 위해 `observeEvent` 확장 함수를 도입하고, 여러 개의 검증을 하나의 그룹으로 묶는 `assertAll`을 적용했습니다.
- **`FlowExtension.kt` 추가:**
- `TestScope` 내에서 Flow 이벤트를 비동기적으로 수집하고 타임아웃을 적용하여 안정적으로 값을 기다릴 수 있는 `observeEvent`, `observeMultipleEvent` 유틸리티 함수를 추가했습니다.
- **`SettingViewModelTest.kt` 수정:**
- `permissionCheckEvent` 검증 시 `replayCache.first()`를 직접 사용하는 대신, 새로 추가된 `observeEvent` 함수를 호출하여 이벤트를 기다리도록 변경했습니다.
- 여러 `coVerify`와 `assertThat` 호출을 `assertAll` 블록으로 그룹화하여 테스트의 가독성과 원자성을 높였습니다.
#️⃣ 이슈 번호
🛠️ 작업 내용
🙇🏻 중점 리뷰 요청
기존 SingleLiveData을 SharedFlow의 replayCache = 1로 설정하여 최대 한 번의 이벤트는 유실되지 않게끔 처리하였습니다.
(Channel의 경우 다른 곳에서 이벤트를 소비할 경우 동작하지 않습니다)
ObserveAsEvent는 해당 PR 에서도 같은 코드를 사용하고 있습니다. PR이 머지가 된다면 해당 코드를 적용하겠습니다.
학생회 페이지에서 푸시 알림 테스트는 통과하였습니다만...혹시 모르니 한 번씩 테스트 부탁드립니다.
📸 이미지 첨부 (Optional)
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.