Skip to content

Commit

Permalink
ui state
Browse files Browse the repository at this point in the history
  • Loading branch information
RubyLichtenstein committed Oct 6, 2023
1 parent db40851 commit 02d67f6
Show file tree
Hide file tree
Showing 17 changed files with 146 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,19 @@ class KtorHttpClient {

suspend inline fun <reified T : ApiResponse<U>, U> safeApiCall(
apiCall: HttpClient.() -> HttpResponse
): Result<U> = runCatching {
): U {
val response = apiCall.invoke(client)
when (response.status) {
return when (response.status) {
HttpStatusCode.OK -> {
val obj = response.body<T>()
if (obj.status != "success") {
throw Error("Server responded with status: ${obj.status}")
error("Server responded with status: ${obj.status}")
} else {
obj.message
}
}

else -> throw Error("An unknown or network error occurred")
else -> error("Server responded with status: ${response.status}")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.rubylichtenstein.data.breeds

import com.rubylichtenstein.data.KtorHttpClient
import com.rubylichtenstein.data.images.BASE_URL
import io.ktor.client.request.get
import javax.inject.Inject

class BreedsRemoteApi @Inject constructor(private val client: KtorHttpClient) {

suspend fun getAllBreeds(): Result<Map<String, List<String>>> {
suspend fun getAllBreeds(): Map<String, List<String>> {
return client.safeApiCall {
get("${com.rubylichtenstein.data.images.BASE_URL}breeds/list/all")
get("${BASE_URL}breeds/list/all")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.rubylichtenstein.data.breeds

import com.rubylichtenstein.domain.breeds.data.BreedsRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -22,7 +22,7 @@ class BreedsRepositoryImpl @Inject constructor(

// Then always fetch from remote
try {
val remoteBreeds = breedsApi.getAllBreeds().getOrThrow()
val remoteBreeds = breedsApi.getAllBreeds()
val breeds = BreedInfoImpl.fromMap(remoteBreeds)
breedsDataStore.save(breeds)
emit(breeds)
Expand All @@ -34,7 +34,7 @@ class BreedsRepositoryImpl @Inject constructor(
}

private suspend fun getBreedsFromLocal(): List<BreedInfoImpl>? {
return breedsDataStore.get.first()
return breedsDataStore.get.firstOrNull()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import javax.inject.Inject
const val BASE_URL = "https://dog.ceo/api/"

interface BreedImagesApi {
suspend fun getBreedImages(breed: String): Result<List<String>>
suspend fun getBreedImages(breed: String): List<String>
}

class BreedImagesApiImpl @Inject constructor(
Expand All @@ -29,7 +29,7 @@ class BreedImagesApiImpl @Inject constructor(
* 2. To get images for a sub-breed:
* getBreedImages("shepherd/australian")
*/
override suspend fun getBreedImages(breed: String): Result<List<String>> {
override suspend fun getBreedImages(breed: String): List<String> {
return client.safeApiCall {
get("${BASE_URL}breed/$breed/images")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,18 @@ class ImagesRepositoryImpl @Inject constructor(
.distinctUntilChanged()

private suspend fun fetchAndSave(breedKey: String) {
val remoteData = getRemoteBreedImages(breedKey).getOrThrow()
val remoteData = getRemoteBreedImages(breedKey)
imagesDataStore.insertAll(remoteData.map { it.fromDogImageEntity() })
}

private suspend fun getRemoteBreedImages(breedKey: String): Result<List<DogImage>> {
return dogBreedApiService.getBreedImages(breedKey).map {
it.map { url ->
DogImage(
breedName = buildDisplayNameFromKey(breedKey),
isFavorite = false,
url = url,
breedKey = breedKey
)
}
private suspend fun getRemoteBreedImages(breedKey: String): List<DogImage> {
return dogBreedApiService.getBreedImages(breedKey).map { url ->
DogImage(
breedName = buildDisplayNameFromKey(breedKey),
isFavorite = false,
url = url,
breedKey = breedKey
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import androidx.navigation.NavController
import com.rubylichtenstein.domain.breeds.BreedEntity
import com.rubylichtenstein.domain.common.capitalizeWords
import com.rubylichtenstein.ui.Screen
import com.rubylichtenstein.ui.common.AsyncResult
import com.rubylichtenstein.ui.common.AsyncStateHandler
import com.rubylichtenstein.ui.common.UiStateWrapper
import com.rubylichtenstein.ui.common.UiState

@Composable
fun BreedsScreen(
Expand All @@ -49,7 +49,7 @@ fun BreedsScreen(
@ExperimentalMaterial3Api
@Composable
fun BreedsScreen(
breedListState: AsyncResult<List<BreedEntity>>,
breedListState: UiState<List<BreedEntity>>,
navigateToDogImages: (BreedEntity) -> Unit
) {
val scrollBehavior =
Expand All @@ -71,7 +71,7 @@ fun BreedsScreen(
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
AsyncStateHandler(breedListState) { data ->
UiStateWrapper(breedListState) { data ->
BreedList(
breeds = data,
onItemClick = navigateToDogImages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.rubylichtenstein.ui.breeds
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rubylichtenstein.domain.breeds.GetBreedsUseCase
import com.rubylichtenstein.ui.common.AsyncResult
import com.rubylichtenstein.ui.common.asAsyncResult
import com.rubylichtenstein.ui.common.UiState
import com.rubylichtenstein.ui.common.asUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
Expand All @@ -16,10 +16,10 @@ class BreedsViewModel @Inject constructor(
) : ViewModel() {

val breedsState = getBreedsUseCase()
.asAsyncResult()
.asUiState()
.stateIn(
viewModelScope,
SharingStarted.Lazily,
AsyncResult.Loading
UiState.Loading
)
}
29 changes: 0 additions & 29 deletions ui/src/main/java/com/rubylichtenstein/ui/common/AsyncResult.kt

This file was deleted.

46 changes: 46 additions & 0 deletions ui/src/main/java/com/rubylichtenstein/ui/common/UiState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.rubylichtenstein.ui.common

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

fun <T, R> UiState<T>.mapSuccess(transform: (T) -> R): UiState<R> {
return when (this) {
is UiState.Loading -> UiState.Loading
is UiState.Success -> UiState.Success(transform(data))
is UiState.Error -> UiState.Error(message)
is UiState.Empty -> UiState.Empty(message)
}
}

sealed interface UiState<out T> {
data class Success<T>(val data: T) : UiState<T>
data class Error(val message: String?) : UiState<Nothing>
data object Loading : UiState<Nothing>
data class Empty(val message: String?) : UiState<Nothing>
}

fun <T> Flow<T>.asUiState(
errorMessage: String? = null
): Flow<UiState<T>> {
return this
.map<T, UiState<T>> {
UiState.Success(it)
}
.catch { emit(UiState.Error(errorMessage ?: it.message)) }
}

fun <T> Flow<List<T>>.asUiState(
emptyMessage: String? = null,
errorMessage: String? = null
): Flow<UiState<List<T>>> {
return this
.map {
if (it.isEmpty()) {
UiState.Empty(emptyMessage)
} else {
UiState.Success(it)
}
}
.catch { emit(UiState.Error(errorMessage ?: it.message)) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp

@Composable
fun <T> AsyncStateHandler(
asyncResult: AsyncResult<T>,
fun <T> UiStateWrapper(
uiState: UiState<T>,
onError: @Composable (String) -> Unit = { message ->
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
Expand All @@ -28,11 +28,21 @@ fun <T> AsyncStateHandler(
CircularProgressIndicator()
}
},
onEmpty: @Composable (String) -> Unit = {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = it,
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center
)
}
},
onSuccess: @Composable (T) -> Unit
) {
when (asyncResult) {
is AsyncResult.Loading -> onLoading()
is AsyncResult.Success -> onSuccess(asyncResult.data)
is AsyncResult.Error -> onError(asyncResult.exception?.message ?: "Unknown error")
when (uiState) {
is UiState.Loading -> onLoading()
is UiState.Success -> onSuccess(uiState.data)
is UiState.Error -> onError(uiState.message ?: "Unknown error")
is UiState.Empty -> onEmpty(uiState.message ?: "No data")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.rubylichtenstein.domain.images.DogImage
import com.rubylichtenstein.ui.common.AsyncResult
import com.rubylichtenstein.ui.common.asAsyncResult
import com.rubylichtenstein.ui.common.UiState
import com.rubylichtenstein.ui.common.asUiState
import com.rubylichtenstein.ui.common.mapSuccess
import kotlinx.coroutines.flow.Flow

Expand All @@ -30,16 +30,16 @@ sealed interface Event {
fun FavoritesPresenter(
events: Flow<Event>,
favoriteImagesFlow: Flow<List<DogImage>>
): AsyncResult<FavoritesModel> {
var favoriteImagesResult by remember { mutableStateOf<AsyncResult<List<DogImage>>>(AsyncResult.Loading) }
): UiState<FavoritesModel> {
var favoriteImagesResult by remember { mutableStateOf<UiState<Collection<DogImage>>>(UiState.Loading) }
var filteredBreeds by remember { mutableStateOf(emptySet<String>()) }

LaunchedEffect(Unit) {
favoriteImagesFlow
.asAsyncResult()
.asUiState()
.collect { images ->
favoriteImagesResult = images
if (images is AsyncResult.Success) {
if (images is UiState.Success) {
val chipsLabels = images.data.map { it.breedName }.toSet()
filteredBreeds = filteredBreeds.intersect(chipsLabels)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.rubylichtenstein.domain.images.DogImage
import com.rubylichtenstein.ui.common.AsyncResult
import com.rubylichtenstein.ui.common.AsyncStateHandler
import com.rubylichtenstein.ui.common.UiState
import com.rubylichtenstein.ui.common.UiStateWrapper
import com.rubylichtenstein.ui.images.DogImagesGrid

@Composable
Expand All @@ -40,7 +40,7 @@ fun FavoritesScreen(

@Composable
fun PureFavoritesScreen(
state: AsyncResult<FavoritesModel>,
state: UiState<FavoritesModel>,
navController: NavController,
onToggleSelectedBreed: (ChipInfo) -> Unit,
onToggleFavorite: (DogImage) -> Unit
Expand All @@ -56,7 +56,7 @@ fun PureFavoritesScreen(
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
AsyncStateHandler(state) {
UiStateWrapper(state) {
if (it.dogImages.isEmpty()) {
EmptyScreen()
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.rubylichtenstein.domain.favorites.GetFavoriteImagesUseCase
import com.rubylichtenstein.domain.favorites.ToggleFavoriteUseCase
import com.rubylichtenstein.domain.images.DogImage
import com.rubylichtenstein.ui.common.AsyncResult
import com.rubylichtenstein.ui.common.UiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
Expand All @@ -19,10 +19,10 @@ import javax.inject.Inject
class FavoritesViewModel @Inject constructor(
val getFavoriteImagesUseCase: GetFavoriteImagesUseCase,
val toggleFavoriteUseCase: ToggleFavoriteUseCase
) : MoleculeViewModel<Event, AsyncResult<FavoritesModel>>() {
) : MoleculeViewModel<Event, UiState<FavoritesModel>>() {

@Composable
override fun models(events: Flow<Event>): AsyncResult<FavoritesModel> {
override fun models(events: Flow<Event>): UiState<FavoritesModel> {
return FavoritesPresenter(
events = events,
favoriteImagesFlow = getFavoriteImagesUseCase()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.rubylichtenstein.domain.breeds.buildDisplayName
import com.rubylichtenstein.domain.common.capitalizeWords
import com.rubylichtenstein.ui.common.AsyncStateHandler
import com.rubylichtenstein.ui.common.UiStateWrapper
import com.rubylichtenstein.ui.favorites.FavoritesViewModel

@OptIn(ExperimentalMaterial3Api::class)
Expand Down Expand Up @@ -70,7 +70,7 @@ fun ImagesScreen(
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
AsyncStateHandler(dogImages) { dogImageList ->
UiStateWrapper(dogImages) { dogImageList ->
DogImagesGrid(
images = dogImageList
) { dogImage -> favoritesViewModel.toggleFavorite(dogImage) }
Expand Down
Loading

0 comments on commit 02d67f6

Please sign in to comment.