-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] PlaceDetailPreviewFragment, PlaceDetailSecondPrevewFragment Compose 마이그레이션 #19
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
0ddde44
044a68a
1043f84
38b5735
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 |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package com.daedan.festabook.presentation.common.component | ||
|
|
||
| import android.util.Patterns | ||
| import androidx.compose.foundation.gestures.detectTapGestures | ||
| import androidx.compose.foundation.text.InlineTextContent | ||
| import androidx.compose.material3.LocalTextStyle | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.graphics.Color | ||
| import androidx.compose.ui.input.pointer.pointerInput | ||
| import androidx.compose.ui.platform.LocalUriHandler | ||
| import androidx.compose.ui.text.SpanStyle | ||
| import androidx.compose.ui.text.TextLayoutResult | ||
| import androidx.compose.ui.text.TextStyle | ||
| import androidx.compose.ui.text.buildAnnotatedString | ||
| import androidx.compose.ui.text.font.FontFamily | ||
| import androidx.compose.ui.text.font.FontStyle | ||
| import androidx.compose.ui.text.font.FontWeight | ||
| import androidx.compose.ui.text.style.TextAlign | ||
| import androidx.compose.ui.text.style.TextDecoration | ||
| import androidx.compose.ui.text.style.TextOverflow | ||
| import androidx.compose.ui.unit.TextUnit | ||
| import com.daedan.festabook.presentation.theme.FestabookColor | ||
|
|
||
| @Composable | ||
| fun URLText( | ||
| text: String, | ||
| modifier: Modifier = Modifier, | ||
| color: Color = Color.Unspecified, | ||
| fontSize: TextUnit = TextUnit.Unspecified, | ||
| fontStyle: FontStyle? = null, | ||
| fontWeight: FontWeight? = null, | ||
| fontFamily: FontFamily? = null, | ||
| letterSpacing: TextUnit = TextUnit.Unspecified, | ||
| textDecoration: TextDecoration? = null, | ||
| textAlign: TextAlign? = null, | ||
| lineHeight: TextUnit = TextUnit.Unspecified, | ||
| overflow: TextOverflow = TextOverflow.Clip, | ||
| softWrap: Boolean = true, | ||
| maxLines: Int = Int.MAX_VALUE, | ||
| minLines: Int = 1, | ||
| inlineContent: Map<String, InlineTextContent> = mapOf(), | ||
| onTextLayout: (TextLayoutResult) -> Unit = {}, | ||
| style: TextStyle = LocalTextStyle.current, | ||
| ) { | ||
| val uriHandler = LocalUriHandler.current | ||
| var layoutResult by remember { mutableStateOf<TextLayoutResult?>(null) } | ||
| val linkedText = | ||
| buildAnnotatedString { | ||
| append(text) | ||
| val urlPattern = Patterns.WEB_URL | ||
| val matcher = urlPattern.matcher(text) | ||
| while (matcher.find()) { | ||
| addStyle( | ||
| style = | ||
| SpanStyle( | ||
| color = FestabookColor.gray500, | ||
| textDecoration = TextDecoration.Underline, | ||
| ), | ||
| start = matcher.start(), | ||
| end = matcher.end(), | ||
| ) | ||
| addStringAnnotation( | ||
| tag = "URL", | ||
| annotation = matcher.group(), | ||
| start = matcher.start(), | ||
| end = matcher.end(), | ||
| ) | ||
| } | ||
| } | ||
| Text( | ||
| text = linkedText, | ||
| modifier = | ||
| modifier.pointerInput(Unit) { | ||
| detectTapGestures { | ||
| layoutResult?.let { result -> | ||
| val position = result.getOffsetForPosition(it) | ||
| linkedText | ||
| .getStringAnnotations("URL", position, position) | ||
| .firstOrNull() | ||
| ?.let { annotation -> | ||
| uriHandler.openUri(annotation.item) | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| color = color, | ||
| fontSize = fontSize, | ||
| fontStyle = fontStyle, | ||
| fontWeight = fontWeight, | ||
| fontFamily = fontFamily, | ||
| letterSpacing = letterSpacing, | ||
| textDecoration = textDecoration, | ||
| textAlign = textAlign, | ||
| lineHeight = lineHeight, | ||
| overflow = overflow, | ||
| softWrap = softWrap, | ||
| maxLines = maxLines, | ||
| minLines = minLines, | ||
| inlineContent = inlineContent, | ||
| onTextLayout = { | ||
| layoutResult = it | ||
| onTextLayout(it) | ||
| }, | ||
| style = style, | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,38 @@ | ||
| package com.daedan.festabook.presentation.placeMap.placeDetailPreview | ||
|
|
||
| import android.os.Bundle | ||
| import android.view.LayoutInflater | ||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import androidx.activity.OnBackPressedCallback | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.runtime.LaunchedEffect | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.platform.ComposeView | ||
| import androidx.compose.ui.platform.ViewCompositionStrategy | ||
| import androidx.fragment.app.Fragment | ||
| import androidx.fragment.app.viewModels | ||
| import androidx.lifecycle.ViewModelProvider | ||
| import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
| import com.daedan.festabook.R | ||
| import com.daedan.festabook.databinding.FragmentPlaceDetailPreviewBinding | ||
| import com.daedan.festabook.di.fragment.FragmentKey | ||
| import com.daedan.festabook.logging.logger | ||
| import com.daedan.festabook.presentation.common.BaseFragment | ||
| import com.daedan.festabook.presentation.common.OnMenuItemReClickListener | ||
| import com.daedan.festabook.presentation.common.loadImage | ||
| import com.daedan.festabook.presentation.common.setFormatDate | ||
| import com.daedan.festabook.presentation.common.showBottomAnimation | ||
| import com.daedan.festabook.presentation.common.showErrorSnackBar | ||
| import com.daedan.festabook.presentation.placeDetail.PlaceDetailActivity | ||
| import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel | ||
| import com.daedan.festabook.presentation.placeMap.PlaceMapViewModel | ||
| import com.daedan.festabook.presentation.placeMap.logging.PlacePreviewClick | ||
| import com.daedan.festabook.presentation.placeMap.model.SelectedPlaceUiState | ||
| import com.daedan.festabook.presentation.placeMap.placeDetailPreview.component.PlaceDetailPreviewScreen | ||
| import com.daedan.festabook.presentation.theme.FestabookTheme | ||
| import com.daedan.festabook.presentation.theme.festabookSpacing | ||
| import dev.zacsweers.metro.AppScope | ||
| import dev.zacsweers.metro.ContributesIntoMap | ||
| import dev.zacsweers.metro.Inject | ||
|
|
@@ -42,13 +54,70 @@ class PlaceDetailPreviewFragment( | |
| } | ||
| } | ||
|
|
||
| override fun onCreateView( | ||
| inflater: LayoutInflater, | ||
| container: ViewGroup?, | ||
| savedInstanceState: Bundle?, | ||
| ): View { | ||
| return ComposeView(requireContext()).apply { | ||
| super.onCreateView(inflater, container, savedInstanceState) | ||
| setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | ||
| setContent { | ||
| FestabookTheme { | ||
| val placeDetailUiState by viewModel.selectedPlaceFlow.collectAsStateWithLifecycle() | ||
| val visible = placeDetailUiState is SelectedPlaceUiState.Success | ||
|
|
||
| LaunchedEffect(placeDetailUiState) { | ||
| backPressedCallback.isEnabled = true | ||
| } | ||
|
|
||
| Box( | ||
| modifier = Modifier.fillMaxSize(), | ||
| contentAlignment = Alignment.BottomCenter, | ||
| ) { | ||
| PlaceDetailPreviewScreen( | ||
| placeUiState = placeDetailUiState, | ||
| visible = visible, | ||
| modifier = | ||
| Modifier | ||
| .padding( | ||
| vertical = festabookSpacing.paddingBody4, | ||
| horizontal = festabookSpacing.paddingScreenGutter, | ||
| ), | ||
| onClick = { selectedPlace -> | ||
| if (selectedPlace !is SelectedPlaceUiState.Success) return@PlaceDetailPreviewScreen | ||
| startPlaceDetailActivity(selectedPlace.value) | ||
| binding.logger.log( | ||
| PlacePreviewClick( | ||
| baseLogData = binding.logger.getBaseLogData(), | ||
| placeName = | ||
| selectedPlace.value.place.title | ||
| ?: "undefined", | ||
| timeTag = | ||
| viewModel.selectedTimeTag.value?.name | ||
| ?: "undefined", | ||
| category = selectedPlace.value.place.category.name, | ||
| ), | ||
| ) | ||
| }, | ||
| onError = { selectedPlace -> | ||
| showErrorSnackBar(selectedPlace.throwable) | ||
|
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. 제 마이그레이션 코드에도 error 상태 처리가 아직 안되어 있는데 나중에 공통 컴포저블 ErrorSnackBar를 만들어서 같이 사용하면 좋을 것 같네요~
Contributor
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. 일단 점진적 마이그레이션이기 때문에 기존의 에러 상태 처리 로직을 구현했습니다 ! |
||
| }, | ||
| onEmpty = { | ||
| backPressedCallback.isEnabled = false | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun onViewCreated( | ||
| view: View, | ||
| savedInstanceState: Bundle?, | ||
| ) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
| setUpObserver() | ||
| setupBinding() | ||
| setUpBackPressedCallback() | ||
| } | ||
|
|
||
|
|
@@ -63,63 +132,6 @@ class PlaceDetailPreviewFragment( | |
| ) | ||
| } | ||
|
|
||
| private fun setupBinding() { | ||
| binding.layoutSelectedPlace.setOnClickListener { | ||
| val selectedPlaceState = viewModel.selectedPlace.value | ||
| if (selectedPlaceState is SelectedPlaceUiState.Success) { | ||
| startPlaceDetailActivity(selectedPlaceState.value) | ||
| binding.logger.log( | ||
| PlacePreviewClick( | ||
| baseLogData = binding.logger.getBaseLogData(), | ||
| placeName = selectedPlaceState.value.place.title ?: "undefined", | ||
| timeTag = viewModel.selectedTimeTag.value?.name ?: "undefined", | ||
| category = selectedPlaceState.value.place.category.name, | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun setUpObserver() { | ||
| viewModel.selectedPlace.observe(viewLifecycleOwner) { selectedPlace -> | ||
| backPressedCallback.isEnabled = true | ||
| binding.layoutSelectedPlace.visibility = | ||
| if (selectedPlace == SelectedPlaceUiState.Empty) View.GONE else View.VISIBLE | ||
|
|
||
| when (selectedPlace) { | ||
| is SelectedPlaceUiState.Loading -> Unit | ||
| is SelectedPlaceUiState.Success -> { | ||
| binding.layoutSelectedPlace.showBottomAnimation() | ||
| updateSelectedPlaceUi(selectedPlace.value) | ||
| } | ||
|
|
||
| is SelectedPlaceUiState.Error -> showErrorSnackBar(selectedPlace.throwable) | ||
| is SelectedPlaceUiState.Empty -> backPressedCallback.isEnabled = false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun updateSelectedPlaceUi(selectedPlace: PlaceDetailUiModel) { | ||
| with(binding) { | ||
| layoutSelectedPlace.visibility = View.VISIBLE | ||
| tvSelectedPlaceTitle.text = | ||
| selectedPlace.place.title ?: getString(R.string.place_list_default_title) | ||
| tvSelectedPlaceLocation.text = | ||
| selectedPlace.place.location ?: getString(R.string.place_list_default_location) | ||
| setFormatDate( | ||
| binding.tvSelectedPlaceTime, | ||
| selectedPlace.startTime, | ||
| selectedPlace.endTime, | ||
| ) | ||
| tvSelectedPlaceHost.text = | ||
| selectedPlace.host ?: getString(R.string.place_detail_default_host) | ||
| tvSelectedPlaceDescription.text = selectedPlace.place.description | ||
| ?: getString(R.string.place_list_default_description) | ||
| cvPlaceCategory.setCategory(selectedPlace.place.category) | ||
| ivSelectedPlaceImage.loadImage(selectedPlace.featuredImage) | ||
| } | ||
| } | ||
|
|
||
| private fun startPlaceDetailActivity(placeDetail: PlaceDetailUiModel) { | ||
| startActivity(PlaceDetailActivity.newIntent(requireContext(), placeDetail)) | ||
| } | ||
|
|
||
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.
이 부분 같은 경우는 backPressedCallback이 액티비티에 종속적이기 떄문에
컴포저블에서 수행하면 람다를 넘겨줘서 처리해줘야 합니다.
저는 이 부분이 불필요한 람다라고 생각이 되어서 프래그먼트에서 처리해 주었는데요.
다시 찾아보니까 BackHandler 라는것이 있어 더 간편하게 구현할 수 있더라구요!
반영했습니다 !