From 95f19189ed481d67269dd42213d94ea286731261 Mon Sep 17 00:00:00 2001 From: chepsi Date: Fri, 27 Oct 2023 19:16:28 +0300 Subject: [PATCH 1/2] Refactor: Sessions Screen Refactor --- .../android254/data/repos/SessionsManager.kt | 31 ++++- .../models/SessionsInformationDomainModel.kt | 6 + .../android254/domain/repos/SessionsRepo.kt | 5 + .../presentation/models/EventDate.kt | 5 +- .../sessions/components/CustomSwitch.kt | 117 ++++++++++++++++++ .../sessions/components/EventDaySelector.kt | 30 ++--- .../components/EventDaySelectorButton.kt | 16 +-- .../components/SessionStateComponent.kt | 78 ++++++------ .../sessions/models/SessionsUiState.kt | 16 ++- .../sessions/view/SessionsScreen.kt | 110 ++-------------- .../sessions/view/SessionsViewModel.kt | 94 +++++++------- 11 files changed, 276 insertions(+), 232 deletions(-) create mode 100644 domain/src/main/java/com/android254/domain/models/SessionsInformationDomainModel.kt create mode 100644 presentation/src/main/java/com/android254/presentation/sessions/components/CustomSwitch.kt diff --git a/data/src/main/java/com/android254/data/repos/SessionsManager.kt b/data/src/main/java/com/android254/data/repos/SessionsManager.kt index b6c682d4..5728e3ea 100644 --- a/data/src/main/java/com/android254/data/repos/SessionsManager.kt +++ b/data/src/main/java/com/android254/data/repos/SessionsManager.kt @@ -18,7 +18,11 @@ package com.android254.data.repos import com.android254.data.repos.mappers.toDomainModel import com.android254.data.repos.mappers.toEntity import com.android254.domain.models.Session +import com.android254.domain.models.SessionsInformationDomainModel import com.android254.domain.repos.SessionsRepo +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import javax.inject.Inject import ke.droidcon.kotlin.datasource.local.dao.BookmarkDao import ke.droidcon.kotlin.datasource.local.model.BookmarkEntity @@ -51,6 +55,24 @@ class SessionsManager @Inject constructor( } }.flowOn(ioDispatcher) } + + override suspend fun fetchSessionsInformation(): Flow = combine( + localSessionsDataSource.getCachedSessions(), + bookmarkDao.getBookmarkIds() + ) { sessions, bookmarks -> + val eventDays = sessions.groupBy { it.startTimestamp.toEventDay() }.keys.toList() + SessionsInformationDomainModel( + sessions = sessions.map { session -> session.toDomainModel().copy(isBookmarked = bookmarks.map { it.sessionId }.contains(session.id.toString())) }, + eventDays = eventDays + ) + } + + private fun Long.toEventDay(): String { + val date = Date(this) + val sdf = SimpleDateFormat("dd", Locale.getDefault()) + return sdf.format(date) + } + override fun fetchBookmarkedSessions(): Flow> { val bookmarksFlow = bookmarkDao.getBookmarkIds() val sessionsFlow = localSessionsDataSource.getCachedSessions() @@ -74,9 +96,13 @@ class SessionsManager @Inject constructor( }.flowOn(ioDispatcher) } - override fun fetchSessionById(sessionId: String): Flow { + override fun fetchFilteredSessions(vararg filters: List) { + // + } + + override fun fetchSessionById(id: String): Flow { val bookmarksFlow = bookmarkDao.getBookmarkIds() - val sessionFlow = localSessionsDataSource.getCachedSessionById(sessionId).map { + val sessionFlow = localSessionsDataSource.getCachedSessionById(id).map { it?.toDomainModel() } return combine(sessionFlow, bookmarksFlow) { session, bookmarks -> @@ -109,6 +135,7 @@ class SessionsManager @Inject constructor( is DataResult.Error -> { Timber.d("Sync sessions failed ${response.message}") } + else -> { } } diff --git a/domain/src/main/java/com/android254/domain/models/SessionsInformationDomainModel.kt b/domain/src/main/java/com/android254/domain/models/SessionsInformationDomainModel.kt new file mode 100644 index 00000000..e261472e --- /dev/null +++ b/domain/src/main/java/com/android254/domain/models/SessionsInformationDomainModel.kt @@ -0,0 +1,6 @@ +package com.android254.domain.models + +data class SessionsInformationDomainModel( + val sessions: List, + val eventDays: List +) diff --git a/domain/src/main/java/com/android254/domain/repos/SessionsRepo.kt b/domain/src/main/java/com/android254/domain/repos/SessionsRepo.kt index 61dc5a74..2e72abd3 100644 --- a/domain/src/main/java/com/android254/domain/repos/SessionsRepo.kt +++ b/domain/src/main/java/com/android254/domain/repos/SessionsRepo.kt @@ -16,13 +16,18 @@ package com.android254.domain.repos import com.android254.domain.models.Session +import com.android254.domain.models.SessionsInformationDomainModel import kotlinx.coroutines.flow.Flow interface SessionsRepo { fun fetchSessions(): Flow> + suspend fun fetchSessionsInformation(): Flow + fun fetchFilteredSessions(query: String): Flow> + fun fetchFilteredSessions(vararg filters: List) + fun fetchBookmarkedSessions(): Flow> fun fetchSessionById(id: String): Flow diff --git a/presentation/src/main/java/com/android254/presentation/models/EventDate.kt b/presentation/src/main/java/com/android254/presentation/models/EventDate.kt index c4005895..a6f9230d 100644 --- a/presentation/src/main/java/com/android254/presentation/models/EventDate.kt +++ b/presentation/src/main/java/com/android254/presentation/models/EventDate.kt @@ -15,8 +15,7 @@ */ package com.android254.presentation.models -import kotlinx.datetime.LocalDate - data class EventDate( - val value: LocalDate + val value: String, + val day: Int ) \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/sessions/components/CustomSwitch.kt b/presentation/src/main/java/com/android254/presentation/sessions/components/CustomSwitch.kt new file mode 100644 index 00000000..e2fe7b97 --- /dev/null +++ b/presentation/src/main/java/com/android254/presentation/sessions/components/CustomSwitch.kt @@ -0,0 +1,117 @@ +package com.android254.presentation.sessions.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.filled.StarOutline +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android254.presentation.utils.ChaiLightAndDarkComposePreview +import com.droidconke.chai.ChaiDCKE22Theme + + +@Composable +fun CustomSwitch( + width: Dp = 72.dp, + height: Dp = 40.dp, + checkedTrackColor: Color = Color(0xFF35898F), + uncheckedTrackColor: Color = Color(0xFFe0e0e0), + gapBetweenThumbAndTrackEdge: Dp = 8.dp, + borderWidth: Dp = 2.dp, + cornerSize: Int = 50, + iconInnerPadding: Dp = 4.dp, + thumbSize: Dp = 24.dp, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + // this is to disable the ripple effect + val interactionSource = remember { + MutableInteractionSource() + } + + // for moving the thumb + val alignment by animateAlignmentAsState(if (checked) 1f else -1f) + + // outer rectangle with border + Box( + modifier = Modifier + .size(width = width, height = height) + .border( + width = borderWidth, + color = if (checked) checkedTrackColor else uncheckedTrackColor, + shape = RoundedCornerShape(percent = cornerSize) + ) + .clickable( + indication = null, + interactionSource = interactionSource + ) { + onCheckedChange(!checked) + }, + contentAlignment = Alignment.Center + ) { + // this is to add padding at the each horizontal side + Box( + modifier = Modifier + .padding( + start = gapBetweenThumbAndTrackEdge, + end = gapBetweenThumbAndTrackEdge + ) + .fillMaxSize(), + contentAlignment = alignment + ) { + // thumb with icon + Icon( + imageVector = if (checked) Icons.Filled.Star else Icons.Filled.StarOutline, + contentDescription = if (checked) "Enabled" else "Disabled", + modifier = Modifier + .size(size = thumbSize) + .background( + color = if (checked) checkedTrackColor else uncheckedTrackColor, + shape = CircleShape + ) + .padding(all = iconInnerPadding), + tint = Color.White + ) + } + } +} + +@Composable +private fun animateAlignmentAsState( + targetBiasValue: Float +): State { + val bias by animateFloatAsState(targetValue = targetBiasValue, label = "") + + return remember { + derivedStateOf { BiasAlignment(horizontalBias = bias, verticalBias = 0f) } + } +} + + +@ChaiLightAndDarkComposePreview +@Composable +private fun CustomSwitchPreview() { + ChaiDCKE22Theme { + CustomSwitch(checked = false, onCheckedChange = {}) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/sessions/components/EventDaySelector.kt b/presentation/src/main/java/com/android254/presentation/sessions/components/EventDaySelector.kt index d4a70fe0..931f8618 100644 --- a/presentation/src/main/java/com/android254/presentation/sessions/components/EventDaySelector.kt +++ b/presentation/src/main/java/com/android254/presentation/sessions/components/EventDaySelector.kt @@ -15,20 +15,22 @@ */ package com.android254.presentation.sessions.components -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.android254.presentation.models.EventDate -import kotlinx.datetime.LocalDate +/* val droidconEventDays = listOf( - EventDate(LocalDate(2023, 11, 16)), - EventDate(LocalDate(2023, 11, 17)), - EventDate(LocalDate(2023, 11, 18)) + EventDate(LocalDate(2023, 11, 16), 1), + EventDate(LocalDate(2023, 11, 17), 2), + EventDate(LocalDate(2023, 11, 18), 3) ) +*/ fun ordinal(i: Int): String { val suffixes = arrayOf("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th") @@ -41,17 +43,17 @@ fun ordinal(i: Int): String { @Composable fun EventDaySelector( selectedDate: EventDate, - updateSelectedDay: (EventDate) -> Unit + updateSelectedDay: (EventDate) -> Unit, + eventDates: List ) { - Row() { - droidconEventDays.forEachIndexed { index, eventDate -> + LazyRow { + items(eventDates) { eventDay -> EventDaySelectorButton( - title = ordinal(eventDate.value.dayOfMonth), - subtitle = "Day ${index + 1}", - onClick = { updateSelectedDay(eventDate) }, - selected = selectedDate == eventDate - ) { - } + title = ordinal(eventDay.value.toInt()), + subtitle = "Day ${eventDay.day}", + onClick = { updateSelectedDay(eventDay) }, + selected = selectedDate == eventDay + ) Spacer(Modifier.width(16.dp)) } } diff --git a/presentation/src/main/java/com/android254/presentation/sessions/components/EventDaySelectorButton.kt b/presentation/src/main/java/com/android254/presentation/sessions/components/EventDaySelectorButton.kt index cf713e69..204471ae 100644 --- a/presentation/src/main/java/com/android254/presentation/sessions/components/EventDaySelectorButton.kt +++ b/presentation/src/main/java/com/android254/presentation/sessions/components/EventDaySelectorButton.kt @@ -19,8 +19,6 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonColors @@ -66,20 +64,8 @@ fun EventDaySelectorButton( } else { MaterialTheme.colorScheme.onSecondaryContainer } - ), - contentPadding: PaddingValues = PaddingValues( - start = 0.dp, - top = 0.dp, - end = 0.dp, - bottom = 0.dp - ), - content: @Composable RowScope.() -> Unit + ) ) { - /*val containerColor = colors.containerColor(enabled).value - val contentColor = colors.contentColor(enabled).value - val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp - val tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp -*/ val containerColor = Color.Black val contentColor = Color.Red val shadowElevation = 0.dp diff --git a/presentation/src/main/java/com/android254/presentation/sessions/components/SessionStateComponent.kt b/presentation/src/main/java/com/android254/presentation/sessions/components/SessionStateComponent.kt index 2c54cada..b1e17b0e 100644 --- a/presentation/src/main/java/com/android254/presentation/sessions/components/SessionStateComponent.kt +++ b/presentation/src/main/java/com/android254/presentation/sessions/components/SessionStateComponent.kt @@ -58,54 +58,48 @@ fun SessionsStateComponent( isRefreshing: Boolean ) { val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isRefreshing) - when (sessionsUiState) { - is SessionsUiState.Loading -> { - SessionLoadingComponent() - } - is SessionsUiState.Empty -> { - val message = sessionsUiState.message - Column( - modifier = Modifier - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - modifier = Modifier.size(70.dp), - painter = painterResource(id = R.drawable.sessions_icon), - contentDescription = stringResource(id = R.string.sessions_icon_description), - tint = ChaiBlue - ) - Spacer(modifier = Modifier.height(20.dp)) - Text( - text = message, - style = TextStyle( - color = ChaiDarkGrey, - fontSize = 18.sp, - fontFamily = MontserratRegular, - textAlign = TextAlign.Center - ) - ) - } - } + if (sessionsUiState.isLoading){ + SessionLoadingComponent() + } - is SessionsUiState.Data -> { - val sessionsList = sessionsUiState.data - SessionListComponent( - swipeRefreshState = swipeRefreshState, - sessions = sessionsList, - navigateToSessionDetails = navigateToSessionDetails, - refreshSessionsList = refreshSessionsList + if (sessionsUiState.isEmpty){ + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + modifier = Modifier.size(70.dp), + painter = painterResource(id = R.drawable.sessions_icon), + contentDescription = stringResource(id = R.string.sessions_icon_description), + tint = ChaiBlue + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = sessionsUiState.isEmptyMessage, + style = TextStyle( + color = ChaiDarkGrey, + fontSize = 18.sp, + fontFamily = MontserratRegular, + textAlign = TextAlign.Center + ) ) } + } - is SessionsUiState.Error -> { - val message = sessionsUiState.message - SessionsErrorComponent(errorMessage = message, retry = retry) - } + if (sessionsUiState.isError){ + SessionsErrorComponent(errorMessage = sessionsUiState.errorMessage, retry = retry) + } - is SessionsUiState.Idle -> {} + if (!sessionsUiState.isEmpty){ + SessionListComponent( + swipeRefreshState = swipeRefreshState, + sessions = sessionsUiState.sessions, + navigateToSessionDetails = navigateToSessionDetails, + refreshSessionsList = refreshSessionsList + ) } } diff --git a/presentation/src/main/java/com/android254/presentation/sessions/models/SessionsUiState.kt b/presentation/src/main/java/com/android254/presentation/sessions/models/SessionsUiState.kt index e5ac7a1d..c8dc327f 100644 --- a/presentation/src/main/java/com/android254/presentation/sessions/models/SessionsUiState.kt +++ b/presentation/src/main/java/com/android254/presentation/sessions/models/SessionsUiState.kt @@ -15,9 +15,10 @@ */ package com.android254.presentation.sessions.models +import com.android254.presentation.models.EventDate import com.android254.presentation.models.SessionPresentationModel -sealed interface SessionsUiState { +/*sealed interface SessionsUiState { object Idle : SessionsUiState object Loading : SessionsUiState data class Data(val data: List) : SessionsUiState @@ -27,4 +28,15 @@ sealed interface SessionsUiState { @JvmInline value class Empty(val message: String) : SessionsUiState -} \ No newline at end of file +}*/ + +data class SessionsUiState( + val isEmpty: Boolean = false, + val isEmptyMessage: String = "", + val isLoading: Boolean = true, + val sessions: List = emptyList(), + val isError: Boolean = false, + val errorMessage: String = "", + val eventDays: List = emptyList(), + val selectedEventDay: EventDate = EventDate("1", 1) +) \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsScreen.kt b/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsScreen.kt index 23bc7ea2..0f627e26 100644 --- a/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsScreen.kt +++ b/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsScreen.kt @@ -17,43 +17,26 @@ package com.android254.presentation.sessions.view import android.content.res.Configuration import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.filled.StarOutline -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment -import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -61,12 +44,12 @@ import com.android254.presentation.common.components.DroidconAppBarWithFilter import com.android254.presentation.common.theme.DroidconKE2023Theme import com.android254.presentation.models.EventDate import com.android254.presentation.models.SessionsFilterOption +import com.android254.presentation.sessions.components.CustomSwitch import com.android254.presentation.sessions.components.EventDaySelector import com.android254.presentation.sessions.components.SessionsFilterPanel import com.android254.presentation.sessions.components.SessionsStateComponent import com.android254.presentation.sessions.models.SessionsUiState import kotlinx.coroutines.launch -import kotlinx.datetime.LocalDate @Composable fun SessionsRoute( @@ -75,14 +58,13 @@ fun SessionsRoute( ) { val isRefreshing by sessionsViewModel.isRefreshing.collectAsStateWithLifecycle() val sessionsUiState by sessionsViewModel.sessionsUiState.collectAsStateWithLifecycle() - val selectedEventDate by sessionsViewModel.selectedEventDate.collectAsStateWithLifecycle() val currentSelections by sessionsViewModel.selectedFilterOptions.collectAsStateWithLifecycle() SessionsScreen( sessionsUiState = sessionsUiState, isRefreshing = isRefreshing, navigateToSessionDetails = navigateToSessionDetails, - selectedEventDate = selectedEventDate, + selectedEventDate = sessionsUiState.selectedEventDay, currentSelections = currentSelections, updateSelectedDay = { sessionsViewModel.updateSelectedDay(it) }, toggleBookmarkFilter = { sessionsViewModel.toggleBookmarkFilter() }, @@ -169,7 +151,8 @@ private fun SessionsScreen( ) { EventDaySelector( selectedDate = selectedEventDate, - updateSelectedDay = updateSelectedDay + updateSelectedDay = updateSelectedDay, + eventDates = sessionsUiState.eventDays ) CustomSwitch(checked = showMySessions.value, onCheckedChange = { showMySessions.value = it @@ -224,12 +207,10 @@ private fun SessionsScreen( ) @Composable fun SessionsScreenPreview() { - DroidconKE2023Theme() { + DroidconKE2023Theme { SessionsScreen( - sessionsUiState = SessionsUiState.Data( - listOf() - ), - selectedEventDate = EventDate(LocalDate(2023, 11, 16)), + sessionsUiState = SessionsUiState(), + selectedEventDate = EventDate("1", day = 1), isRefreshing = false, currentSelections = listOf(), updateSelectedDay = {}, @@ -241,81 +222,4 @@ fun SessionsScreenPreview() { clearSelectedFilterList = {} ) } -} - -@Composable -fun CustomSwitch( - width: Dp = 72.dp, - height: Dp = 40.dp, - checkedTrackColor: Color = Color(0xFF35898F), - uncheckedTrackColor: Color = Color(0xFFe0e0e0), - gapBetweenThumbAndTrackEdge: Dp = 8.dp, - borderWidth: Dp = 2.dp, - cornerSize: Int = 50, - iconInnerPadding: Dp = 4.dp, - thumbSize: Dp = 24.dp, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit -) { - // this is to disable the ripple effect - val interactionSource = remember { - MutableInteractionSource() - } - - // for moving the thumb - val alignment by animateAlignmentAsState(if (checked) 1f else -1f) - - // outer rectangle with border - Box( - modifier = Modifier - .size(width = width, height = height) - .border( - width = borderWidth, - color = if (checked) checkedTrackColor else uncheckedTrackColor, - shape = RoundedCornerShape(percent = cornerSize) - ) - .clickable( - indication = null, - interactionSource = interactionSource - ) { - onCheckedChange(!checked) - }, - contentAlignment = Alignment.Center - ) { - // this is to add padding at the each horizontal side - Box( - modifier = Modifier - .padding( - start = gapBetweenThumbAndTrackEdge, - end = gapBetweenThumbAndTrackEdge - ) - .fillMaxSize(), - contentAlignment = alignment - ) { - // thumb with icon - Icon( - imageVector = if (checked) Icons.Filled.Star else Icons.Filled.StarOutline, - contentDescription = if (checked) "Enabled" else "Disabled", - modifier = Modifier - .size(size = thumbSize) - .background( - color = if (checked) checkedTrackColor else uncheckedTrackColor, - shape = CircleShape - ) - .padding(all = iconInnerPadding), - tint = Color.White - ) - } - } -} - -@Composable -private fun animateAlignmentAsState( - targetBiasValue: Float -): State { - val bias by animateFloatAsState(targetValue = targetBiasValue, label = "") - - return remember { - derivedStateOf { BiasAlignment(horizontalBias = bias, verticalBias = 0f) } - } } \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsViewModel.kt b/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsViewModel.kt index 246f7191..fbbf312a 100644 --- a/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsViewModel.kt +++ b/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsViewModel.kt @@ -17,6 +17,7 @@ package com.android254.presentation.sessions.view import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android254.domain.models.Session import com.android254.domain.repos.SessionsRepo import com.android254.domain.work.SyncDataWorkManager import com.android254.presentation.models.EventDate @@ -25,15 +26,14 @@ import com.android254.presentation.sessions.mappers.toPresentationModel import com.android254.presentation.sessions.models.SessionsUiState import com.android254.presentation.sessions.utils.SessionsFilterCategory import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.datetime.LocalDate import timber.log.Timber -import javax.inject.Inject @HiltViewModel class SessionsViewModel @Inject constructor( @@ -48,14 +48,8 @@ class SessionsViewModel @Inject constructor( private val _filterState: MutableStateFlow = MutableStateFlow(SessionsFilterState()) - private val _selectedEventDate: MutableStateFlow = MutableStateFlow( - EventDate( - LocalDate(year = 2023, monthNumber = 11, dayOfMonth = 16) - ) - ) - val selectedEventDate = _selectedEventDate.asStateFlow() - private val _sessionsUiState = MutableStateFlow(SessionsUiState.Idle) + private val _sessionsUiState = MutableStateFlow(SessionsUiState()) val sessionsUiState = _sessionsUiState.asStateFlow() val isRefreshing = syncDataWorkManager.isSyncing @@ -148,58 +142,54 @@ class SessionsViewModel @Inject constructor( } private suspend fun fetchAllSessions() { - _sessionsUiState.value = SessionsUiState.Loading - sessionsRepo.fetchSessions().collectLatest { sessions -> - try { - _sessionsUiState.value = if (sessions.isNotEmpty()) { - SessionsUiState.Data(data = sessions.map { it.toPresentationModel() }) - } else { - SessionsUiState.Empty("No sessions found") - } - } catch (e: Exception) { - _sessionsUiState.value = SessionsUiState.Error( - message = e.message ?: "An unexpected error occurred" - ) - } + updateIsLoadingState() + sessionsRepo.fetchSessionsInformation().collectLatest { sessions -> + val sessionDays = sessions.eventDays.mapIndexed { index, day -> EventDate(value = day, day = index + 1) } + _sessionsUiState.value = _sessionsUiState.value.copy( + eventDays = sessionDays, + selectedEventDay = sessionDays.first() + ) + updateSessions(sessions.sessions) } } private suspend fun fetchFilteredSessions(query: String) { - _sessionsUiState.value = SessionsUiState.Loading + updateIsLoadingState() sessionsRepo.fetchFilteredSessions(query = query).collectLatest { sessions -> - try { - _sessionsUiState.value = if (sessions.isNotEmpty()) { - SessionsUiState.Data(data = sessions.map { it.toPresentationModel() }) - } else { - SessionsUiState.Empty("No sessions found") - } - } catch (e: Exception) { - _sessionsUiState.value = SessionsUiState.Error( - message = - e.message ?: "Sessions not found" - ) - } + updateSessions(sessions) } } + private fun updateSessions(sessions: List) { + val newState = if (sessions.isEmpty()) { + _sessionsUiState.value.copy( + isEmpty = sessions.isEmpty(), + sessions = emptyList(), + isEmptyMessage = "No sessions found", + isLoading = false + ) + } else { + _sessionsUiState.value.copy( + isEmpty = sessions.isEmpty(), + sessions = sessions.map { session -> session.toPresentationModel() }, + isEmptyMessage = "", + isLoading = false + ) + } + _sessionsUiState.value = newState + } + private suspend fun fetchBookmarkSessions() { - _sessionsUiState.value = SessionsUiState.Loading + updateIsLoadingState() sessionsRepo.fetchBookmarkedSessions().collectLatest { sessions -> - try { - _sessionsUiState.value = if (sessions.isNotEmpty()) { - SessionsUiState.Data(data = sessions.map { it.toPresentationModel() }) - } else { - SessionsUiState.Empty("No bookmarked sessions found") - } - } catch (e: Exception) { - _sessionsUiState.value = SessionsUiState.Error( - message = - e.message ?: "Bookmark sessions not found" - ) - } + updateSessions(sessions) } } + private fun updateIsLoadingState() { + _sessionsUiState.value = _sessionsUiState.value.copy(isLoading = true) + } + private fun getQuery(): String { val separator = "," val prefix = "(" @@ -256,7 +246,9 @@ class SessionsViewModel @Inject constructor( } fun updateSelectedDay(date: EventDate) { - _selectedEventDate.value = date + _sessionsUiState.value = _sessionsUiState.value.copy( + selectedEventDay = date + ) viewModelScope.launch { fetchFilteredSessions(query = getQuery()) } @@ -281,9 +273,9 @@ class SessionsViewModel @Inject constructor( fun toggleBookmarkFilter() { viewModelScope.launch { _filterState.value = SessionsFilterState() - val previousState = _filterState?.value?.isBookmarked ?: false + val previousState = _filterState.value?.isBookmarked ?: false _filterState.value = _filterState.value?.copy(isBookmarked = !previousState) - if (_filterState?.value?.isBookmarked == true) { + if (_filterState.value?.isBookmarked == true) { fetchBookmarkSessions() } else { fetchAllSessions() From ecc0b4601fb038324aa3a0f73fb56a7de94a6cab Mon Sep 17 00:00:00 2001 From: chepsi Date: Fri, 27 Oct 2023 21:03:48 +0300 Subject: [PATCH 2/2] Fix: Failing tests --- .../sessions/view/SessionsScreen.kt | 2 +- .../sessions/view/SessionsViewModel.kt | 1 - .../sessions/view/SessionScreenTest.kt | 112 +++++------------- 3 files changed, 28 insertions(+), 87 deletions(-) diff --git a/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsScreen.kt b/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsScreen.kt index 0f627e26..8efd909c 100644 --- a/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsScreen.kt +++ b/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsScreen.kt @@ -76,7 +76,7 @@ fun SessionsRoute( } @Composable -private fun SessionsScreen( +fun SessionsScreen( sessionsUiState: SessionsUiState, selectedEventDate: EventDate, isRefreshing: Boolean, diff --git a/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsViewModel.kt b/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsViewModel.kt index fbbf312a..c3690dec 100644 --- a/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsViewModel.kt +++ b/presentation/src/main/java/com/android254/presentation/sessions/view/SessionsViewModel.kt @@ -39,7 +39,6 @@ import timber.log.Timber class SessionsViewModel @Inject constructor( private val sessionsRepo: SessionsRepo, private val syncDataWorkManager: SyncDataWorkManager - ) : ViewModel() { private val _selectedFilterOptions: MutableStateFlow> = diff --git a/presentation/src/test/java/com/android254/presentation/sessions/view/SessionScreenTest.kt b/presentation/src/test/java/com/android254/presentation/sessions/view/SessionScreenTest.kt index 68d44ab9..4a4dc689 100644 --- a/presentation/src/test/java/com/android254/presentation/sessions/view/SessionScreenTest.kt +++ b/presentation/src/test/java/com/android254/presentation/sessions/view/SessionScreenTest.kt @@ -19,14 +19,9 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText -import androidx.navigation.testing.TestNavHostController -import androidx.test.core.app.ApplicationProvider -import com.android254.domain.models.Session -import com.android254.domain.repos.SessionsRepo import com.android254.presentation.common.theme.DroidconKE2023Theme -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf +import com.android254.presentation.models.EventDate +import com.android254.presentation.sessions.models.SessionsUiState import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -43,8 +38,6 @@ import org.robolectric.shadows.ShadowLog @RunWith(RobolectricTestRunner::class) @Config(instrumentedPackages = ["androidx.loader.content"], sdk = [33]) class SessionScreenTest { - private val repo = mockk() - private val mockSyncDataWorkManager = FakeSyncWorkManager() @get:Rule val composeTestRule = createComposeRule() @@ -57,16 +50,21 @@ class SessionScreenTest { @Test fun hasExpectedButtons() = runTest { - val navController = TestNavHostController( - ApplicationProvider.getApplicationContext() - ) - every { repo.fetchSessions() } returns flowOf(mockSessions) - every { repo.fetchBookmarkedSessions() } returns flowOf(mockSessions) composeTestRule.setContent { DroidconKE2023Theme { - SessionsRoute( - sessionsViewModel = SessionsViewModel(repo, mockSyncDataWorkManager) + SessionsScreen( + sessionsUiState = SessionsUiState(eventDays = listOf(EventDate("16", 1), EventDate("17", 2), EventDate("18", 3))), + isRefreshing = true, + selectedEventDate = EventDate("1", 1), + currentSelections = emptyList(), + updateSelectedDay = {}, + navigateToSessionDetails = {}, + toggleBookmarkFilter = {}, + refreshSessionList = {}, + updateSelectedFilterOptionList = {}, + fetchSessionWithFilter = {}, + clearSelectedFilterList = {} ) } } @@ -80,17 +78,21 @@ class SessionScreenTest { @Test fun `should show topBar`() = runTest { - val navController = TestNavHostController( - ApplicationProvider.getApplicationContext() - ) - - every { repo.fetchSessions() } returns flowOf(mockSessions) - every { repo.fetchBookmarkedSessions() } returns flowOf(mockSessions) composeTestRule.setContent { DroidconKE2023Theme() { - SessionsRoute( - sessionsViewModel = SessionsViewModel(repo, mockSyncDataWorkManager) + SessionsScreen( + sessionsUiState = SessionsUiState(), + isRefreshing = true, + selectedEventDate = EventDate("1", 1), + currentSelections = emptyList(), + updateSelectedDay = {}, + navigateToSessionDetails = {}, + toggleBookmarkFilter = {}, + refreshSessionList = {}, + updateSelectedFilterOptionList = {}, + fetchSessionWithFilter = {}, + clearSelectedFilterList = {} ) } } @@ -98,64 +100,4 @@ class SessionScreenTest { composeTestRule.onNodeWithTag("droidcon_topBar_with_Filter").assertExists() composeTestRule.onNodeWithTag("droidcon_topBar_with_Filter").assertIsDisplayed() } -} - -val mockSessions = listOf( - Session( - id = "1", - endDateTime = "2023-08-17T12:00:00Z", - endTime = "12:00 PM", - isBookmarked = false, - isKeynote = true, - isServiceSession = false, - sessionImage = "https://example.com/session-1.jpg", - startDateTime = "2023-08-17T10:00:00Z", - startTime = "10:00 AM", - rooms = "Room 1", - speakers = "John Doe, Jane Doe", - remote_id = "1234567890", - description = "This is a keynote session about the future of technology.", - sessionFormat = "Keynote", - sessionLevel = "Beginner", - slug = "keynote-session", - title = "The Future of Technology" - ), - Session( - id = "2", - endDateTime = "2023-08-17T13:00:00Z", - endTime = "1:00 PM", - isBookmarked = true, - isKeynote = false, - isServiceSession = false, - sessionImage = "https://example.com/session-2.jpg", - startDateTime = "2023-08-17T11:00:00Z", - startTime = "11:00 AM", - rooms = "Room 2", - speakers = "Steve Smith, Bill Jones", - remote_id = "9876543210", - description = "This is a session about the latest trends in artificial intelligence.", - sessionFormat = "Workshop", - sessionLevel = "Intermediate", - slug = "ai-trends", - title = "The Latest Trends in Artificial Intelligence" - ), - Session( - id = "3", - endDateTime = "2023-08-17T14:00:00Z", - endTime = "2:00 PM", - isBookmarked = false, - isKeynote = false, - isServiceSession = true, - sessionImage = null, - startDateTime = "2023-08-17T12:00:00Z", - startTime = "12:00 PM", - rooms = "Room 3", - speakers = "No speakers", - remote_id = "", - description = "This is a service session about how to use the conference app.", - sessionFormat = "Service Session", - sessionLevel = "All Levels", - slug = "conference-app", - title = "How to Use the Conference App" - ) -) \ No newline at end of file +} \ No newline at end of file