-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 홈화면 Compose 마이그레이션 #22
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 6 commits
60c862a
3af132e
bb90581
19e9842
c73796e
9ef8418
920ea5c
0ce44cf
e0c981e
a9d84b4
f849f6f
1e98ebb
9427a8f
c8325fe
dba3acb
db92686
aec51d3
ff3745a
14522f3
88cbab0
85477d6
69775f6
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,184 +1,49 @@ | ||
| package com.daedan.festabook.presentation.home | ||
|
|
||
| import android.os.Bundle | ||
| import android.view.LayoutInflater | ||
| import android.view.View | ||
| import androidx.fragment.app.Fragment | ||
| import android.view.ViewGroup | ||
| import androidx.compose.ui.platform.ComposeView | ||
| import androidx.compose.ui.platform.ViewCompositionStrategy | ||
| import androidx.fragment.app.viewModels | ||
| import androidx.lifecycle.ViewModelProvider | ||
| import androidx.recyclerview.widget.LinearLayoutManager | ||
| import androidx.recyclerview.widget.PagerSnapHelper | ||
| import androidx.recyclerview.widget.RecyclerView | ||
| import com.daedan.festabook.R | ||
| import com.daedan.festabook.databinding.FragmentHomeBinding | ||
| import com.daedan.festabook.di.fragment.FragmentKey | ||
| import com.daedan.festabook.logging.logger | ||
| import com.daedan.festabook.logging.model.home.ExploreClickLogData | ||
| import com.daedan.festabook.logging.model.home.HomeViewLogData | ||
| import com.daedan.festabook.logging.model.home.ScheduleClickLogData | ||
| import com.daedan.festabook.presentation.common.BaseFragment | ||
| import com.daedan.festabook.presentation.common.formatFestivalPeriod | ||
| import com.daedan.festabook.presentation.common.showErrorSnackBar | ||
| import com.daedan.festabook.presentation.explore.ExploreActivity | ||
| import com.daedan.festabook.presentation.home.adapter.CenterItemMotionEnlarger | ||
| import com.daedan.festabook.presentation.home.adapter.FestivalUiState | ||
| import com.daedan.festabook.presentation.home.adapter.LineUpItemOfDayAdapter | ||
| import com.daedan.festabook.presentation.home.adapter.PosterAdapter | ||
| import com.daedan.festabook.presentation.home.component.HomeScreen | ||
| import dev.zacsweers.metro.AppScope | ||
| import dev.zacsweers.metro.ContributesIntoMap | ||
| import dev.zacsweers.metro.Inject | ||
| import dev.zacsweers.metro.binding | ||
| import timber.log.Timber | ||
|
|
||
| @ContributesIntoMap(scope = AppScope::class, binding = binding<Fragment>()) | ||
| @ContributesIntoMap(scope = AppScope::class, binding = binding<androidx.fragment.app.Fragment>()) | ||
| @FragmentKey(HomeFragment::class) | ||
| class HomeFragment @Inject constructor( | ||
| private val centerItemMotionEnlarger: RecyclerView.OnScrollListener, | ||
| ) : BaseFragment<FragmentHomeBinding>() { | ||
| class HomeFragment @Inject constructor() : BaseFragment<FragmentHomeBinding>() { | ||
| override val layoutId: Int = R.layout.fragment_home | ||
|
|
||
| @Inject | ||
| override lateinit var defaultViewModelProviderFactory: ViewModelProvider.Factory | ||
| private val viewModel: HomeViewModel by viewModels({ requireActivity() }) | ||
|
|
||
| private val posterAdapter: PosterAdapter by lazy { | ||
| PosterAdapter() | ||
| } | ||
|
|
||
| private val lineupOfDayAdapter: LineUpItemOfDayAdapter by lazy { | ||
| LineUpItemOfDayAdapter() | ||
| } | ||
|
|
||
| override fun onViewCreated( | ||
| view: View, | ||
| override fun onCreateView( | ||
| inflater: LayoutInflater, | ||
| container: ViewGroup?, | ||
| savedInstanceState: Bundle?, | ||
| ) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
| binding.lifecycleOwner = viewLifecycleOwner | ||
| setupObservers() | ||
| setupAdapters() | ||
| setupNavigateToScheduleButton() | ||
| setupNavigateToExploreButton() | ||
| } | ||
|
|
||
| private fun setupNavigateToExploreButton() { | ||
| binding.layoutTitleWithIcon.setOnClickListener { | ||
| binding.logger.log(ExploreClickLogData(binding.logger.getBaseLogData())) | ||
|
|
||
| startActivity(ExploreActivity.newIntent(requireContext())) | ||
| } | ||
| } | ||
|
|
||
| private fun setupNavigateToScheduleButton() { | ||
| binding.btnNavigateToSchedule.setOnClickListener { | ||
| binding.logger.log( | ||
| ScheduleClickLogData( | ||
| baseLogData = binding.logger.getBaseLogData(), | ||
| ), | ||
| ) | ||
|
|
||
| viewModel.navigateToScheduleClick() | ||
| } | ||
| } | ||
|
|
||
| private fun setupObservers() { | ||
| viewModel.festivalUiState.observe(viewLifecycleOwner) { festivalUiState -> | ||
| when (festivalUiState) { | ||
| is FestivalUiState.Loading -> {} | ||
| is FestivalUiState.Success -> handleSuccessState(festivalUiState) | ||
| is FestivalUiState.Error -> { | ||
| showErrorSnackBar(festivalUiState.throwable) | ||
| Timber.w( | ||
| festivalUiState.throwable, | ||
| "HomeFragment: ${festivalUiState.throwable.message}", | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| viewModel.lineupUiState.observe(viewLifecycleOwner) { lineupUiState -> | ||
| when (lineupUiState) { | ||
| is LineupUiState.Loading -> {} | ||
| is LineupUiState.Success -> { | ||
| lineupOfDayAdapter.submitList(lineupUiState.lineups.getLineupItems()) | ||
| } | ||
|
|
||
| is LineupUiState.Error -> { | ||
| showErrorSnackBar(lineupUiState.throwable) | ||
| Timber.w( | ||
| lineupUiState.throwable, | ||
| "HomeFragment: ${lineupUiState.throwable.message}", | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun setupAdapters() { | ||
| binding.rvHomePoster.adapter = posterAdapter | ||
| binding.rvHomeLineup.adapter = lineupOfDayAdapter | ||
| attachSnapHelper() | ||
| addScrollEffectListener() | ||
| } | ||
|
|
||
| private fun handleSuccessState(festivalUiState: FestivalUiState.Success) { | ||
| binding.tvHomeOrganizationTitle.text = | ||
| festivalUiState.organization.universityName | ||
| binding.tvHomeFestivalTitle.text = | ||
| festivalUiState.organization.festival.festivalName | ||
| binding.tvHomeFestivalDate.text = | ||
| formatFestivalPeriod( | ||
| festivalUiState.organization.festival.startDate, | ||
| festivalUiState.organization.festival.endDate, | ||
| ) | ||
|
|
||
| val posterUrls = | ||
| festivalUiState.organization.festival.festivalImages | ||
| .sortedBy { it.sequence } | ||
| .map { it.imageUrl } | ||
|
|
||
| if (posterUrls.isNotEmpty()) { | ||
| posterAdapter.submitList(posterUrls) { | ||
| scrollToInitialPosition(posterUrls.size) | ||
| ): View { | ||
| return ComposeView(requireContext()).apply { | ||
| setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | ||
| setContent { | ||
| HomeScreen( | ||
| viewModel = viewModel, | ||
| onNavigateToExplore = { | ||
| startActivity(ExploreActivity.newIntent(requireContext())) | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
| binding.logger.log( | ||
| HomeViewLogData( | ||
| baseLogData = binding.logger.getBaseLogData(), | ||
| universityName = festivalUiState.organization.universityName, | ||
| festivalId = festivalUiState.organization.id, | ||
| ), | ||
| ) | ||
| } | ||
|
|
||
| private fun attachSnapHelper() { | ||
| PagerSnapHelper().attachToRecyclerView(binding.rvHomePoster) | ||
| } | ||
|
|
||
| private fun scrollToInitialPosition(size: Int) { | ||
| val safeMaxValue = Int.MAX_VALUE / INFINITE_SCROLL_SAFETY_FACTOR | ||
| val initialPosition = safeMaxValue - (safeMaxValue % size) | ||
|
|
||
| val layoutManager = binding.rvHomePoster.layoutManager as? LinearLayoutManager ?: return | ||
|
|
||
| val itemWidth = resources.getDimensionPixelSize(R.dimen.poster_item_width) | ||
| val offset = (binding.rvHomePoster.width / 2) - (itemWidth / 2) | ||
|
|
||
| layoutManager.scrollToPositionWithOffset(initialPosition, offset) | ||
|
|
||
| binding.rvHomePoster.post { | ||
| (centerItemMotionEnlarger as CenterItemMotionEnlarger).expandCenterItem(binding.rvHomePoster) | ||
| } | ||
| } | ||
|
|
||
| private fun addScrollEffectListener() { | ||
| binding.rvHomePoster.addOnScrollListener(centerItemMotionEnlarger) | ||
| } | ||
|
|
||
| override fun onDestroyView() { | ||
| binding.rvHomePoster.clearOnScrollListeners() | ||
| super.onDestroyView() | ||
| } | ||
|
|
||
| companion object { | ||
| private const val INFINITE_SCROLL_SAFETY_FACTOR = 4 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,32 +1,35 @@ | ||
| package com.daedan.festabook.presentation.home | ||
|
|
||
| import androidx.lifecycle.LiveData | ||
| import androidx.lifecycle.MutableLiveData | ||
| import androidx.lifecycle.ViewModel | ||
| import androidx.lifecycle.viewModelScope | ||
| import com.daedan.festabook.di.viewmodel.ViewModelKey | ||
| import com.daedan.festabook.di.viewmodel.ViewModelScope | ||
| import com.daedan.festabook.domain.repository.FestivalRepository | ||
| import com.daedan.festabook.presentation.common.SingleLiveData | ||
| import com.daedan.festabook.presentation.home.adapter.FestivalUiState | ||
| import dev.zacsweers.metro.AppScope | ||
| import dev.zacsweers.metro.ContributesIntoMap | ||
| import dev.zacsweers.metro.Inject | ||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.SharedFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.asSharedFlow | ||
| import kotlinx.coroutines.flow.asStateFlow | ||
| import kotlinx.coroutines.launch | ||
|
|
||
| @ContributesIntoMap(AppScope::class) | ||
| @ViewModelKey(HomeViewModel::class) | ||
| class HomeViewModel @Inject constructor( | ||
| private val festivalRepository: FestivalRepository, | ||
|
||
| ) : ViewModel() { | ||
| private val _festivalUiState = MutableLiveData<FestivalUiState>() | ||
| val festivalUiState: LiveData<FestivalUiState> get() = _festivalUiState | ||
| private val _festivalUiState = MutableStateFlow<FestivalUiState>(FestivalUiState.Loading) | ||
| val festivalUiState: StateFlow<FestivalUiState> = _festivalUiState.asStateFlow() | ||
|
|
||
| private val _lineupUiState = MutableLiveData<LineupUiState>() | ||
| val lineupUiState: LiveData<LineupUiState> get() = _lineupUiState | ||
| private val _lineupUiState = MutableStateFlow<LineupUiState>(LineupUiState.Loading) | ||
| val lineupUiState: StateFlow<LineupUiState> = _lineupUiState.asStateFlow() | ||
|
|
||
| private val _navigateToScheduleEvent: SingleLiveData<Unit> = SingleLiveData() | ||
| val navigateToScheduleEvent: LiveData<Unit> get() = _navigateToScheduleEvent | ||
| private val _navigateToScheduleEvent = | ||
| MutableSharedFlow<Unit>(replay = 0, extraBufferCapacity = 1) | ||
| val navigateToScheduleEvent: SharedFlow<Unit> = _navigateToScheduleEvent.asSharedFlow() | ||
|
|
||
| init { | ||
| loadFestival() | ||
|
|
@@ -48,7 +51,7 @@ class HomeViewModel @Inject constructor( | |
| } | ||
|
|
||
| fun navigateToScheduleClick() { | ||
| _navigateToScheduleEvent.setValue(Unit) | ||
| _navigateToScheduleEvent.tryEmit(Unit) | ||
| } | ||
|
|
||
| private fun loadLineup() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package com.daedan.festabook.presentation.home.component | ||
|
|
||
| import androidx.compose.foundation.border | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.Spacer | ||
| import androidx.compose.foundation.layout.aspectRatio | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.height | ||
| import androidx.compose.foundation.layout.width | ||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.draw.clip | ||
| import androidx.compose.ui.text.style.TextOverflow | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
| import com.daedan.festabook.presentation.common.component.CoilImage | ||
| import com.daedan.festabook.presentation.theme.FestabookColor | ||
| import com.daedan.festabook.presentation.theme.FestabookTypography | ||
|
|
||
| @Composable | ||
| fun HomeArtistItem( | ||
| artistName: String, | ||
| artistImageUrl: String, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| Column( | ||
| modifier = modifier.width(68.dp), | ||
| ) { | ||
| CoilImage( | ||
| url = artistImageUrl, | ||
| contentDescription = null, | ||
| modifier = | ||
| Modifier | ||
| .fillMaxWidth() | ||
| .aspectRatio(1f) | ||
| .clip(HomeArtistItem.ArtistImage) | ||
| .border(1.dp, FestabookColor.gray300, HomeArtistItem.ArtistImage), | ||
| ) | ||
|
|
||
| Spacer(modifier = Modifier.height(4.dp)) | ||
|
|
||
| Text( | ||
| text = artistName, | ||
| style = FestabookTypography.labelLarge, | ||
| color = FestabookColor.gray700, | ||
| maxLines = 1, | ||
| overflow = TextOverflow.Ellipsis, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| object HomeArtistItem { | ||
| val ArtistImage = RoundedCornerShape( | ||
| topStartPercent = 50, | ||
| topEndPercent = 50, | ||
|
||
| bottomEndPercent = 50, | ||
| bottomStartPercent = 5, | ||
| ) | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| private fun HomeArtistItemPreview() { | ||
| HomeArtistItem( | ||
| artistName = "실리카겔", | ||
|
Comment on lines
+66
to
+68
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. 이건 TMI일 수도 있는데, Theme으로 감싸주지 않고 Preview를 실행하는건 실제 사용할 때와 다르게 동작할 수 있어 주의해야 하는 것으로 알고있어요 ! |
||
| artistImageUrl = "sample", | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package com.daedan.festabook.presentation.home.component | ||
|
|
||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.Spacer | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.height | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.material3.HorizontalDivider | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.text.style.TextOverflow | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
| import com.daedan.festabook.presentation.theme.FestabookColor | ||
| import com.daedan.festabook.presentation.theme.FestabookTypography | ||
|
|
||
| @Composable | ||
| fun HomeFestivalInfo( | ||
| festivalName: String, | ||
| festivalDate: String, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| Column( | ||
| modifier = modifier.fillMaxWidth(), | ||
| ) { | ||
| Text( | ||
| text = festivalName, | ||
| style = FestabookTypography.displayMedium, | ||
| color = FestabookColor.black, | ||
| modifier = Modifier.padding(horizontal = 20.dp), | ||
| ) | ||
|
|
||
| Spacer(modifier = Modifier.height(8.dp)) | ||
|
|
||
| Text( | ||
| text = festivalDate, | ||
| style = FestabookTypography.bodyLarge, | ||
| color = FestabookColor.gray500, | ||
| modifier = Modifier.padding(horizontal = 20.dp), | ||
| ) | ||
| } | ||
| } | ||
|
Comment on lines
22
to
39
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.
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. 이 부분도 아직 반영이 안되어 있어요 |
||
|
|
||
| @Preview(showBackground = true) | ||
| @Composable | ||
| private fun HomeFestivalInfoPreview() { | ||
| HomeFestivalInfo( | ||
| festivalName = "2025 가천 Water Festival\n: AQUA WAVE", | ||
| festivalDate = "2025년 10월 15일 - 10월 17일", | ||
| ) | ||
| } | ||
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.
임포트를 안하고 사용하신 이유가 있으실까요?
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.
😅 수정했습니다