Simplify network layer (#175)

* Use Ktor for network requests

* Remove V0 version

* Remove Retrofit dependency

* Fix url

* Update versions of dependencies

* Revert kotlinx-datetime

Due to https://github.com/Kotlin/kotlinx-datetime/issues/304

* Rename leftovers

* Remove OkHttp

* Remove unused manifest

* Remove unused Hilt module

* Fix building empty image URLs

* Use OkHttp as engine for Ktor

* Reduce visibility of internal classes

* Fix first set up test

* Store only auth token, not header

* Remove UnitInfo/FoodInfo/VersionInfo/NewShoppingListItemInfo

* Remove RecipeSummaryInfo and ShoppingListsInfo

* Remove FullShoppingListInfo

* Remove ParseRecipeURLInfo

* Remove FullRecipeInfo

* Sign out if access token does not work

* Rename getVersionInfo method

* Update version name
This commit is contained in:
Kirill Kamakin
2023-11-05 15:01:19 +01:00
committed by GitHub
parent 888783bf14
commit 5ed1acb678
144 changed files with 1216 additions and 2796 deletions

View File

@@ -1,25 +1,25 @@
package gq.kirmanak.mealient.shopping_lists.network
import gq.kirmanak.mealient.datasource.models.FoodInfo
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.UnitInfo
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
interface ShoppingListsDataSource {
suspend fun getAllShoppingLists(): List<ShoppingListInfo>
suspend fun getAllShoppingLists(): List<GetShoppingListsSummaryResponse>
suspend fun getShoppingList(id: String): FullShoppingListInfo
suspend fun getShoppingList(id: String): GetShoppingListResponse
suspend fun deleteShoppingListItem(id: String)
suspend fun updateShoppingListItem(item: ShoppingListItemInfo)
suspend fun updateShoppingListItem(item: GetShoppingListItemResponse)
suspend fun getFoods(): List<FoodInfo>
suspend fun getFoods(): List<GetFoodResponse>
suspend fun getUnits(): List<UnitInfo>
suspend fun getUnits(): List<GetUnitResponse>
suspend fun addShoppingListItem(item: NewShoppingListItemInfo)
suspend fun addShoppingListItem(item: CreateShoppingListItemRequest)
}

View File

@@ -1,43 +1,41 @@
package gq.kirmanak.mealient.shopping_lists.network
import gq.kirmanak.mealient.datasource.models.FoodInfo
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.UnitInfo
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.model_mapper.ModelMapper
import gq.kirmanak.mealient.datasource.MealieDataSource
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
import javax.inject.Inject
class ShoppingListsDataSourceImpl @Inject constructor(
private val v1Source: MealieDataSourceV1,
private val modelMapper: ModelMapper,
private val dataSource: MealieDataSource,
) : ShoppingListsDataSource {
override suspend fun getAllShoppingLists(): List<ShoppingListInfo> {
val response = v1Source.getShoppingLists(1, -1)
return response.items.map { modelMapper.toShoppingListInfo(it) }
override suspend fun getAllShoppingLists(): List<GetShoppingListsSummaryResponse> {
val response = dataSource.getShoppingLists(1, -1)
return response.items
}
override suspend fun getShoppingList(
id: String
): FullShoppingListInfo = modelMapper.toFullShoppingListInfo(v1Source.getShoppingList(id))
): GetShoppingListResponse = dataSource.getShoppingList(id)
override suspend fun deleteShoppingListItem(
id: String
) = v1Source.deleteShoppingListItem(id)
) = dataSource.deleteShoppingListItem(id)
override suspend fun updateShoppingListItem(
item: ShoppingListItemInfo
) = v1Source.updateShoppingListItem(item)
item: GetShoppingListItemResponse
) = dataSource.updateShoppingListItem(item)
override suspend fun getFoods(): List<FoodInfo> = modelMapper.toFoodInfo(v1Source.getFoods())
override suspend fun getFoods(): List<GetFoodResponse> = dataSource.getFoods().items
override suspend fun getUnits(): List<UnitInfo> = modelMapper.toUnitInfo(v1Source.getUnits())
override suspend fun getUnits(): List<GetUnitResponse> = dataSource.getUnits().items
override suspend fun addShoppingListItem(
item: NewShoppingListItemInfo
) = v1Source.addShoppingListItem(modelMapper.toV1CreateRequest(item))
item: CreateShoppingListItemRequest
) = dataSource.addShoppingListItem(item)
}

View File

