├── .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 |
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)
--------------------------------------------------------------------------------