Skip to content

Commit

Permalink
Merge pull request #65 from krzdabrowski/feature/pull-to-refresh
Browse files Browse the repository at this point in the history
Pull To Refresh in Material3
  • Loading branch information
krzdabrowski authored Jan 14, 2024
2 parents d2baa08 + 10ac8ee commit d59e8d8
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,13 @@ class RocketsViewModel @Inject constructor(
},
)

private fun refreshRockets(): Flow<PartialState> = flow {
private fun refreshRockets(): Flow<PartialState> = flow<PartialState> {
refreshRocketsUseCase()
.onFailure {
emit(Error(it))
}
}.onStart {
emit(Loading)
}

private fun rocketClicked(uri: String): Flow<PartialState> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package eu.krzdabrowski.starter.basicfeature.presentation.composable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
Expand Down Expand Up @@ -35,7 +35,7 @@ fun RocketsListContent(
)

if (index < rocketList.lastIndex) {
Divider(
HorizontalDivider(
modifier = Modifier.testTag(ROCKET_DIVIDER_TEST_TAG),
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package eu.krzdabrowski.starter.basicfeature.presentation.composable

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.krzdabrowski.starter.basicfeature.R
import eu.krzdabrowski.starter.basicfeature.presentation.RocketsEvent
import eu.krzdabrowski.starter.basicfeature.presentation.RocketsEvent.OpenWebBrowserWithDetails
Expand Down Expand Up @@ -45,16 +49,21 @@ internal fun RocketsScreen(
onIntent: (RocketsIntent) -> Unit,
) {
val snackbarHostState = remember { SnackbarHostState() }
val pullToRefreshState = rememberPullToRefreshState()

HandlePullToRefresh(
pullState = pullToRefreshState,
uiState = uiState,
onIntent = onIntent,
)

Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
) {
// TODO: migrate from accompanist to built-in pull-to-refresh when added to Material3
SwipeRefresh(
state = rememberSwipeRefreshState(uiState.isLoading),
onRefresh = { onIntent(RefreshRockets) },
) { paddingValues ->
Box(
modifier = Modifier
.padding(it),
.padding(paddingValues)
.nestedScroll(pullToRefreshState.nestedScrollConnection),
) {
if (uiState.rockets.isNotEmpty()) {
RocketsAvailableContent(
Expand All @@ -67,6 +76,31 @@ internal fun RocketsScreen(
uiState = uiState,
)
}

PullToRefreshContainer(
state = pullToRefreshState,
modifier = Modifier
.align(Alignment.TopCenter),
)
}
}
}

@Composable
private fun HandlePullToRefresh(
pullState: PullToRefreshState,
uiState: RocketsUiState,
onIntent: (RocketsIntent) -> Unit,
) {
if (pullState.isRefreshing) {
LaunchedEffect(true) {
onIntent(RefreshRockets)
}
}

if (uiState.isLoading.not()) {
LaunchedEffect(true) {
pullState.endRefresh()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ class RocketsViewModelTest {
private lateinit var getRocketsUseCase: GetRocketsUseCase

// there is some issue with mocking functional interface with kotlin.Result(Unit)
private val refreshRocketsUseCase: RefreshRocketsUseCase = RefreshRocketsUseCase {
Result.failure(IllegalStateException("Test error"))
}
private lateinit var refreshRocketsUseCase: RefreshRocketsUseCase

@SpyK
private var savedStateHandle = SavedStateHandle()
Expand All @@ -46,6 +44,9 @@ class RocketsViewModelTest {
@BeforeEach
fun setUp() {
MockKAnnotations.init(this)
refreshRocketsUseCase = RefreshRocketsUseCase {
Result.success(Unit)
}
}

@Test
Expand All @@ -66,6 +67,25 @@ class RocketsViewModelTest {
}
}

@Test
fun `should show loading state with no error state first during rockets refresh`() = runTest {
// Given
every { getRocketsUseCase() } returns emptyFlow()
setUpRocketsViewModel()

// When
objectUnderTest.acceptIntent(RefreshRockets)

// Then
objectUnderTest.uiState.test {
val actualItem = awaitItem()
println(actualItem)

assertTrue(actualItem.isLoading)
assertFalse(actualItem.isError)
}
}

@Test
fun `should show fetched rockets with no loading & error state during init rockets retrieval success`() = runTest {
// Given
Expand Down Expand Up @@ -120,6 +140,9 @@ class RocketsViewModelTest {
every { getRocketsUseCase() } returns flowOf(
Result.success(testRocketsFromDomain),
)
refreshRocketsUseCase = RefreshRocketsUseCase {
Result.failure(IllegalStateException("Test error"))
}
setUpRocketsViewModel()

// When
Expand Down
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ accompanist = "0.32.0"
coil = "2.5.0"
compose-bom = "2023.10.01"
compose-compiler = "1.5.8"
compose-material3 = "1.2.0-beta02"
desugar = "2.0.4"
hilt = "2.50"
kotlin = "1.9.22"
Expand Down Expand Up @@ -53,7 +54,7 @@ ktlint = { id = "org.jmailen.kotlinter", version.re
accompanist-swipe-refresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" }
coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" }
desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" }
detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version.ref = "detekt-compose-rules" }
hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
Expand Down

0 comments on commit d59e8d8

Please sign in to comment.