Skip to content

Commit a6d5821

Browse files
committed
Merge branch 'feature/ui_state' into develop
# Conflicts: # build.gradle.kts # buildSrc/src/main/kotlin/Dep.kt # gradle/wrapper/gradle-wrapper.properties # presentation/build.gradle.kts
2 parents 16da7cf + 60bb0af commit a6d5821

File tree

14 files changed

+313
-78
lines changed

14 files changed

+313
-78
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.github.shinhyo.brba.app.di
2+
3+
import dagger.Module
4+
import dagger.Provides
5+
import dagger.hilt.InstallIn
6+
import dagger.hilt.components.SingletonComponent
7+
import io.github.shinhyo.brba.domain.di.DefaultDispatcher
8+
import io.github.shinhyo.brba.domain.di.IoDispatcher
9+
import io.github.shinhyo.brba.domain.di.MainDispatcher
10+
import io.github.shinhyo.brba.domain.di.MainImmediateDispatcher
11+
import kotlinx.coroutines.CoroutineDispatcher
12+
import kotlinx.coroutines.Dispatchers
13+
14+
@InstallIn(SingletonComponent::class)
15+
@Module
16+
internal object CoroutinesModule {
17+
@DefaultDispatcher
18+
@Provides
19+
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
20+
21+
@IoDispatcher
22+
@Provides
23+
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
24+
25+
@MainDispatcher
26+
@Provides
27+
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
28+
29+
@MainImmediateDispatcher
30+
@Provides
31+
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
32+
}

data/src/main/java/io/github/shinhyo/brba/data/repository/CharactersRepositoryImpl.kt

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,50 @@ import io.github.shinhyo.brba.data.mapper.toCharacterEntity
2222
import io.github.shinhyo.brba.data.remote.api.BaBrApi
2323
import io.github.shinhyo.brba.domain.model.Character
2424
import io.github.shinhyo.brba.domain.repository.CharactersRepository
25-
import kotlinx.coroutines.flow.Flow
26-
import kotlinx.coroutines.flow.combine
27-
import kotlinx.coroutines.flow.flow
28-
import kotlinx.coroutines.flow.flowOf
29-
import kotlinx.coroutines.flow.map
25+
import io.github.shinhyo.brba.domain.result.Result
26+
import kotlinx.coroutines.flow.*
27+
import java.util.*
3028
import javax.inject.Inject
29+
import kotlin.random.Random
3130

