Skip to content

Commit a303e34

Browse files
committed
Merge branch 'pull-refresh' into main
2 parents 1661ad9 + 54cec14 commit a303e34

18 files changed

+853
-142
lines changed

app/build.gradle.kts

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ dependencies {
8484
implementation("com.google.accompanist:accompanist-systemuicontroller:0.30.1")
8585
implementation("com.google.accompanist:accompanist-placeholder-material:0.30.1")
8686
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.6")
87-
implementation("androidx.compose.ui:ui-tooling-preview-android:1.5.3")
88-
implementation("androidx.navigation:navigation-compose:2.7.4")
87+
implementation("androidx.compose.ui:ui-tooling-preview-android:1.5.4")
88+
implementation("androidx.navigation:navigation-compose:2.7.5")
8989
lintChecks("com.slack.lint.compose:compose-lint-checks:1.2.0")
9090

9191
// Coroutines

app/src/main/java/dev/shorthouse/coinwatch/data/repository/cachedCoin/CachedCoinRepository.kt

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,5 @@ interface CachedCoinRepository {
1414
): Result<List<CachedCoin>>
1515

1616
fun getCachedCoins(): Flow<Result<List<CachedCoin>>>
17-
suspend fun insertCachedCoins(cachedCoins: List<CachedCoin>)
18-
suspend fun deleteAllCachedCoins()
17+
suspend fun refreshCachedCoins(coins: List<CachedCoin>)
1918
}

app/src/main/java/dev/shorthouse/coinwatch/data/repository/cachedCoin/CachedCoinRepositoryImpl.kt

+2-16
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,9 @@ class CachedCoinRepositoryImpl @Inject constructor(
6060
.flowOn(ioDispatcher)
6161
}
6262

63-
override suspend fun insertCachedCoins(cachedCoins: List<CachedCoin>) {
63+
override suspend fun refreshCachedCoins(coins: List<CachedCoin>) {
6464
withContext(ioDispatcher) {
65-
try {
66-
coinLocalDataSource.insertCachedCoins(cachedCoins)
67-
} catch (e: Exception) {
68-
Timber.e("insertCachedCoins error ${e.message}")
69-
}
70-
}
71-
}
72-
73-
override suspend fun deleteAllCachedCoins() {
74-
withContext(ioDispatcher) {
75-
try {
76-
coinLocalDataSource.deleteAllCachedCoins()
77-
} catch (e: Exception) {
78-
Timber.e("deleteAllCachedCoins error ${e.message}")
79-
}
65+
coinLocalDataSource.refreshCachedCoins(coins)
8066
}
8167
}
8268
}

app/src/main/java/dev/shorthouse/coinwatch/data/source/local/CachedCoinDao.kt

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.room.Dao
44
import androidx.room.Insert
55
import androidx.room.OnConflictStrategy
66
import androidx.room.Query
7+
import androidx.room.Transaction
78
import dev.shorthouse.coinwatch.data.source.local.model.CachedCoin
89
import kotlinx.coroutines.flow.Flow
910

@@ -17,4 +18,10 @@ interface CachedCoinDao {
1718

1819
@Query("DELETE FROM CachedCoin")
1920
fun deleteAllCachedCoins()
21+
22+
@Transaction
23+
fun refreshCachedCoins(coins: List<CachedCoin>) {
24+
deleteAllCachedCoins()
25+
insertCachedCoins(coins)
26+
}
2027
}

app/src/main/java/dev/shorthouse/coinwatch/data/source/local/CoinLocalDataSource.kt

+7-31
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,11 @@ import dev.shorthouse.coinwatch.data.source.local.model.CachedCoin
44
import dev.shorthouse.coinwatch.data.source.local.model.FavouriteCoin
55
import kotlinx.coroutines.flow.Flow
66