@@ -1,25 +1,25 @@
package gq.kirmanak.mealient.shopping_lists.repo
import gq.kirmanak.mealient.datasource.models.FoodInfo
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.UnitInfo
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
interface ShoppingListsRepo {
suspend fun getShoppingLists(): List<ShoppingListInfo>
suspend fun getShoppingLists(): List<GetShoppingListsSummaryResponse>
suspend fun getShoppingList(id: String): FullShoppingListInfo
suspend fun getShoppingList(id: String): GetShoppingListResponse
suspend fun deleteShoppingListItem(id: String)
suspend fun updateShoppingListItem(item: ShoppingListItemInfo)
suspend fun updateShoppingListItem(item: GetShoppingListItemResponse)
suspend fun getFoods(): List<FoodInfo>
suspend fun getFoods(): List<GetFoodResponse>
suspend fun getUnits(): List<UnitInfo>
suspend fun getUnits(): List<GetUnitResponse>
suspend fun addShoppingListItem(item: NewShoppingListItemInfo)
suspend fun addShoppingListItem(item: CreateShoppingListItemRequest)
}

View File

@@ -1,11 +1,11 @@
package gq.kirmanak.mealient.shopping_lists.repo
import gq.kirmanak.mealient.datasource.models.FoodInfo
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.UnitInfo
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.shopping_lists.network.ShoppingListsDataSource
import javax.inject.Inject
@@ -15,12 +15,12 @@ class ShoppingListsRepoImpl @Inject constructor(
private val logger: Logger,
) : ShoppingListsRepo {
override suspend fun getShoppingLists(): List<ShoppingListInfo> {
override suspend fun getShoppingLists(): List<GetShoppingListsSummaryResponse> {
logger.v { "getShoppingLists() called" }
return dataSource.getAllShoppingLists()
}
override suspend fun getShoppingList(id: String): FullShoppingListInfo {
override suspend fun getShoppingList(id: String): GetShoppingListResponse {
logger.v { "getShoppingListItems() called with: id = $id" }
return dataSource.getShoppingList(id)
}
@@ -30,22 +30,22 @@ class ShoppingListsRepoImpl @Inject constructor(
dataSource.deleteShoppingListItem(id)
}
override suspend fun updateShoppingListItem(item: ShoppingListItemInfo) {
override suspend fun updateShoppingListItem(item: GetShoppingListItemResponse) {
logger.v { "updateShoppingListItem() called with: item = $item" }
dataSource.updateShoppingListItem(item)
}
override suspend fun getFoods(): List<FoodInfo> {
override suspend fun getFoods(): List<GetFoodResponse> {
logger.v { "getFoods() called" }
return dataSource.getFoods()
}
override suspend fun getUnits(): List<UnitInfo> {
override suspend fun getUnits(): List<GetUnitResponse> {
logger.v { "getUnits() called" }
return dataSource.getUnits()
}
override suspend fun addShoppingListItem(item: NewShoppingListItemInfo) {
override suspend fun addShoppingListItem(item: CreateShoppingListItemRequest) {
logger.v { "addShoppingListItem() called with: item = $item" }
dataSource.addShoppingListItem(item)
}

View File

@@ -1,11 +1,11 @@
package gq.kirmanak.mealient.shopping_lists.ui
import gq.kirmanak.mealient.datasource.models.FoodInfo
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
import gq.kirmanak.mealient.datasource.models.UnitInfo
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
data class ShoppingListData(
val foods: List<FoodInfo>,
val units: List<UnitInfo>,
val shoppingList: FullShoppingListInfo,
val foods: List<GetFoodResponse>,
val units: List<GetUnitResponse>,
val shoppingList: GetShoppingListResponse,
)

View File

@@ -1,10 +1,10 @@
package gq.kirmanak.mealient.shopping_lists.ui
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
data class ShoppingListEditingState(
val deletedItemIds: Set<String> = emptySet(),
val editingItemIds: Set<String> = emptySet(),
val modifiedItems: Map<String, ShoppingListItemInfo> = emptyMap(),
val modifiedItems: Map<String, GetShoppingListItemResponse> = emptyMap(),
val newItems: List<ShoppingListItemState.NewItem> = emptyList(),
)

View File

@@ -54,14 +54,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import gq.kirmanak.mealient.AppTheme
import gq.kirmanak.mealient.Dimens
import gq.kirmanak.mealient.datasource.models.FoodInfo
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo
import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo
import gq.kirmanak.mealient.datasource.models.RecipeSettingsInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo
import gq.kirmanak.mealient.datasource.models.UnitInfo
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemRecipeReferenceResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
import gq.kirmanak.mealient.shopping_list.R
import gq.kirmanak.mealient.shopping_lists.ui.composables.LazyColumnWithLoadingState
import gq.kirmanak.mealient.shopping_lists.util.data
@@ -401,21 +397,21 @@ private fun ShoppingListItemEditorFoodRow(
}
class ShoppingListItemEditorState(
val foods: List<FoodInfo>,
val units: List<UnitInfo>,
val foods: List<GetFoodResponse>,
val units: List<GetUnitResponse>,
val position: Int,
val listId: String,
note: String = "",
quantity: String = "1.0",
isFood: Boolean = false,
food: FoodInfo? = null,
unit: UnitInfo? = null,
food: GetFoodResponse? = null,
unit: GetUnitResponse? = null,
) {
constructor(
state: ShoppingListItemState.ExistingItem,
foods: List<FoodInfo>,
units: List<UnitInfo>,
foods: List<GetFoodResponse>,
units: List<GetUnitResponse>,
) : this(
foods = foods,
units = units,
@@ -434,9 +430,9 @@ class ShoppingListItemEditorState(
var isFood: Boolean by mutableStateOf(isFood)
var food: FoodInfo? by mutableStateOf(food)
var food: GetFoodResponse? by mutableStateOf(food)
var unit: UnitInfo? by mutableStateOf(unit)
var unit: GetUnitResponse? by mutableStateOf(unit)
var foodsExpanded: Boolean by mutableStateOf(false)
@@ -624,40 +620,8 @@ fun PreviewShoppingListItemEditing() {
}
private object PreviewData {
val teaWithMilkRecipe = FullRecipeInfo(
remoteId = "1",
name = "Tea with milk",
recipeYield = "1 serving",
recipeIngredients = listOf(
RecipeIngredientInfo(
note = "Tea bag",
food = "",
unit = "",
quantity = 1.0,
title = "",
),
RecipeIngredientInfo(
note = "",
food = "Milk",
unit = "ml",
quantity = 500.0,
title = "",
),
),
recipeInstructions = listOf(
RecipeInstructionInfo("Boil water"),
RecipeInstructionInfo("Put tea bag in a cup"),
RecipeInstructionInfo("Pour water into the cup"),
RecipeInstructionInfo("Wait for 5 minutes"),
RecipeInstructionInfo("Remove tea bag"),
RecipeInstructionInfo("Add milk"),
),
settings = RecipeSettingsInfo(
disableAmounts = false
),
)
val blackTeaBags = ShoppingListItemInfo(
val blackTeaBags = GetShoppingListItemResponse(
id = "1",
shoppingListId = "1",
checked = false,
@@ -668,17 +632,14 @@ private object PreviewData {
unit = null,
food = null,
recipeReferences = listOf(
ShoppingListItemRecipeReferenceInfo(
shoppingListId = "1",
id = "1",
GetShoppingListItemRecipeReferenceResponse(
recipeId = "1",
recipeQuantity = 1.0,
recipe = teaWithMilkRecipe,
),
),
)
val milk = ShoppingListItemInfo(
val milk = GetShoppingListItemResponse(
id = "2",
shoppingListId = "1",
checked = true,
@@ -686,15 +647,12 @@ private object PreviewData {
isFood = true,
note = "Cold",
quantity = 500.0,
unit = UnitInfo("ml", ""),
food = FoodInfo("Milk", ""),
unit = GetUnitResponse("ml", ""),
food = GetFoodResponse("Milk", ""),
recipeReferences = listOf(
ShoppingListItemRecipeReferenceInfo(
shoppingListId = "1",
id = "2",
GetShoppingListItemRecipeReferenceResponse(
recipeId = "1",
recipeQuantity = 500.0,
recipe = teaWithMilkRecipe,
),
),
)

View File

@@ -1,22 +1,22 @@
package gq.kirmanak.mealient.shopping_lists.ui
import gq.kirmanak.mealient.datasource.models.FoodInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.UnitInfo
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
import java.util.UUID
internal data class ShoppingListScreenState(
val name: String,
val listId: String,
val items: List<ShoppingListItemState>,
val foods: List<FoodInfo>,
val units: List<UnitInfo>,
val foods: List<GetFoodResponse>,
val units: List<GetUnitResponse>,
)
sealed class ShoppingListItemState {
data class ExistingItem(
val item: ShoppingListItemInfo,
val item: GetShoppingListItemResponse,
val isEditing: Boolean = false,
) : ShoppingListItemState()

View File

@@ -8,8 +8,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.architecture.valueUpdatesOnly
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo
@@ -113,7 +113,7 @@ internal class ShoppingListViewModel @Inject constructor(
): LoadingState<ShoppingListScreenState> {
logger.v { "buildLoadingState() called with: loadingState = $loadingState, editingState = $editingState" }
return loadingState.map { data ->
val existingItems = data.shoppingList.items
val existingItems = data.shoppingList.listItems
.filter { it.id !in editingState.deletedItemIds }
.map {
ShoppingListItemState.ExistingItem(
@@ -208,7 +208,7 @@ internal class ShoppingListViewModel @Inject constructor(
updateItemInformation(updatedItem)
}
private fun updateItemInformation(updatedItem: ShoppingListItemInfo) {
private fun updateItemInformation(updatedItem: GetShoppingListItemResponse) {
logger.v { "updateItemInformation() called with: updatedItem = $updatedItem" }
val id = updatedItem.id
viewModelScope.launch {
@@ -259,14 +259,15 @@ internal class ShoppingListViewModel @Inject constructor(
) {
logger.v { "onAddConfirm() called with: state = $state" }
val item = state.item
val newItem = NewShoppingListItemInfo(
val newItem = CreateShoppingListItemRequest(
shoppingListId = item.listId,
note = item.note,
quantity = item.quantity.toDouble(),
isFood = item.isFood,
unit = item.unit,
food = item.food,
unitId = item.unit?.id,
foodId = item.food?.id,
position = item.position,
checked = false,
)
viewModelScope.launch {
val result = runCatchingExceptCancel {

View File

@@ -22,7 +22,7 @@ import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import gq.kirmanak.mealient.AppTheme
import gq.kirmanak.mealient.Dimens
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
import gq.kirmanak.mealient.shopping_list.R
import gq.kirmanak.mealient.shopping_lists.ui.composables.LazyColumnWithLoadingState
import gq.kirmanak.mealient.shopping_lists.ui.destinations.ShoppingListScreenDestination
@@ -46,7 +46,7 @@ fun ShoppingListsScreen(
lazyColumnContent = { items ->
items(items) { shoppingList ->
ShoppingListCard(
shoppingListEntity = shoppingList,
shoppingList = shoppingList,
onItemClick = { clickedEntity ->
val shoppingListId = clickedEntity.id
navigator.navigate(ShoppingListScreenDestination(shoppingListId))
@@ -61,21 +61,23 @@ fun ShoppingListsScreen(
@Preview
private fun PreviewShoppingListCard() {
AppTheme {
ShoppingListCard(shoppingListEntity = ShoppingListInfo("1", "Weekend shopping"))
ShoppingListCard(
shoppingList = GetShoppingListsSummaryResponse("1", "Weekend shopping"),
)
}
}
@Composable
private fun ShoppingListCard(
shoppingListEntity: ShoppingListInfo?,
shoppingList: GetShoppingListsSummaryResponse?,
modifier: Modifier = Modifier,
onItemClick: (ShoppingListInfo) -> Unit = {},
onItemClick: (GetShoppingListsSummaryResponse) -> Unit = {},
) {
Card(
modifier = modifier
.padding(horizontal = Dimens.Medium, vertical = Dimens.Small)
.fillMaxWidth()
.clickable { shoppingListEntity?.let { onItemClick(it) } },
.clickable { shoppingList?.let { onItemClick(it) } },
) {
Row(
modifier = Modifier.padding(Dimens.Medium),
@@ -87,7 +89,7 @@ private fun ShoppingListCard(
modifier = Modifier.height(Dimens.Large),
)
Text(
text = shoppingListEntity?.name.orEmpty(),
text = shoppingList?.name.orEmpty(),
modifier = Modifier.padding(start = Dimens.Medium),
)
}

View File

@@ -7,10 +7,14 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.architecture.valueUpdatesOnly
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo
import gq.kirmanak.mealient.shopping_lists.util.LoadingHelper
import gq.kirmanak.mealient.shopping_lists.util.LoadingHelperFactory
import gq.kirmanak.mealient.shopping_lists.util.LoadingState
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -22,10 +26,11 @@ class ShoppingListsViewModel @Inject constructor(
loadingHelperFactory: LoadingHelperFactory,
) : ViewModel() {
private val loadingHelper = loadingHelperFactory.create(viewModelScope) {
shoppingListsRepo.getShoppingLists()
}
val loadingState = loadingHelper.loadingState
private val loadingHelper: LoadingHelper<List<GetShoppingListsSummaryResponse>> =
loadingHelperFactory.create(viewModelScope) { shoppingListsRepo.getShoppingLists() }
val loadingState: StateFlow<LoadingState<List<GetShoppingListsSummaryResponse>>> =
loadingHelper.loadingState
private var _errorToShowInSnackbar by mutableStateOf<Throwable?>(null)
val errorToShowInSnackBar: Throwable? get() = _errorToShowInSnackbar