3231
open class CharactersRepositoryImpl @Inject constructor(
3332
private val api: BaBrApi,
3433
private val db: AppDatabase
3534
) : CharactersRepository {
3635

37-
override fun getCharacterList(): Flow<List<Character>> = flow { emit(api.getCharacters()) }
38-
.map { it.map { r -> r.toCharacter() } }
36+
companion object {
37+
const val MIN_RATIO = 1.2f
38+
}
39+
40+
private val random: Random by lazy { Random(Calendar.getInstance().timeInMillis) }
41+
42+
override fun getCharacterList(): Flow<Result<List<Character>>> {
43+
44+
fun changeRatio(list: List<Character>) =
45+
list.map { c ->
46+
val nextInt = random.nextInt(4) * 0.15f
47+
c.copy(ratio = MIN_RATIO + nextInt)
48+
}
49+
50+
fun addFavoriteToList(list: List<Character>) =
51+
db.characterDao().getAll()
52+
.map { dblist ->
53+
list.toMutableList().map {
54+
it.copy(
55+
favorite = dblist.find { i ->
56+
it.charId == i.charId
57+
}?.favorite ?: false
58+
)
59+
}
60+
}
61+
62+
return flow { emit(api.getCharacters()) }
63+
.map { it.map { r -> r.toCharacter() } }
64+
.map { changeRatio(it) }
65+
.flatMapConcat { addFavoriteToList(it) }
66+
.map { Result.Success(it) }
67+
}
68+
3969

4070
override fun getFavoriteList(isAsc: Boolean): Flow<List<Character>> =
4171
db.characterDao().getFavorite(isAsc = isAsc)
@@ -51,16 +81,6 @@ open class CharactersRepositoryImpl @Inject constructor(
5181
res.copy(favorite = entity?.favorite ?: false)
5282
}
5383

54-
override fun addFavoriteStatus(listCharacter: List<Character>): Flow<List<Character>> =
55-
flowOf(listCharacter)
56-
.combine(
57-
db.characterDao().getAll()
58-
) { list: List<Character>, db: List<CharacterEntity> ->
59-
list.toMutableList().map {
60-
it.copy(favorite = db.find { i -> it.charId == i.charId }?.favorite ?: false)
61-
}
62-
}
63-
6484
override fun updateFavorite(character: Character): Flow<Boolean> = flowOf(character)
6585
.map { it.toCharacterEntity().copy(favorite = !character.favorite) }
6686
.map { db.characterDao().insert(it) }
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.github.shinhyo.brba.domain.di
2+
3+
import javax.inject.Qualifier
4+
5+
@Retention(AnnotationRetention.BINARY)
6+
@Qualifier
7+
annotation class DefaultDispatcher
8+
9+
@Retention(AnnotationRetention.BINARY)
10+
@Qualifier
11+
annotation class IoDispatcher
12+
13+
@Retention(AnnotationRetention.BINARY)
14+
@Qualifier
15+
annotation class MainDispatcher
16+
17+
@Retention(AnnotationRetention.BINARY)
18+
@Qualifier
19+
annotation class MainImmediateDispatcher

domain/src/main/java/io/github/shinhyo/brba/domain/repository/CharactersRepository.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
package io.github.shinhyo.brba.domain.repository
1717

1818
import io.github.shinhyo.brba.domain.model.Character
19+
import io.github.shinhyo.brba.domain.result.Result
1920
import kotlinx.coroutines.flow.Flow
2021

2122
interface CharactersRepository {
22-
fun getCharacterList(): Flow<List<Character>>
23+
fun getCharacterList(): Flow<Result<List<Character>>>
2324
fun getFavoriteList(isAsc: Boolean = false): Flow<List<Character>>
2425
fun getCharacterById(id: Long): Flow<Character>
25-
fun addFavoriteStatus(listCharacter: List<Character>): Flow<List<Character>>
2626
fun updateFavorite(character: Character): Flow<Boolean>
2727
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.github.shinhyo.brba.domain.result
2+
3+
import io.github.shinhyo.brba.domain.result.Result.Success
4+
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.map
6+
7+
/**
8+
* A generic class that holds a value with its loading status.
9+
* @param <T>
10+
*/
11+
sealed class Result<out R> {
12+
13+
data class Success<out T>(val data: T) : Result<T>()
14+
data class Error(val exception: Exception) : Result<Nothing>()
15+
16+
override fun toString(): String {
17+
return when (this) {
18+
is Success<*> -> "Success[data=$data]"
19+
is Error -> "Error[exception=$exception]"
20+
}
21+
}
22+
}
23+
24+
/**
25+
* `true` if [Result] is of type [Success] & holds non-null [Success.data].
26+
*/
27+
val Result<*>.succeeded
28+
get() = this is Success && data != null
29+
30+
fun <T> Result<T>.successOr(fallback: T): T {
31+
return (this as? Success<T>)?.data ?: fallback
32+
}
33+
34+
inline fun <R, T> Result<T>.mapTransform(transform: (T) -> R): Result<R> = when (this) {
35+
is Result.Success -> Result.Success(transform(data))
36+
is Result.Error -> Result.Error(exception)
37+
}
38+
39+
inline fun <R, T> Flow<Result<T>>.mapTransform(crossinline transform: (T) -> R): Flow<Result<R>> =
40+
this.map {
41+
when (it) {
42+
is Result.Success -> Result.Success(transform(it.data))
43+
is Result.Error -> Result.Error(it.exception)
44+
}
45+
}

domain/src/main/java/io/github/shinhyo/brba/domain/usecase/CharactersUseCase.kt

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,7 @@ package io.github.shinhyo.brba.domain.usecase
1818
import io.github.shinhyo.brba.domain.model.Character
1919
import io.github.shinhyo.brba.domain.repository.CharactersRepository
2020
import kotlinx.coroutines.flow.Flow
21-
import kotlinx.coroutines.flow.map
22-
import java.util.Calendar
2321
import javax.inject.Inject
24-
import kotlin.random.Random
25-
26-
class GetCharacterListUseCase @Inject constructor(
27-
private val repo: CharactersRepository
28-
) {
29-
private val random: Random by lazy { Random(Calendar.getInstance().timeInMillis) }
30-
31-
fun execute(): Flow<List<Character>> = repo.getCharacterList()
32-
.map { it.map { c -> c.copy(ratio = random.nextInt(4).let { r -> 1.4f + r * 0.15f }) } }
33-
}
3422

3523
class GetFavoriteListUseCase @Inject constructor(
3624
private val repo: CharactersRepository
@@ -44,12 +32,6 @@ class GetCharacterUseCase @Inject constructor(
4432
fun execute(id: Long): Flow<Character> = repo.getCharacterById(id)
4533
}
4634

47-
class AddFavoriteToListUseCase @Inject constructor(
48-
private val repo: CharactersRepository
49-
) {
50-
fun execute(list: List<Character>): Flow<List<Character>> = repo.addFavoriteStatus(list)
51-
}
52-
5335
class UpdateFavoriteUseCase @Inject constructor(
5436
private val repo: CharactersRepository
5537
) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.github.shinhyo.brba.domain.usecase
2+
3+
import io.github.shinhyo.brba.domain.result.Result
4+
import kotlinx.coroutines.CoroutineDispatcher
5+
import kotlinx.coroutines.flow.Flow
6+
import kotlinx.coroutines.flow.catch
7+
import kotlinx.coroutines.flow.flowOn
8+
9+
/**
10+
* Executes business logic in its execute method and keep posting updates to the result as
11+
* [Result<R>].
12+
* Handling an exception (emit [Result.Error] to the result) is the subclasses's responsibility.
13+
*/
14+
abstract class FlowUseCase<in P, R>(private val coroutineDispatcher: CoroutineDispatcher) {
15+
operator fun invoke(parameters: P): Flow<Result<R>> = execute(parameters)
16+
.catch { e -> emit(Result.Error(Exception(e))) }
17+
.flowOn(coroutineDispatcher)
18+
19+
20+
protected abstract fun execute(parameters: P): Flow<Result<R>>
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.github.shinhyo.brba.domain.usecase
2+
3+
import io.github.shinhyo.brba.domain.di.IoDispatcher
4+
import io.github.shinhyo.brba.domain.model.Character
5+
import io.github.shinhyo.brba.domain.repository.CharactersRepository
6+
import io.github.shinhyo.brba.domain.result.Result
7+
import kotlinx.coroutines.CoroutineDispatcher
8+
import kotlinx.coroutines.flow.Flow
9+
import javax.inject.Inject
10+
11+
class GetCharacterListUseCase @Inject constructor(
12+
private val charactersRepository: CharactersRepository,
13+
@IoDispatcher dispatcher: CoroutineDispatcher,
14+
) : FlowUseCase<Unit, List<Character>>(dispatcher) {
15+
16+
override fun execute(parameters: Unit): Flow<Result<List<Character>>> =
17+
charactersRepository.getCharacterList()
18+
19+
}

presentation/src/main/java/io/github/shinhyo/brba/presentation/ui/NavGraph.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package io.github.shinhyo.brba.presentation.ui
1818
import androidx.activity.compose.BackHandler
1919
import androidx.compose.foundation.layout.navigationBarsPadding
2020
import androidx.compose.foundation.layout.padding
21+
import androidx.compose.foundation.lazy.rememberLazyListState
2122
import androidx.compose.foundation.rememberScrollState
2223
import androidx.compose.material.*
2324
import androidx.compose.runtime.Composable
@@ -100,17 +101,19 @@ fun NavScreen(
100101
) {
101102
val modifier = Modifier.padding(it)
102103
val listScrollState = rememberScrollState()
104+
val favoriteScrollState = rememberLazyListState()
103105
when (selectedTab.value) {
104106
BottomNavTabs.LIST -> ListScreen(
105-
hiltViewModel(),
106-
actions.moveDetail,
107-
listScrollState,
108-
modifier
107+
modifier = modifier,
108+
viewModel = hiltViewModel(),
109+
scrollState = listScrollState,
110+
select = actions.moveDetail,
109111
)
110112
BottomNavTabs.FAVORITE -> FavoriteScreen(
111-
hiltViewModel(),
112-
actions.moveDetail,
113-
modifier
113+
modifier = modifier,
114+
viewModel = hiltViewModel(),
115+
scrollState = favoriteScrollState,
116+
select = actions.moveDetail,
114117
)
115118
}
116119
}

presentation/src/main/java/io/github/shinhyo/brba/presentation/ui/favorite/FavoriteScreen.kt

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import androidx.compose.foundation.background
1919
import androidx.compose.foundation.clickable
2020
import androidx.compose.foundation.layout.*
2121
import androidx.compose.foundation.lazy.LazyColumn
22+
import androidx.compose.foundation.lazy.LazyListState
2223
import androidx.compose.foundation.lazy.items
2324
import androidx.compose.material.Card
2425
import androidx.compose.material.Icon
@@ -47,27 +48,51 @@ import io.github.shinhyo.brba.presentation.ui.common.IconFavorite
4748

4849
@Composable
4950
fun FavoriteScreen(
51+
modifier: Modifier = Modifier,
5052
viewModel: FavoriteViewModel,
53+
scrollState: LazyListState,
54+
select: (Character) -> Unit,
55+
) {
56+
Body(
57+
modifier = modifier,
58+
viewModel = viewModel,
59+
scrollState = scrollState,
60+
select = select,
61+
)
62+
}
63+
64+
@Composable
65+
private fun Body(
66+
modifier: Modifier,
67+
viewModel: FavoriteViewModel,
68+
scrollState: LazyListState,
5169
select: (Character) -> Unit,
52-
modifier: Modifier = Modifier
5370
) {
5471
val list = viewModel.list.collectAsState()
5572
if (list.value.isEmpty()) {
5673
EmptyScreen()
5774
} else {
58-
ListScreen(modifier, list, select, viewModel)
75+
ListScreen(
76+
modifier = modifier,
77+
list = list,
78+
select = select,
79+
viewModel = viewModel,
80+
state = scrollState
81+
)
5982
}
6083
}
6184

6285
@Composable
6386
private fun ListScreen(
6487
modifier: Modifier,
88+
viewModel: FavoriteViewModel,
6589
list: State<List<Character>>,
90+
state: LazyListState,
6691
select: (Character) -> Unit,
67-
viewModel: FavoriteViewModel
6892
) {
6993
LazyColumn(
7094
modifier = modifier,
95+
state = state,
7196
contentPadding = PaddingValues(start = 8.dp, top = 32.dp, end = 8.dp, bottom = 32.dp),
7297
verticalArrangement = Arrangement.spacedBy(8.dp),
7398
) {

0 commit comments

Comments
 (0)