7-
class CoinLocalDataSource(
8-
private val favouriteCoinDao: FavouriteCoinDao,
9-
private val cachedCoinDao: CachedCoinDao
10-
) {
11-
fun getFavouriteCoins(): Flow<List<FavouriteCoin>> {
12-
return favouriteCoinDao.getFavouriteCoins()
13-
}
14-
15-
fun isCoinFavourite(coinId: String): Flow<Boolean> {
16-
return favouriteCoinDao.isCoinFavourite(coinId = coinId)
17-
}
18-
19-
suspend fun insertFavouriteCoin(favouriteCoin: FavouriteCoin) {
20-
favouriteCoinDao.insert(favouriteCoin)
21-
}
22-
23-
suspend fun deleteFavouriteCoin(favouriteCoin: FavouriteCoin) {
24-
favouriteCoinDao.delete(favouriteCoin)
25-
}
26-
27-
fun getCachedCoins(): Flow<List<CachedCoin>> {
28-
return cachedCoinDao.getCachedCoins()
29-
}
30-
31-
suspend fun insertCachedCoins(coins: List<CachedCoin>) {
32-
cachedCoinDao.insertCachedCoins(coins)
33-
}
34-
35-
suspend fun deleteAllCachedCoins() {
36-
cachedCoinDao.deleteAllCachedCoins()
37-
}
7+
interface CoinLocalDataSource {
8+
fun getFavouriteCoins(): Flow<List<FavouriteCoin>>
9+
fun isCoinFavourite(coinId: String): Flow<Boolean>
10+
suspend fun insertFavouriteCoin(favouriteCoin: FavouriteCoin)
11+
suspend fun deleteFavouriteCoin(favouriteCoin: FavouriteCoin)
12+
fun getCachedCoins(): Flow<List<CachedCoin>>
13+
suspend fun refreshCachedCoins(coins: List<CachedCoin>)
3814
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package dev.shorthouse.coinwatch.data.source.local
2+
3+
import dev.shorthouse.coinwatch.data.source.local.model.CachedCoin
4+
import dev.shorthouse.coinwatch.data.source.local.model.FavouriteCoin
5+
import kotlinx.coroutines.flow.Flow
6+
7+
class CoinLocalDataSourceImpl(
8+
private val favouriteCoinDao: FavouriteCoinDao,
9+
private val cachedCoinDao: CachedCoinDao
10+
) : CoinLocalDataSource {
11+
override fun getFavouriteCoins(): Flow<List<FavouriteCoin>> {
12+
return favouriteCoinDao.getFavouriteCoins()
13+
}
14+
15+
override fun isCoinFavourite(coinId: String): Flow<Boolean> {
16+
return favouriteCoinDao.isCoinFavourite(coinId = coinId)
17+
}
18+
19+
override suspend fun insertFavouriteCoin(favouriteCoin: FavouriteCoin) {
20+
favouriteCoinDao.insert(favouriteCoin)
21+
}
22+
23+
override suspend fun deleteFavouriteCoin(favouriteCoin: FavouriteCoin) {
24+
favouriteCoinDao.delete(favouriteCoin)
25+
}
26+
27+
override fun getCachedCoins(): Flow<List<CachedCoin>> {
28+
return cachedCoinDao.getCachedCoins()
29+
}
30+
31+
override suspend fun refreshCachedCoins(coins: List<CachedCoin>) {
32+
cachedCoinDao.refreshCachedCoins(coins)
33+
}
34+
}

app/src/main/java/dev/shorthouse/coinwatch/domain/RefreshCachedCoinsUseCase.kt

+12-13
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
11
package dev.shorthouse.coinwatch.domain
22

33
import dev.shorthouse.coinwatch.common.Result
4-
import dev.shorthouse.coinwatch.data.datastore.UserPreferencesRepository
4+
import dev.shorthouse.coinwatch.data.datastore.CoinSort
5+
import dev.shorthouse.coinwatch.data.datastore.Currency
56
import dev.shorthouse.coinwatch.data.repository.cachedCoin.CachedCoinRepository
67
import dev.shorthouse.coinwatch.data.source.local.model.CachedCoin
78
import javax.inject.Inject
8-
import kotlinx.coroutines.flow.first
99

1010
class RefreshCachedCoinsUseCase @Inject constructor(
11-
private val cachedCoinRepository: CachedCoinRepository,
12-
private val userPreferencesRepository: UserPreferencesRepository
11+
private val cachedCoinRepository: CachedCoinRepository
1312
) {
14-
suspend operator fun invoke(): Result<List<CachedCoin>> {
15-
return refreshCachedCoins()
13+
suspend operator fun invoke(coinSort: CoinSort, currency: Currency): Result<List<CachedCoin>> {
14+
return refreshCachedCoins(coinSort = coinSort, currency = currency)
1615
}
1716

18-
private suspend fun refreshCachedCoins(): Result<List<CachedCoin>> {
19-
val userPreferences = userPreferencesRepository.userPreferencesFlow.first()
20-
17+
private suspend fun refreshCachedCoins(
18+
coinSort: CoinSort,
19+
currency: Currency
20+
): Result<List<CachedCoin>> {
2121
val remoteCoinsResult = cachedCoinRepository.getRemoteCoins(
22-
coinSort = userPreferences.coinSort,
23-
currency = userPreferences.currency
22+
coinSort = coinSort,
23+
currency = currency
2424
)
2525

2626
if (remoteCoinsResult is Result.Success) {
27-
cachedCoinRepository.deleteAllCachedCoins()
28-
cachedCoinRepository.insertCachedCoins(remoteCoinsResult.data)
27+
cachedCoinRepository.refreshCachedCoins(remoteCoinsResult.data)
2928
}
3029

3130
return remoteCoinsResult

app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dev.shorthouse.coinwatch.navigation
22

3+
import androidx.compose.animation.EnterTransition
4+
import androidx.compose.animation.ExitTransition
35
import androidx.compose.runtime.Composable
46
import androidx.navigation.NavHostController
57
import androidx.navigation.compose.NavHost
@@ -15,7 +17,9 @@ fun AppNavHost(navController: NavHostController = rememberNavController()) {
1517

1618
NavHost(
1719
navController = navController,
18-
startDestination = Screen.NavigationBar.route
20+
startDestination = Screen.NavigationBar.route,
21+
enterTransition = { EnterTransition.None },
22+
exitTransition = { ExitTransition.None }
1923
) {
2024
composable(Screen.NavigationBar.route) {
2125
NavigationBarScaffold(onNavigateDetails = onNavigateDetails)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package dev.shorthouse.coinwatch.ui.component.pullrefresh
2+
3+
// https://issuetracker.google.com/issues/261760718
4+
5+
/*
6+
* Copyright 2022 The Android Open Source Project
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.geometry.Offset
23+
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
24+
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
25+
import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag
26+
import androidx.compose.ui.input.nestedscroll.nestedScroll
27+
import androidx.compose.ui.platform.debugInspectorInfo
28+
import androidx.compose.ui.platform.inspectable
29+
import androidx.compose.ui.unit.Velocity
30+
31+
/**
32+
* A nested scroll modifier that provides scroll events to [state].
33+
*
34+
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
35+
* order to receive scroll events. For example:
36+
*
37+
* @sample androidx.compose.material.samples.PullRefreshSample
38+
*
39+
* @param state The [PullRefreshState] associated with this pull-to-refresh component.
40+
* The state will be updated by this modifier.
41+
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored.
42+
*/
43+
// TODO(b/244423199): Move pullRefresh into its own material library similar to material-ripple.
44+
fun Modifier.pullRefresh(
45+
state: PullRefreshState,
46+
enabled: Boolean = true
47+
) = inspectable(
48+
inspectorInfo = debugInspectorInfo {
49+
name = "pullRefresh"
50+
properties["state"] = state
51+
properties["enabled"] = enabled
52+
}
53+
) {
54+
Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
55+
}
56+
57+
/**
58+
* A nested scroll modifier that provides [onPull] and [onRelease] callbacks to aid building custom
59+
* pull refresh components.
60+
*
61+
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
62+
* order to receive scroll events. For example:
63+
*
64+
* @sample androidx.compose.material.samples.CustomPullRefreshSample
65+
*
66+
* @param onPull Callback for dispatching vertical scroll delta, takes float pullDelta as argument.
67+
* Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling
68+
* down despite being at the top of a scrollable component), whereas negative delta (swiping up) is
69+
* dispatched first (in case it is needed to push the indicator back up), and then the unconsumed
70+
* delta is passed on to the child. The callback returns how much delta was consumed.
71+
* @param onRelease Callback for when drag is released, takes float flingVelocity as argument.
72+
* The callback returns how much velocity was consumed - in most cases this should only consume
73+
* velocity if pull refresh has been dragged already and the velocity is positive (the fling is
74+
* downwards), as an upwards fling should typically still scroll a scrollable component beneath the
75+
* pullRefresh. This is invoked before any remaining velocity is passed to the child.
76+
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored and neither
77+
* [onPull] nor [onRelease] will be invoked.
78+
*/
79+
fun Modifier.pullRefresh(
80+
onPull: (pullDelta: Float) -> Float,
81+
onRelease: suspend (flingVelocity: Float) -> Float,
82+
enabled: Boolean = true
83+
) = inspectable(
84+
inspectorInfo = debugInspectorInfo {
85+
name = "pullRefresh"
86+
properties["onPull"] = onPull
87+
properties["onRelease"] = onRelease
88+
properties["enabled"] = enabled
89+
}
90+
) {
91+
Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
92+
}
93+
94+
private class PullRefreshNestedScrollConnection(
95+
private val onPull: (pullDelta: Float) -> Float,
96+
private val onRelease: suspend (flingVelocity: Float) -> Float,
97+
private val enabled: Boolean
98+
) : NestedScrollConnection {
99+
100+
override fun onPreScroll(
101+
available: Offset,
102+
source: NestedScrollSource
103+
): Offset = when {
104+
!enabled -> Offset.Zero
105+
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
106+
else -> Offset.Zero
107+
}
108+
109+
override fun onPostScroll(
110+
consumed: Offset,
111+
available: Offset,
112+
source: NestedScrollSource
113+
): Offset = when {
114+
!enabled -> Offset.Zero
115+
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
116+
else -> Offset.Zero
117+
}
118+
119+
override suspend fun onPreFling(available: Velocity): Velocity {
120+
return Velocity(0f, onRelease(available.y))
121+
}
122+
}

0 commit comments

Comments
 (0)