-
Notifications
You must be signed in to change notification settings - Fork 0
[Fix] MyPage UiState/UiEvent 리팩토링 및 UserNickname 관련 로컬 저장소 로직 변경 #382
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
a680f62
14f1191
a2d431c
8c778a9
2d74352
3e2eac6
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 |
|---|---|---|
|
|
@@ -13,15 +13,14 @@ import com.eatssu.android.domain.usecase.user.GetUserNickNameUseCase | |
| import com.eatssu.android.domain.usecase.user.SetUserCollegeDepartmentUseCase | ||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.SharedFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.asStateFlow | ||
| import kotlinx.coroutines.flow.catch | ||
| import kotlinx.coroutines.flow.collectLatest | ||
| import kotlinx.coroutines.flow.onStart | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.withContext | ||
| import timber.log.Timber | ||
| import java.time.LocalDate | ||
| import javax.inject.Inject | ||
|
|
@@ -56,35 +55,37 @@ class MainViewModel @Inject constructor( | |
| ) | ||
| } | ||
|
|
||
| fun fetchAndCheckNickname() { | ||
| private fun fetchAndCheckNickname() { | ||
| viewModelScope.launch { | ||
| getUserNickNameUseCase().onStart { | ||
| _uiState.value = UiState.Loading | ||
| }.catch { e -> | ||
| _uiState.value = UiState.Error | ||
| _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.not_found))) | ||
| Timber.e(e.toString()) | ||
| }.collectLatest { result -> | ||
| Timber.d(result.toString()) | ||
| result.result?.let { userInfo -> | ||
| val nickname = userInfo.nickname | ||
|
|
||
| if (nickname.isNullOrBlank() || nickname.startsWith("user-")) { | ||
| _uiState.value = UiState.Success(MainState.NicknameNull) | ||
| _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname))) | ||
| return@let | ||
| } | ||
|
|
||
| _uiState.value = UiState.Success(MainState.NicknameExists(nickname)) | ||
| _uiEvent.emit( | ||
| UiEvent.ShowToast( | ||
| String.format( | ||
| context.getString(R.string.hello_user), | ||
| nickname | ||
| ) | ||
| _uiState.value = UiState.Loading | ||
| runCatching { | ||
| withContext(Dispatchers.IO) { getUserNickNameUseCase() } | ||
|
Member
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. 바뀐 코드가 더 좋네요!! |
||
| }.onSuccess { nickname -> | ||
| // 1) 닉네임 없음/기본 프리셋 | ||
| if (nickname.isNullOrBlank() || nickname.startsWith("user-")) { | ||
| _uiState.value = UiState.Success(MainState.NicknameNull) | ||
| _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname))) | ||
| return@launch // ← 아래 분기 실행 막기 | ||
| } | ||
|
|
||
| // 2) 정상 닉네임 | ||
| _uiState.value = UiState.Success(MainState.NicknameExists(nickname)) | ||
| _uiEvent.emit( | ||
| UiEvent.ShowToast( | ||
| String.format( | ||
| context.getString(R.string.hello_user), | ||
| nickname | ||
| ) | ||
| ) | ||
| } | ||
| ) | ||
| }.onFailure { e -> | ||
| _uiState.value = UiState.Error | ||
| _uiEvent.emit( | ||
| UiEvent.ShowToast( | ||
| context.getString(R.string.not_found) | ||
| ) | ||
| ) | ||
| Timber.e(e) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,14 +4,14 @@ import android.content.Context | |
| import android.content.Intent | ||
| import android.content.pm.PackageManager | ||
| import android.graphics.Paint | ||
| import android.net.Uri | ||
| import android.os.Build | ||
| import android.os.Bundle | ||
| import android.provider.Settings | ||
| import android.view.LayoutInflater | ||
| import android.view.View | ||
| import androidx.appcompat.app.AlertDialog | ||
| import androidx.core.content.ContextCompat | ||
| import androidx.core.net.toUri | ||
| import androidx.fragment.app.activityViewModels | ||
| import androidx.fragment.app.viewModels | ||
| import androidx.lifecycle.Lifecycle | ||
|
|
@@ -20,15 +20,18 @@ import androidx.lifecycle.repeatOnLifecycle | |
| import com.eatssu.android.R | ||
| import com.eatssu.android.databinding.FragmentMyPageBinding | ||
| import com.eatssu.android.presentation.MainViewModel | ||
| import com.eatssu.android.presentation.UiEvent | ||
| import com.eatssu.android.presentation.UiState | ||
| import com.eatssu.android.presentation.base.BaseFragment | ||
| import com.eatssu.android.presentation.login.LoginActivity | ||
| import com.eatssu.android.presentation.mypage.myreview.MyReviewListActivity | ||
| import com.eatssu.android.presentation.mypage.terms.WebViewActivity | ||
| import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity | ||
| import com.eatssu.android.presentation.util.showToast | ||
| import com.eatssu.common.enums.ScreenId | ||
| import com.google.android.gms.oss.licenses.OssLicensesMenuActivity | ||
| import com.google.android.material.snackbar.Snackbar | ||
| import dagger.hilt.android.AndroidEntryPoint | ||
| import kotlinx.coroutines.flow.collectLatest | ||
| import kotlinx.coroutines.launch | ||
| import timber.log.Timber | ||
| import java.time.LocalDateTime | ||
|
|
@@ -46,46 +49,85 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN) | |
|
|
||
| override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
|
|
||
| binding.tvSignout.paintFlags = Paint.UNDERLINE_TEXT_FLAG | ||
| setupObservers() | ||
| setOnClickListener() | ||
| } | ||
|
|
||
| override fun onResume() { | ||
| super.onResume() | ||
| myPageViewModel.fetchMyInfo() // 닉네임 변경 등으로부터 복귀 시 정보 갱신 | ||
| } | ||
|
|
||
|
Comment on lines
+57
to
+61
Member
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. 결과적으로 닉네임 수정 후 화면에 바로 반영 안되는 오류는 onresume에서 호출하는 방식으로 해결하신것 같아요 onResume에서 api로 닉네임을 계속 호출하고 있잖아요 혹시 이 부분에 대해서는 어떻게 생각하시나요??
Member
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. 헉 사실 이슈를 수정하려고 한건 아닌데 어쩌다보니 함께 수정이 되었습니다.. 😮 유리님께서 말씀하신 방법으로 동작합니다! 닉네임 수정하면 setNicknameUsecase에서 api호출이랑 sharedPreference set 함수 둘다 호출되고
Member
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. 아하 그렇군요!! 수고하셨씁니당~! |
||
| private fun setupObservers() { | ||
| viewLifecycleOwner.lifecycleScope.launch { | ||
| repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| myPageViewModel.uiState.collect { | ||
| binding.tvAppVersion.text = it.appVersion | ||
| launch { | ||
| myPageViewModel.uiState.collectLatest { ui -> | ||
| when (ui) { | ||
| is UiState.Init, UiState.Loading -> Unit // 닉네임만 불러옴으로 로딩 인디케이터 없음 | ||
| is UiState.Success -> { | ||
| ui.data?.let { render(it) } | ||
| } | ||
|
|
||
| if (it.nickname.isNotEmpty()) { | ||
| binding.tvNickname.text = it.nickname | ||
| is UiState.Error -> { | ||
| showToast(getString(R.string.not_found)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| binding.alarmSwitch.setOnCheckedChangeListener(null) | ||
| binding.alarmSwitch.isChecked = it.isAlarmOn | ||
| binding.alarmSwitch.setOnCheckedChangeListener { _, isChecked -> | ||
| handleAlarmSwitchChange(isChecked) | ||
| } | ||
| launch { | ||
| myPageViewModel.uiEvent.collectLatest { event -> | ||
| when (event) { | ||
| is UiEvent.ShowToast -> showToast(event.message) | ||
| else -> Unit | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun render(state: MyPageState) { | ||
| // 앱 버전 | ||
| binding.tvAppVersion.text = state.appVersion | ||
|
|
||
| // 닉네임 | ||
| if (state.hasNickname) { | ||
| binding.tvNickname.text = state.nickname | ||
| } else { | ||
| // 필요 시 미설정 안내 문구 | ||
| binding.tvNickname.text = "닉네임을 설정해주세요" | ||
|
Contributor
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. |
||
| } | ||
|
|
||
| // 알람 스위치 (리스너 잠시 해제 후 값 반영) | ||
| binding.alarmSwitch.setOnCheckedChangeListener(null) | ||
| binding.alarmSwitch.isChecked = state.isAlarmOn | ||
| binding.alarmSwitch.setOnCheckedChangeListener { _, isChecked -> | ||
| handleAlarmSwitchChange(isChecked) | ||
| } | ||
| } | ||
|
|
||
| private fun handleAlarmSwitchChange(isChecked: Boolean) { | ||
| val nowDatetime = LocalDateTime.now() | ||
| val formattedDate = nowDatetime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) | ||
|
|
||
| if (isChecked) { | ||
| if (checkNotificationPermission(requireContext())) { | ||
| myPageViewModel.setNotificationOn() | ||
| showSnackbar("EAT-SSU 알림 수신을 동의하였습니다.\n$formattedDate") | ||
| showToast("EAT-SSU 알림 수신을 동의하였습니다.\n$formattedDate") | ||
| } else { | ||
| showNotificationPermissionDialog() | ||
| // 권한 미허용이면 스위치 원복 | ||
| binding.alarmSwitch.setOnCheckedChangeListener(null) | ||
| binding.alarmSwitch.isChecked = false | ||
| binding.alarmSwitch.setOnCheckedChangeListener { _, checked -> | ||
| handleAlarmSwitchChange(checked) | ||
| } | ||
|
Comment on lines
+121
to
+126
Contributor
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. |
||
| } | ||
| } else { | ||
| myPageViewModel.setNotificationOff() | ||
| showSnackbar("EAT-SSU 알림 수신을 거부하였습니다.\n$formattedDate") | ||
| showToast("EAT-SSU 알림 수신을 거부하였습니다.\n$formattedDate") | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -111,8 +153,10 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN) | |
| } | ||
|
|
||
| binding.llSignout.setOnClickListener { | ||
| // 현재 Success 상태에서 안전하게 닉네임 추출 | ||
| val nickname = (myPageViewModel.uiState.value as? UiState.Success)?.data?.nickname | ||
| Intent(requireContext(), SignOutActivity::class.java).apply { | ||
| putExtra("nickname", myPageViewModel.uiState.value.nickname) | ||
| putExtra("nickname", nickname) | ||
| startActivity(this) | ||
| } | ||
| } | ||
|
|
@@ -121,13 +165,9 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN) | |
| startActivity(Intent(requireContext(), DeveloperActivity::class.java)) | ||
| } | ||
|
|
||
| binding.llOss.setOnClickListener { | ||
| moveToOss() | ||
| } | ||
| binding.llOss.setOnClickListener { moveToOss() } | ||
|
|
||
| binding.llAppVersion.setOnClickListener { | ||
| moveToPlayStore() | ||
| } | ||
| binding.llAppVersion.setOnClickListener { moveToPlayStore() } | ||
|
|
||
| binding.llServiceRule.setOnClickListener { | ||
| startWebView( | ||
|
|
@@ -182,16 +222,15 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN) | |
| try { | ||
| startActivity(Intent(requireContext(), OssLicensesMenuActivity::class.java)) | ||
| } catch (e: Exception) { | ||
| showSnackbar("오픈소스 라이브러리를 불러올 수 없습니다.") | ||
| showToast("오픈소스 라이브러리를 불러올 수 없습니다.") | ||
| Timber.e("Error opening OSS Licenses: ${e.message}") | ||
| } | ||
| } | ||
|
|
||
| private fun moveToPlayStore() { | ||
| val appPackageName = requireContext().packageName | ||
| val uri = Uri.parse("market://details?id=$appPackageName") | ||
| val fallbackUri = Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName") | ||
|
|
||
| val uri = "market://details?id=$appPackageName".toUri() | ||
| val fallbackUri = "https://play.google.com/store/apps/details?id=$appPackageName".toUri() | ||
| try { | ||
| startActivity(Intent(Intent.ACTION_VIEW, uri)) | ||
| } catch (e: Exception) { | ||
|
|
@@ -206,10 +245,6 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN) | |
| context.startActivity(intent) | ||
| } | ||
|
|
||
| private fun showSnackbar(message: String) { | ||
| Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show() | ||
| } | ||
|
|
||
| private fun startWebView(url: String, title: String, screenId: ScreenId) { | ||
| val intent = Intent(requireContext(), WebViewActivity::class.java).apply { | ||
| putExtra("URL", url) | ||
|
|
||
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.
오홋 앞으로 단순 api 호출 작업에서는 flow를 덜어내는 방향으로 진행하실 예정일까요?
저번에 제휴지도 부분에서 리뷰 코멘트로 단순 api호출 결과를 flow로 계속 방출하는 게 잘 이해가 안된다고 답변을 남겼던거같은데
그 후로 팀내에서 정확히 정한 부분은 없는 것 같아서요~!
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.
유리님 말씀이 맞는 것 같아 viewmodel -> ui 말고는 suspend로 수정하려고 합니다! 이부분을 이야기하지 않은 것을 까먹었네요 😅 좋은 인사이트 감사합니다