Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Fruitties ViewModel on iOS #34

Merged
merged 19 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.fruitties.android.R
import com.example.fruitties.android.di.App
import com.example.fruitties.database.CartItemDetails
import com.example.fruitties.model.CartItemDetails
import com.example.fruitties.model.Fruittie
import com.example.fruitties.viewmodel.MainViewModel

Expand All @@ -79,7 +79,7 @@ fun ListScreen() {
extras = extras,
)

val uiState by viewModel.uiState.collectAsState()
val uiState by viewModel.homeUiState.collectAsState()
val cartState by viewModel.cartUiState.collectAsState()

Scaffold(
Expand Down Expand Up @@ -108,7 +108,7 @@ fun ListScreen() {
var expanded by remember { mutableStateOf(false) }
Row(modifier = Modifier.padding(16.dp)) {
Text(
text = "Cart has ${cartState.itemList.count()} items",
text = "Item types in cart: ${cartState.cartDetails.count()}",
cartland marked this conversation as resolved.
Show resolved Hide resolved
modifier = Modifier.weight(1f).padding(12.dp),
)
Button(onClick = { expanded = !expanded }) {
Expand All @@ -120,11 +120,11 @@ fun ListScreen() {
enter = fadeIn(animationSpec = tween(1000)),
exit = fadeOut(animationSpec = tween(1000)),
) {
CartDetailsView(cartState.itemList)
CartDetailsView(cartState.cartDetails)
}

LazyColumn {
items(items = uiState.itemList, key = { it.id }) { item ->
items(items = uiState.fruitties, key = { it.id }) { item ->
FruittieItem(
item = item,
onAddToCart = viewModel::addItemToCart,
Expand Down
18 changes: 9 additions & 9 deletions Fruitties/iosApp/iosApp/CartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ import SwiftUI
import shared

struct CartView : View {
let cart: Cart
let dataRepository: DataRepository
let cartDetails: [CartItemDetails]
let mainViewModel: MainViewModel
@State
private var expanded = false

var body: some View {
if (cart.items.isEmpty) {
if (cartDetails.isEmpty) {
Text("Cart is empty, add some items").padding()
} else {
HStack {
Text("Cart has \(cart.items.count) items").padding()
Text("Item types in cart: \(cartDetails.count)").padding()
Spacer()
Button {
expanded.toggle()
Expand All @@ -42,22 +42,22 @@ struct CartView : View {
}.padding()
}
if (expanded) {
CartDetailsView(dataRepository: dataRepository)
CartDetailsView(mainViewModel: mainViewModel)
}
}
}
}

struct CartDetailsView: View {
let dataRepository: DataRepository
let mainViewModel: MainViewModel
@State
private var details: CartDetails = CartDetails(items: [])
private var cartUiState: CartUiState = CartUiState(cartDetails: [])
cartland marked this conversation as resolved.
Show resolved Hide resolved

var body: some View {
VStack {
ForEach(details.items, id: \.fruittie.id) { item in
ForEach(cartUiState.cartDetails, id: \.fruittie.id) { item in
Text("\(item.fruittie.name): \(item.count)")
}
}.collectWithLifecycle(dataRepository.cartDetails, binding: $details)
}.collectWithLifecycle(mainViewModel.cartUiState, binding: $cartUiState)
}
}
34 changes: 17 additions & 17 deletions Fruitties/iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import Foundation

struct ContentView: View {
@ObservedObject var uiModel: UIModel
init(appContainer: AppContainer) {
self.uiModel = UIModel(dataRepository: appContainer.dataRepository)
init(mainViewModel: MainViewModel) {
self.uiModel = UIModel(mainViewModel: mainViewModel)
}

var body: some View {
Text("Fruitties").font(.largeTitle).fontWeight(.bold)
CartView(cart: uiModel.cart, dataRepository: uiModel.dataRepository)
CartView(cartDetails: uiModel.cartDetails, mainViewModel: uiModel.mainViewModel)
ScrollView {
LazyVStack {
ForEach(uiModel.fruitties, id: \.self) { value in
Expand Down Expand Up @@ -67,37 +67,37 @@ struct FruittieView: View {
}

class UIModel: ObservableObject {
cartland marked this conversation as resolved.
Show resolved Hide resolved
let dataRepository : DataRepository
init(dataRepository: DataRepository) {
self.dataRepository = dataRepository
let mainViewModel: MainViewModel
init(mainViewModel: MainViewModel) {
self.mainViewModel = mainViewModel
}
@Published
private(set) var fruitties: [Fruittie] = []
@Published
private(set) var cart: Cart = Cart(items: [])
private(set) var cartDetails: [CartItemDetails] = []

@MainActor
func observeDatabase() async {
for await fruitties in dataRepository.getData() {
self.fruitties = fruitties
func observeHomeUiState() async {
cartland marked this conversation as resolved.
Show resolved Hide resolved
for await homeUiState in mainViewModel.homeUiState {
self.fruitties = homeUiState.fruitties
}
}

@MainActor
func watchCart() async {
for await cart in dataRepository.getCart() {
self.cart = cart
func observeCartUiState() async {
for await cartUiState in mainViewModel.cartUiState {
self.cartDetails = cartUiState.cartDetails
}
}

func addToCart(fruittie: Fruittie) async {
cartland marked this conversation as resolved.
Show resolved Hide resolved
try? await dataRepository.addToCart(fruittie: fruittie)
mainViewModel.addItemToCart(fruittie: fruittie)
}

@MainActor
func activate() async {
async let db: () = observeDatabase()
async let cartUpdate: () = watchCart()
await (db, cartUpdate)
async let home: () = observeHomeUiState()
async let cart: () = observeCartUiState()
await (home, cart)
}
}
4 changes: 3 additions & 1 deletion Fruitties/iosApp/iosApp/iOSApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ struct iOSApp: App {
let appContainer = AppContainer(factory: Factory())
var body: some Scene {
WindowGroup {
ContentView(appContainer: appContainer)
// TODO: Find a better owner for the ViewModelStore.
let iosViewModelOwner = IOSViewModelOwner(appContainer: appContainer)
ContentView(mainViewModel: iosViewModelOwner.mainViewModel)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@
package com.example.fruitties

import com.example.fruitties.database.AppDatabase
import com.example.fruitties.database.Cart
import com.example.fruitties.database.CartDataStore
import com.example.fruitties.database.CartDetails
import com.example.fruitties.database.CartItemDetails
import com.example.fruitties.model.CartItemDetails
import com.example.fruitties.model.Fruittie
import com.example.fruitties.network.FruittieApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.count
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch

Expand All @@ -36,27 +33,21 @@ class DataRepository(
private val scope: CoroutineScope,
) {
@OptIn(ExperimentalCoroutinesApi::class)
val cartDetails: Flow<CartDetails>
val cartDetails: Flow<List<CartItemDetails>>
get() = cartDataStore.cart.mapLatest {
val ids = it.items.map { it.id }
val fruitties = database.fruittieDao().loadMapped(ids)
CartDetails(
items = it.items.mapNotNull {
fruitties[it.id]?.let { fruittie ->
CartItemDetails(fruittie, it.count)
}
},
)
it.items.mapNotNull {
fruitties[it.id]?.let { fruittie ->
CartItemDetails(fruittie, it.count)
}
}
}

suspend fun addToCart(fruittie: Fruittie) {
cartDataStore.add(fruittie)
}

fun getCart(): Flow<Cart> {
return cartDataStore.cart
}

fun getData(): Flow<List<Fruittie>> {
scope.launch {
if (database.fruittieDao().count() < 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,11 @@ import okio.use
data class Cart(
val items: List<CartItem>,
)
class CartDetails(
val items: List<CartItemDetails>,
)

@Serializable
data class CartItem(
val id: Long,
val count: Int,
)
data class CartItemDetails(
val fruittie: Fruittie,
val count: Int,
)
internal object CartJsonSerializer : OkioSerializer<Cart> {
override val defaultValue: Cart = Cart(emptyList())
override suspend fun readFrom(source: BufferedSource): Cart {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import kotlinx.coroutines.SupervisorJob
class AppContainer(
private val factory: Factory,
) {

val dataRepository: DataRepository by lazy {
DataRepository(
api = factory.createApi(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ data class Fruittie(
@SerialName("calories")
val calories: String,
)

data class CartItemDetails(
val fruittie: Fruittie,
val count: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.fruitties.DataRepository
import com.example.fruitties.database.CartItemDetails
import com.example.fruitties.di.AppContainer
import com.example.fruitties.model.CartItemDetails
import com.example.fruitties.model.Fruittie
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -34,7 +34,7 @@ import kotlinx.coroutines.launch

class MainViewModel(private val repository: DataRepository) : ViewModel() {

val uiState: StateFlow<HomeUiState> =
val homeUiState: StateFlow<HomeUiState> =
repository.getData().map { HomeUiState(it) }
.stateIn(
scope = viewModelScope,
Expand All @@ -43,7 +43,7 @@ class MainViewModel(private val repository: DataRepository) : ViewModel() {
)

val cartUiState: StateFlow<CartUiState> =
repository.cartDetails.map { CartUiState(it.items) }
repository.cartDetails.map { CartUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
Expand Down Expand Up @@ -73,11 +73,11 @@ class MainViewModel(private val repository: DataRepository) : ViewModel() {
/**
* Ui State for ListScreen
*/
data class HomeUiState(val itemList: List<Fruittie> = listOf())
data class HomeUiState(val fruitties: List<Fruittie> = listOf())

/**
* Ui State for Cart
*/
data class CartUiState(val itemList: List<CartItemDetails> = listOf())
data class CartUiState(val cartDetails: List<CartItemDetails> = listOf())

private const val TIMEOUT_MILLIS = 5_000L
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.fruitties.di.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
import com.example.fruitties.di.AppContainer
import com.example.fruitties.viewmodel.MainViewModel
import kotlin.reflect.KClass

class IOSViewModelOwner(private val appContainer: AppContainer) : ViewModelStoreOwner {
cartland marked this conversation as resolved.
Show resolved Hide resolved
private var _viewModelStore: ViewModelStore? = null
override val viewModelStore: ViewModelStore
get() = _viewModelStore ?: ViewModelStore().also {
this._viewModelStore = it
}

val mainViewModel: MainViewModel = viewModel(
modelClass = MainViewModel::class,
viewModelStoreOwner = this,
factory = MainViewModel.Factory,
extras = MutableCreationExtras().apply {
set(MainViewModel.APP_CONTAINER_KEY, appContainer)
} as CreationExtras,
)

// TODO: Clear the ViewModelStore when going out of scope.
// fun clearViewModelStore() {
// viewModelStore.clear()
// }
}

private fun <VM : ViewModel> viewModel(
modelClass: KClass<VM>,
viewModelStoreOwner: ViewModelStoreOwner,
key: String? = null,
factory: ViewModelProvider.Factory? = null,
extras: CreationExtras = CreationExtras.Empty
): VM {
val provider =
if (factory != null) {
ViewModelProvider.create(viewModelStoreOwner.viewModelStore, factory, extras)
} else {
ViewModelProvider.create(viewModelStoreOwner)
}
return if (key != null) {
provider[key, modelClass]
} else {
provider[modelClass]
}
}
Loading