diff --git a/Fruitties/.gitignore b/Fruitties/.gitignore new file mode 100644 index 0000000..e510fa9 --- /dev/null +++ b/Fruitties/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +.idea +.DS_Store +build +captures +.externalNativeBuild +.cxx +local.properties +xcuserdata \ No newline at end of file diff --git a/Fruitties/androidApp/build.gradle.kts b/Fruitties/androidApp/build.gradle.kts new file mode 100644 index 0000000..cd5351b --- /dev/null +++ b/Fruitties/androidApp/build.gradle.kts @@ -0,0 +1,65 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.kotlinAndroid) +} + +android { + namespace = "com.example.fruitties.android" + compileSdk = 34 + defaultConfig { + applicationId = "com.example.fruitties.android" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(projects.shared) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.paging.compose.android) + implementation(libs.androidx.viewmodel.compose) + debugImplementation(libs.compose.ui.tooling) +} diff --git a/Fruitties/androidApp/src/main/AndroidManifest.xml b/Fruitties/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b5a5e47 --- /dev/null +++ b/Fruitties/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Fruitties/androidApp/src/main/java/com/example/fruitties/android/MainActivity.kt b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/MainActivity.kt new file mode 100644 index 0000000..30e6778 --- /dev/null +++ b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/MainActivity.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fruitties.android + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import com.example.fruitties.android.ui.ListScreen + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MyApplicationTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + ListScreen() + } + } + } + } +} diff --git a/Fruitties/androidApp/src/main/java/com/example/fruitties/android/MyApplicationTheme.kt b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/MyApplicationTheme.kt new file mode 100644 index 0000000..2181632 --- /dev/null +++ b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/MyApplicationTheme.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.fruitties.android + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun MyApplicationTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colors = if (darkTheme) { + darkColorScheme( + primary = Color(0xFFBB86FC), + secondary = Color(0xFF03DAC5), + tertiary = Color(0xFF3700B3), + ) + } else { + lightColorScheme( + primary = Color(0xFF6200EE), + secondary = Color(0xFF03DAC5), + tertiary = Color(0xFF3700B3), + ) + } + val typography = Typography( + bodyMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + ), + ) + val shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp), + ) + + MaterialTheme( + colorScheme = colors, + typography = typography, + shapes = shapes, + content = content, + ) +} diff --git a/Fruitties/androidApp/src/main/java/com/example/fruitties/android/di/App.kt b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/di/App.kt new file mode 100644 index 0000000..fcc8383 --- /dev/null +++ b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/di/App.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fruitties.android.di + +import android.app.Application +import com.example.fruitties.di.AppContainer +import com.example.fruitties.di.Factory + +class App : Application() { + /** AppContainer instance used by the rest of classes to obtain dependencies */ + lateinit var container: AppContainer + override fun onCreate() { + super.onCreate() + container = AppContainer(Factory(this)) + } +} diff --git a/Fruitties/androidApp/src/main/java/com/example/fruitties/android/ui/ListScreen.kt b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/ui/ListScreen.kt new file mode 100644 index 0000000..060afa5 --- /dev/null +++ b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/ui/ListScreen.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fruitties.android.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.fruitties.android.R +import com.example.fruitties.database.CartItemDetails +import com.example.fruitties.model.Fruittie + +@Composable +fun ListScreen(viewModel: MainViewModel = viewModel(factory = MainViewModel.Factory)) { + val uiState by viewModel.uiState.collectAsState() + val cartState by viewModel.cartUiState.collectAsState() + + Scaffold( + topBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(MaterialTheme.colorScheme.primary), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.frutties), + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .padding(vertical = 16.dp, horizontal = 16.dp) + .weight(1.0f), + textAlign = TextAlign.Center, + ) + } + }, + ) { + Column( + modifier = Modifier.padding(it), + ) { + var expanded by remember { mutableStateOf(false) } + Row (modifier = Modifier.padding(16.dp)) { + Text( + text = "Cart has ${cartState.itemList.count()} items", + modifier = Modifier.weight(1f).padding(12.dp) + ) + Button(onClick = { expanded = !expanded } ) { + Text(text = if (expanded) "collapse" else "expand") + } + } + AnimatedVisibility( + visible = expanded, + enter = fadeIn(animationSpec = tween(1000)), + exit = fadeOut(animationSpec = tween(1000)) + ) { + CardDetailsView(cartState.itemList) + } + + LazyColumn { + items(items = uiState.itemList, key = { it.id }) { item -> + FruittieItem( + item = item, + onAddToCart = viewModel::addItemToCart, + ) + } + } + } + } +} + +@Composable +fun FruittieItem( + item: Fruittie, + onAddToCart: (fruittie: Fruittie) -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .clip(RoundedCornerShape(8.dp)), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp, + ), + ) { + Column { + Text( + text = item.name, + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 8.dp), + ) + Row { + Text( + text = item.fullName, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.padding(vertical = 4.dp)) + Box( + modifier = Modifier + .height(50.dp) + .fillMaxWidth(), + ) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .align(Alignment.TopEnd), + verticalAlignment = Alignment.CenterVertically, + ) { + Button(onClick = { onAddToCart(item) }) { + Text(stringResource(R.string.add)) + } + } + } + } + } + } +} + +@Composable +fun CardDetailsView(cart: List, modifier: Modifier = Modifier) { + Column ( + modifier.padding(horizontal = 32.dp) + ){ + cart.forEach { item -> + Text(text = "${item.fruittie.name} : ${item.count}") + } + } +} + +@Preview +@Composable +fun ItemPreview() { + FruittieItem( + Fruittie(name = "Fruit", fullName = "Fruitus Mangorus", calories = "240"), + onAddToCart = {}, + ) +} diff --git a/Fruitties/androidApp/src/main/java/com/example/fruitties/android/ui/ListViewModel.kt b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/ui/ListViewModel.kt new file mode 100644 index 0000000..405e017 --- /dev/null +++ b/Fruitties/androidApp/src/main/java/com/example/fruitties/android/ui/ListViewModel.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fruitties.android.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.example.fruitties.DataRepository +import com.example.fruitties.android.di.App +import com.example.fruitties.database.CartItemDetails +import com.example.fruitties.model.Fruittie +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class MainViewModel(private val repository: DataRepository) : ViewModel() { + + val uiState: StateFlow = + repository.getData().map { HomeUiState(it) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), + initialValue = HomeUiState() + ) + + val cartUiState: StateFlow = + repository.cartDetails.map { CartUiState(it.items) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), + initialValue = CartUiState() + ) + + fun addItemToCart(fruittie: Fruittie) { + viewModelScope.launch { + repository.addToCart(fruittie) + } + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val application = (this[APPLICATION_KEY] as App) + val repository = application.container.dataRepository + MainViewModel(repository = repository) + } + } + } +} + +/** + * Ui State for ListScreen + */ +data class HomeUiState(val itemList: List = listOf()) + +/** + * Ui State for Cart + */ +data class CartUiState(val itemList: List = listOf()) + +private const val TIMEOUT_MILLIS = 5_000L diff --git a/Fruitties/androidApp/src/main/res/values/strings.xml b/Fruitties/androidApp/src/main/res/values/strings.xml new file mode 100644 index 0000000..3cdffa3 --- /dev/null +++ b/Fruitties/androidApp/src/main/res/values/strings.xml @@ -0,0 +1,23 @@ + + + + "Frutties " + Save + Add + Loading + Retry + \ No newline at end of file diff --git a/Fruitties/androidApp/src/main/res/values/styles.xml b/Fruitties/androidApp/src/main/res/values/styles.xml new file mode 100644 index 0000000..89e4abf --- /dev/null +++ b/Fruitties/androidApp/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + +