├── .gitignore ├── LICENSE ├── README.md ├── androidApp ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── nagyrobi144 │ │ └── dogify │ │ └── android │ │ ├── AppModule.kt │ │ ├── DogifyApplication.kt │ │ ├── MainActivity.kt │ │ ├── MainScreen.kt │ │ └── MainViewModel.kt │ └── res │ └── values │ ├── colors.xml │ └── styles.xml ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── Podfile ├── iosApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── iosApp │ ├── BreedUIView.swift │ ├── BreedsGridUIView.swift │ ├── ContentView.swift │ ├── Info.plist │ ├── MainViewModel.swift │ └── iOSApp.swift ├── settings.gradle.kts └── shared ├── .gitignore ├── build.gradle.kts └── src ├── androidMain ├── AndroidManifest.xml └── kotlin │ └── com │ └── nagyrobi144 │ └── dogify │ ├── database │ └── DriverFactory.kt │ └── util │ └── DispatcherProvider.kt ├── androidTest └── kotlin │ └── com │ └── nagyrobi144 │ └── dogify │ └── Platform.kt ├── commonMain ├── kotlin │ └── com │ │ └── nagyrobi144 │ │ └── dogify │ │ ├── api │ │ ├── BreedsApi.kt │ │ ├── KtorApi.kt │ │ └── model │ │ │ ├── BreedImageResponse.kt │ │ │ └── BreedsResponse.kt │ │ ├── database │ │ └── DriverFactory.kt │ │ ├── di │ │ └── KoinModule.kt │ │ ├── model │ │ └── Breed.kt │ │ ├── repository │ │ ├── BreedsLocalSource.kt │ │ ├── BreedsRemoteSource.kt │ │ ├── BreedsRepository.kt │ │ ├── DefaultBreedsLocalSource.kt │ │ └── DefaultBreedsRemoteSource.kt │ │ ├── usecase │ │ ├── FetchBreedsUseCase.kt │ │ ├── GetBreedsUseCase.kt │ │ └── ToggleFavouriteStateUseCase.kt │ │ └── util │ │ └── DispatcherProvider.kt └── sqldelight │ └── com │ └── nagyrobi144 │ └── dogify │ └── db │ └── Breeds.sq ├── commonTest └── kotlin │ └── com │ └── nagyrobi144 │ └── dogify │ ├── BreedsRepositoryTest.kt │ ├── FakeBreedsLocalSource.kt │ ├── FakeBreedsRemoteSource.kt │ └── Platform.kt ├── iosMain └── kotlin │ └── com │ └── nagyrobi144 │ └── dogify │ ├── database │ └── DriverFactory.kt │ └── util │ └── DispatcherProvider.kt └── iosTest └── kotlin └── com └── nagyrobi144 └── dogify └── Platform.kt /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle GUI config 2 | gradle-app.setting 3 | 4 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 5 | !gradle-wrapper.jar 6 | 7 | # Cache of project 8 | .gradletasknamecache 9 | 10 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 11 | # gradle/wrapper/gradle-wrapper.properties 12 | 13 | .idea 14 | *.iml 15 | .gradle 16 | /local.properties 17 | /.idea/caches 18 | /.idea/libraries 19 | /.idea/modules.xml 20 | /.idea/workspace.xml 21 | /.idea/navEditor.xml 22 | /.idea/assetWizardSettings.xml 23 | .DS_Store 24 | /build 25 | /captures 26 | 27 | 28 | *.xcworkspacedata 29 | *.xcuserstate 30 | *.xcscheme 31 | xcschememanagement.plist 32 | *.xcbkptlist 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Simplifying Application Development with Kotlin Multiplatform Mobile 5 | 6 | Simplifying Application Development with Kotlin Multiplatform Mobile 7 | 8 | This is the code repository for [Simplifying Application Development with Kotlin Multiplatform Mobile](https://www.packtpub.com/product/simplifying-application-development-with-kotlin-multiplatform-mobile/9781801812580), published by Packt. 9 | 10 | **Write robust native applications for iOS and Android efficiently** 11 | 12 | ## What is this book about? 13 | Sharing code between platforms can help developers gain a competitive edge, and Kotlin Multiplatform Mobile (KMM) offers a sensible way to do it. KMM helps mobile teams share code between Android and iOS in a flexible way, leaving room for native development. 14 | 15 | This book covers the following exciting features: 16 | * Get acquainted with the multiplatform approach and KMM's competitive edge 17 | * Understand how Kotlin Multiplatform works under the hood 18 | * Get up and running with the Kotlin language quickly in the context of Swift 19 | * Find out how to share code between Android and iOS 20 | * Explore tips and best practices in KMM to increase app development efficiency 21 | * Discover adoption tips to integrate KMM into existing or new production apps 22 | 23 | If you feel this book is for you, get your [copy](https://www.amazon.com/Simplifying-Application-Development-Kotlin-Multiplatform/dp/1801812586) today! 24 | 25 | 26 | ## Instructions and Navigations 27 | All of the code is organized into folders. For example, Chapter05. 28 | 29 | The code will look like the following: 30 | ``` 31 | android { 32 | compileSdkVersion(30) 33 | sourceSets["main"].manifest.srcFile 34 | ("src/androidMain/AndroidManifest.xml") 35 | defaultConfig { 36 | minSdkVersion(23) 37 | targetSdkVersion(30) 38 | } 39 | } 40 | ``` 41 | 42 | **Following is what you need for this book:** 43 | This book is for native Android and iOS developers who want to build high-quality apps using an efficient development process. Knowledge of the framework and the languages used is necessary, that is, Android with Java or Kotlin and iOS with Objective-C or Swift. For Swift developers, the book assumes no knowledge of Kotlin as this will be covered in the context of Swift. 44 | 45 | With the following software and hardware list you can run all code files present in the book (Chapter 1-10). 46 | 47 | ### Software and Hardware List 48 | | Chapter | Software/Hardware required | OS required | 49 | | -------- | ------------------------------------ | ----------------------------------- | 50 | | 1-10 | Android Studio Artic Fox | Windows, Mac OS X, and Linux | 51 | | 1-10 | Android Studio KMM Plugin| Windows, Mac OS X, and Linux | 52 | 53 | 54 | We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it](https://static.packt-cdn.com/downloads/9781801812580_ColorImages.pdf). 55 | 56 | ### Related products 57 | * How to Build Android Apps with Kotlin [[Packt]](https://www.packtpub.com/product/how-to-build-android-apps-with-kotlin/9781838984113) [[Amazon]](https://www.amazon.com/Build-Android-Apps-Kotlin-hands/dp/1838984119) 58 | 59 | * Android UI Development with Jetpack Compose [[Packt]](https://www.packtpub.com/product/android-ui-development-with-jetpack-compose/9781801812160) [[Amazon]](https://www.amazon.com/Android-Development-Jetpack-Compose-declarative-dp-1801812160/dp/1801812160/ref=mt_other?_encoding=UTF8&me=&qid=) 60 | 61 | 62 | ## Get to Know the Author 63 | **Róbert Nagy** 64 | is a Senior Android Developer at Octopus Energy. He is an Android and Kotlin developer with a Bachelor of Science in Computer Science. He has designed, developed, and maintained multiple sophisticated Android apps ranging from 100K+ downloads to 10M+ in the financial, IoT, health, social, and energy industries. Some projects that he has been a part of include a social platform for kids, a lightning system controller, and Bloom and Wild. 65 | ### Download a free PDF 66 | 67 | If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.
68 |

https://packt.link/free-ebook/9781801812580

-------------------------------------------------------------------------------- /androidApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | } 5 | 6 | val composeVersion = "1.1.0-rc01" 7 | 8 | dependencies { 9 | implementation(project(":shared")) 10 | implementation("androidx.appcompat:appcompat:1.4.1") 11 | // Android Lifecycle 12 | val lifecycleVersion = "2.3.1" 13 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") 14 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0") 15 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0"){ 16 | version { 17 | strictly("1.5.0-native-mt") 18 | } 19 | } 20 | // Android Kotlin extensions 21 | implementation("androidx.core:core-ktx:1.7.0") 22 | //region Jetpack Compose 23 | implementation("androidx.activity:activity-compose:1.4.0") 24 | implementation("androidx.compose.ui:ui:$composeVersion") 25 | // Tooling support (Previews, etc.) 26 | implementation("androidx.compose.ui:ui-tooling:1.2.0-alpha01") 27 | // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) 28 | implementation("androidx.compose.foundation:foundation:$composeVersion") 29 | // Material Design 30 | implementation("androidx.compose.material:material:$composeVersion") 31 | // Material design icons 32 | implementation("androidx.compose.material:material-icons-core:$composeVersion") 33 | implementation("androidx.compose.material:material-icons-extended:$composeVersion") 34 | implementation("io.coil-kt:coil-compose:1.4.0") 35 | implementation("com.google.accompanist:accompanist-swiperefresh:0.24.0-alpha") 36 | //endregion Jetpack compose 37 | } 38 | 39 | android { 40 | compileSdk = 31 41 | defaultConfig { 42 | applicationId = "com.nagyrobi144.dogify.android" 43 | minSdk = 23 44 | targetSdk = 31 45 | versionCode = 1 46 | versionName = "1.0" 47 | } 48 | buildTypes { 49 | getByName("release") { 50 | isMinifyEnabled = false 51 | } 52 | } 53 | 54 | buildFeatures { 55 | compose = true 56 | } 57 | 58 | compileOptions { 59 | sourceCompatibility = JavaVersion.VERSION_1_8 60 | targetCompatibility = JavaVersion.VERSION_1_8 61 | } 62 | 63 | kotlinOptions { 64 | jvmTarget = "1.8" 65 | } 66 | 67 | composeOptions { 68 | kotlinCompilerExtensionVersion = "1.1.0-rc02" 69 | } 70 | } -------------------------------------------------------------------------------- /androidApp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /androidApp/src/main/java/com/nagyrobi144/dogify/android/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.android 2 | 3 | import org.koin.androidx.viewmodel.dsl.viewModel 4 | import org.koin.dsl.module 5 | 6 | val viewModelModule = module { 7 | viewModel { MainViewModel(get(), get(), get(), get()) } 8 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/com/nagyrobi144/dogify/android/DogifyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.android 2 | 3 | import android.app.Application 4 | import com.nagyrobi144.dogify.di.initKoin 5 | import org.koin.android.ext.koin.androidContext 6 | 7 | class DogifyApplication : Application() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | initKoin { 12 | androidContext(this@DogifyApplication) 13 | modules(viewModelModule) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/com/nagyrobi144/dogify/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.android 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.material.MaterialTheme 7 | import org.koin.androidx.viewmodel.ext.android.viewModel 8 | 9 | class MainActivity : AppCompatActivity() { 10 | 11 | private val viewModel by viewModel() 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | 16 | setContent { 17 | MaterialTheme { 18 | MainScreen(viewModel) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /androidApp/src/main/java/com/nagyrobi144/dogify/android/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.android 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.lazy.GridCells 8 | import androidx.compose.foundation.lazy.LazyVerticalGrid 9 | import androidx.compose.foundation.lazy.items 10 | import androidx.compose.material.* 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.Favorite 13 | import androidx.compose.material.icons.outlined.FavoriteBorder 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.collectAsState 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.rememberCoroutineScope 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.layout.ContentScale 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import androidx.compose.ui.unit.dp 23 | import coil.compose.rememberImagePainter 24 | import com.google.accompanist.swiperefresh.SwipeRefresh 25 | import com.google.accompanist.swiperefresh.rememberSwipeRefreshState 26 | import com.nagyrobi144.dogify.model.Breed 27 | import kotlinx.coroutines.launch 28 | 29 | @Composable 30 | fun MainScreen(viewModel: MainViewModel) { 31 | val state by viewModel.state.collectAsState() 32 | val breeds by viewModel.breeds.collectAsState() 33 | val events by viewModel.events.collectAsState(Unit) 34 | val isRefreshing by viewModel.isRefreshing.collectAsState() 35 | val shouldFilterFavourites by viewModel.shouldFilterFavourites.collectAsState() 36 | 37 | val scaffoldState = rememberScaffoldState() 38 | val snackbarCoroutineScope = rememberCoroutineScope() 39 | 40 | Scaffold(scaffoldState = scaffoldState) { 41 | SwipeRefresh( 42 | state = rememberSwipeRefreshState(isRefreshing = isRefreshing), 43 | onRefresh = viewModel::refresh 44 | ) { 45 | Column( 46 | Modifier 47 | .fillMaxSize() 48 | .padding(8.dp) 49 | ) { 50 | Row( 51 | Modifier 52 | .wrapContentWidth(Alignment.End) 53 | .padding(8.dp) 54 | ) { 55 | Text(text = "Filter favourites") 56 | Switch( 57 | checked = shouldFilterFavourites, 58 | modifier = Modifier.padding(horizontal = 8.dp), 59 | onCheckedChange = { viewModel.onToggleFavouriteFilter() } 60 | ) 61 | } 62 | when (state) { 63 | MainViewModel.State.LOADING -> { 64 | Spacer(Modifier.weight(1f)) 65 | CircularProgressIndicator(Modifier.align(Alignment.CenterHorizontally)) 66 | Spacer(Modifier.weight(1f)) 67 | } 68 | MainViewModel.State.NORMAL -> Breeds( 69 | breeds = breeds, 70 | onFavouriteTapped = viewModel::onFavouriteTapped 71 | ) 72 | 73 | MainViewModel.State.ERROR -> { 74 | Spacer(Modifier.weight(1f)) 75 | Text( 76 | text = "Oops something went wrong...", 77 | modifier = Modifier.align(Alignment.CenterHorizontally) 78 | ) 79 | Spacer(Modifier.weight(1f)) 80 | } 81 | MainViewModel.State.EMPTY -> { 82 | Spacer(Modifier.weight(1f)) 83 | Text( 84 | text = "Oops looks like there are no ${if (shouldFilterFavourites) "favourites" else "dogs"}", 85 | modifier = Modifier.align(Alignment.CenterHorizontally) 86 | ) 87 | Spacer(Modifier.weight(1f)) 88 | } 89 | } 90 | if (events == MainViewModel.Event.Error) { 91 | snackbarCoroutineScope.launch { 92 | scaffoldState.snackbarHostState.apply { 93 | currentSnackbarData?.dismiss() 94 | showSnackbar("Oops something went wrong...") 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | @OptIn(ExperimentalFoundationApi::class) 104 | @Composable 105 | fun Breeds(breeds: List, onFavouriteTapped: (Breed) -> Unit = {}) { 106 | LazyVerticalGrid(cells = GridCells.Fixed(2)) { 107 | items(breeds) { 108 | Column(Modifier.padding(8.dp)) { 109 | Image( 110 | painter = rememberImagePainter(it.imageUrl), 111 | contentDescription = "${it.name}-image", 112 | modifier = Modifier 113 | .aspectRatio(1f) 114 | .fillMaxWidth() 115 | .align(Alignment.CenterHorizontally), 116 | contentScale = ContentScale.Crop 117 | 118 | ) 119 | Row(Modifier.padding(vertical = 8.dp)) { 120 | Text( 121 | text = it.name, 122 | modifier = Modifier 123 | .align(Alignment.CenterVertically) 124 | ) 125 | Spacer(Modifier.weight(1f)) 126 | Icon( 127 | if (it.isFavourite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, 128 | contentDescription = "Mark as favourite", 129 | modifier = Modifier.clickable { 130 | onFavouriteTapped(it) 131 | } 132 | ) 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | @Preview 140 | @Composable 141 | fun BreedsPreview() { 142 | MaterialTheme { 143 | Surface { 144 | Breeds(breeds = (0 until 10).map { 145 | Breed( 146 | name = "Breed $it", 147 | imageUrl = "", 148 | isFavourite = it % 2 == 0 149 | ) 150 | }) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /androidApp/src/main/java/com/nagyrobi144/dogify/android/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.android 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.nagyrobi144.dogify.model.Breed 6 | import com.nagyrobi144.dogify.repository.BreedsRepository 7 | import com.nagyrobi144.dogify.usecase.FetchBreedsUseCase 8 | import com.nagyrobi144.dogify.usecase.GetBreedsUseCase 9 | import com.nagyrobi144.dogify.usecase.ToggleFavouriteStateUseCase 10 | import kotlinx.coroutines.flow.* 11 | import kotlinx.coroutines.launch 12 | 13 | class MainViewModel( 14 | breedsRepository: BreedsRepository, 15 | private val getBreeds: GetBreedsUseCase, 16 | private val fetchBreeds: FetchBreedsUseCase, 17 | private val onToggleFavouriteState: ToggleFavouriteStateUseCase 18 | ) : ViewModel() { 19 | 20 | private val _state = MutableStateFlow(State.LOADING) 21 | val state: StateFlow = _state 22 | 23 | private val _isRefreshing = MutableStateFlow(false) 24 | val isRefreshing: StateFlow = _isRefreshing 25 | 26 | private val _events = MutableSharedFlow() 27 | val events: SharedFlow = _events 28 | 29 | private val _shouldFilterFavourites = MutableStateFlow(false) 30 | val shouldFilterFavourites: StateFlow = _shouldFilterFavourites 31 | 32 | val breeds = 33 | breedsRepository.breeds.combine(shouldFilterFavourites) { breeds, shouldFilterFavourites -> 34 | if (shouldFilterFavourites) { 35 | breeds.filter { it.isFavourite } 36 | } else { 37 | breeds 38 | }.also { 39 | _state.value = if (it.isEmpty()) State.EMPTY else State.NORMAL 40 | } 41 | }.stateIn( 42 | viewModelScope, 43 | SharingStarted.WhileSubscribed(), 44 | emptyList() 45 | ) 46 | 47 | init { 48 | loadData() 49 | } 50 | 51 | private fun loadData(isForceRefresh: Boolean = false) { 52 | val getData: suspend () -> List = 53 | { if (isForceRefresh) fetchBreeds() else getBreeds() } 54 | 55 | if (isForceRefresh) { 56 | _isRefreshing.value = true 57 | } else { 58 | _state.value = State.LOADING 59 | } 60 | 61 | viewModelScope.launch { 62 | _state.value = try { 63 | getData() 64 | State.NORMAL 65 | } catch (e: Exception) { 66 | State.ERROR 67 | } 68 | _isRefreshing.value = false 69 | } 70 | } 71 | 72 | fun refresh() { 73 | loadData(true) 74 | } 75 | 76 | fun onToggleFavouriteFilter() { 77 | _shouldFilterFavourites.value = !shouldFilterFavourites.value 78 | } 79 | 80 | fun onFavouriteTapped(breed: Breed) { 81 | viewModelScope.launch { 82 | try { 83 | onToggleFavouriteState(breed) 84 | } catch (e: Exception) { 85 | _events.emit(Event.Error) 86 | } 87 | } 88 | } 89 | 90 | enum class State { 91 | LOADING, 92 | NORMAL, 93 | ERROR, 94 | EMPTY 95 | } 96 | 97 | enum class Event { 98 | Error 99 | } 100 | } -------------------------------------------------------------------------------- /androidApp/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /androidApp/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") 9 | classpath("com.android.tools.build:gradle:7.0.4") 10 | classpath("com.squareup.sqldelight:gradle-plugin:1.5.3") 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | tasks.register("clean", Delete::class) { 22 | delete(rootProject.buildDir) 23 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" 3 | 4 | #Kotlin 5 | kotlin.code.style=official 6 | 7 | #Android 8 | android.useAndroidX=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Simplifying-Application-Development-with-Kotlin-Multiplatform-Mobile/4f820abbef327a53b702f92f2e346fa86b89f36d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jul 17 15:39:41 EEST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /iosApp/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'iosApp' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | platform :ios, '14.1' 8 | 9 | # Pods for iosApp 10 | pod 'KMPNativeCoroutinesRxSwift' 11 | pod 'Kingfisher' 12 | end 13 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 11 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; 12 | A73A53D926AF1D32001FC9EB /* BreedsGridUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A73A53D826AF1D32001FC9EB /* BreedsGridUIView.swift */; }; 13 | A73A53DB26AF1DA2001FC9EB /* BreedUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A73A53DA26AF1DA2001FC9EB /* BreedUIView.swift */; }; 14 | A7D09C5126A86ED100D3FCC9 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D09C5026A86ED100D3FCC9 /* MainViewModel.swift */; }; 15 | EB1A369F9C85147572CBF730 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ECFC75E2D71E48E9FE90685D /* Pods_iosApp.framework */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 7555FFB4242A642300829871 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; 33 | 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 35 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | 7555FFB1242A642300829871 /* shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = shared.framework; path = "../shared/build/xcode-frameworks/shared.framework"; sourceTree = ""; }; 37 | 9D365404F3A8DEA0DCC1B062 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; }; 38 | A73A53D826AF1D32001FC9EB /* BreedsGridUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedsGridUIView.swift; sourceTree = ""; }; 39 | A73A53DA26AF1DA2001FC9EB /* BreedUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedUIView.swift; sourceTree = ""; }; 40 | A7D09C5026A86ED100D3FCC9 /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; }; 41 | D8F3EDDA23A6B1AE85247681 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; 42 | ECFC75E2D71E48E9FE90685D /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | 7555FF78242A565900829871 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | EB1A369F9C85147572CBF730 /* Pods_iosApp.framework in Frameworks */, 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 5DC1C6480AA5286CF2D01FEB /* Pods */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | D8F3EDDA23A6B1AE85247681 /* Pods-iosApp.debug.xcconfig */, 61 | 9D365404F3A8DEA0DCC1B062 /* Pods-iosApp.release.xcconfig */, 62 | ); 63 | path = Pods; 64 | sourceTree = ""; 65 | }; 66 | 7555FF72242A565900829871 = { 67 | isa = PBXGroup; 68 | children = ( 69 | 7555FF7D242A565900829871 /* iosApp */, 70 | 7555FF7C242A565900829871 /* Products */, 71 | 7555FFB0242A642200829871 /* Frameworks */, 72 | 5DC1C6480AA5286CF2D01FEB /* Pods */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | 7555FF7C242A565900829871 /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 7555FF7B242A565900829871 /* iosApp.app */, 80 | ); 81 | name = Products; 82 | sourceTree = ""; 83 | }; 84 | 7555FF7D242A565900829871 /* iosApp */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 7555FF82242A565900829871 /* ContentView.swift */, 88 | 7555FF8C242A565B00829871 /* Info.plist */, 89 | 2152FB032600AC8F00CF470E /* iOSApp.swift */, 90 | A7D09C5026A86ED100D3FCC9 /* MainViewModel.swift */, 91 | A73A53D826AF1D32001FC9EB /* BreedsGridUIView.swift */, 92 | A73A53DA26AF1DA2001FC9EB /* BreedUIView.swift */, 93 | ); 94 | path = iosApp; 95 | sourceTree = ""; 96 | }; 97 | 7555FFB0242A642200829871 /* Frameworks */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 7555FFB1242A642300829871 /* shared.framework */, 101 | ECFC75E2D71E48E9FE90685D /* Pods_iosApp.framework */, 102 | ); 103 | name = Frameworks; 104 | sourceTree = ""; 105 | }; 106 | /* End PBXGroup section */ 107 | 108 | /* Begin PBXNativeTarget section */ 109 | 7555FF7A242A565900829871 /* iosApp */ = { 110 | isa = PBXNativeTarget; 111 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; 112 | buildPhases = ( 113 | 1104EA907B51A1AF1D42368B /* [CP] Check Pods Manifest.lock */, 114 | 7555FFB5242A651A00829871 /* Run Script */, 115 | 7555FF77242A565900829871 /* Sources */, 116 | 7555FF78242A565900829871 /* Frameworks */, 117 | 7555FF79242A565900829871 /* Resources */, 118 | 7555FFB4242A642300829871 /* Embed Frameworks */, 119 | E2A0BDA6E8D8A082C7DDCFA7 /* [CP] Embed Pods Frameworks */, 120 | ); 121 | buildRules = ( 122 | ); 123 | dependencies = ( 124 | ); 125 | name = iosApp; 126 | productName = iosApp; 127 | productReference = 7555FF7B242A565900829871 /* iosApp.app */; 128 | productType = "com.apple.product-type.application"; 129 | }; 130 | /* End PBXNativeTarget section */ 131 | 132 | /* Begin PBXProject section */ 133 | 7555FF73242A565900829871 /* Project object */ = { 134 | isa = PBXProject; 135 | attributes = { 136 | LastSwiftUpdateCheck = 1130; 137 | LastUpgradeCheck = 1130; 138 | ORGANIZATIONNAME = orgName; 139 | TargetAttributes = { 140 | 7555FF7A242A565900829871 = { 141 | CreatedOnToolsVersion = 11.3.1; 142 | }; 143 | }; 144 | }; 145 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; 146 | compatibilityVersion = "Xcode 9.3"; 147 | developmentRegion = en; 148 | hasScannedForEncodings = 0; 149 | knownRegions = ( 150 | en, 151 | Base, 152 | ); 153 | mainGroup = 7555FF72242A565900829871; 154 | productRefGroup = 7555FF7C242A565900829871 /* Products */; 155 | projectDirPath = ""; 156 | projectRoot = ""; 157 | targets = ( 158 | 7555FF7A242A565900829871 /* iosApp */, 159 | ); 160 | }; 161 | /* End PBXProject section */ 162 | 163 | /* Begin PBXResourcesBuildPhase section */ 164 | 7555FF79242A565900829871 /* Resources */ = { 165 | isa = PBXResourcesBuildPhase; 166 | buildActionMask = 2147483647; 167 | files = ( 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | /* End PBXResourcesBuildPhase section */ 172 | 173 | /* Begin PBXShellScriptBuildPhase section */ 174 | 1104EA907B51A1AF1D42368B /* [CP] Check Pods Manifest.lock */ = { 175 | isa = PBXShellScriptBuildPhase; 176 | buildActionMask = 2147483647; 177 | files = ( 178 | ); 179 | inputFileListPaths = ( 180 | ); 181 | inputPaths = ( 182 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 183 | "${PODS_ROOT}/Manifest.lock", 184 | ); 185 | name = "[CP] Check Pods Manifest.lock"; 186 | outputFileListPaths = ( 187 | ); 188 | outputPaths = ( 189 | "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt", 190 | ); 191 | runOnlyForDeploymentPostprocessing = 0; 192 | shellPath = /bin/sh; 193 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 194 | showEnvVarsInLog = 0; 195 | }; 196 | 7555FFB5242A651A00829871 /* Run Script */ = { 197 | isa = PBXShellScriptBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | ); 201 | inputFileListPaths = ( 202 | ); 203 | inputPaths = ( 204 | ); 205 | name = "Run Script"; 206 | outputFileListPaths = ( 207 | ); 208 | outputPaths = ( 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | shellPath = /bin/sh; 212 | shellScript = "cd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; 213 | }; 214 | E2A0BDA6E8D8A082C7DDCFA7 /* [CP] Embed Pods Frameworks */ = { 215 | isa = PBXShellScriptBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | ); 219 | inputFileListPaths = ( 220 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", 221 | ); 222 | name = "[CP] Embed Pods Frameworks"; 223 | outputFileListPaths = ( 224 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | shellPath = /bin/sh; 228 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh\"\n"; 229 | showEnvVarsInLog = 0; 230 | }; 231 | /* End PBXShellScriptBuildPhase section */ 232 | 233 | /* Begin PBXSourcesBuildPhase section */ 234 | 7555FF77242A565900829871 /* Sources */ = { 235 | isa = PBXSourcesBuildPhase; 236 | buildActionMask = 2147483647; 237 | files = ( 238 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, 239 | A73A53DB26AF1DA2001FC9EB /* BreedUIView.swift in Sources */, 240 | A73A53D926AF1D32001FC9EB /* BreedsGridUIView.swift in Sources */, 241 | 7555FF83242A565900829871 /* ContentView.swift in Sources */, 242 | A7D09C5126A86ED100D3FCC9 /* MainViewModel.swift in Sources */, 243 | ); 244 | runOnlyForDeploymentPostprocessing = 0; 245 | }; 246 | /* End PBXSourcesBuildPhase section */ 247 | 248 | /* Begin XCBuildConfiguration section */ 249 | 7555FFA3242A565B00829871 /* Debug */ = { 250 | isa = XCBuildConfiguration; 251 | buildSettings = { 252 | ALWAYS_SEARCH_USER_PATHS = NO; 253 | CLANG_ANALYZER_NONNULL = YES; 254 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 255 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 256 | CLANG_CXX_LIBRARY = "libc++"; 257 | CLANG_ENABLE_MODULES = YES; 258 | CLANG_ENABLE_OBJC_ARC = YES; 259 | CLANG_ENABLE_OBJC_WEAK = YES; 260 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 261 | CLANG_WARN_BOOL_CONVERSION = YES; 262 | CLANG_WARN_COMMA = YES; 263 | CLANG_WARN_CONSTANT_CONVERSION = YES; 264 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 265 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 266 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 267 | CLANG_WARN_EMPTY_BODY = YES; 268 | CLANG_WARN_ENUM_CONVERSION = YES; 269 | CLANG_WARN_INFINITE_RECURSION = YES; 270 | CLANG_WARN_INT_CONVERSION = YES; 271 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 272 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 273 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 274 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 275 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 276 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 277 | CLANG_WARN_STRICT_PROTOTYPES = YES; 278 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 279 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 280 | CLANG_WARN_UNREACHABLE_CODE = YES; 281 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 282 | COPY_PHASE_STRIP = NO; 283 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 284 | ENABLE_STRICT_OBJC_MSGSEND = YES; 285 | ENABLE_TESTABILITY = YES; 286 | GCC_C_LANGUAGE_STANDARD = gnu11; 287 | GCC_DYNAMIC_NO_PIC = NO; 288 | GCC_NO_COMMON_BLOCKS = YES; 289 | GCC_OPTIMIZATION_LEVEL = 0; 290 | GCC_PREPROCESSOR_DEFINITIONS = ( 291 | "DEBUG=1", 292 | "$(inherited)", 293 | ); 294 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 295 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 296 | GCC_WARN_UNDECLARED_SELECTOR = YES; 297 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 298 | GCC_WARN_UNUSED_FUNCTION = YES; 299 | GCC_WARN_UNUSED_VARIABLE = YES; 300 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 301 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 302 | MTL_FAST_MATH = YES; 303 | ONLY_ACTIVE_ARCH = YES; 304 | SDKROOT = iphoneos; 305 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 306 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 307 | }; 308 | name = Debug; 309 | }; 310 | 7555FFA4242A565B00829871 /* Release */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ALWAYS_SEARCH_USER_PATHS = NO; 314 | CLANG_ANALYZER_NONNULL = YES; 315 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 316 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 317 | CLANG_CXX_LIBRARY = "libc++"; 318 | CLANG_ENABLE_MODULES = YES; 319 | CLANG_ENABLE_OBJC_ARC = YES; 320 | CLANG_ENABLE_OBJC_WEAK = YES; 321 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 322 | CLANG_WARN_BOOL_CONVERSION = YES; 323 | CLANG_WARN_COMMA = YES; 324 | CLANG_WARN_CONSTANT_CONVERSION = YES; 325 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 326 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 327 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 328 | CLANG_WARN_EMPTY_BODY = YES; 329 | CLANG_WARN_ENUM_CONVERSION = YES; 330 | CLANG_WARN_INFINITE_RECURSION = YES; 331 | CLANG_WARN_INT_CONVERSION = YES; 332 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 333 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 334 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 335 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 336 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 337 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 338 | CLANG_WARN_STRICT_PROTOTYPES = YES; 339 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 340 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 341 | CLANG_WARN_UNREACHABLE_CODE = YES; 342 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 343 | COPY_PHASE_STRIP = NO; 344 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 345 | ENABLE_NS_ASSERTIONS = NO; 346 | ENABLE_STRICT_OBJC_MSGSEND = YES; 347 | GCC_C_LANGUAGE_STANDARD = gnu11; 348 | GCC_NO_COMMON_BLOCKS = YES; 349 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 350 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 351 | GCC_WARN_UNDECLARED_SELECTOR = YES; 352 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 353 | GCC_WARN_UNUSED_FUNCTION = YES; 354 | GCC_WARN_UNUSED_VARIABLE = YES; 355 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 356 | MTL_ENABLE_DEBUG_INFO = NO; 357 | MTL_FAST_MATH = YES; 358 | SDKROOT = iphoneos; 359 | SWIFT_COMPILATION_MODE = wholemodule; 360 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 361 | VALIDATE_PRODUCT = YES; 362 | }; 363 | name = Release; 364 | }; 365 | 7555FFA6242A565B00829871 /* Debug */ = { 366 | isa = XCBuildConfiguration; 367 | baseConfigurationReference = D8F3EDDA23A6B1AE85247681 /* Pods-iosApp.debug.xcconfig */; 368 | buildSettings = { 369 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 370 | CODE_SIGN_STYLE = Automatic; 371 | ENABLE_PREVIEWS = YES; 372 | FRAMEWORK_SEARCH_PATHS = ( 373 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 374 | "\"${PODS_CONFIGURATION_BUILD_DIR}\"/**", 375 | "\"${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers\"", 376 | "\"${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesRxSwift/KMPNativeCoroutinesRxSwift.framework/Headers\"", 377 | ); 378 | INFOPLIST_FILE = iosApp/Info.plist; 379 | LD_RUNPATH_SEARCH_PATHS = ( 380 | "$(inherited)", 381 | "@executable_path/Frameworks", 382 | ); 383 | OTHER_LDFLAGS = ( 384 | "$(inherited)", 385 | "-framework", 386 | shared, 387 | ); 388 | PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | SWIFT_VERSION = 5.0; 391 | TARGETED_DEVICE_FAMILY = "1,2"; 392 | }; 393 | name = Debug; 394 | }; 395 | 7555FFA7242A565B00829871 /* Release */ = { 396 | isa = XCBuildConfiguration; 397 | baseConfigurationReference = 9D365404F3A8DEA0DCC1B062 /* Pods-iosApp.release.xcconfig */; 398 | buildSettings = { 399 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 400 | CODE_SIGN_STYLE = Automatic; 401 | ENABLE_PREVIEWS = YES; 402 | FRAMEWORK_SEARCH_PATHS = ( 403 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 404 | "\"${PODS_CONFIGURATION_BUILD_DIR}\"/**", 405 | "\"${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers\"", 406 | "\"${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesRxSwift/KMPNativeCoroutinesRxSwift.framework/Headers\"", 407 | ); 408 | INFOPLIST_FILE = iosApp/Info.plist; 409 | LD_RUNPATH_SEARCH_PATHS = ( 410 | "$(inherited)", 411 | "@executable_path/Frameworks", 412 | ); 413 | OTHER_LDFLAGS = ( 414 | "$(inherited)", 415 | "-framework", 416 | shared, 417 | ); 418 | PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp; 419 | PRODUCT_NAME = "$(TARGET_NAME)"; 420 | SWIFT_VERSION = 5.0; 421 | TARGETED_DEVICE_FAMILY = "1,2"; 422 | }; 423 | name = Release; 424 | }; 425 | /* End XCBuildConfiguration section */ 426 | 427 | /* Begin XCConfigurationList section */ 428 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { 429 | isa = XCConfigurationList; 430 | buildConfigurations = ( 431 | 7555FFA3242A565B00829871 /* Debug */, 432 | 7555FFA4242A565B00829871 /* Release */, 433 | ); 434 | defaultConfigurationIsVisible = 0; 435 | defaultConfigurationName = Release; 436 | }; 437 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 438 | isa = XCConfigurationList; 439 | buildConfigurations = ( 440 | 7555FFA6242A565B00829871 /* Debug */, 441 | 7555FFA7242A565B00829871 /* Release */, 442 | ); 443 | defaultConfigurationIsVisible = 0; 444 | defaultConfigurationName = Release; 445 | }; 446 | /* End XCConfigurationList section */ 447 | }; 448 | rootObject = 7555FF73242A565900829871 /* Project object */; 449 | } 450 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iosApp/iosApp/BreedUIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedUIView.swift 3 | // iosApp 4 | // 5 | // Created by Robert Nagy on 26/07/2021. 6 | // Copyright © 2021 orgName. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import shared 11 | import Kingfisher 12 | 13 | struct BreedUIView: View { 14 | 15 | var breed: Breed 16 | var onFavouriteTapped: (Breed) -> Void = {_ in } 17 | 18 | var body: some View { 19 | VStack{ 20 | KFImage(URL(string: breed.imageUrl)) 21 | .resizable() 22 | .scaledToFit() 23 | .cornerRadius(16) 24 | HStack{ 25 | Text(breed.name) 26 | .padding(16) 27 | Spacer() 28 | Button(action: { onFavouriteTapped(breed) }, label: { 29 | if(breed.isFavourite){ 30 | Image(systemName: "heart.fill") 31 | .resizable() 32 | .aspectRatio(1, contentMode: .fit) 33 | .frame(width: 24) 34 | } else { 35 | Image(systemName: "heart") 36 | .resizable() 37 | .aspectRatio(1, contentMode: .fit) 38 | .frame(width: 24) 39 | } 40 | }).padding(16) 41 | 42 | } 43 | } 44 | } 45 | } 46 | 47 | struct BreedUIView_Previews: PreviewProvider { 48 | static var previews: some View { 49 | BreedUIView(breed:Breed(name: "beagle", imageUrl:"https://images.dog.ceo//breeds//beagle//n02088364_161.jpg", isFavourite: false)) 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /iosApp/iosApp/BreedsGridUIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedsGridUIView.swift 3 | // iosApp 4 | // 5 | // Created by Robert Nagy on 26/07/2021. 6 | // Copyright © 2021 orgName. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import shared 11 | 12 | struct BreedsGridUIView: View { 13 | var breeds: Array 14 | var onFavouriteTapped: (Breed) -> Void = {_ in } 15 | 16 | var body: some View { 17 | let columns = [ 18 | GridItem(.flexible(minimum: 128, maximum: 256), spacing: 16), 19 | GridItem(.flexible(minimum: 128, maximum: 256), spacing: 16) 20 | ] 21 | ScrollView{ 22 | LazyVGrid(columns: columns, spacing: 16){ 23 | ForEach(breeds, id: \.name){ breed in 24 | BreedUIView(breed: breed, onFavouriteTapped: onFavouriteTapped) 25 | } 26 | }.padding(.horizontal, 16) 27 | } 28 | } 29 | } 30 | 31 | struct BreedsGridUIView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | BreedsGridUIView(breeds: Array( 34 | arrayLiteral: 35 | Breed( 36 | name: "beagle", 37 | imageUrl: "https://images.dog.ceo//breeds//beagle//n02088364_161.jpg", 38 | isFavourite: false 39 | ), 40 | Breed( 41 | name: "affenpinscher", 42 | imageUrl: "https://images.dog.ceo//breeds//affenpinscher//n02110627_3001.jpg", 43 | isFavourite: true 44 | ) 45 | )) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import shared 3 | 4 | struct ContentView: View { 5 | 6 | @ObservedObject private var viewModel: MainViewModel 7 | 8 | init() { 9 | KoinModuleKt.doInitKoin() 10 | viewModel = MainViewModel.init() 11 | } 12 | 13 | var body: some View { 14 | VStack{ 15 | Toggle("Filter favourites", isOn: $viewModel.shouldFilterFavourites) 16 | .padding(16) 17 | Button("Refresh breeds", action: { viewModel.fetchData()} ) 18 | .frame(alignment: .center) 19 | .padding(.bottom, 16) 20 | ZStack{ 21 | switch viewModel.state { 22 | case MainViewModel.State.LOADING: 23 | ProgressView() 24 | .frame(alignment:.center) 25 | case MainViewModel.State.NORMAL: 26 | BreedsGridUIView(breeds: viewModel.filteredBreeds, onFavouriteTapped: viewModel.onFavouriteTapped) 27 | case MainViewModel.State.EMPTY: 28 | Text("Ooops looks like there are no breeds") 29 | .frame(alignment: .center) 30 | .font(.headline) 31 | case MainViewModel.State.ERROR: 32 | Text("Ooops something went wrong...") 33 | .frame(alignment: .center) 34 | .font(.headline) 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIRequiredDeviceCapabilities 29 | 30 | armv7 31 | 32 | UISupportedInterfaceOrientations 33 | 34 | UIInterfaceOrientationPortrait 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | 38 | UISupportedInterfaceOrientations~ipad 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationPortraitUpsideDown 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UILaunchScreen 46 | 47 | 48 | -------------------------------------------------------------------------------- /iosApp/iosApp/MainViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewModel.swift 3 | // iosApp 4 | // 5 | // Created by Robert Nagy on 21/07/2021. 6 | // Copyright © 2021 orgName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import shared 11 | import KMPNativeCoroutinesRxSwift 12 | import RxSwift 13 | import Combine 14 | 15 | class MainViewModel: ObservableObject { 16 | 17 | private let repository = BreedsRepository.init() 18 | private let getBreeds = GetBreedsUseCase.init() 19 | private let fetchBreeds = FetchBreedsUseCase.init() 20 | private let onToggleFavouriteState = ToggleFavouriteStateUseCase.init() 21 | 22 | @Published 23 | private(set) var state = State.LOADING 24 | 25 | @Published 26 | var shouldFilterFavourites = false 27 | 28 | @Published 29 | private(set) var filteredBreeds: [Breed] = [] 30 | 31 | @Published 32 | private var breeds: [Breed] = [] 33 | 34 | private let disposeBag = DisposeBag() 35 | 36 | init() { 37 | createObservable(for: repository.breedsNative).subscribe(onNext: { breeds in 38 | DispatchQueue.main.async { 39 | self.breeds = breeds 40 | } 41 | }).disposed(by: disposeBag) 42 | 43 | $breeds.combineLatest($shouldFilterFavourites, { breeds, shouldFilterFavourites -> [Breed] in 44 | var result: [Breed] = [] 45 | if(shouldFilterFavourites){ 46 | result.append(contentsOf: breeds.filter{ $0.isFavourite }) 47 | } else { 48 | result.append(contentsOf: breeds) 49 | } 50 | if(result.isEmpty){ 51 | self.state = State.EMPTY 52 | } else { 53 | self.state = State.NORMAL 54 | } 55 | return result 56 | }).assign(to: &$filteredBreeds) 57 | 58 | getData() 59 | } 60 | 61 | func getData(){ 62 | state = State.LOADING 63 | 64 | createSingle(for: getBreeds.invokeNative()).subscribe(onSuccess: { _ in 65 | DispatchQueue.main.async { 66 | self.state = State.NORMAL 67 | } 68 | }, onFailure: { error in 69 | DispatchQueue.main.async { 70 | self.state = State.ERROR 71 | } 72 | }).disposed(by: disposeBag) 73 | } 74 | 75 | func fetchData() { 76 | state = State.LOADING 77 | 78 | createSingle(for: fetchBreeds.invokeNative()).subscribe(onSuccess: { _ in 79 | DispatchQueue.main.async { 80 | self.state = State.NORMAL 81 | } 82 | }, onFailure: { error in 83 | DispatchQueue.main.async { 84 | self.state = State.ERROR 85 | } 86 | }).disposed(by: disposeBag) 87 | } 88 | 89 | func onFavouriteTapped(breed: Breed){ 90 | createSingle(for: onToggleFavouriteState.invokeNative(breed: breed)).subscribe(onFailure: { error in 91 | // We're going ignoring the failure, as it will be represented by the stream of breds 92 | }).disposed(by: disposeBag) 93 | } 94 | 95 | enum State { 96 | case NORMAL 97 | case LOADING 98 | case ERROR 99 | case EMPTY 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | rootProject.name = "Dogify" 10 | include(":androidApp") 11 | include(":shared") -------------------------------------------------------------------------------- /shared/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle GUI config 2 | gradle-app.setting 3 | 4 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 5 | !gradle-wrapper.jar 6 | 7 | # Cache of project 8 | .gradletasknamecache 9 | 10 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 11 | # gradle/wrapper/gradle-wrapper.properties 12 | 13 | .idea 14 | *.iml 15 | .gradle 16 | /local.properties 17 | /.idea/caches 18 | /.idea/libraries 19 | /.idea/modules.xml 20 | /.idea/workspace.xml 21 | /.idea/navEditor.xml 22 | /.idea/assetWizardSettings.xml 23 | .DS_Store 24 | /build 25 | /captures 26 | 27 | 28 | *.xcworkspacedata 29 | *.xcuserstate 30 | *.xcscheme 31 | xcschememanagement.plist 32 | *.xcbkptlist 33 | -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | id("com.android.library") 6 | kotlin("plugin.serialization") version "1.6.10" 7 | id("com.squareup.sqldelight") 8 | id("com.rickclephas.kmp.nativecoroutines") version "0.11.1" 9 | } 10 | 11 | kotlin { 12 | android() 13 | 14 | val iosTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget = when { 15 | System.getenv("SDK_NAME")?.startsWith("iphoneos") == true -> ::iosArm64 16 | System.getenv("NATIVE_ARCH")?.startsWith("arm") == true -> ::iosSimulatorArm64 17 | else -> ::iosX64 18 | } 19 | 20 | iosTarget("ios") { 21 | binaries { 22 | framework { 23 | baseName = "shared" 24 | } 25 | } 26 | } 27 | sourceSets { 28 | val ktorVersion = "2.0.0-beta-1" 29 | val sqlDelightVersion = "1.5.3" 30 | val koinVersion = "3.1.4" 31 | val commonMain by getting { 32 | dependencies { 33 | api("io.insert-koin:koin-core:$koinVersion") 34 | // Ktor 35 | implementation("io.ktor:ktor-client-core:$ktorVersion") 36 | implementation("io.ktor:ktor-client-json:$ktorVersion") 37 | implementation("io.ktor:ktor-client-logging:$ktorVersion") 38 | implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") 39 | implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") 40 | 41 | // Serialization 42 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2") 43 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt") { 44 | version { 45 | strictly("1.6.0-native-mt") 46 | } 47 | } 48 | // Sql Delight 49 | implementation("com.squareup.sqldelight:runtime:$sqlDelightVersion") 50 | implementation("com.squareup.sqldelight:coroutines-extensions:$sqlDelightVersion") 51 | } 52 | } 53 | val commonTest by getting { 54 | dependencies { 55 | implementation(kotlin("test-common")) 56 | implementation(kotlin("test-annotations-common")) 57 | } 58 | } 59 | val androidMain by getting { 60 | dependencies { 61 | implementation("io.ktor:ktor-client-android:$ktorVersion") 62 | implementation("com.squareup.sqldelight:android-driver:$sqlDelightVersion") 63 | api("io.insert-koin:koin-android:$koinVersion") 64 | } 65 | } 66 | val androidTest by getting { 67 | dependencies { 68 | implementation(kotlin("test-junit")) 69 | implementation("junit:junit:4.13.2") 70 | } 71 | } 72 | val iosMain by getting { 73 | dependencies { 74 | implementation("io.ktor:ktor-client-ios:$ktorVersion") 75 | implementation("com.squareup.sqldelight:native-driver:$sqlDelightVersion") 76 | } 77 | } 78 | val iosTest by getting 79 | } 80 | } 81 | 82 | android { 83 | compileSdk = 31 84 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 85 | defaultConfig { 86 | minSdk = 23 87 | targetSdk = 31 88 | } 89 | } 90 | 91 | sqldelight { 92 | database("DogifyDatabase") { 93 | packageName = "com.nagyrobi144.dogify.db" 94 | sourceFolders = listOf("sqldelight") 95 | } 96 | } -------------------------------------------------------------------------------- /shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/nagyrobi144/dogify/database/DriverFactory.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.database 2 | 3 | import com.nagyrobi144.dogify.db.DogifyDatabase 4 | import com.squareup.sqldelight.android.AndroidSqliteDriver 5 | import com.squareup.sqldelight.db.SqlDriver 6 | import org.koin.android.ext.koin.androidContext 7 | import org.koin.core.scope.Scope 8 | 9 | internal actual fun Scope.createDriver(databaseName: String): SqlDriver = 10 | AndroidSqliteDriver(DogifyDatabase.Schema, androidContext(), databaseName) -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/nagyrobi144/dogify/util/DispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.util 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | 5 | internal actual fun getDispatcherProvider(): DispatcherProvider = AndroidDispatcherProvider() 6 | 7 | private class AndroidDispatcherProvider: DispatcherProvider{ 8 | override val main = Dispatchers.Main 9 | override val io = Dispatchers.IO 10 | override val unconfined = Dispatchers.Unconfined 11 | } -------------------------------------------------------------------------------- /shared/src/androidTest/kotlin/com/nagyrobi144/dogify/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.runBlocking 5 | 6 | actual fun runTest(block: suspend CoroutineScope.() -> T) = runBlocking(block = block) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/api/BreedsApi.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.api 2 | 3 | import com.nagyrobi144.dogify.api.model.BreedImageResponse 4 | import com.nagyrobi144.dogify.api.model.BreedsResponse 5 | import io.ktor.client.call.* 6 | import io.ktor.client.request.* 7 | import kotlin.collections.get 8 | 9 | /** 10 | * Ktor Networking Api for getting information about a Breed entity 11 | */ 12 | internal class BreedsApi : KtorApi() { 13 | 14 | suspend fun getBreeds(): BreedsResponse = client.get { 15 | apiUrl("breeds/list") 16 | }.body() 17 | 18 | suspend fun getRandomBreedImageFor(breed: String): BreedImageResponse = client.get { 19 | apiUrl("breed/$breed/images/random") 20 | }.body() 21 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/api/KtorApi.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.api 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.plugins.* 5 | import io.ktor.client.plugins.logging.* 6 | import io.ktor.client.request.* 7 | import io.ktor.http.* 8 | import io.ktor.serialization.kotlinx.json.* 9 | import kotlinx.serialization.json.Json 10 | import kotlin.native.concurrent.SharedImmutable 11 | 12 | internal abstract class KtorApi { 13 | 14 | val client = httpClient 15 | 16 | /** 17 | * Use this method for configuring the request url 18 | */ 19 | fun HttpRequestBuilder.apiUrl(path: String) { 20 | url { 21 | takeFrom("https://dog.ceo") 22 | path("api", path) 23 | } 24 | } 25 | } 26 | 27 | private val jsonConfiguration 28 | get() = Json { 29 | prettyPrint = true 30 | ignoreUnknownKeys = true 31 | useAlternativeNames = false 32 | } 33 | 34 | @SharedImmutable 35 | private val httpClient = HttpClient { 36 | install(ContentNegotiation) { 37 | json(jsonConfiguration) 38 | } 39 | install(Logging) { 40 | logger = Logger.SIMPLE 41 | level = LogLevel.ALL 42 | } 43 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/api/model/BreedImageResponse.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.api.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | internal data class BreedImageResponse( 8 | @SerialName("message") 9 | val breedImageUrl: String 10 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/api/model/BreedsResponse.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.api.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | @Serializable 8 | internal data class BreedsResponse(@SerialName("message") val breeds: List) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/database/DriverFactory.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.database 2 | 3 | import com.squareup.sqldelight.db.SqlDriver 4 | import org.koin.core.scope.Scope 5 | 6 | internal expect fun Scope.createDriver(databaseName: String): SqlDriver -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/di/KoinModule.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.di 2 | 3 | import com.nagyrobi144.dogify.api.BreedsApi 4 | import com.nagyrobi144.dogify.database.createDriver 5 | import com.nagyrobi144.dogify.db.DogifyDatabase 6 | import com.nagyrobi144.dogify.repository.* 7 | import com.nagyrobi144.dogify.repository.DefaultBreedsLocalSource 8 | import com.nagyrobi144.dogify.repository.DefaultBreedsRemoteSource 9 | import com.nagyrobi144.dogify.usecase.FetchBreedsUseCase 10 | import com.nagyrobi144.dogify.usecase.GetBreedsUseCase 11 | import com.nagyrobi144.dogify.usecase.ToggleFavouriteStateUseCase 12 | import com.nagyrobi144.dogify.util.getDispatcherProvider 13 | import org.koin.core.context.startKoin 14 | import org.koin.dsl.KoinAppDeclaration 15 | import org.koin.dsl.module 16 | 17 | private val utilityModule = module { 18 | factory { getDispatcherProvider() } 19 | single { DogifyDatabase(createDriver("dogify.db")) } 20 | } 21 | 22 | private val apiModule = module { 23 | factory { BreedsApi() } 24 | } 25 | 26 | private val repositoryModule = module { 27 | single { BreedsRepository() } 28 | 29 | factory { DefaultBreedsRemoteSource(get(), get()) } 30 | factory { DefaultBreedsLocalSource(get(), get()) } 31 | } 32 | 33 | private val usecaseModule = module { 34 | factory { GetBreedsUseCase() } 35 | factory { FetchBreedsUseCase() } 36 | factory { ToggleFavouriteStateUseCase() } 37 | } 38 | 39 | private val sharedModules = listOf(usecaseModule, repositoryModule, apiModule, utilityModule) 40 | 41 | fun initKoin(appDeclaration: KoinAppDeclaration) = startKoin { 42 | modules(sharedModules) 43 | appDeclaration() 44 | } 45 | 46 | fun initKoin() = initKoin { } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/model/Breed.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.model 2 | 3 | data class Breed(val name: String, val imageUrl: String, val isFavourite: Boolean = false) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/repository/BreedsLocalSource.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.repository 2 | 3 | import com.nagyrobi144.dogify.model.Breed 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface BreedsLocalSource { 7 | 8 | val breeds: Flow> 9 | 10 | suspend fun selectAll(): List 11 | 12 | suspend fun insert(breed: Breed) 13 | 14 | suspend fun update(breed: Breed) 15 | 16 | suspend fun clear() 17 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/repository/BreedsRemoteSource.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.repository 2 | 3 | interface BreedsRemoteSource { 4 | 5 | suspend fun getBreeds(): List 6 | 7 | suspend fun getBreedImage(breed: String): String 8 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/repository/BreedsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.repository 2 | 3 | import com.nagyrobi144.dogify.model.Breed 4 | import kotlinx.coroutines.async 5 | import kotlinx.coroutines.awaitAll 6 | import kotlinx.coroutines.supervisorScope 7 | import org.koin.core.component.KoinComponent 8 | import org.koin.core.component.get 9 | 10 | class BreedsRepository: KoinComponent { 11 | 12 | private val remoteSource: BreedsRemoteSource = get(null) 13 | private val localSource: BreedsLocalSource = get(null) 14 | 15 | val breeds = localSource.breeds 16 | 17 | internal suspend fun get() = with(localSource.selectAll()) { 18 | if (isNullOrEmpty()) { 19 | return@with fetch() 20 | } else { 21 | this 22 | } 23 | } 24 | 25 | internal suspend fun fetch() = supervisorScope { 26 | remoteSource.getBreeds().map { 27 | async { Breed(name = it, imageUrl = remoteSource.getBreedImage(it)) } 28 | }.awaitAll().also { 29 | localSource.clear() 30 | it.map { async { localSource.insert(it) } }.awaitAll() 31 | } 32 | } 33 | 34 | internal suspend fun update(breed: Breed) = localSource.update(breed) 35 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/repository/DefaultBreedsLocalSource.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.repository 2 | 3 | import com.nagyrobi144.dogify.db.DogifyDatabase 4 | import com.nagyrobi144.dogify.model.Breed 5 | import com.nagyrobi144.dogify.util.DispatcherProvider 6 | import com.squareup.sqldelight.runtime.coroutines.asFlow 7 | import com.squareup.sqldelight.runtime.coroutines.mapToList 8 | import kotlinx.coroutines.flow.map 9 | import kotlinx.coroutines.withContext 10 | 11 | internal class DefaultBreedsLocalSource( 12 | database: DogifyDatabase, 13 | private val dispatcherProvider: DispatcherProvider 14 | ): BreedsLocalSource { 15 | private val dao = database.breedsQueries 16 | 17 | override val breeds = dao.selectAll().asFlow().mapToList() 18 | .map { breeds -> breeds.map { Breed(it.name, it.imageUrl, it.isFavourite ?: false) } } 19 | 20 | override suspend fun selectAll() = withContext(dispatcherProvider.io) { 21 | dao.selectAll { name, imageUrl, isFavourite -> Breed(name, imageUrl, isFavourite ?: false) } 22 | .executeAsList() 23 | } 24 | 25 | override suspend fun insert(breed: Breed) = withContext(dispatcherProvider.io) { 26 | dao.insert(breed.name, breed.imageUrl, breed.isFavourite) 27 | } 28 | 29 | override suspend fun update(breed: Breed) = withContext(dispatcherProvider.io) { 30 | dao.update(breed.imageUrl, breed.isFavourite, breed.name) 31 | } 32 | 33 | override suspend fun clear() = withContext(dispatcherProvider.io) { 34 | dao.clear() 35 | } 36 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/repository/DefaultBreedsRemoteSource.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.repository 2 | 3 | import com.nagyrobi144.dogify.api.BreedsApi 4 | import com.nagyrobi144.dogify.util.DispatcherProvider 5 | import kotlinx.coroutines.withContext 6 | 7 | internal class DefaultBreedsRemoteSource( 8 | private val api: BreedsApi, 9 | private val dispatcherProvider: DispatcherProvider 10 | ): BreedsRemoteSource { 11 | 12 | override suspend fun getBreeds() = withContext(dispatcherProvider.io) { 13 | api.getBreeds().breeds 14 | } 15 | 16 | override suspend fun getBreedImage(breed: String) = withContext(dispatcherProvider.io) { 17 | api.getRandomBreedImageFor(breed).breedImageUrl 18 | } 19 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/usecase/FetchBreedsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.usecase 2 | 3 | import com.nagyrobi144.dogify.model.Breed 4 | import com.nagyrobi144.dogify.repository.BreedsRepository 5 | import org.koin.core.component.KoinComponent 6 | import org.koin.core.component.get 7 | import org.koin.core.component.inject 8 | 9 | class FetchBreedsUseCase : KoinComponent { 10 | 11 | private val breedsRepository: BreedsRepository = get() 12 | 13 | suspend operator fun invoke(): List = breedsRepository.fetch() 14 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/usecase/GetBreedsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.usecase 2 | 3 | import com.nagyrobi144.dogify.api.BreedsApi 4 | import com.nagyrobi144.dogify.model.Breed 5 | import com.nagyrobi144.dogify.repository.BreedsRepository 6 | import org.koin.core.component.KoinComponent 7 | import org.koin.core.component.get 8 | import org.koin.core.component.inject 9 | import kotlin.native.concurrent.SharedImmutable 10 | import kotlin.native.concurrent.ThreadLocal 11 | 12 | class GetBreedsUseCase : KoinComponent { 13 | 14 | private val breedsRepository: BreedsRepository = get() 15 | 16 | suspend operator fun invoke(): List = breedsRepository.get() 17 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/usecase/ToggleFavouriteStateUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.usecase 2 | 3 | import com.nagyrobi144.dogify.model.Breed 4 | import com.nagyrobi144.dogify.repository.BreedsRepository 5 | import org.koin.core.component.KoinComponent 6 | import org.koin.core.component.get 7 | import org.koin.core.component.inject 8 | 9 | class ToggleFavouriteStateUseCase: KoinComponent { 10 | 11 | private val breedsRepository: BreedsRepository = get() 12 | 13 | suspend operator fun invoke(breed: Breed){ 14 | breedsRepository.update(breed.copy(isFavourite = !breed.isFavourite)) 15 | } 16 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/nagyrobi144/dogify/util/DispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.util 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | 5 | /** 6 | * A Dispatcher abstraction in order to ease testing coroutines 7 | */ 8 | interface DispatcherProvider { 9 | val main: CoroutineDispatcher 10 | val io: CoroutineDispatcher 11 | val unconfined: CoroutineDispatcher 12 | } 13 | 14 | internal expect fun getDispatcherProvider(): DispatcherProvider -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/nagyrobi144/dogify/db/Breeds.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE breeds( 2 | name TEXT NOT NULL, 3 | imageUrl TEXT NOT NULL, 4 | isFavourite INTEGER AS Boolean DEFAULT 0 5 | ); 6 | 7 | insert: 8 | INSERT OR REPLACE INTO breeds(name, imageUrl, isFavourite) VALUES (?, ?, ?); 9 | 10 | update: 11 | UPDATE breeds SET imageUrl = ?, isFavourite = ? WHERE name = ?; 12 | 13 | selectAll: 14 | SELECT * FROM breeds; 15 | 16 | clear: 17 | DELETE FROM breeds; -------------------------------------------------------------------------------- /shared/src/commonTest/kotlin/com/nagyrobi144/dogify/BreedsRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify 2 | 3 | import com.nagyrobi144.dogify.di.initKoin 4 | import com.nagyrobi144.dogify.repository.BreedsLocalSource 5 | import com.nagyrobi144.dogify.repository.BreedsRemoteSource 6 | import com.nagyrobi144.dogify.repository.BreedsRepository 7 | import kotlinx.coroutines.flow.first 8 | import org.koin.dsl.module 9 | import kotlin.test.BeforeTest 10 | import kotlin.test.Test 11 | import kotlin.test.assertEquals 12 | 13 | class BreedsRepositoryTest { 14 | 15 | private lateinit var sut: BreedsRepository 16 | 17 | @BeforeTest 18 | fun setup() { 19 | initKoin { 20 | modules(module { 21 | single { FakeBreedsLocalSource() } 22 | factory { FakeBreedsRemoteSource() } 23 | }) 24 | } 25 | 26 | sut = BreedsRepository() 27 | } 28 | 29 | 30 | @Test 31 | fun `When get is called Then breeds are returned and cached`() = runTest { 32 | assertEquals(emptyList(), sut.breeds.first()) 33 | 34 | assertEquals(listOf(breed1, breed2), sut.get()) 35 | 36 | assertEquals(listOf(breed1, breed2), sut.breeds.first()) 37 | } 38 | } -------------------------------------------------------------------------------- /shared/src/commonTest/kotlin/com/nagyrobi144/dogify/FakeBreedsLocalSource.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify 2 | 3 | import com.nagyrobi144.dogify.model.Breed 4 | import com.nagyrobi144.dogify.repository.BreedsLocalSource 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | 8 | class FakeBreedsLocalSource : BreedsLocalSource { 9 | 10 | private val _breeds = MutableStateFlow>(emptyList()) 11 | 12 | override val breeds: Flow> 13 | get() = _breeds 14 | 15 | override suspend fun selectAll(): List = _breeds.value 16 | 17 | override suspend fun insert(breed: Breed) { 18 | _breeds.value = _breeds.value + breed 19 | } 20 | 21 | override suspend fun update(breed: Breed) { 22 | _breeds.value = _breeds.value.map { 23 | if (it.name == breed.name) { 24 | breed 25 | } else { 26 | it 27 | } 28 | } 29 | } 30 | 31 | override suspend fun clear() { 32 | _breeds.value = emptyList() 33 | } 34 | } -------------------------------------------------------------------------------- /shared/src/commonTest/kotlin/com/nagyrobi144/dogify/FakeBreedsRemoteSource.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify 2 | 3 | import com.nagyrobi144.dogify.model.Breed 4 | import com.nagyrobi144.dogify.repository.BreedsRemoteSource 5 | 6 | class FakeBreedsRemoteSource : BreedsRemoteSource { 7 | 8 | private val data = mutableMapOf( 9 | breed1.name to breed1.imageUrl, 10 | breed2.name to breed2.imageUrl 11 | ) 12 | 13 | override suspend fun getBreeds() = data.keys.toList() 14 | 15 | override suspend fun getBreedImage(breed: String) = data[breed]!! 16 | 17 | } 18 | 19 | val breed1 = Breed("vizsla", "vizsla-url") 20 | val breed2 = Breed("kuvasz", "kuvasz-url") -------------------------------------------------------------------------------- /shared/src/commonTest/kotlin/com/nagyrobi144/dogify/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | 5 | expect fun runTest(block: suspend CoroutineScope.() -> T): T -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/com/nagyrobi144/dogify/database/DriverFactory.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.database 2 | 3 | import com.nagyrobi144.dogify.db.DogifyDatabase 4 | import com.squareup.sqldelight.db.SqlDriver 5 | import com.squareup.sqldelight.drivers.native.NativeSqliteDriver 6 | import org.koin.core.scope.Scope 7 | 8 | internal actual fun Scope.createDriver(databaseName: String): SqlDriver = 9 | NativeSqliteDriver(DogifyDatabase.Schema, databaseName) 10 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/com/nagyrobi144/dogify/util/DispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify.util 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | 5 | internal actual fun getDispatcherProvider(): DispatcherProvider = IosDispatcherProvider() 6 | 7 | private class IosDispatcherProvider : DispatcherProvider { 8 | override val main = Dispatchers.Main 9 | override val io = Dispatchers.Default 10 | override val unconfined = Dispatchers.Unconfined 11 | } -------------------------------------------------------------------------------- /shared/src/iosTest/kotlin/com/nagyrobi144/dogify/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.nagyrobi144.dogify 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.runBlocking 5 | 6 | actual fun runTest(block: suspend CoroutineScope.() -> T) = runBlocking(block = block) --------------------------------------------------------------------------------