Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 19 additions & 154 deletions app/src/main/java/com/daedan/festabook/presentation/home/HomeFragment.kt
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 android.view.ViewGroup
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.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>())
@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,
Comment on lines 21 to 22
Copy link
Contributor

Choose a reason for hiding this comment

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

클래스에 @Inject를 붙이는걸로 변경하는게 좋을 것 같아용

Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분 아직 반영이 안되어있네요~

) : 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()
Expand All @@ -48,7 +51,7 @@ class HomeViewModel @Inject constructor(
}

fun navigateToScheduleClick() {
_navigateToScheduleEvent.setValue(Unit)
_navigateToScheduleEvent.tryEmit(Unit)
}

private fun loadLineup() {
Expand All @@ -58,10 +61,8 @@ class HomeViewModel @Inject constructor(
val result = festivalRepository.getLineUpGroupByDate()
result
.onSuccess { lineups ->
_lineupUiState.value =
LineupUiState.Success(
lineups.toUiModel(),
)
val lineupItems = lineups.toUiModel().getLineupItems()
_lineupUiState.value = LineupUiState.Success(lineupItems)
}.onFailure {
_lineupUiState.value = LineupUiState.Error(it)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sealed interface LineupUiState {
data object Loading : LineupUiState

data class Success(
val lineups: LineUpItemGroupUiModel,
val lineups: List<LineUpItemOfDayUiModel>,
) : LineupUiState

data class Error(
Expand Down
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,
)
}
}

private object HomeArtistItem {
val ArtistImage = RoundedCornerShape(
topStartPercent = 50,
topEndPercent = 50,
Copy link
Contributor

Choose a reason for hiding this comment

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

private 을 다는건 어떻게 생각하시나요?

bottomEndPercent = 50,
bottomStartPercent = 5,
)
}

@Preview
@Composable
private fun HomeArtistItemPreview() {
HomeArtistItem(
artistName = "실리카겔",
Comment on lines +65 to +67
Copy link
Contributor

Choose a reason for hiding this comment

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

이건 TMI일 수도 있는데, Theme으로 감싸주지 않고 Preview를 실행하는건 실제 사용할 때와 다르게 동작할 수 있어 주의해야 하는 것으로 알고있어요 !

artistImageUrl = "sample",
)
}
Loading