Skip to content

[AN] refactor: 라이브러리 화면 컴포즈 마이그레이션#860

Open
rosemin928 wants to merge 4 commits intodevelop-anfrom
feat/#859-library-compose-migration
Open

[AN] refactor: 라이브러리 화면 컴포즈 마이그레이션#860
rosemin928 wants to merge 4 commits intodevelop-anfrom
feat/#859-library-compose-migration

Conversation

@rosemin928
Copy link
Copy Markdown
Contributor

@rosemin928 rosemin928 commented Mar 22, 2026

📌 변경 내용 & 이유

  • 라이브러리 화면 컴포즈 마이그레이션

📸 스크린샷 (선택)

UI 변경이 있을 경우 첨부

🧪 테스트 방법 (선택)

📢 논의하고 싶은 내용

🧩 관련 이슈

#859

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 라이브러리 화면이 현대화된 UI로 업데이트되었습니다.
    • 프로필 섹션, 북마크 헤더, 북마크 목록 아이템이 개선되었습니다.
    • 빈 상태 화면(로그인 필요, 북마크 없음)이 추가되었습니다.
  • 스타일

    • 새로운 색상 테마가 적용되었습니다.
  • 리팩토링

    • 라이브러리 화면 아키텍처가 개선되었습니다.

- `BookmarkAdapter`, `BookmarkViewHolder`, `BookmarkClickListener` 등 기존 XML 기반 리사이클러뷰 관련 코드 삭제
- `fragment_library.xml` 및 관련 레이아웃 XML 파일 삭제
- `LibraryScreen`, `BookmarkItem`, `LibraryProfileSection` 등 Compose 기반 UI 컴포넌트 구현
- `LibraryViewModel`의 `LiveData`를 `StateFlow`로 변경하고 상태 관리 로직 개선
- `LibraryFragment`에서 `ComposeView`를 사용하여 UI를 렌더링하도록 변경 및 미디어 컨트롤러 연동 로직 수정
- XML 리소스 기반의 색상(HearitBlack1, HearitGray4 등) 및 폰트(Pretendard) 정의 추가
- `BookmarkItem`, `LibraryProfileSection`, `LibraryHeaderSection` 등에 구체적인 텍스트 스타일(fontSize, fontFamily) 적용
- 컴포넌트 간 간격(Spacer), 패딩, 마진 값을 XML 디자인 가이드에 맞춰 조정
- `LibraryScreen` 배경에 `HearitPurple3`와 `HearitBlack1`을 사용하는 세밀한 수직 그라데이션 적용
- 프로필 및 헤더 섹션의 아이콘 크기 및 틴트 설정 최적화
- `Color.kt`에 XML 기반 공통 컬러 및 시맨틱 컬러 추가
- `BookmarkItem`, `LibraryEmptyViews`, `LibraryScreen` 등 Library 컴포넌트들의 하드코딩된 색상과 폰트를 공통 테마(`HearitBlack1`, `Gray4`, `PretendardFontFamily` 등)로 교체
- 불필요한 주석 제거 및 코드 포맷팅 정리
@rosemin928 rosemin928 self-assigned this Mar 22, 2026
@rosemin928 rosemin928 added AN 안드로이드 파트 🔨 refactor 구조 및 컨벤션 변경 labels Mar 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

Walkthrough

라이브러리 기능을 RecyclerView 및 XML 기반 UI에서 Jetpack Compose로 마이그레이션합니다. LiveData 상태 관리를 StateFlow로 전환하고, 관련 어댑터, 뷰 홀더, 리스너 클래스를 제거한 후 새로운 Compose 컴포넌트로 대체합니다.

Changes

Cohort / File(s) Summary
Compose UI Components
android/app/src/main/java/com/onair/hearit/presentation/library/component/BookmarkItem.kt, LibraryEmptyViews.kt, LibraryHeaderSection.kt, LibraryProfileSection.kt, LibraryScreen.kt
새로운 Jetpack Compose UI 컴포넌트 추가. 북마크 항목, 빈 상태 뷰, 헤더/프로필 섹션, 전체 라이브러리 화면 구성 요소 포함.
RecyclerView 구현 제거
android/app/src/main/java/com/onair/hearit/presentation/library/BookmarkAdapter.kt, BookmarkClickListener.kt, BookmarkViewHolder.kt
RecyclerView ListAdapter, 클릭 리스너 인터페이스, ViewHolder 클래스 제거.
Fragment 마이그레이션
android/app/src/main/java/com/onair/hearit/presentation/library/LibraryFragment.kt
XML/ViewBinding 기반 UI를 ComposeView 기반으로 전환. 어댑터 및 수동 리스너 제거, Compose 콜백으로 네비게이션/액션 위임.
상태 관리 리팩토링
android/app/src/main/java/com/onair/hearit/presentation/library/LibraryViewModel.kt, BookmarkOptionBottomSheet.kt
LiveData에서 StateFlow로 상태 관리 전환. 플로우 수집 시 뷰 생명주기 바인딩 추가.
레이아웃 리소스 제거
android/app/src/main/res/layout/fragment_library.xml, item_bookmark.xml, layout_library_when_no_login.xml, layout_library_when_no_bookmark.xml
XML 데이터 바인딩 레이아웃 파일 전체 제거 (Compose로 대체).
테마 업데이트
android/app/src/main/java/com/onair/hearit/presentation/theme/Color.kt
새로운 색상 상수 추가: HearitBlack1, HearitDarkBlack, HearitDarkGray, HearitPurple3, HearitPurple4, BackgroundDark, SurfaceBlack, PrimaryPurple. 하위 호환성을 위해 기존 색상을 별칭으로 재정의.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR 설명이 기본 템플릿 구조를 따르고 있으나, 변경 내용 섹션이 매우 간략하고 기술적 세부사항 및 테스트 방법이 부족합니다. 변경 내용을 더 상세히 작성해주세요. (예: RecyclerView에서 Compose로의 마이그레이션 범위, LiveData에서 StateFlow로의 전환 이유, 호환성 영향 등) 테스트 방법도 추가하여 리뷰어가 검증할 수 있도록 해주세요.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경 사항의 주요 내용을 명확하게 설명합니다. 라이브러리 화면의 Jetpack Compose 마이그레이션이라는 핵심 변경점을 정확히 표현했습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#859-library-compose-migration

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.

Tip

You can customize the tone of the review comments and chat replies.

Configure the tone_instructions setting to customize the tone of the review comments and chat replies. For example, you can set the tone to Act like a strict teacher, Act like a pirate and more.

Copy link
Copy Markdown

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
android/app/src/main/java/com/onair/hearit/presentation/library/LibraryFragment.kt (1)

248-253: ⚠️ Potential issue | 🟠 Major

onStop에서 onDestroyView로 MediaController 해제 시점 변경

현재 onStop에서 MediaController를 해제하고 있으나, 백그라운드 재생 시나리오에서 컨트롤러 연결이 유지되어야 합니다. ScriptFragment 등 다른 Fragment에서는 onDestroyView에서 해제하고 있으므로, 동일하게 onDestroyView에서 정리하는 것이 적절합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/LibraryFragment.kt`
around lines 248 - 253, 현재 LibraryFragment의 onStop에서 mediaController를 해제하고 있는데
백그라운드 재생을 위해 해제 시점을 onDestroyView로 옮겨야 합니다;
mediaController?.removeListener(playerListener), mediaController?.release(),
mediaController = null 호출을 onStop에서 제거하고 대신 Fragment의 onDestroyView 오버라이드(또는 기존
onDestroyView가 있다면 그곳)에 동일한 정리 로직을 추가하여 MediaController와 playerListener를 해제하도록
변경하세요.
🧹 Nitpick comments (4)
android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryHeaderSection.kt (1)

66-74: 접근성을 위한 contentDescription 국제화 권장

하드코딩된 영어 문자열 대신 stringResource를 사용하면 접근성 서비스(TalkBack)에서 올바른 언어로 읽어줄 수 있습니다.

♻️ stringResource 사용 제안
 Image(
-    painter = painterResource(id = if (isPlaying) R.drawable.img_pause else R.drawable.img_play),
-    contentDescription = "Play All",
+    painter = painterResource(id = if (isPlaying) R.drawable.img_pause else R.drawable.img_play),
+    contentDescription = stringResource(
+        id = if (isPlaying) R.string.library_pause_all else R.string.library_play_all
+    ),
     modifier =
         Modifier
             .size(56.dp)
             .padding(4.dp)
             .clickable { onPlayAllClick() },
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryHeaderSection.kt`
around lines 66 - 74, Replace the hardcoded contentDescription in the Image
composable inside LibraryHeaderSection with localized strings via
stringResource; use different resource keys depending on isPlaying (e.g.,
R.string.play_all vs R.string.pause_all) so TalkBack reads the correct localized
phrase, and ensure those string keys are added to strings.xml with appropriate
translations; keep the clickable/onPlayAllClick behavior unchanged.
android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryEmptyViews.kt (1)

57-74: 로그인 버튼의 접근성 개선 권장

Box + clickable 조합은 동작하지만, Button 컴포넌트나 semantics { role = Role.Button } 추가 시 접근성 서비스에서 버튼으로 인식됩니다.

♻️ 접근성 향상을 위한 semantics 추가 제안
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
...
 Box(
     modifier =
         Modifier
             .fillMaxWidth()
             .height(56.dp)
             .clip(RoundedCornerShape(8.dp))
             .background(PrimaryPurple)
-            .clickable { onLoginClick() },
+            .clickable(
+                onClick = onLoginClick,
+                role = Role.Button,
+            ),
     contentAlignment = Alignment.Center,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryEmptyViews.kt`
around lines 57 - 74, Replace the clickable Box with an accessible button role:
either swap the Box for a Button composable or add semantics { role =
Role.Button } to the Modifier on the existing Box; ensure the onLoginClick
callback is passed to the Button's onClick (or retained on the clickable
modifier) and keep the visual styling (height, clip, background, text styles) so
accessibility services will announce it as a button (refer to the Box currently
using Modifier.clickable and the onLoginClick handler).
android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryProfileSection.kt (1)

86-90: contentDescription 국제화 권장

LibraryHeaderSection.kt와 동일하게, 접근성을 위해 stringResource를 사용하는 것이 좋습니다.

♻️ stringResource 사용 제안
 Icon(
     painter = painterResource(id = R.drawable.ic_setting),
-    contentDescription = "Setting",
+    contentDescription = stringResource(id = R.string.all_setting),
     tint = Color.Unspecified,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryProfileSection.kt`
around lines 86 - 90, Replace the hardcoded contentDescription "Setting" in the
Icon call inside LibraryProfileSection (the Icon using painterResource(id =
R.drawable.ic_setting)) with a localized stringResource lookup (e.g.
stringResource(R.string.<setting_label>)); add the corresponding entry to
strings.xml (use the same key used elsewhere, e.g. "setting" or match
LibraryHeaderSection) so accessibility reads the localized value.
android/app/src/main/java/com/onair/hearit/presentation/library/component/BookmarkItem.kt (1)

59-59: 잘못된 colorCode 형식에 대한 예외 처리 고려

Color.parseColor()는 잘못된 hex 문자열이 전달될 경우 IllegalArgumentException을 발생시킵니다. 서버 데이터가 항상 유효하다고 보장되지 않는다면, 기본 색상으로 폴백하는 것이 안전합니다.

🛡️ 안전한 색상 파싱 제안
 Box(
     modifier =
         Modifier
             .size(60.dp)
             .clip(RoundedCornerShape(8.dp))
-            .background(Color(android.graphics.Color.parseColor(bookmark.category.colorCode))),
+            .background(
+                runCatching {
+                    Color(android.graphics.Color.parseColor(bookmark.category.colorCode))
+                }.getOrDefault(Gray1)
+            ),
     contentAlignment = Alignment.Center,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/BookmarkItem.kt`
at line 59, The background color parsing in BookmarkItem.kt uses
Color(android.graphics.Color.parseColor(bookmark.category.colorCode)) which can
throw IllegalArgumentException for invalid hex strings; update this by wrapping
the parse in a safe helper or try/catch (e.g., a private parseColorOrDefault
function) that attempts
android.graphics.Color.parseColor(bookmark.category.colorCode) and on failure
returns a predefined fallback Color (e.g., Color.Gray or a theme color), then
use that safe result in the .background(...) call so invalid server colorCode
values don't crash the UI.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/BookmarkItem.kt`:
- Around line 115-120: The progress calculation in BookmarkItem is using
(lastPlayTime ?: 0L) / playTime without converting milliseconds to seconds,
causing ~1000x error; fix it by reusing the existing
calculateProgress(lastPlayTimeMillis: Long?, totalPlayTimeSec: Int) from
Extensions.kt or apply the same conversion (divide lastPlayTimeMillis by 1000
before dividing by playTime), updating the code in BookmarkItem (where progress
is computed) to call calculateProgress(bookmark.lastPlayTime, bookmark.playTime)
or perform the millisecond-to-second conversion so units match.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryScreen.kt`:
- Around line 45-60: The derived shouldLoadMore currently captures a stale
isLoading because remember { derivedStateOf { ... isLoading ... } } doesn't
track plain Boolean changes; fix it by including isLoading in the remember keys
so the derived state is recreated when loading changes (e.g., change to
remember(isLoading) { derivedStateOf { val lastVisibleItemIndex =
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 val
totalItemsCount = listState.layoutInfo.totalItemsCount !isLoading &&
totalItemsCount > 0 && lastVisibleItemIndex >= totalItemsCount - 3 } } ),
ensuring LaunchedEffect(shouldLoadMore) and onLoadMore() see the up-to-date
isLoading value.

---

Outside diff comments:
In
`@android/app/src/main/java/com/onair/hearit/presentation/library/LibraryFragment.kt`:
- Around line 248-253: 현재 LibraryFragment의 onStop에서 mediaController를 해제하고 있는데
백그라운드 재생을 위해 해제 시점을 onDestroyView로 옮겨야 합니다;
mediaController?.removeListener(playerListener), mediaController?.release(),
mediaController = null 호출을 onStop에서 제거하고 대신 Fragment의 onDestroyView 오버라이드(또는 기존
onDestroyView가 있다면 그곳)에 동일한 정리 로직을 추가하여 MediaController와 playerListener를 해제하도록
변경하세요.

---

Nitpick comments:
In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/BookmarkItem.kt`:
- Line 59: The background color parsing in BookmarkItem.kt uses
Color(android.graphics.Color.parseColor(bookmark.category.colorCode)) which can
throw IllegalArgumentException for invalid hex strings; update this by wrapping
the parse in a safe helper or try/catch (e.g., a private parseColorOrDefault
function) that attempts
android.graphics.Color.parseColor(bookmark.category.colorCode) and on failure
returns a predefined fallback Color (e.g., Color.Gray or a theme color), then
use that safe result in the .background(...) call so invalid server colorCode
values don't crash the UI.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryEmptyViews.kt`:
- Around line 57-74: Replace the clickable Box with an accessible button role:
either swap the Box for a Button composable or add semantics { role =
Role.Button } to the Modifier on the existing Box; ensure the onLoginClick
callback is passed to the Button's onClick (or retained on the clickable
modifier) and keep the visual styling (height, clip, background, text styles) so
accessibility services will announce it as a button (refer to the Box currently
using Modifier.clickable and the onLoginClick handler).

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryHeaderSection.kt`:
- Around line 66-74: Replace the hardcoded contentDescription in the Image
composable inside LibraryHeaderSection with localized strings via
stringResource; use different resource keys depending on isPlaying (e.g.,
R.string.play_all vs R.string.pause_all) so TalkBack reads the correct localized
phrase, and ensure those string keys are added to strings.xml with appropriate
translations; keep the clickable/onPlayAllClick behavior unchanged.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryProfileSection.kt`:
- Around line 86-90: Replace the hardcoded contentDescription "Setting" in the
Icon call inside LibraryProfileSection (the Icon using painterResource(id =
R.drawable.ic_setting)) with a localized stringResource lookup (e.g.
stringResource(R.string.<setting_label>)); add the corresponding entry to
strings.xml (use the same key used elsewhere, e.g. "setting" or match
LibraryHeaderSection) so accessibility reads the localized value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 96c3f44b-ed9f-4ad6-92ef-2a4a8f671212

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd87f5 and c9ffb94.

📒 Files selected for processing (16)
  • android/app/src/main/java/com/onair/hearit/presentation/library/BookmarkAdapter.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/BookmarkClickListener.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/BookmarkOptionBottomSheet.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/BookmarkViewHolder.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/LibraryFragment.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/LibraryViewModel.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/component/BookmarkItem.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryEmptyViews.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryHeaderSection.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryProfileSection.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryScreen.kt
  • android/app/src/main/java/com/onair/hearit/presentation/theme/Color.kt
  • android/app/src/main/res/layout/fragment_library.xml
  • android/app/src/main/res/layout/item_bookmark.xml
  • android/app/src/main/res/layout/layout_library_when_no_bookmark.xml
  • android/app/src/main/res/layout/layout_library_when_no_login.xml
💤 Files with no reviewable changes (7)
  • android/app/src/main/res/layout/layout_library_when_no_bookmark.xml
  • android/app/src/main/res/layout/fragment_library.xml
  • android/app/src/main/java/com/onair/hearit/presentation/library/BookmarkClickListener.kt
  • android/app/src/main/res/layout/layout_library_when_no_login.xml
  • android/app/src/main/res/layout/item_bookmark.xml
  • android/app/src/main/java/com/onair/hearit/presentation/library/BookmarkViewHolder.kt
  • android/app/src/main/java/com/onair/hearit/presentation/library/BookmarkAdapter.kt

Comment on lines +115 to +120
val progress =
if (bookmark.playTime > 0L) {
(bookmark.lastPlayTime ?: 0L).toFloat() / bookmark.playTime.toFloat()
} else {
0f
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# lastPlayTime이 사용되는 다른 위치에서 단위를 확인
rg -n "lastPlayTime" --type=kotlin -C3

Repository: woowacourse-teams/2025-hEARit

Length of output: 40737


BookmarkItem에서 진행률 계산 시 단위 변환 누락

코드베이스의 다른 위치들(Extensions.ktcalculateProgress, BindingAdapter.kt)에서는 lastPlayTime(밀리초)을 1000으로 나누어 초 단위로 변환한 후 playTime(초)로 나누고 있습니다. 그러나 현재 BookmarkItem.kt 라인 117의 코드는 이 변환을 수행하지 않아 진행률이 실제보다 약 1000배 크게 계산됩니다.

이미 Extensions.kt에 올바르게 구현된 calculateProgress(lastPlayTimeMillis: Long?, totalPlayTimeSec: Int) 함수가 존재하므로, 이 함수를 재사용하거나 동일한 변환 로직을 적용해야 합니다.

해결 방법

기존 calculateProgress 함수를 사용하거나, 인라인 수정 시:

 val progress =
     if (bookmark.playTime > 0L) {
-        (bookmark.lastPlayTime ?: 0L).toFloat() / bookmark.playTime.toFloat()
+        ((bookmark.lastPlayTime ?: 0L) / 1000f) / bookmark.playTime.toFloat()
     } else {
         0f
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/BookmarkItem.kt`
around lines 115 - 120, The progress calculation in BookmarkItem is using
(lastPlayTime ?: 0L) / playTime without converting milliseconds to seconds,
causing ~1000x error; fix it by reusing the existing
calculateProgress(lastPlayTimeMillis: Long?, totalPlayTimeSec: Int) from
Extensions.kt or apply the same conversion (divide lastPlayTimeMillis by 1000
before dividing by playTime), updating the code in BookmarkItem (where progress
is computed) to call calculateProgress(bookmark.lastPlayTime, bookmark.playTime)
or perform the millisecond-to-second conversion so units match.

Comment on lines +45 to +60
val shouldLoadMore by remember {
derivedStateOf {
val lastVisibleItemIndex =
listState.layoutInfo.visibleItemsInfo
.lastOrNull()
?.index ?: 0
val totalItemsCount = listState.layoutInfo.totalItemsCount
!isLoading && totalItemsCount > 0 && lastVisibleItemIndex >= totalItemsCount - 3
}
}

LaunchedEffect(shouldLoadMore) {
if (shouldLoadMore) {
onLoadMore()
}
}
Copy link
Copy Markdown

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

🌐 Web query:

In Jetpack Compose, if a non-State parameter (e.g., Boolean isLoading) is referenced inside remember { derivedStateOf { ... } } without remember keys, will updates on recomposition be reflected, or can it keep a stale captured value?

💡 Result:

No, updates to a non-State parameter like Boolean isLoading referenced inside remember { derivedStateOf { ... } } without remember keys will NOT be reflected. The derivedStateOf will keep a stale captured value. The remember without keys creates the derivedStateOf object once and reuses it across recompositions. The derivedStateOf calculation lambda captures the value of isLoading at creation time (stale value). It only invalidates/recomputes when State objects read inside the lambda change. Since isLoading is a plain non-State Boolean parameter (not a State delegate like by mutableStateOf), no State is read, so it never invalidates even if the composable recomposes with a new isLoading value. To fix: Use remember(isLoading) { derivedStateOf { ... } } (recreates on isLoading change) or better, remember(isLoading) { isLoading && ... } or just if (isLoading) { ... } directly since no State reads are needed and derivedStateOf adds overhead.

Citations:


isLoading 캡처가 고정되어 페이지네이션 트리거가 오동작할 수 있습니다.

Line 45-53의 remember { derivedStateOf { ... isLoading ... } }isLoading 최신값을 반영하지 못합니다. derivedStateOf는 State 객체 변경에만 반응하고, 순수 Boolean 파라미터 변경은 감지하지 않기 때문에 캡처된 값이 고정됩니다. 결과적으로 Line 56-60에서 onLoadMore()가 중복 호출되어 불필요한 추가 요청이 발생합니다.

수정 제안
-    val shouldLoadMore by remember {
+    val shouldLoadMore by remember(listState, isLoading, uiState) {
         derivedStateOf {
             val lastVisibleItemIndex =
                 listState.layoutInfo.visibleItemsInfo
                     .lastOrNull()
                     ?.index ?: 0
             val totalItemsCount = listState.layoutInfo.totalItemsCount
-            !isLoading && totalItemsCount > 0 && lastVisibleItemIndex >= totalItemsCount - 3
+            uiState is BookmarkUiState.LoggedIn &&
+                !isLoading &&
+                totalItemsCount > 0 &&
+                lastVisibleItemIndex >= totalItemsCount - 3
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/onair/hearit/presentation/library/component/LibraryScreen.kt`
around lines 45 - 60, The derived shouldLoadMore currently captures a stale
isLoading because remember { derivedStateOf { ... isLoading ... } } doesn't
track plain Boolean changes; fix it by including isLoading in the remember keys
so the derived state is recreated when loading changes (e.g., change to
remember(isLoading) { derivedStateOf { val lastVisibleItemIndex =
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 val
totalItemsCount = listState.layoutInfo.totalItemsCount !isLoading &&
totalItemsCount > 0 && lastVisibleItemIndex >= totalItemsCount - 3 } } ),
ensuring LaunchedEffect(shouldLoadMore) and onLoadMore() see the up-to-date
isLoading value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AN 안드로이드 파트 🔨 refactor 구조 및 컨벤션 변경

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant