Skip to content

MainActivity companion object 안티패턴 제거 및 SharedFlow 이벤트 버스 전환#372

Merged
unam98 merged 2 commits intodevelopfrom
feature/wave0-decouple-screens
Apr 2, 2026
Merged

MainActivity companion object 안티패턴 제거 및 SharedFlow 이벤트 버스 전환#372
unam98 merged 2 commits intodevelopfrom
feature/wave0-decouple-screens

Conversation

@unam98
Copy link
Copy Markdown
Collaborator

@unam98 unam98 commented Apr 2, 2026

작업 배경

  • MainActivity.companion에 Fragment 인스턴스를 직접 보관하고, isVisitorMode를 static mutable 변수로 전역 공유하는 안티패턴이 존재
  • Fragment 생명주기와 무관한 참조로 메모리 릭/NPE 위험, 화면 간 강결합 발생

변경 사항

구분 파일 내용
신규 ScreenRefreshEventBus.kt @Singleton SharedFlow 기반 화면 간 이벤트 디스패처
신규 VisitorModeManager.kt @Singleton 방문자 모드 상태 관리 (토큰 기반 판단)
수정 MainActivity.kt companion에서 Fragment 참조, isVisitorMode, updateXxxScreen() 제거
수정 DiscoverFragment.kt self-registration 제거, 이벤트 버스 collect 추가
수정 StorageScrapFragment.kt self-registration 제거, 이벤트 버스 collect 추가
수정 MainPager.kt companion 참조 제거
수정 DrawActivity.kt VisitorModeManager 주입으로 교체
수정 CourseDetailActivity.kt VisitorModeManager + 이벤트 버스 주입
수정 DiscoverUploadActivity.kt 이벤트 버스로 교체
수정 DiscoverRecommendAdapter.kt isVisitorMode 람다 파라미터로 변경
수정 DiscoverMarathonAdapter.kt isVisitorMode 람다 파라미터로 변경
수정 BaseVisitorFragment.kt VisitorModeManager 주입
수정 DiscoverMultiViewAdapter/Holder/Factory isVisitorMode 람다 전달 체인

영향 범위

  • 화면 간 통신 방식 변경 (Fragment 직접 참조 → SharedFlow 이벤트)
  • 방문자 모드 판단 방식 변경 (static 변수 → Hilt Singleton)
  • 런타임 동작은 동일, 내부 구조만 변경

Test Plan

  • 디버그 빌드 성공 확인
  • 방문자 모드 진입 후 코스 업로드/스크랩 차단 동작 확인
  • 코스 업로드 후 탐색 화면 새로고침 동작 확인
  • 코스 상세 → 뒤로가기 시 스크랩 화면 새로고침 동작 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Centralized visitor-mode handling for consistent behavior across screens and actions.
  • New Features
    • Screen refresh events now automatically update Discover and Storage lists after uploads or related actions.
  • Behavior
    • Upload/navigation actions and heart/toggle interactions honor visitor-mode checks; navigation guards and analytics now reflect the centralized visitor-mode state.

…이벤트 버스로 전환

- ScreenRefreshEventBus 도입: Fragment 직접 참조 대신 SharedFlow 기반 이벤트 통신
- VisitorModeManager 도입: static mutable 변수 대신 Hilt Singleton으로 방문자 모드 관리
- MainActivity.companion에서 Fragment 인스턴스 참조, isVisitorMode, updateXxxScreen() 제거
- DiscoverFragment/StorageScrapFragment의 self-registration(onAttach/onDestroy) 제거
- Adapter에 isVisitorMode 람다 파라미터 전달로 정적 참조 제거
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Replaces static MainActivity state with injected singletons: VisitorModeManager centralizes visitor-mode checks and ScreenRefreshEventBus provides SharedFlow-based refresh events; callers are updated to inject/use these instead of companion flags or static methods.

Changes

Cohort / File(s) Summary
Event & Manager Infrastructure
app/src/main/java/com/runnect/runnect/presentation/event/ScreenRefreshEventBus.kt, app/src/main/java/com/runnect/runnect/presentation/event/VisitorModeManager.kt
Added ScreenRefreshEventBus (sealed ScreenRefreshEvent, SharedFlow-based bus with emit) and VisitorModeManager (Hilt singleton exposing isVisitorMode getter).
MainActivity & Pager
app/src/main/java/com/runnect/runnect/presentation/MainActivity.kt, app/src/main/java/com/runnect/runnect/presentation/MainPager.kt
Removed companion static flags and fragment references from MainActivity; injected VisitorModeManager; MainPager no longer assigns DiscoverFragment into a static field.
Activities: consumer changes
app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailActivity.kt, app/src/main/java/com/runnect/runnect/presentation/draw/DrawActivity.kt, app/src/main/java/com/runnect/runnect/presentation/discover/upload/DiscoverUploadActivity.kt
Injected VisitorModeManager and/or ScreenRefreshEventBus; replaced reads of MainActivity.isVisitorMode with visitorModeManager.isVisitorMode; replaced direct MainActivity refresh calls with screenRefreshEventBus.emit(...) (launched via lifecycleScope).
Fragments: consumer & subscription changes
app/src/main/java/com/runnect/runnect/binding/BaseVisitorFragment.kt, app/src/main/java/com/runnect/runnect/presentation/discover/DiscoverFragment.kt, app/src/main/java/com/runnect/runnect/presentation/storage/StorageScrapFragment.kt
Injected VisitorModeManager and ScreenRefreshEventBus; removed static MainActivity references and lifecycle assignments; added collectors on screenRefreshEventBus.events to refresh content on RefreshDiscoverCourses / RefreshStorageScrap.
Adapters & ViewHolders: parameter threading
app/src/main/java/com/runnect/runnect/presentation/discover/adapter/.../DiscoverMultiViewAdapter.kt, app/src/main/java/com/runnect/runnect/presentation/discover/adapter/.../DiscoverMultiViewHolder.kt, app/src/main/java/com/runnect/runnect/presentation/discover/adapter/.../DiscoverMultiViewHolderFactory.kt, app/src/main/java/com/runnect/runnect/presentation/discover/adapter/DiscoverMarathonAdapter.kt, app/src/main/java/com/runnect/runnect/presentation/discover/adapter/DiscoverRecommendAdapter.kt
Threaded isVisitorMode: () -> Boolean callback through adapters, factories, and view holders; removed direct static access to MainActivity.isVisitorMode and adjusted click handlers to use the injected predicate.

Sequence Diagram(s)

sequenceDiagram
    participant Upload as DiscoverUploadActivity
    participant Bus as ScreenRefreshEventBus
    participant Discover as DiscoverFragment
    participant Scrap as StorageScrapFragment

    rect rgba(200,200,255,0.5)
    Upload->>Bus: emit(RefreshDiscoverCourses)
    end

    rect rgba(200,255,200,0.5)
    Bus->>Discover: events -> RefreshDiscoverCourses
    Discover->>Discover: refreshDiscoverCourses()
    end

    rect rgba(255,200,200,0.5)
    Bus->>Scrap: events -> RefreshStorageScrap
    Scrap->>Scrap: getMyScrapCourses()
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • 바텀 네비바 뷰페이저2 사용 #365: Introduced MainPager behavior that previously assigned DiscoverFragment into MainActivity.discoverFragment; this PR removes that static wiring.
  • 리팩토링 #366: Introduced BaseVisitorFragment usage of MainActivity.isVisitorMode which is refactored here to use VisitorModeManager.

Poem

🐇 I hopped from static fields to flows and light,

injected managers guide visitor's sight.
Events now ripple, fragments wake and run,
no more companion shadows—fresh and fun.
✨ nibble the change, a shiny new sun.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title in Korean describes the main objective: removing MainActivity companion object anti-pattern and transitioning to SharedFlow event bus, which accurately reflects the core changes across multiple files (removing static fields, injecting VisitorModeManager, and implementing ScreenRefreshEventBus).

✏️ 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 feature/wave0-decouple-screens

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.

@unam98 unam98 self-assigned this Apr 2, 2026
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: 3

🧹 Nitpick comments (2)
app/src/main/java/com/runnect/runnect/presentation/event/VisitorModeManager.kt (1)

14-18: Optional: Consider caching login status for cleaner code.

The isVisitorMode getter performs a SharedPreferences read (getAccessToken() → synchronized PreferenceManager.getString()) on every invocation. In the current implementation, this is called only in click event listeners (DiscoverRecommendAdapter and DiscoverMarathonAdapter), so the frequency is low. However, caching the value at the call site can simplify the code and avoid repeated disk access:

