diff --git a/Fruitties/.editorconfig b/Fruitties/.editorconfig new file mode 100644 index 0000000..4a594a1 --- /dev/null +++ b/Fruitties/.editorconfig @@ -0,0 +1,35 @@ +[*.{kt,kts}] +# Kotlin style typically requires functions to start with a lowercase letter. +# Composable functions should start with a capital letter. +ktlint_function_naming_ignore_when_annotated_with = Composable + +# ktlint always puts a new line after a multi-line assignment, like this: +# 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), +# ) +# } +# But we actually prefer to keep some multi-line assignments on the same line, like this: +# 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), +# ) +# } +ktlint_standard_multiline-expression-wrapping = disabled diff --git a/Fruitties/androidApp/src/main/AndroidManifest.xml b/Fruitties/androidApp/src/main/AndroidManifest.xml index b5a5e47..0c6c7bd 100644 --- a/Fruitties/androidApp/src/main/AndroidManifest.xml +++ b/Fruitties/androidApp/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ 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 index fcc8383..cbac9c4 100644 --- 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 @@ -23,6 +23,7 @@ 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 index 8feab5f..e7acdc3 100644 --- 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 @@ -91,7 +91,7 @@ fun ListScreen() { topBar = { CenterAlignedTopAppBar( title = { - Text(text = stringResource(R.string.frutties),) + Text(text = stringResource(R.string.frutties)) }, colors = TopAppBarColors( containerColor = MaterialTheme.colorScheme.primary, @@ -105,7 +105,7 @@ fun ListScreen() { contentWindowInsets = WindowInsets.safeDrawing.only( // Do not include Bottom so scrolled content is drawn below system bars. // Include Horizontal because some devices have camera cutouts on the side. - WindowInsetsSides.Top + WindowInsetsSides.Horizontal + WindowInsetsSides.Top + WindowInsetsSides.Horizontal, ), ) { paddingValues -> Column( @@ -145,8 +145,8 @@ fun ListScreen() { item { Spacer( Modifier.windowInsetsBottomHeight( - WindowInsets.systemBars - ) + WindowInsets.systemBars, + ), ) } } @@ -216,7 +216,10 @@ fun FruittieItem( } @Composable -fun CartDetailsView(cart: List, modifier: Modifier = Modifier) { +fun CartDetailsView( + cart: List, + modifier: Modifier = Modifier, +) { Column( modifier.padding(horizontal = 32.dp), ) { diff --git a/Fruitties/androidApp/src/main/res/values/strings.xml b/Fruitties/androidApp/src/main/res/values/strings.xml index 0aacd3a..7036394 100644 --- a/Fruitties/androidApp/src/main/res/values/strings.xml +++ b/Fruitties/androidApp/src/main/res/values/strings.xml @@ -16,8 +16,5 @@ --> "Frutties" - Save Add - Loading - Retry \ No newline at end of file diff --git a/Fruitties/androidApp/src/main/res/xml/data_extraction_rules.xml b/Fruitties/androidApp/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..18c1f47 --- /dev/null +++ b/Fruitties/androidApp/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Fruitties/androidApp/src/main/res/xml/full_backup_content.xml b/Fruitties/androidApp/src/main/res/xml/full_backup_content.xml new file mode 100644 index 0000000..86ab695 --- /dev/null +++ b/Fruitties/androidApp/src/main/res/xml/full_backup_content.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/Fruitties/gradle/libs.versions.toml b/Fruitties/gradle/libs.versions.toml index c427f48..adf4402 100644 --- a/Fruitties/gradle/libs.versions.toml +++ b/Fruitties/gradle/libs.versions.toml @@ -16,20 +16,20 @@ agp = "8.9.1" androidx-activityCompose = "1.10.1" androidx-paging = "3.3.6" -androidx-room = "2.7.0-rc03" -androidx-lifecycle = "2.9.0-alpha13" +androidx-room = "2.7.0" +androidx-lifecycle = "2.9.0-beta01" atomicfu = "0.27.0" compose = "1.7.8" -compose-material3 = "1.3.1" +compose-material3 = "1.3.2" dataStore = "1.1.4" kotlin = "2.1.10" kotlinx-coroutines = "1.10.1" kotlinxDatetime = "0.6.1" -ksp = "2.1.10-1.0.29" +ksp = "2.1.10-1.0.30" ktorVersion = "3.0.3" pagingComposeAndroid = "3.3.6" skie = "0.10.1" -sqlite = "2.5.0-rc03" +sqlite = "2.5.0" spotless = "7.0.2" okio = "3.10.2" runner = "1.6.2" @@ -54,10 +54,7 @@ compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } -kotlinx-coroutines-core-iosarm64 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-iossimulatorarm64", version.ref = "kotlinx-coroutines" } -kotlinx-coroutines-core-iossimulatorarm64 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-iossimulatorarm64", version.ref = "kotlinx-coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } -ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktorVersion" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorVersion" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVersion" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktorVersion" } @@ -77,7 +74,6 @@ androidLibrary = { id = "com.android.library", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } -kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } skie = { id = "co.touchlab.skie", version.ref = "skie" } diff --git a/Fruitties/iosApp/iosApp.xcodeproj/project.pbxproj b/Fruitties/iosApp/iosApp.xcodeproj/project.pbxproj index 18a39cc..fedc556 100644 --- a/Fruitties/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/Fruitties/iosApp/iosApp.xcodeproj/project.pbxproj @@ -333,7 +333,7 @@ "-framework", shared, ); - PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp; + PRODUCT_BUNDLE_IDENTIFIER = com.example.fruitties.ios; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -362,7 +362,7 @@ "-framework", shared, ); - PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp; + PRODUCT_BUNDLE_IDENTIFIER = com.example.fruitties.ios; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Fruitties/iosApp/iosApp/Info.plist b/Fruitties/iosApp/iosApp/Info.plist index 8044709..9a269f5 100644 --- a/Fruitties/iosApp/iosApp/Info.plist +++ b/Fruitties/iosApp/iosApp/Info.plist @@ -25,6 +25,8 @@ UIApplicationSupportsMultipleScenes + UILaunchScreen + UIRequiredDeviceCapabilities armv7 @@ -42,7 +44,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UILaunchScreen - - \ No newline at end of file + diff --git a/Fruitties/shared/build.gradle.kts b/Fruitties/shared/build.gradle.kts index 0abf420..65f3e74 100644 --- a/Fruitties/shared/build.gradle.kts +++ b/Fruitties/shared/build.gradle.kts @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) @@ -138,7 +136,6 @@ kotlin { } } } - } dependencies { diff --git a/Fruitties/shared/src/androidMain/kotlin/com/example/fruitties/di/Factory.android.kt b/Fruitties/shared/src/androidMain/kotlin/com/example/fruitties/di/Factory.android.kt index eb2fb30..9391469 100644 --- a/Fruitties/shared/src/androidMain/kotlin/com/example/fruitties/di/Factory.android.kt +++ b/Fruitties/shared/src/androidMain/kotlin/com/example/fruitties/di/Factory.android.kt @@ -20,29 +20,31 @@ import androidx.room.Room import androidx.sqlite.driver.bundled.BundledSQLiteDriver import com.example.fruitties.database.AppDatabase import com.example.fruitties.database.CartDataStore -import com.example.fruitties.database.dbFileName +import com.example.fruitties.database.DB_FILE_NAME import com.example.fruitties.network.FruittieApi import kotlinx.coroutines.Dispatchers -actual class Factory(private val app: Application) { +actual class Factory( + private val app: Application, +) { actual fun createRoomDatabase(): AppDatabase { - val dbFile = app.getDatabasePath(dbFileName) - return Room.databaseBuilder( - context = app, - name = dbFile.absolutePath, - ) - .setDriver(BundledSQLiteDriver()) + val dbFile = app.getDatabasePath(DB_FILE_NAME) + return Room + .databaseBuilder( + context = app, + name = dbFile.absolutePath, + ).setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) .build() } - actual fun createCartDataStore(): CartDataStore { - return CartDataStore { - app.filesDir.resolve( - "cart.json", - ).absolutePath + actual fun createCartDataStore(): CartDataStore = + CartDataStore { + app.filesDir + .resolve( + "cart.json", + ).absolutePath } - } actual fun createApi(): FruittieApi = commonCreateApi() } diff --git a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/DataRepository.kt b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/DataRepository.kt index 82a25a1..9adc853 100644 --- a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/DataRepository.kt +++ b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/DataRepository.kt @@ -57,9 +57,7 @@ class DataRepository( return loadData() } - fun loadData(): Flow> { - return database.fruittieDao().getAllAsFlow() - } + fun loadData(): Flow> = database.fruittieDao().getAllAsFlow() suspend fun refreshData() { val response = api.getData() diff --git a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/AppDatabase.kt b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/AppDatabase.kt index 495eccb..1182dfb 100644 --- a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/AppDatabase.kt +++ b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/AppDatabase.kt @@ -32,4 +32,5 @@ abstract class AppDatabase : RoomDatabase() { expect object AppDatabaseConstructor : RoomDatabaseConstructor { override fun initialize(): AppDatabase } -internal const val dbFileName = "fruits.db" + +internal const val DB_FILE_NAME = "fruits.db" diff --git a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/CartDataStore.kt b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/CartDataStore.kt index 4c9f3e9..2cd8afb 100644 --- a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/CartDataStore.kt +++ b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/CartDataStore.kt @@ -33,22 +33,28 @@ import okio.use data class Cart( val items: List, ) + @Serializable data class CartItem( val id: Long, val count: Int, ) + internal object CartJsonSerializer : OkioSerializer { override val defaultValue: Cart = Cart(emptyList()) - override suspend fun readFrom(source: BufferedSource): Cart { - return json.decodeFromString(source.readUtf8()) - } - override suspend fun writeTo(t: Cart, sink: BufferedSink) { + + override suspend fun readFrom(source: BufferedSource): Cart = json.decodeFromString(source.readUtf8()) + + override suspend fun writeTo( + t: Cart, + sink: BufferedSink, + ) { sink.use { it.writeUtf8(json.encodeToString(Cart.serializer(), t)) } } } + class CartDataStore( private val produceFilePath: () -> String, ) { @@ -63,9 +69,15 @@ class CartDataStore( ) val cart: Flow get() = db.data + suspend fun add(fruittie: Fruittie) = update(fruittie, 1) + suspend fun remove(fruittie: Fruittie) = update(fruittie, -1) - suspend fun update(fruittie: Fruittie, diff: Int) { + + suspend fun update( + fruittie: Fruittie, + diff: Int, + ) { db.updateData { prevCart -> val newItems = mutableListOf() var found = false diff --git a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/FruittieDao.kt b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/FruittieDao.kt index 5e095fb..beab284 100644 --- a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/FruittieDao.kt +++ b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/database/FruittieDao.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.Flow @Dao interface FruittieDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(fruittie: Fruittie) @@ -46,5 +45,5 @@ interface FruittieDao { @MapColumn(columnName = "id") Long, Fruittie, - > + > } diff --git a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/di/Factory.kt b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/di/Factory.kt index 2e25bc8..079df30 100644 --- a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/di/Factory.kt +++ b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/di/Factory.kt @@ -27,17 +27,20 @@ import kotlinx.serialization.json.Json expect class Factory { fun createRoomDatabase(): AppDatabase + fun createApi(): FruittieApi + fun createCartDataStore(): CartDataStore } -internal fun commonCreateApi(): FruittieApi = FruittieNetworkApi( - client = HttpClient { - install(ContentNegotiation) { - json(json, contentType = ContentType.Any) - } - }, - apiUrl = "https://android.github.io/kotlin-multiplatform-samples/fruitties-api", -) +internal fun commonCreateApi(): FruittieApi = + FruittieNetworkApi( + client = HttpClient { + install(ContentNegotiation) { + json(json, contentType = ContentType.Any) + } + }, + apiUrl = "https://android.github.io/kotlin-multiplatform-samples/fruitties-api", + ) val json = Json { ignoreUnknownKeys = true } diff --git a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/network/FruittieApi.kt b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/network/FruittieApi.kt index f1f4f92..0da71cc 100644 --- a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/network/FruittieApi.kt +++ b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/network/FruittieApi.kt @@ -25,8 +25,10 @@ interface FruittieApi { suspend fun getData(pageNumber: Int = 0): FruittiesResponse } -class FruittieNetworkApi(private val client: HttpClient, private val apiUrl: String) : FruittieApi { - +class FruittieNetworkApi( + private val client: HttpClient, + private val apiUrl: String, +) : FruittieApi { override suspend fun getData(pageNumber: Int): FruittiesResponse { val url = "$apiUrl/$pageNumber.json" return try { diff --git a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/viewmodel/MainViewModel.kt b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/viewmodel/MainViewModel.kt index 81e14f2..6bbc342 100644 --- a/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/viewmodel/MainViewModel.kt +++ b/Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/viewmodel/MainViewModel.kt @@ -33,10 +33,13 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -class MainViewModel(private val repository: DataRepository) : ViewModel() { - +class MainViewModel( + private val repository: DataRepository, +) : ViewModel() { val homeUiState: StateFlow = - repository.getData().map { HomeUiState(it) } + repository + .getData() + .map { HomeUiState(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), @@ -44,7 +47,8 @@ class MainViewModel(private val repository: DataRepository) : ViewModel() { ) val cartUiState: StateFlow = - repository.cartDetails.map { CartUiState(it) } + repository.cartDetails + .map { CartUiState(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), @@ -58,7 +62,6 @@ class MainViewModel(private val repository: DataRepository) : ViewModel() { } companion object { - val APP_CONTAINER_KEY = CreationExtras.Key() val Factory: ViewModelProvider.Factory = viewModelFactory { @@ -90,11 +93,15 @@ class MainViewModel(private val repository: DataRepository) : ViewModel() { /** * Ui State for the home screen */ -data class HomeUiState(val fruitties: List = listOf()) +data class HomeUiState( + val fruitties: List = listOf(), +) /** * Ui State for the cart */ -data class CartUiState(val cartDetails: List = listOf()) +data class CartUiState( + val cartDetails: List = listOf(), +) private const val TIMEOUT_MILLIS = 5_000L diff --git a/Fruitties/shared/src/iosMain/kotlin/com/example/fruitties/di/Factory.native.kt b/Fruitties/shared/src/iosMain/kotlin/com/example/fruitties/di/Factory.native.kt index 87d1cc0..65bf22f 100644 --- a/Fruitties/shared/src/iosMain/kotlin/com/example/fruitties/di/Factory.native.kt +++ b/Fruitties/shared/src/iosMain/kotlin/com/example/fruitties/di/Factory.native.kt @@ -19,7 +19,7 @@ import androidx.room.Room import androidx.sqlite.driver.bundled.BundledSQLiteDriver import com.example.fruitties.database.AppDatabase import com.example.fruitties.database.CartDataStore -import com.example.fruitties.database.dbFileName +import com.example.fruitties.database.DB_FILE_NAME import com.example.fruitties.network.FruittieApi import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.coroutines.Dispatchers @@ -31,20 +31,19 @@ import platform.Foundation.NSUserDomainMask actual class Factory { actual fun createRoomDatabase(): AppDatabase { - val dbFile = "${fileDirectory()}/$dbFileName" - return Room.databaseBuilder( - name = dbFile, - ) - .setDriver(BundledSQLiteDriver()) + val dbFile = "${fileDirectory()}/$DB_FILE_NAME" + return Room + .databaseBuilder( + name = dbFile, + ).setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) .build() } - actual fun createCartDataStore(): CartDataStore { - return CartDataStore { + actual fun createCartDataStore(): CartDataStore = + CartDataStore { "${fileDirectory()}/cart.json" } - } @OptIn(ExperimentalForeignApi::class) private fun fileDirectory(): String { diff --git a/Fruitties/shared/src/iosMain/kotlin/com/example/fruitties/di/viewmodel/IOSViewModelOwner.kt b/Fruitties/shared/src/iosMain/kotlin/com/example/fruitties/di/viewmodel/IOSViewModelOwner.kt index 26c989c..036946a 100644 --- a/Fruitties/shared/src/iosMain/kotlin/com/example/fruitties/di/viewmodel/IOSViewModelOwner.kt +++ b/Fruitties/shared/src/iosMain/kotlin/com/example/fruitties/di/viewmodel/IOSViewModelOwner.kt @@ -11,7 +11,9 @@ import com.example.fruitties.viewmodel.MainViewModel * This is used with from iOS with Kotlin Multiplatform (KMP). */ @Suppress("unused") // Android Studio is not aware of iOS usage. -class IOSViewModelOwner(appContainer: AppContainer) : ViewModelStoreOwner { +class IOSViewModelOwner( + appContainer: AppContainer, +) : ViewModelStoreOwner { override val viewModelStore: ViewModelStore = ViewModelStore() // Create an instance of MainViewModel with the CreationExtras.