-
Notifications
You must be signed in to change notification settings - Fork 1
Compose 테마 구축 및 마이페이지 Compose UI 전환 #374
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,155 +1,153 @@ | ||
| package com.runnect.runnect.presentation.mypage | ||
|
|
||
| import android.app.Activity | ||
| import android.app.Activity.RESULT_OK | ||
| import android.content.Intent | ||
| import android.os.Bundle | ||
| import android.view.LayoutInflater | ||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import androidx.activity.result.ActivityResultLauncher | ||
| import androidx.activity.result.contract.ActivityResultContracts | ||
| import androidx.core.view.isVisible | ||
| import androidx.compose.runtime.collectAsState | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.ui.platform.ComposeView | ||
| import androidx.compose.ui.platform.ViewCompositionStrategy | ||
| import androidx.fragment.app.Fragment | ||
| import androidx.fragment.app.activityViewModels | ||
| import androidx.fragment.app.commit | ||
| import androidx.fragment.app.replace | ||
| import coil3.load | ||
| import com.kakao.sdk.common.util.KakaoCustomTabsClient | ||
| import com.kakao.sdk.talk.TalkApiClient | ||
| import com.runnect.runnect.BuildConfig | ||
| import com.runnect.runnect.R | ||
| import com.runnect.runnect.binding.BaseVisitorFragment | ||
| import com.runnect.runnect.databinding.FragmentMyPageBinding | ||
| import com.runnect.runnect.presentation.event.VisitorModeManager | ||
| import com.runnect.runnect.presentation.login.LoginActivity | ||
| import com.runnect.runnect.presentation.mypage.editname.MyPageEditNameActivity | ||
| import com.runnect.runnect.presentation.mypage.history.MyHistoryActivity | ||
| import com.runnect.runnect.presentation.mypage.reward.MyRewardActivity | ||
| import com.runnect.runnect.presentation.mypage.setting.MySettingFragment | ||
| import com.runnect.runnect.presentation.mypage.upload.MyUploadActivity | ||
| import com.runnect.runnect.presentation.ui.theme.RunnectTheme | ||
| import com.runnect.runnect.util.analytics.Analytics | ||
| import com.runnect.runnect.util.analytics.EventName.EVENT_CLICK_GOAL_REWARD | ||
| import com.runnect.runnect.util.analytics.EventName.EVENT_CLICK_RUNNING_RECORD | ||
| import com.runnect.runnect.util.analytics.EventName.EVENT_CLICK_UPLOADED_COURSE | ||
| import com.runnect.runnect.util.extension.getStampResId | ||
| import com.runnect.runnect.util.extension.repeatOnStarted | ||
| import com.runnect.runnect.util.extension.showSnackbar | ||
| import dagger.hilt.android.AndroidEntryPoint | ||
| import kotlinx.coroutines.flow.collectLatest | ||
| import javax.inject.Inject | ||
|
|
||
| @AndroidEntryPoint | ||
| class MyPageFragment : BaseVisitorFragment<FragmentMyPageBinding>(R.layout.fragment_my_page) { | ||
| class MyPageFragment : Fragment() { | ||
| @Inject | ||
| lateinit var visitorModeManager: VisitorModeManager | ||
|
|
||
| private val viewModel: MyPageViewModel by activityViewModels() | ||
| private lateinit var resultEditNameLauncher: ActivityResultLauncher<Intent> | ||
|
|
||
| override val visitorContainer by lazy { binding.clVisitorMode } | ||
| override val contentViews by lazy { listOf(binding.constraintInside) } | ||
|
|
||
| override fun onContentModeInit() { | ||
| binding.lifecycleOwner = this@MyPageFragment.viewLifecycleOwner | ||
| viewModel.intent(MyPageIntent.LoadUserInfo) | ||
| addListener() | ||
| addObserver() | ||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
| setResultEditNameLauncher() | ||
| } | ||
|
|
||
| override fun onCreateView( | ||
| inflater: LayoutInflater, | ||
| container: ViewGroup?, | ||
| savedInstanceState: Bundle? | ||
| ): View { | ||
| return ComposeView(requireContext()).apply { | ||
| setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | ||
| setContent { | ||
| RunnectTheme { | ||
| if (visitorModeManager.isVisitorMode) { | ||
| VisitorModeScreen( | ||
| onSignUpClick = { navigateToLogin() } | ||
| ) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. collectAsState()ViewModel의 val state by viewModel.state.collectAsState()
|
||
| } else { | ||
| val state by viewModel.state.collectAsState() | ||
|
|
||
| val stampResId = if (!state.isLoading) { | ||
| getStampResourceId(state.stampId) | ||
| } else { | ||
| R.drawable.user_profile_basic | ||
| } | ||
|
|
||
| MyPageScreen( | ||
| state = state.copy(profileImgResId = stampResId), | ||
| onEditProfileClick = { navigateToEditName() }, | ||
| onHistoryClick = { | ||
| Analytics.logClickedItemEvent(EVENT_CLICK_RUNNING_RECORD) | ||
| navigateTo<MyHistoryActivity>() | ||
| }, | ||
| onRewardClick = { | ||
| Analytics.logClickedItemEvent(EVENT_CLICK_GOAL_REWARD) | ||
| navigateTo<MyRewardActivity>() | ||
| }, | ||
| onUploadClick = { | ||
| Analytics.logClickedItemEvent(EVENT_CLICK_UPLOADED_COURSE) | ||
| navigateTo<MyUploadActivity>() | ||
| }, | ||
| onSettingClick = { moveToSettingFragment() }, | ||
| onKakaoInquiryClick = { inquiryKakao() } | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
| if (!visitorModeManager.isVisitorMode) { | ||
| viewModel.intent(MyPageIntent.LoadUserInfo) | ||
| } | ||
| } | ||
|
|
||
| private fun setResultEditNameLauncher() { | ||
| resultEditNameLauncher = | ||
| registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> | ||
| if (result.resultCode == RESULT_OK) { | ||
| if (result.resultCode == Activity.RESULT_OK) { | ||
| val name = result.data?.getStringExtra(EXTRA_NICK_NAME) | ||
| ?: viewModel.currentState.nickname | ||
| viewModel.intent(MyPageIntent.UpdateNickname(name)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun addListener() { | ||
| with(binding) { | ||
| ivMyPageEditFrame.setOnClickListener { | ||
| val intent = Intent(requireContext(), MyPageEditNameActivity::class.java) | ||
| intent.putExtra(EXTRA_NICK_NAME, viewModel.currentState.nickname) | ||
| val stampResId = getStampResourceId() | ||
| intent.putExtra(EXTRA_PROFILE, stampResId) | ||
| resultEditNameLauncher.launch(intent) | ||
| } | ||
|
|
||
| viewMyPageMainRewardFrame.setOnClickListener { | ||
| Analytics.logClickedItemEvent(EVENT_CLICK_GOAL_REWARD) | ||
| navigateTo<MyRewardActivity>() | ||
| } | ||
| viewMyPageMainHistoryFrame.setOnClickListener { | ||
| Analytics.logClickedItemEvent(EVENT_CLICK_RUNNING_RECORD) | ||
| navigateTo<MyHistoryActivity>() | ||
| } | ||
|
|
||
| viewMyPageMainUploadFrame.setOnClickListener { | ||
| Analytics.logClickedItemEvent(EVENT_CLICK_UPLOADED_COURSE) | ||
| navigateTo<MyUploadActivity>() | ||
| } | ||
| viewMyPageMainSettingFrame.setOnClickListener { | ||
| moveToSettingFragment() | ||
| } | ||
| viewMyPageMainKakaoChannelInquiryFrame.setOnClickListener { | ||
| inquiryKakao() | ||
| } | ||
| } | ||
| private fun navigateToEditName() { | ||
| val intent = Intent(requireContext(), MyPageEditNameActivity::class.java) | ||
| intent.putExtra(EXTRA_NICK_NAME, viewModel.currentState.nickname) | ||
| val stampResId = getStampResourceId(viewModel.currentState.stampId) | ||
| intent.putExtra(EXTRA_PROFILE, stampResId) | ||
| resultEditNameLauncher.launch(intent) | ||
|
Comment on lines
105
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
file="$(fd 'MyPageEditNameActivity\.kt$' app/src/main/java | head -n1)"
if [[ -n "${file:-}" ]]; then
echo "== $file =="
sed -n '1,260p' "$file"
fi
echo
echo "== Result extras / profile update usages =="
rg -n -C3 'EXTRA_PROFILE|EXTRA_NICK_NAME|UpdateProfileImg|setResult\(|putExtra\(' app/src/main/javaRepository: Runnect/Runnect-Android Length of output: 42919 Profile image changes in The activity receives Evidence from MyPageEditNameActivity.kt// Lines 69-72: Result only includes nickname
setResult(
RESULT_OK,
Intent().putExtra(EXTRA_NICK_NAME, viewModel.nickName.value)
)🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 코드 유지 — MyPageEditNameActivity의 setResult에 닉네임만 포함하는 것은 이 PR 이전부터 동일한 pre-existing 동작. getUserInfo() API 재호출로 동기화되는 기존 구조. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
🧠 Learnings used |
||
| } | ||
|
|
||
| private fun moveToSettingFragment() { | ||
| val bundle = Bundle().apply { putString(ACCOUNT_INFO_TAG, viewModel.currentState.email) } | ||
| requireActivity().supportFragmentManager.commit { | ||
| this.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) | ||
| setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) | ||
| replace<MySettingFragment>(R.id.fl_main, args = bundle) | ||
| } | ||
| } | ||
|
|
||
| private fun addObserver() { | ||
| repeatOnStarted { | ||
| viewModel.state.collectLatest { state -> | ||
| bindState(state) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun bindState(state: MyPageUiState) { | ||
| setLoadingState(state.isLoading) | ||
|
|
||
| if (!state.isLoading && state.error == null) { | ||
| with(binding) { | ||
| tvMyPageUserName.text = state.nickname | ||
| tvMyPageUserLv.text = state.level | ||
| pbMyPageProgress.progress = state.levelPercent | ||
| tvMyPageProgressCurrent.text = state.levelPercent.toString() | ||
| ivMyPageProfile.load(state.profileImgResId) | ||
| } | ||
|
|
||
| val stampResId = getStampResourceId() | ||
| viewModel.intent(MyPageIntent.UpdateProfileImg(stampResId)) | ||
| } | ||
|
|
||
| state.error?.let { | ||
| context?.showSnackbar(anchorView = binding.root, message = it) | ||
| } | ||
| } | ||
|
|
||
| private fun inquiryKakao() { | ||
| val url = TalkApiClient.instance.channelChatUrl(BuildConfig.KAKAO_CHANNEL_ID) | ||
| KakaoCustomTabsClient.openWithDefault(requireActivity(), url) | ||
| } | ||
|
|
||
| private fun getStampResourceId(): Int { | ||
| private fun navigateToLogin() { | ||
| startActivity(Intent(requireContext(), LoginActivity::class.java)) | ||
| requireActivity().finish() | ||
| } | ||
|
|
||
| private fun getStampResourceId(stampId: String): Int { | ||
| return requireContext().getStampResId( | ||
| stampId = viewModel.currentState.stampId, | ||
| stampId = stampId, | ||
| resNameParam = RES_NAME, | ||
| resType = RES_STAMP_TYPE, | ||
| packageName = requireContext().packageName | ||
| ) | ||
| } | ||
|
|
||
| private fun setLoadingState(isLoading: Boolean) { | ||
| with(binding) { | ||
| indeterminateBar.isVisible = isLoading | ||
| ivMyPageEditFrame.isClickable = !isLoading | ||
| viewMyPageMainSettingFrame.isClickable = !isLoading | ||
| } | ||
| } | ||
|
|
||
| private inline fun <reified T : Activity> navigateTo() { | ||
| startActivity(Intent(requireContext(), T::class.java)) | ||
| requireActivity().overridePendingTransition( | ||
|
|
||
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.
ComposeView 호스트 패턴
기존 Fragment 구조(ViewPager)를 유지하면서 내부 UI만 Compose로 교체하는 방식입니다.
ComposeView: Android View 계층 안에 Compose UI를 삽입하는 브릿지setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed): Fragment의 View 생명주기에 맞춰 Compose를 자동 해제. ViewPager에서 Fragment가 재사용될 때 메모리 릭 방지setContent { }: 이 블록 안에서 Composable 함수를 호출하면 Compose UI가 렌더링됨ComposeView 공식 문서
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.
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed) 이게 컴포즈가 아닌 xml 기반이었다고 하면 어떤 개념과 유사한지, 이 코드를 추가했을 때 사이드 이펙트 등 문제가 생길 여지는 없는지.
코드 짤 때 누락하면 안 되는 필수적인 부분으로 보이는데 그렇다면 이걸 프리셋으로 설정해놓은 베이스나 유틸 코드를 만들어 보는 건 어떤지
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.
XML 대응 개념
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)는 XML 기반에서onDestroyView()에서 리소스 해제하는 것과 동일한 역할입니다.Fragment에서 View가 파괴될 때(
onDestroyView) Compose의 composition도 함께 해제하여:사이드 이펙트
없습니다. 오히려 빠뜨리면 메모리 릭이 생기는 필수 코드입니다.
베이스 유틸 제안에 대해
좋은 아이디어입니다. Fragment에서 ComposeView를 호스팅하는 패턴이 반복될 것이므로:
다만 플랜 원칙에 따라 5~6개 화면 전환 후 패턴이 확정되면 그때 추출하는 게 안전합니다. 지금은 1개 화면이라 섣부른 추상화 위험.
ViewCompositionStrategy 공식 문서