♻️ Option: Cache at call site
// In Fragment/Activity before creating adapter:
val isVisitorMode = visitorModeManager.isVisitorMode
adapter = DiscoverMultiViewAdapter(
    // ...
    isVisitorMode = { isVisitorMode },  // captured once
    // ...
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/runnect/runnect/presentation/event/VisitorModeManager.kt`
around lines 14 - 18, The isVisitorMode getter calls context.getAccessToken()
(which reads SharedPreferences) on every access; to avoid repeated disk reads
and simplify call sites, capture visitorModeManager.isVisitorMode once where the
adapters are created (e.g., in the Fragment/Activity before constructing
DiscoverMultiViewAdapter/DiscoverRecommendAdapter/DiscoverMarathonAdapter) and
pass that captured Boolean into the adapters (or pass a lambda that returns the
captured value) instead of passing the live getter; this keeps the existing
VisitorModeManager.isVisitorMode, reduces SharedPreferences accesses from
LoginStatus.getLoginStatus(context.getAccessToken()), and makes adapter code
cleaner.
app/src/main/java/com/runnect/runnect/presentation/MainActivity.kt (1)

43-45: Remove the dead visitor-mode hook.

checkVisitorMode() no longer does any initialization, but onCreate() still calls it. Keeping the empty hook makes the startup path look stateful when it isn't.

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

In `@app/src/main/java/com/runnect/runnect/presentation/MainActivity.kt` around
lines 43 - 45, Remove the dead hook by deleting the empty checkVisitorMode()
function and its call from MainActivity.onCreate(), since VisitorModeManager now
handles visitor-mode via Hilt; search for the checkVisitorMode() declaration and
any invocation in MainActivity (including onCreate()) and remove both so startup
path is not misleading, keeping any real initialization in VisitorModeManager
instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/runnect/runnect/presentation/discover/DiscoverFragment.kt`:
- Around line 98-108: The refresh events are currently lost because
ScreenRefreshEventBus uses MutableSharedFlow with no replay; update the event
bus to buffer the last event (e.g., initialize
MutableSharedFlow<ScreenRefreshEvent>(replay = 1)) or replace it with a
StateFlow that holds the current refresh state so new collectors always see the
latest value; ensure this change is made in the ScreenRefreshEventBus
initialization (affecting collectors like
DiscoverFragment.collectScreenRefreshEvents and StorageScrapFragment) so
ScreenRefreshEvent.RefreshDiscoverCourses emitted while the view is destroyed
will be delivered when the fragment re-subscribes.

In
`@app/src/main/java/com/runnect/runnect/presentation/discover/upload/DiscoverUploadActivity.kt`:
- Around line 124-127: The event emission is happening after startActivity()
inside DiscoverUploadActivity and inside lifecycleScope.launch, which can be
cancelled when the Activity finishes and lost with MutableSharedFlow(replay=0);
move or duplicate the emit so it cannot be missed: emit
ScreenRefreshEvent.RefreshDiscoverCourses before calling startActivity() (or
change the flow to replay=1 in ScreenRefreshEventBus), or perform the emit from
a non-cancellable/app-scoped coroutine (e.g., an application-level scope or the
fragment/viewmodel scope) instead of lifecycleScope.launch so the refresh event
is delivered reliably; reference screenRefreshEventBus.emit, startActivity(),
lifecycleScope.launch, applyScreenExitAnimation, and
ScreenRefreshEventBus/MutableSharedFlow(replay=0) when applying the change.

In
`@app/src/main/java/com/runnect/runnect/presentation/event/ScreenRefreshEventBus.kt`:
- Line 16: The MutableSharedFlow _events in ScreenRefreshEventBus currently uses
the default replay=0 so emissions sent in
DiscoverUploadActivity.handleReturnToDiscover() can be missed by the target
Fragment; change the MutableSharedFlow construction in ScreenRefreshEventBus to
use replay = 1 so the latest event is available to late subscribers, and if you
need to avoid delivering stale events to future subscribers call
_events.resetReplayCache() after the consumer processes the event (refer to the
ScreenRefreshEventBus._events symbol and
DiscoverUploadActivity.handleReturnToDiscover() to locate the emit and
collection sites).

---

Nitpick comments:
In
`@app/src/main/java/com/runnect/runnect/presentation/event/VisitorModeManager.kt`:
- Around line 14-18: The isVisitorMode getter calls context.getAccessToken()
(which reads SharedPreferences) on every access; to avoid repeated disk reads
and simplify call sites, capture visitorModeManager.isVisitorMode once where the
adapters are created (e.g., in the Fragment/Activity before constructing
DiscoverMultiViewAdapter/DiscoverRecommendAdapter/DiscoverMarathonAdapter) and
pass that captured Boolean into the adapters (or pass a lambda that returns the
captured value) instead of passing the live getter; this keeps the existing
VisitorModeManager.isVisitorMode, reduces SharedPreferences accesses from
LoginStatus.getLoginStatus(context.getAccessToken()), and makes adapter code
cleaner.

In `@app/src/main/java/com/runnect/runnect/presentation/MainActivity.kt`:
- Around line 43-45: Remove the dead hook by deleting the empty
checkVisitorMode() function and its call from MainActivity.onCreate(), since
VisitorModeManager now handles visitor-mode via Hilt; search for the
checkVisitorMode() declaration and any invocation in MainActivity (including
onCreate()) and remove both so startup path is not misleading, keeping any real
initialization in VisitorModeManager instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 929d25d1-2022-44e6-8dac-80a9af9aa1bb

📥 Commits

Reviewing files that changed from the base of the PR and between f6db01e and 65ee4fc.

📒 Files selected for processing (15)
  • app/src/main/java/com/runnect/runnect/binding/BaseVisitorFragment.kt
  • app/src/main/java/com/runnect/runnect/presentation/MainActivity.kt
  • app/src/main/java/com/runnect/runnect/presentation/MainPager.kt
  • app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailActivity.kt
  • app/src/main/java/com/runnect/runnect/presentation/discover/DiscoverFragment.kt
  • app/src/main/java/com/runnect/runnect/presentation/discover/adapter/DiscoverMarathonAdapter.kt
  • app/src/main/java/com/runnect/runnect/presentation/discover/adapter/DiscoverRecommendAdapter.kt
  • app/src/main/java/com/runnect/runnect/presentation/discover/adapter/multiview/DiscoverMultiViewAdapter.kt
  • app/src/main/java/com/runnect/runnect/presentation/discover/adapter/multiview/DiscoverMultiViewHolder.kt
  • app/src/main/java/com/runnect/runnect/presentation/discover/adapter/multiview/DiscoverMultiViewHolderFactory.kt
  • app/src/main/java/com/runnect/runnect/presentation/discover/upload/DiscoverUploadActivity.kt
  • app/src/main/java/com/runnect/runnect/presentation/draw/DrawActivity.kt
  • app/src/main/java/com/runnect/runnect/presentation/event/ScreenRefreshEventBus.kt
  • app/src/main/java/com/runnect/runnect/presentation/event/VisitorModeManager.kt
  • app/src/main/java/com/runnect/runnect/presentation/storage/StorageScrapFragment.kt

Comment on lines +15 to +17
class ScreenRefreshEventBus @Inject constructor() {
private val _events = MutableSharedFlow<ScreenRefreshEvent>()
val events: SharedFlow<ScreenRefreshEvent> = _events.asSharedFlow()
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

기존엔 생명주기를 고려하지 않은 코드여서 안정적이지 않았음. 그래서 개선을 해내야 하는데 지금의 방식은 sharedFlow를 쓰면 이벤트를 쏘고나서 백그라운드 전환 등으로 이벤트가 소실됐을 때 복구가 안 돼서 문제가 생길 것 같은데 검토 필요해보임.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

반영 완료 (1356ba1)

  • MutableSharedFlow(replay = 1)로 변경하여 마지막 이벤트를 버퍼링
  • late subscriber도 최신 이벤트를 수신 가능
  • 이 앱에서 이벤트는 "화면 새로고침" 용도라 stale replay 시 최악의 경우 불필요한 리프레시 1회 — 실질적 부작용 없음
  • SharedFlow 공식 문서 참고

- checkVisitorMode() dead code 삭제
- collectScreenRefreshEvents()를 addObserver() 안으로 이동
- MutableSharedFlow(replay=1)로 이벤트 소실 방지
- DiscoverUploadActivity에서 emit을 startActivity 전으로 이동
@unam98
Copy link
Copy Markdown
Collaborator Author

unam98 commented Apr 2, 2026

코드래빗 Nitpick 대응

VisitorModeManager SharedPreferences 캐싱 제안 — 현재 코드 유지

  • isVisitorMode 호출은 사용자 클릭 시에만 발생 (하트 버튼 탭) — 초당 수십 회 호출되는 상황이 아님
  • SharedPreferences 읽기는 메모리 캐시에서 반환되므로 실질적 디스크 I/O 없음 (Android 공식 문서getSharedPreferences는 첫 호출 시에만 파일을 읽고 이후 메모리 캐시)
  • 캐싱하면 로그인 상태 변경 시 stale 값을 참조할 위험이 있어 오히려 버그 가능성 증가

@unam98 unam98 merged commit 0c266e0 into develop Apr 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant