├── AndroidApp ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ └── item_index.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── kernel │ │ │ │ └── kmpproject │ │ │ │ └── ui │ │ │ │ ├── App.kt │ │ │ │ └── main │ │ │ │ ├── adapter │ │ │ │ ├── IndexViewHolder.kt │ │ │ │ └── IndexesAdapter.kt │ │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml │ └── test │ │ └── java │ │ └── com │ │ └── kernel │ │ └── kmpproject │ │ └── ExampleUnitTest.kt ├── proguard-rules.pro └── build.gradle ├── extras ├── jacoco.gradle ├── hooks │ └── pre-commit ├── ktlint.gradle ├── install-git-hooks.gradle ├── detekt.gradle └── detekt.yml ├── images ├── Code_ios.png ├── Code_android.png ├── Screenshot_ios.png ├── Screenshot_android.png ├── architecture.drawio └── architecture.svg ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── iOSApp ├── iOSApp │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── AppDelegate.swift │ ├── Extensions.swift │ └── ViewController.swift ├── iOSApp.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── project.pbxproj ├── iOSAppTests │ ├── Info.plist │ └── iOSAppTests.swift └── iOSAppUITests │ ├── Info.plist │ └── iOSAppUITests.swift ├── SharedCode ├── src │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── kernel │ │ │ └── kmpproject │ │ │ ├── Common.kt │ │ │ ├── Logger.kt │ │ │ └── Async.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── kernel │ │ │ └── kmpproject │ │ │ ├── Common.kt │ │ │ ├── data │ │ │ ├── repository │ │ │ │ ├── BaseRepository.kt │ │ │ │ ├── AppBaseRepository.kt │ │ │ │ ├── IndexesRepository.kt │ │ │ │ ├── QuoteRepository.kt │ │ │ │ ├── AppQuoteRepository.kt │ │ │ │ └── AppIndexesRepository.kt │ │ │ ├── source │ │ │ │ └── network │ │ │ │ │ ├── AppNetworkSource.kt │ │ │ │ │ ├── NetworkSource.kt │ │ │ │ │ └── FmpApi.kt │ │ │ └── entity │ │ │ │ ├── IndexEntity.kt │ │ │ │ └── QuoteEntity.kt │ │ │ ├── domain │ │ │ ├── mapper │ │ │ │ └── Mapper.kt │ │ │ ├── model │ │ │ │ ├── Index.kt │ │ │ │ └── Quote.kt │ │ │ └── usecase │ │ │ │ ├── GetIndexesUseCase.kt │ │ │ │ ├── GetQuoteUseCase.kt │ │ │ │ ├── AppGetIndexesUseCase.kt │ │ │ │ └── AppGetQuoteUseCase.kt │ │ │ ├── Logger.kt │ │ │ ├── ui │ │ │ ├── base │ │ │ │ ├── BaseViewState.kt │ │ │ │ └── BaseViewModel.kt │ │ │ └── indexes │ │ │ │ ├── IndexesViewState.kt │ │ │ │ └── IndexesViewModel.kt │ │ │ ├── Async.kt │ │ │ ├── utils │ │ │ └── coroutines │ │ │ │ └── CoroutineExt.kt │ │ │ └── di │ │ │ └── Injector.kt │ ├── iOSMain │ │ └── kotlin │ │ │ └── com │ │ │ └── kernel │ │ │ └── kmpproject │ │ │ ├── Common.kt │ │ │ ├── Logger.kt │ │ │ └── Async.kt │ ├── commonTest │ │ └── kotlin │ │ │ └── com │ │ │ └── kernel │ │ │ └── kmpproject │ │ │ └── Test.kt │ └── androidTest │ │ └── kotlin │ │ └── com │ │ └── kernel │ │ └── kmpproject │ │ ├── QuoteMapperTest.kt │ │ ├── data │ │ └── source │ │ │ └── network │ │ │ └── AppNetworkSource.kt │ │ ├── IndexesMapperTest.kt │ │ ├── IndexesViewModelTest.kt │ │ ├── QuoteUseCaseTest.kt │ │ └── IndexesUseCaseTest.kt ├── .gitignore └── build.gradle ├── settings.gradle ├── gradle.properties ├── .editorconfig ├── .gitignore ├── gradlew.bat ├── README.md └── gradlew /AndroidApp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /extras/jacoco.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.vanniktech.android.junit.jacoco" 2 | -------------------------------------------------------------------------------- /images/Code_ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/images/Code_ios.png -------------------------------------------------------------------------------- /images/Code_android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/images/Code_android.png -------------------------------------------------------------------------------- /images/Screenshot_ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/images/Screenshot_ios.png -------------------------------------------------------------------------------- /images/Screenshot_android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/images/Screenshot_android.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /AndroidApp/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | KMP PROJECT 3 | 4 | -------------------------------------------------------------------------------- /iOSApp/iOSApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SharedCode/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/Common.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | expect fun platformName(): String -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | enableFeaturePreview('GRADLE_METADATA') 2 | 3 | rootProject.name = 'KmpProject' 4 | 5 | include ':AndroidApp', ':SharedCode' 6 | -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /SharedCode/src/iOSMain/kotlin/com/kernel/kmpproject/Common.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | actual fun platformName(): String { 4 | return "iOS" 5 | } -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /SharedCode/src/androidMain/kotlin/com/kernel/kmpproject/Common.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | actual fun platformName(): String { 4 | return "Android" 5 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/repository/BaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.repository 2 | 3 | interface BaseRepository -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kernel0x/kmpapp/HEAD/AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536m 2 | org.gradle.parallel=true 3 | 4 | android.useAndroidX=true 5 | android.enableJetifier=true 6 | 7 | kotlin.code.style=official 8 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/domain/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.domain.mapper 2 | 3 | interface Mapper { 4 | fun transform(model: T): E 5 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{kt, kts}] 8 | indent_size = 2 9 | insert_final_newline = false 10 | max_line_length = 120 11 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | expect fun logError(tag: String, message: String, error: Throwable? = null) 4 | expect fun logInfo(tag: String, message: String) -------------------------------------------------------------------------------- /extras/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running git pre-commit hook" 4 | 5 | ./gradlew check 6 | 7 | RESULT=$? 8 | 9 | # return 1 exit code if running checks fails 10 | [ $RESULT -ne 0 ] && exit 1 11 | exit 0 12 | -------------------------------------------------------------------------------- /iOSApp/iOSApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AndroidApp/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /AndroidApp/src/main/java/com/kernel/kmpproject/ui/App.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.ui 2 | 3 | import androidx.multidex.MultiDexApplication 4 | 5 | open class App : MultiDexApplication() { 6 | 7 | override fun onCreate() { 8 | super.onCreate() 9 | } 10 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/ui/base/BaseViewState.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.ui.base 2 | 3 | sealed class BaseViewState 4 | object Loading : BaseViewState() 5 | object Done : BaseViewState() 6 | data class Error(val message: String) : BaseViewState() -------------------------------------------------------------------------------- /SharedCode/src/commonTest/kotlin/com/kernel/kmpproject/Test.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class Test { 7 | 8 | @Test 9 | fun `simple test`() { 10 | assertEquals(0, 0) 11 | } 12 | } -------------------------------------------------------------------------------- /extras/ktlint.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "org.jlleitschuh.gradle.ktlint" 2 | 3 | ktlint { 4 | version = "0.36.0" 5 | verbose = true 6 | android = false 7 | outputToConsole = true 8 | } 9 | 10 | check.dependsOn ktlintFormat 11 | check.dependsOn ktlintCheck 12 | -------------------------------------------------------------------------------- /SharedCode/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml 3 | .gradle 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /captures 13 | .externalNativeBuild -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Apr 14 11:53:02 MSK 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 7 | -------------------------------------------------------------------------------- /extras/install-git-hooks.gradle: -------------------------------------------------------------------------------- 1 | task installGitHook(type: Copy) { 2 | from new File(rootProject.rootDir, 'extras/hooks/pre-commit') 3 | into { new File(rootProject.rootDir, '.git/hooks') } 4 | fileMode 0777 5 | } 6 | 7 | tasks.getByPath(':SharedCode:preBuild').dependsOn installGitHook 8 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/domain/model/Index.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.domain.model 2 | 3 | typealias Indexes = List 4 | 5 | data class Index( 6 | val ticker: String, 7 | val changes: Float, 8 | val price: Float, 9 | val indexName: String 10 | ) -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/domain/model/Quote.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.domain.model 2 | 3 | typealias Quotes = List 4 | 5 | data class Quote( 6 | val dayLow: String, 7 | val dayHigh: String, 8 | val yearHigh: String, 9 | val yearLow: String 10 | ) -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/repository/AppBaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.repository 2 | 3 | import com.kernel.kmpproject.data.source.network.NetworkSource 4 | 5 | abstract class AppBaseRepository(protected val networkSource: NetworkSource) : BaseRepository -------------------------------------------------------------------------------- /extras/detekt.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "io.gitlab.arturbosch.detekt" 2 | 3 | detekt { 4 | toolVersion = "1.0.0" 5 | input = files("src") 6 | filters = ".*/resources/.*,.*/build/.*" 7 | config = files(file("$project.rootDir/extras/detekt.yml")) 8 | } 9 | 10 | check.dependsOn 'detekt' 11 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/repository/IndexesRepository.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.repository 2 | 3 | import com.kernel.kmpproject.domain.model.Indexes 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface IndexesRepository { 7 | suspend fun getMajor(): Flow 8 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/domain/usecase/GetIndexesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.domain.usecase 2 | 3 | import com.kernel.kmpproject.domain.model.Indexes 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface GetIndexesUseCase { 7 | suspend operator fun invoke(): Flow 8 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/repository/QuoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.repository 2 | 3 | import com.kernel.kmpproject.domain.model.Quote 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface QuoteRepository { 7 | suspend fun getQuoteIndex(symbol: String): Flow 8 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/domain/usecase/GetQuoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.domain.usecase 2 | 3 | import com.kernel.kmpproject.domain.model.Quote 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface GetQuoteUseCase { 7 | suspend operator fun invoke(symbol: String): Flow 8 | } -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /AndroidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /SharedCode/src/androidMain/kotlin/com/kernel/kmpproject/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import android.util.Log 4 | 5 | actual fun logError(tag: String, message: String, error: Throwable?) { 6 | Log.e(tag, message, error) 7 | } 8 | 9 | actual fun logInfo(tag: String, message: String) { 10 | Log.i(tag, message) 11 | } -------------------------------------------------------------------------------- /iOSApp/iOSApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SharedCode/src/iOSMain/kotlin/com/kernel/kmpproject/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import platform.Foundation.NSLog 4 | 5 | actual fun logError(tag: String, message: String, error: Throwable?) { 6 | NSLog("$tag: $message") 7 | } 8 | 9 | actual fun logInfo(tag: String, message: String) { 10 | NSLog("$tag: $message") 11 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/source/network/AppNetworkSource.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.source.network 2 | 3 | class AppNetworkSource(private val fmpApi: FmpApi) : NetworkSource { 4 | override suspend fun getMajorIndexesList() = fmpApi.getMajorsIndexes() 5 | override suspend fun getQuotes(symbol: String) = fmpApi.getQuotes(symbol) 6 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/source/network/NetworkSource.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.source.network 2 | 3 | import com.kernel.kmpproject.data.entity.MajorIndexesListEntity 4 | import com.kernel.kmpproject.data.entity.QuoteEntity 5 | 6 | interface NetworkSource { 7 | suspend fun getMajorIndexesList(): MajorIndexesListEntity 8 | suspend fun getQuotes(symbol: String): List 9 | } -------------------------------------------------------------------------------- /AndroidApp/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /AndroidApp/src/test/java/com/kernel/kmpproject/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /extras/detekt.yml: -------------------------------------------------------------------------------- 1 | autoCorrect: true 2 | 3 | build: 4 | warningThreshold: 5 5 | failThreshold: 10 6 | weights: 7 | complexity: 2 8 | formatting: 0 9 | LongParameterList: 1 10 | comments: 0.5 11 | 12 | style: 13 | active: true 14 | WildcardImport: 15 | active: true 16 | MaxLineLength: 17 | active: true 18 | maxLineLength: 120 19 | excludePackageStatements: true 20 | excludeImportStatements: true 21 | excludeCommentStatements: true -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/ui/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.ui.base 2 | 3 | import com.kernel.kmpproject.di.Injector 4 | import dev.icerock.moko.mvvm.viewmodel.ViewModel 5 | import kotlin.coroutines.CoroutineContext 6 | import kotlinx.coroutines.CoroutineScope 7 | import org.kodein.di.erased.instance 8 | 9 | open class BaseViewModel : ViewModel(), CoroutineScope { 10 | 11 | override val coroutineContext by Injector.instance() 12 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/domain/usecase/AppGetIndexesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.domain.usecase 2 | 3 | import com.kernel.kmpproject.data.repository.IndexesRepository 4 | import com.kernel.kmpproject.domain.model.Indexes 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | class AppGetIndexesUseCase( 8 | private val indexRepository: IndexesRepository 9 | ) : GetIndexesUseCase { 10 | 11 | override suspend fun invoke(): Flow = indexRepository.getMajor() 12 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/domain/usecase/AppGetQuoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.domain.usecase 2 | 3 | import com.kernel.kmpproject.data.repository.QuoteRepository 4 | import com.kernel.kmpproject.domain.model.Quote 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | class AppGetQuoteUseCase( 8 | private val quoteRepository: QuoteRepository 9 | ) : GetQuoteUseCase { 10 | 11 | override suspend fun invoke(symbol: String): Flow = quoteRepository.getQuoteIndex(symbol) 12 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/repository/AppQuoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.repository 2 | 3 | import com.kernel.kmpproject.data.entity.toDomain 4 | import com.kernel.kmpproject.data.source.network.NetworkSource 5 | import kotlinx.coroutines.flow.flow 6 | 7 | class AppQuoteRepository(networkSource: NetworkSource) : AppBaseRepository(networkSource), QuoteRepository { 8 | 9 | override suspend fun getQuoteIndex(symbol: String) = flow { emit(networkSource.getQuotes(symbol).toDomain()[0]) } 10 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/ui/indexes/IndexesViewState.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.ui.indexes 2 | 3 | import com.kernel.kmpproject.domain.model.Indexes 4 | import com.kernel.kmpproject.domain.model.Quote 5 | 6 | sealed class IndexesViewState 7 | object Empty : IndexesViewState() 8 | object Loading : IndexesViewState() 9 | data class Error(val message: String) : IndexesViewState() 10 | data class ShowMajorIndexes(val indexes: Indexes) : IndexesViewState() 11 | data class ShowQuote(val quote: Quote) : IndexesViewState() -------------------------------------------------------------------------------- /SharedCode/src/androidTest/kotlin/com/kernel/kmpproject/QuoteMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import com.kernel.kmpproject.data.entity.QuoteEntity 4 | import com.kernel.kmpproject.data.entity.toDomain 5 | import kotlin.test.Test 6 | import org.junit.Assert 7 | 8 | class QuoteMapperTest { 9 | 10 | @Test 11 | fun `Check mapping QuoteEntity to Quote`() { 12 | val inputData = QuoteEntity("", "", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0) 13 | 14 | val outputData = inputData.toDomain() 15 | 16 | Assert.assertEquals(outputData.dayHigh, "0.0") 17 | } 18 | } -------------------------------------------------------------------------------- /SharedCode/src/androidTest/kotlin/com/kernel/kmpproject/data/source/network/AppNetworkSource.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.source.network 2 | 3 | import com.kernel.kmpproject.data.entity.MajorIndexesListEntity 4 | import com.kernel.kmpproject.data.entity.QuoteEntity 5 | 6 | class AppNetworkSource(private val fmpApi: FmpApi) : NetworkSource { 7 | 8 | override suspend fun getMajorIndexesList() = 9 | MajorIndexesListEntity(emptyList()) 10 | 11 | override suspend fun getQuotes(symbol: String) = 12 | listOf( 13 | QuoteEntity("", "", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0) 14 | ) 15 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/Async.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import kotlin.coroutines.CoroutineContext 4 | import kotlin.coroutines.EmptyCoroutineContext 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Job 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | expect val ApplicationDispatcher: CoroutineContext 10 | 11 | expect fun CoroutineScope.launchInMain(block: suspend CoroutineScope.() -> Unit): Job 12 | 13 | expect fun Flow.flowOnBackground(): Flow 14 | 15 | expect fun runBlocking( 16 | context: CoroutineContext = EmptyCoroutineContext, 17 | block: suspend CoroutineScope.() -> T 18 | ): T -------------------------------------------------------------------------------- /AndroidApp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/entity/IndexEntity.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.entity 2 | 3 | import com.kernel.kmpproject.domain.model.Index 4 | import com.kernel.kmpproject.domain.model.Indexes 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class IndexEntity( 9 | val ticker: String, 10 | val changes: Float, 11 | val price: Float, 12 | val indexName: String 13 | ) 14 | 15 | @Serializable 16 | data class MajorIndexesListEntity( 17 | val majorIndexesList: List 18 | ) 19 | 20 | internal fun List.toDomain(): Indexes = map { it.toDomain() } 21 | 22 | internal fun IndexEntity.toDomain(): Index = Index( 23 | ticker = ticker, 24 | changes = changes, 25 | price = price, 26 | indexName = indexName 27 | ) -------------------------------------------------------------------------------- /SharedCode/src/androidMain/kotlin/com/kernel/kmpproject/Async.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import kotlin.coroutines.CoroutineContext 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flowOn 8 | import kotlinx.coroutines.launch 9 | 10 | actual val ApplicationDispatcher: CoroutineContext = Dispatchers.Default 11 | 12 | actual fun CoroutineScope.launchInMain(block: suspend CoroutineScope.() -> Unit) = 13 | launch(Dispatchers.Main) { block() } 14 | 15 | actual fun Flow.flowOnBackground(): Flow = flowOn(Dispatchers.Default) 16 | 17 | actual fun runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T = 18 | kotlinx.coroutines.runBlocking(context, block) -------------------------------------------------------------------------------- /iOSApp/iOSAppTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /iOSApp/iOSAppUITests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /AndroidApp/src/main/java/com/kernel/kmpproject/ui/main/adapter/IndexViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.ui.main.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.kernel.kmpproject.R 7 | import com.kernel.kmpproject.domain.model.Index 8 | import kotlinx.android.synthetic.main.item_index.view.* 9 | 10 | class IndexViewHolder(view: ViewGroup) : 11 | RecyclerView.ViewHolder(LayoutInflater.from(view.context).inflate(R.layout.item_index, view, false)) { 12 | 13 | fun bind(index: Index) { 14 | with(itemView) { 15 | changes.text = index.changes.toString() 16 | indexName.text = index.indexName 17 | ticker.text = index.ticker 18 | price.text = index.price.toString() 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | 16 | ## Build generated 17 | build/ 18 | DerivedData 19 | build.xcarchive 20 | 21 | ## Various settings 22 | *.pbxuser 23 | !default.pbxuser 24 | *.mode1v3 25 | !default.mode1v3 26 | *.mode2v3 27 | !default.mode2v3 28 | *.perspectivev3 29 | !default.perspectivev3 30 | xcuserdata 31 | 32 | ## Other 33 | *.xccheckout 34 | *.moved-aside 35 | *.xcuserstate 36 | *.xcscmblueprint 37 | 38 | ## Obj-C/Swift specific 39 | *.hmap 40 | *.ipa 41 | 42 | # CocoaPods 43 | Pods/ 44 | 45 | #Node Modules 46 | node_modules 47 | 48 | # Firebase 49 | AndroidApp/google-services.json -------------------------------------------------------------------------------- /AndroidApp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /SharedCode/src/androidTest/kotlin/com/kernel/kmpproject/IndexesMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import com.kernel.kmpproject.data.entity.IndexEntity 4 | import com.kernel.kmpproject.data.entity.toDomain 5 | import kotlin.test.Test 6 | import org.junit.Assert 7 | 8 | class IndexesMapperTest { 9 | 10 | @Test 11 | fun `Check mapping IndexEntity to Index`() { 12 | val inputData = listOf( 13 | IndexEntity(".DJI", 28.7089f, 23719.4f, "Dow Jones") 14 | ) 15 | 16 | val outputData = inputData.toDomain() 17 | 18 | Assert.assertEquals(outputData.size, inputData.size) 19 | 20 | outputData.forEach { 21 | Assert.assertEquals(it.ticker, ".DJI") 22 | Assert.assertEquals(it.changes, 28.7089f) 23 | Assert.assertEquals(it.price, 23719.4f) 24 | Assert.assertEquals(it.indexName, "Dow Jones") 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /iOSApp/iOSAppTests/iOSAppTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import iOSApp 3 | 4 | class iOSAppTests: XCTestCase { 5 | 6 | override func setUp() { 7 | // Put setup code here. This method is called before the invocation of each test method in the class. 8 | } 9 | 10 | override func tearDown() { 11 | // Put teardown code here. This method is called after the invocation of each test method in the class. 12 | } 13 | 14 | func testExample() { 15 | // This is an example of a functional test case. 16 | // Use XCTAssert and related functions to verify your tests produce the correct results. 17 | } 18 | 19 | func testPerformanceExample() { 20 | // This is an example of a performance test case. 21 | self.measure { 22 | // Put the code you want to measure the time of here. 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/entity/QuoteEntity.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.entity 2 | 3 | import com.kernel.kmpproject.domain.model.Quote 4 | import com.kernel.kmpproject.domain.model.Quotes 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class QuoteEntity( 9 | val symbol: String, 10 | val name: String, 11 | val price: Double, 12 | val changesPercentage: Double, 13 | val change: Double, 14 | val dayLow: Double, 15 | val dayHigh: Double, 16 | val yearHigh: Double, 17 | val yearLow: Double, 18 | val timestamp: Long 19 | ) 20 | 21 | internal fun List.toDomain(): Quotes = map { it.toDomain() } 22 | 23 | internal fun QuoteEntity.toDomain(): Quote = Quote( 24 | dayLow = dayLow.toString(), 25 | dayHigh = dayHigh.toString(), 26 | yearLow = yearLow.toString(), 27 | yearHigh = yearHigh.toString() 28 | ) -------------------------------------------------------------------------------- /AndroidApp/src/main/java/com/kernel/kmpproject/ui/main/adapter/IndexesAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.ui.main.adapter 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.kernel.kmpproject.domain.model.Index 6 | import com.kernel.kmpproject.domain.model.Indexes 7 | import kotlinx.coroutines.Job 8 | 9 | class IndexesAdapter(private val listener: (Index) -> Unit) : RecyclerView.Adapter() { 10 | 11 | var items: Indexes = emptyList() 12 | set(value) { 13 | field = value 14 | notifyDataSetChanged() 15 | } 16 | 17 | override fun getItemCount() = items.size 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IndexViewHolder { 20 | return IndexViewHolder(parent) 21 | } 22 | 23 | override fun onBindViewHolder(holder: IndexViewHolder, position: Int) { 24 | holder.bind(items[position]) 25 | holder.itemView.setOnClickListener { 26 | listener.invoke(items[position]) 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /AndroidApp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/repository/AppIndexesRepository.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.repository 2 | 3 | import com.kernel.kmpproject.data.entity.toDomain 4 | import com.kernel.kmpproject.data.source.network.NetworkSource 5 | import com.kernel.kmpproject.domain.model.Indexes 6 | import com.kernel.kmpproject.logInfo 7 | import com.kernel.kmpproject.utils.coroutines.timer 8 | import kotlin.time.ExperimentalTime 9 | import kotlin.time.seconds 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.map 12 | 13 | class AppIndexesRepository(networkSource: NetworkSource) : AppBaseRepository(networkSource), IndexesRepository { 14 | 15 | private companion object { 16 | @ExperimentalTime 17 | val updateInterval = 5.seconds 18 | } 19 | 20 | override suspend fun getMajor(): Flow { 21 | return timer(repeatEvery = updateInterval) 22 | .map { 23 | logInfo("Indexes", networkSource.getMajorIndexesList().majorIndexesList.size.toString()) 24 | networkSource.getMajorIndexesList().majorIndexesList.toDomain() 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /iOSApp/iOSAppUITests/iOSAppUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class iOSAppUITests: XCTestCase { 4 | 5 | override func setUp() { 6 | // Put setup code here. This method is called before the invocation of each test method in the class. 7 | 8 | // In UI tests it is usually best to stop immediately when a failure occurs. 9 | continueAfterFailure = false 10 | 11 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 12 | XCUIApplication().launch() 13 | 14 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 15 | } 16 | 17 | override func tearDown() { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() { 22 | // Use recording to get started writing UI tests. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/utils/coroutines/CoroutineExt.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.utils.coroutines 2 | 3 | import kotlin.coroutines.CoroutineContext 4 | import kotlin.time.Duration 5 | import kotlinx.coroutines.CoroutineExceptionHandler 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.CoroutineStart 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.Job 10 | import kotlinx.coroutines.flow.flow 11 | import kotlinx.coroutines.launch 12 | 13 | /** 14 | * Equivalent to [launch] but return [Unit] instead of [Job]. 15 | * 16 | * Mainly for usage when you want to lift [launch] to return. Example: 17 | * 18 | * ``` 19 | * override fun loadData() = launchSilent { 20 | * // code 21 | * } 22 | * ``` 23 | */ 24 | fun launchSilent( 25 | context: CoroutineContext = Dispatchers.Main, 26 | exceptionHandler: CoroutineExceptionHandler? = null, 27 | job: Job, 28 | start: CoroutineStart = CoroutineStart.DEFAULT, 29 | block: suspend CoroutineScope.() -> Unit 30 | ) { 31 | val coroutineScope = if (exceptionHandler != null) { 32 | CoroutineScope(context + job + exceptionHandler) 33 | } else { 34 | CoroutineScope(context + job) 35 | } 36 | coroutineScope.launch(context, start, block) 37 | } 38 | 39 | fun timer( 40 | delay: Duration = Duration.ZERO, 41 | repeatEvery: Duration 42 | ) = flow { 43 | kotlinx.coroutines.delay(delay.toLongMilliseconds()) 44 | while (true) { 45 | emit(Unit) 46 | kotlinx.coroutines.delay(repeatEvery.toLongMilliseconds()) 47 | } 48 | } -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/ui/indexes/IndexesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.ui.indexes 2 | 3 | import com.kernel.kmpproject.di.Injector 4 | import com.kernel.kmpproject.domain.usecase.GetIndexesUseCase 5 | import com.kernel.kmpproject.domain.usecase.GetQuoteUseCase 6 | import com.kernel.kmpproject.flowOnBackground 7 | import com.kernel.kmpproject.launchInMain 8 | import com.kernel.kmpproject.ui.base.BaseViewModel 9 | import dev.icerock.moko.mvvm.livedata.MutableLiveData 10 | import kotlinx.coroutines.flow.catch 11 | import kotlinx.coroutines.flow.collect 12 | import kotlinx.coroutines.flow.onStart 13 | import org.kodein.di.erased.instance 14 | 15 | class IndexesViewModel : BaseViewModel() { 16 | 17 | private val getIndexesUseCase by Injector.instance() 18 | private val getQuoteUseCase by Injector.instance() 19 | 20 | var getViewData = MutableLiveData(Empty) 21 | 22 | fun getMajorIndexes() = launchInMain { 23 | getIndexesUseCase() 24 | .onStart { getViewData.postValue(Loading) } 25 | .flowOnBackground() 26 | .catch { getViewData.postValue(Error("Something went wrong")) } 27 | .collect { getViewData.postValue(ShowMajorIndexes(it)) } 28 | } 29 | 30 | fun getQuote(symbol: String) = launchInMain { 31 | getQuoteUseCase.invoke(symbol) 32 | .onStart { getViewData.postValue(Loading) } 33 | .flowOnBackground() 34 | .catch { getViewData.postValue(Error("Something went wrong")) } 35 | .collect { getViewData.postValue(ShowQuote(it)) } 36 | } 37 | } -------------------------------------------------------------------------------- /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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/data/source/network/FmpApi.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.data.source.network 2 | 3 | import com.kernel.kmpproject.data.entity.MajorIndexesListEntity 4 | import com.kernel.kmpproject.data.entity.QuoteEntity 5 | import com.kernel.kmpproject.platformName 6 | import io.ktor.client.HttpClient 7 | import io.ktor.client.features.logging.DEFAULT 8 | import io.ktor.client.features.logging.LogLevel 9 | import io.ktor.client.features.logging.Logger 10 | import io.ktor.client.features.logging.Logging 11 | import io.ktor.client.request.get 12 | import io.ktor.http.userAgent 13 | import kotlinx.serialization.UnstableDefault 14 | import kotlinx.serialization.internal.ArrayListSerializer 15 | import kotlinx.serialization.json.Json 16 | import kotlinx.serialization.json.JsonConfiguration 17 | 18 | private const val BASE_URL = "https://financialmodelingprep.com/api/v3" 19 | 20 | @UseExperimental(UnstableDefault::class) 21 | class FmpApi { 22 | 23 | private val client: HttpClient by lazy { 24 | HttpClient { 25 | install(Logging) { 26 | logger = Logger.DEFAULT 27 | level = LogLevel.ALL 28 | } 29 | } 30 | } 31 | 32 | suspend fun getMajorsIndexes(): MajorIndexesListEntity = client.get("$BASE_URL/majors-indexes") { 33 | userAgent(platformName()) 34 | }.let { 35 | Json(JsonConfiguration()).parse(MajorIndexesListEntity.serializer(), it) 36 | } 37 | 38 | suspend fun getQuotes(symbol: String): List = 39 | client.get("$BASE_URL/quote/$symbol") { 40 | userAgent(platformName()) 41 | }.let { 42 | Json(JsonConfiguration()).parse(ArrayListSerializer(QuoteEntity.serializer()), it) 43 | } 44 | } -------------------------------------------------------------------------------- /AndroidApp/src/main/res/layout/item_index.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 21 | 22 | 29 | 30 | 37 | 38 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /AndroidApp/src/main/java/com/kernel/kmpproject/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.ui.main 2 | 3 | import android.os.Bundle 4 | import android.widget.Toast 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.lifecycle.ViewModelProviders 7 | import com.kernel.kmpproject.R 8 | import com.kernel.kmpproject.ui.indexes.* 9 | import com.kernel.kmpproject.ui.main.adapter.IndexesAdapter 10 | import kotlinx.android.synthetic.main.activity_main.* 11 | 12 | class MainActivity : AppCompatActivity() { 13 | 14 | private lateinit var viewModel: IndexesViewModel 15 | private var adapter: IndexesAdapter = IndexesAdapter { viewModel.getQuote(it.ticker) } 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_main) 20 | 21 | recycler.adapter = adapter 22 | 23 | viewModel = ViewModelProviders.of(this).get(IndexesViewModel::class.java) 24 | observeViewState() 25 | 26 | viewModel.getMajorIndexes() 27 | } 28 | 29 | private fun observeViewState() { 30 | viewModel.getViewData.addObserver { updateViewState(it) } 31 | } 32 | 33 | private fun updateViewState(state: IndexesViewState) = runOnUiThread { 34 | when (state) { 35 | is Loading -> { 36 | Toast.makeText(this, "Loading...", Toast.LENGTH_SHORT).show() 37 | } 38 | is Error -> { 39 | Toast.makeText(this, state.message, Toast.LENGTH_LONG).show() 40 | } 41 | is ShowMajorIndexes -> { 42 | adapter.items = state.indexes 43 | } 44 | is ShowQuote -> { 45 | Toast.makeText(this, state.quote.dayLow + " - " + state.quote.dayHigh, Toast.LENGTH_LONG).show() 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SharedCode/src/commonMain/kotlin/com/kernel/kmpproject/di/Injector.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject.di 2 | 3 | import com.kernel.kmpproject.ApplicationDispatcher 4 | import com.kernel.kmpproject.data.repository.AppIndexesRepository 5 | import com.kernel.kmpproject.data.repository.AppQuoteRepository 6 | import com.kernel.kmpproject.data.repository.IndexesRepository 7 | import com.kernel.kmpproject.data.repository.QuoteRepository 8 | import com.kernel.kmpproject.data.source.network.AppNetworkSource 9 | import com.kernel.kmpproject.data.source.network.FmpApi 10 | import com.kernel.kmpproject.data.source.network.NetworkSource 11 | import com.kernel.kmpproject.domain.usecase.AppGetIndexesUseCase 12 | import com.kernel.kmpproject.domain.usecase.AppGetQuoteUseCase 13 | import com.kernel.kmpproject.domain.usecase.GetIndexesUseCase 14 | import com.kernel.kmpproject.domain.usecase.GetQuoteUseCase 15 | import kotlin.coroutines.CoroutineContext 16 | import kotlin.native.concurrent.ThreadLocal 17 | import org.kodein.di.Kodein 18 | import org.kodein.di.erased.bind 19 | import org.kodein.di.erased.instance 20 | import org.kodein.di.erased.provider 21 | import org.kodein.di.erased.singleton 22 | 23 | @ThreadLocal 24 | val Injector = Kodein { 25 | 26 | bind() with provider { ApplicationDispatcher } 27 | 28 | bind() with singleton { AppGetIndexesUseCase(instance()) } 29 | 30 | bind() with singleton { AppGetQuoteUseCase(instance()) } 31 | 32 | bind() with provider { AppIndexesRepository(instance()) } 33 | 34 | bind() with provider { AppQuoteRepository(instance()) } 35 | 36 | bind() with provider { AppNetworkSource(instance()) } 37 | 38 | bind() with provider { FmpApi() } 39 | } -------------------------------------------------------------------------------- /iOSApp/iOSApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SharedCode/src/androidTest/kotlin/com/kernel/kmpproject/IndexesViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import android.os.Looper 4 | import com.kernel.kmpproject.domain.model.Quote 5 | import com.kernel.kmpproject.ui.indexes.IndexesViewModel 6 | import com.kernel.kmpproject.ui.indexes.ShowMajorIndexes 7 | import com.kernel.kmpproject.ui.indexes.ShowQuote 8 | import io.mockk.every 9 | import io.mockk.mockk 10 | import io.mockk.mockkStatic 11 | import kotlin.test.BeforeTest 12 | import kotlin.test.Test 13 | import kotlin.test.assertTrue 14 | import kotlin.test.fail 15 | 16 | class IndexesViewModelTest { 17 | 18 | private lateinit var viewModel: IndexesViewModel 19 | 20 | @BeforeTest 21 | fun setUp() { 22 | val mockMainThreadLooper = mockk() 23 | every { mockMainThreadLooper.thread } returns Thread.currentThread() 24 | mockkStatic(Looper::class) 25 | every { Looper.getMainLooper() } returns mockMainThreadLooper 26 | viewModel = IndexesViewModel() 27 | } 28 | 29 | @Test 30 | fun `Check state ShowMajorIndexes`() { 31 | viewModel.getViewData.addObserver { 32 | when (it) { 33 | is Error -> { 34 | fail("State is Error!") 35 | } 36 | is ShowMajorIndexes -> { 37 | assertTrue(it.indexes.isEmpty()) 38 | } 39 | } 40 | } 41 | viewModel.getMajorIndexes() 42 | viewModel.getViewData.value = ShowMajorIndexes(emptyList()) 43 | } 44 | 45 | @Test 46 | fun `Check state ShowQuote`() { 47 | viewModel.getViewData.addObserver { 48 | when (it) { 49 | is Error -> { 50 | fail("State is Error!") 51 | } 52 | is ShowQuote -> { 53 | assertTrue(it.quote.dayLow.isEmpty()) 54 | } 55 | } 56 | } 57 | viewModel.getQuote("") 58 | viewModel.getViewData.value = ShowQuote(Quote("", "", "", "")) 59 | } 60 | } -------------------------------------------------------------------------------- /SharedCode/src/androidTest/kotlin/com/kernel/kmpproject/QuoteUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import com.kernel.kmpproject.data.entity.QuoteEntity 4 | import com.kernel.kmpproject.data.repository.AppQuoteRepository 5 | import com.kernel.kmpproject.data.source.network.NetworkSource 6 | import com.kernel.kmpproject.di.Injector 7 | import com.kernel.kmpproject.domain.usecase.AppGetQuoteUseCase 8 | import com.kernel.kmpproject.domain.usecase.GetQuoteUseCase 9 | import io.mockk.every 10 | import io.mockk.mockk 11 | import kotlin.test.Test 12 | import kotlin.test.assertTrue 13 | import kotlin.test.fail 14 | import kotlinx.coroutines.flow.catch 15 | import kotlinx.coroutines.flow.collect 16 | import kotlinx.coroutines.withTimeoutOrNull 17 | import org.kodein.di.erased.instance 18 | 19 | class QuoteUseCaseTest { 20 | 21 | private val getQuoteUseCase by Injector.instance() 22 | 23 | @Test 24 | fun `Check dayLow is zero`() { 25 | runBlocking { 26 | withTimeoutOrNull(250) { 27 | getQuoteUseCase("") 28 | .flowOnBackground() 29 | .catch { fail(it.message) } 30 | .collect { 31 | assertTrue(it.dayLow == "0.0") 32 | } 33 | } 34 | } 35 | } 36 | 37 | @Test 38 | fun `Check yearHigh is not zero`() { 39 | val networkSource = mockk() 40 | every { runBlocking { networkSource.getQuotes("") } } returns 41 | listOf( 42 | QuoteEntity("", "", 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0) 43 | ) 44 | val quoteRepository = AppQuoteRepository(networkSource) 45 | val getQuoteUseCase = AppGetQuoteUseCase(quoteRepository) 46 | 47 | runBlocking { 48 | withTimeoutOrNull(250) { 49 | getQuoteUseCase("") 50 | .flowOnBackground() 51 | .catch { fail(it.message) } 52 | .collect { 53 | assertTrue(it.yearHigh != "0.0") 54 | } 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /AndroidApp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | defaultConfig { 8 | applicationId "com.kernel.kmpproject" 9 | minSdkVersion 21 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | 22 | packagingOptions { 23 | exclude("META-INF/*.kotlin_module") 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 30 | implementation 'androidx.appcompat:appcompat:1.1.0' 31 | implementation 'androidx.core:core-ktx:1.2.0' 32 | testImplementation 'junit:junit:4.12' 33 | androidTestImplementation 'androidx.test.ext:junit:1.1.0' 34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 35 | 36 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' 37 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3' 38 | 39 | implementation project(':SharedCode') 40 | 41 | implementation "android.arch.lifecycle:extensions:1.1.1" 42 | implementation "android.arch.lifecycle:viewmodel:1.1.1" 43 | 44 | implementation("dev.icerock.moko:mvvm:0.4.0") 45 | 46 | implementation 'androidx.multidex:multidex:2.0.0' 47 | androidTestImplementation('androidx.multidex:multidex-instrumentation:2.0.0') { 48 | exclude group: 'com.android.support', module: 'multidex' 49 | } 50 | 51 | implementation 'androidx.recyclerview:recyclerview:1.1.0' 52 | } 53 | -------------------------------------------------------------------------------- /images/architecture.drawio: -------------------------------------------------------------------------------- 1 | 7VpRc+I2EP41PJKxJGzgkSOk18517qZp02dhC9DUtlxZBHK/vmtbMjZygkkMeKbxQ4LXkizt93270sKAzKP9L5Imm99FwMIBdoL9gNwPMEaIjOBfZnkpLJ47LQxryQPd6GB45D+ZNjrauuUBS2sNlRCh4knd6Is4Zr6q2aiUYldvthJh/a0JXTPL8OjT0Lb+zQO1KawTPD7YvzK+3pg3I0+vL6KmsV5JuqGB2FVMZDEgcymEKj5F+zkLM+cZvxT9Hl55Wk5Msli16TDcr55Gfy4omlDC09++/Jw8PA31KM803OoF68mqF+OBZyYVB4d8o0sW/hApV1zE8GgplBLRgHwxDWYhX2cPlEjAulFRCDcIPsLKk2ywaL/OSHK3pCn372Dmai5ifxGGACbL2ikwzeJ1/mbnDrtgY3FwsIwzC5W+QUK3WfEwnItQyHzCBDljMl3k40nxDzNPYhFnL9ELhjmz/aueRCU+QGwmIqbkCzTRHUa6h6Y0djTCuwNBkEF9UyWHMVJNynU59AE3+KChOwNG3GMYxxaMuBWMD3hK0CVhRCOvbziSHuP4XjnOFiNE8AVxxG7v9OhZON5TRS0sDRKs4vPMR8ahnuW1anhbiVjphAnZsoTYMY/0e5C+bxqiA++Pj0REXMv5boPv3Uu5fmy7XkSUx904vwxK73d+OUQXIWyC+uX9ieX9H5KlLB4OsJcTD9ataB6yOsGjDC7vx6McootQNOmZGlDTBs8LlXYDfF6rfOlVG2ymjXe8f7eiaFAEDVY11fsujaEIdYURJr08bgi2pncv5WmLnl9qjglmLqg6rQqpjDHrNyx6zaABcpK9vZA/WJKlUyE5HDgw+NsB1CG9ZncOjYNsZLGVfn4f1BfZtCDLfER4KbZxwAKTlZuy41EqXeRXlnR1bvdhhkwWWdiwH7+6B9htuGKPCfWzdjvI/fV9QVVCHenhaKdMSMvMPLmYHpp2ynVuvY88Z4+C66M0a07nihaa0znudqoruuQFgINe/oI4jp05TTPNfIrlvPMIQjdXS9N5pL9q0Zn8tFqKPYnZh9xaM0+c7YxuPjVy3lnv9hnFvKyikVkcSAGdOtnhkvv7+WT0oR1uOUQXGX08rQephsP2VXe42LX8z78/duN7x5nC9SHfl0N04fupUye/e+PThWvnh2/8mb1Z64hovKXhr3GyVacjSFkzd+zYNc+vVwPdUew5PoTnV1PgC7hkvq6pxUJmfv2yAs486SkdgqzTDaoliubMOLJRRU2wIvdSsNrlKwvO65Uhz4K9XrMcHBUsrXolsTh2oM24BWtCtlIdadsdnWaB08QC52LqnvaYBmVcbUGDO6ty7cCF+kyGekho3OVclwxe08H50iH+bZA7D/FfLxXiXacu7hFqCvHoiiEe4abCYF/E3ccYL4U+L5L7IZp0RIzjqI+8mwsd4RZKvxkzyqNM78L+VfgxvvmmAOEW31F3ngjehv1KieAiCBPXO4nwlTPDqAHgk18ZnV3Ce6U01+abKBIEPrDBGqysu5xZvftQ6e+MQt/3ZcoksKUsj0O8UlmBPJHCZ2nK43WGTgx/ZkDLZ/3d08ODpOsI+HSqPti4sv97xdAd10Po5NYFQ9RQsXpLYGdJQ++YLerlJbFTxWXrTW0K3WlC4zb63w/TiGbe+YAqYGsAFA1DJqtSKGbwKYWzpUDw5bQAt4eftubPKj8QJov/AA== -------------------------------------------------------------------------------- /iOSApp/iOSApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "iphone", 5 | "size": "20x20", 6 | "scale": "2x" 7 | }, 8 | { 9 | "idiom": "iphone", 10 | "size": "20x20", 11 | "scale": "3x" 12 | }, 13 | { 14 | "idiom": "iphone", 15 | "size": "29x29", 16 | "scale": "2x" 17 | }, 18 | { 19 | "idiom": "iphone", 20 | "size": "29x29", 21 | "scale": "3x" 22 | }, 23 | { 24 | "idiom": "iphone", 25 | "size": "40x40", 26 | "scale": "2x" 27 | }, 28 | { 29 | "idiom": "iphone", 30 | "size": "40x40", 31 | "scale": "3x" 32 | }, 33 | { 34 | "idiom": "iphone", 35 | "size": "60x60", 36 | "scale": "2x" 37 | }, 38 | { 39 | "idiom": "iphone", 40 | "size": "60x60", 41 | "scale": "3x" 42 | }, 43 | { 44 | "idiom": "ipad", 45 | "size": "20x20", 46 | "scale": "1x" 47 | }, 48 | { 49 | "idiom": "ipad", 50 | "size": "20x20", 51 | "scale": "2x" 52 | }, 53 | { 54 | "idiom": "ipad", 55 | "size": "29x29", 56 | "scale": "1x" 57 | }, 58 | { 59 | "idiom": "ipad", 60 | "size": "29x29", 61 | "scale": "2x" 62 | }, 63 | { 64 | "idiom": "ipad", 65 | "size": "40x40", 66 | "scale": "1x" 67 | }, 68 | { 69 | "idiom": "ipad", 70 | "size": "40x40", 71 | "scale": "2x" 72 | }, 73 | { 74 | "idiom": "ipad", 75 | "size": "76x76", 76 | "scale": "1x" 77 | }, 78 | { 79 | "idiom": "ipad", 80 | "size": "76x76", 81 | "scale": "2x" 82 | }, 83 | { 84 | "idiom": "ipad", 85 | "size": "83.5x83.5", 86 | "scale": "2x" 87 | }, 88 | { 89 | "idiom": "ios-marketing", 90 | "size": "1024x1024", 91 | "scale": "1x" 92 | } 93 | ], 94 | "info": { 95 | "version": 1, 96 | "author": "xcode" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /AndroidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /SharedCode/src/androidTest/kotlin/com/kernel/kmpproject/IndexesUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import com.kernel.kmpproject.data.entity.IndexEntity 4 | import com.kernel.kmpproject.data.entity.MajorIndexesListEntity 5 | import com.kernel.kmpproject.data.repository.AppIndexesRepository 6 | import com.kernel.kmpproject.data.source.network.NetworkSource 7 | import com.kernel.kmpproject.di.Injector 8 | import com.kernel.kmpproject.domain.usecase.AppGetIndexesUseCase 9 | import com.kernel.kmpproject.domain.usecase.GetIndexesUseCase 10 | import io.mockk.every 11 | import io.mockk.mockk 12 | import kotlin.test.Test 13 | import kotlin.test.assertTrue 14 | import kotlin.test.fail 15 | import kotlinx.coroutines.flow.catch 16 | import kotlinx.coroutines.flow.collect 17 | import kotlinx.coroutines.withTimeoutOrNull 18 | import org.kodein.di.erased.instance 19 | 20 | class IndexesUseCaseTest { 21 | 22 | private val getIndexesUseCase by Injector.instance() 23 | 24 | @Test 25 | fun `Check indexes is empty`() { 26 | runBlocking { 27 | withTimeoutOrNull(250) { 28 | getIndexesUseCase() 29 | .flowOnBackground() 30 | .catch { fail(it.message) } 31 | .collect { 32 | assertTrue(it.isEmpty()) 33 | } 34 | } 35 | } 36 | } 37 | 38 | @Test 39 | fun `Check indexes is not empty`() { 40 | val networkSource = mockk() 41 | every { runBlocking { networkSource.getMajorIndexesList() } } returns 42 | MajorIndexesListEntity( 43 | listOf( 44 | IndexEntity(".DJI", 28.7089f, 23719.4f, "Dow Jones") 45 | ) 46 | ) 47 | val indexesRepository = AppIndexesRepository(networkSource) 48 | val getIndexesUseCase = AppGetIndexesUseCase(indexesRepository) 49 | 50 | runBlocking { 51 | withTimeoutOrNull(250) { 52 | getIndexesUseCase() 53 | .flowOnBackground() 54 | .catch { fail(it.message) } 55 | .collect { 56 | assertTrue(it.isNotEmpty()) 57 | } 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /iOSApp/iOSApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | // Override point for customization after application launch. 10 | return true 11 | } 12 | 13 | func applicationWillResignActive(_ application: UIApplication) { 14 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 15 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 16 | } 17 | 18 | func applicationDidEnterBackground(_ application: UIApplication) { 19 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 20 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 21 | } 22 | 23 | func applicationWillEnterForeground(_ application: UIApplication) { 24 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 25 | } 26 | 27 | func applicationDidBecomeActive(_ application: UIApplication) { 28 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 29 | } 30 | 31 | func applicationWillTerminate(_ application: UIApplication) { 32 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /iOSApp/iOSApp/Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | 5 | func displayToast(_ message : String) { 6 | 7 | guard let delegate = UIApplication.shared.delegate as? AppDelegate, let window = delegate.window else { 8 | return 9 | } 10 | if let toast = window.subviews.first(where: { $0 is UILabel && $0.tag == -1001 }) { 11 | toast.removeFromSuperview() 12 | } 13 | 14 | let toastView = UILabel() 15 | toastView.backgroundColor = UIColor.black.withAlphaComponent(0.7) 16 | toastView.textColor = UIColor.white 17 | toastView.textAlignment = .center 18 | toastView.font = UIFont(name: "Font-name", size: 17) 19 | toastView.layer.cornerRadius = 25 20 | toastView.text = message 21 | toastView.numberOfLines = 0 22 | toastView.alpha = 0 23 | toastView.translatesAutoresizingMaskIntoConstraints = false 24 | toastView.tag = -1001 25 | 26 | window.addSubview(toastView) 27 | 28 | let horizontalCenterContraint: NSLayoutConstraint = NSLayoutConstraint(item: toastView, attribute: .centerX, relatedBy: .equal, toItem: window, attribute: .centerX, multiplier: 1, constant: 0) 29 | 30 | let widthContraint: NSLayoutConstraint = NSLayoutConstraint(item: toastView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: (self.frame.size.width-25) ) 31 | 32 | let verticalContraint: [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(>=200)-[toastView(==50)]-68-|", options: [.alignAllCenterX, .alignAllCenterY], metrics: nil, views: ["toastView": toastView]) 33 | 34 | NSLayoutConstraint.activate([horizontalCenterContraint, widthContraint]) 35 | NSLayoutConstraint.activate(verticalContraint) 36 | 37 | UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseIn, animations: { 38 | toastView.alpha = 1 39 | }, completion: nil) 40 | 41 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3), execute: { 42 | UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseIn, animations: { 43 | toastView.alpha = 0 44 | }, completion: { finished in 45 | toastView.removeFromSuperview() 46 | }) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /iOSApp/iOSApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SharedCode 3 | 4 | class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 5 | 6 | @IBOutlet weak var tableView: UITableView! 7 | 8 | private var viewModel: IndexesViewModel! 9 | 10 | internal var indexes: [Index] = [] 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | tableView.dataSource = self 16 | tableView.delegate = self 17 | 18 | viewModel = IndexesViewModel() 19 | observeViewState() 20 | 21 | viewModel.getMajorIndexes() 22 | } 23 | 24 | func observeViewState() { 25 | viewModel.getViewData.addObserver { (state) in 26 | self.updateViewState(state: state as! IndexesViewState) 27 | } 28 | } 29 | 30 | func updateViewState(state: IndexesViewState) { 31 | switch state { 32 | case is Loading: 33 | view.displayToast("Loading...") 34 | case is Error: 35 | view.displayToast("Error") 36 | case is ShowMajorIndexes: 37 | let successState = state as! ShowMajorIndexes 38 | update(list: successState.indexes) 39 | case is ShowQuote: 40 | let successState = state as! ShowQuote 41 | view.displayToast(successState.quote.dayLow + " - " + successState.quote.dayHigh) 42 | default: break 43 | } 44 | } 45 | 46 | internal func update(list: [Index]) { 47 | indexes.removeAll() 48 | indexes.append(contentsOf: list) 49 | tableView.reloadData() 50 | } 51 | 52 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 53 | return indexes.count 54 | } 55 | 56 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 57 | let cell = tableView.dequeueReusableCell(withIdentifier: "indexListCell", for: indexPath) 58 | let item = indexes[indexPath.row] 59 | 60 | cell.textLabel?.text = item.ticker + " " + item.indexName + " " + item.price.description + " " + item.changes.description 61 | 62 | return cell 63 | } 64 | 65 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 66 | viewModel.getQuote(symbol: indexes[indexPath.row].ticker) 67 | } 68 | 69 | deinit { 70 | viewModel.onCleared() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /SharedCode/src/iOSMain/kotlin/com/kernel/kmpproject/Async.kt: -------------------------------------------------------------------------------- 1 | package com.kernel.kmpproject 2 | 3 | import kotlin.coroutines.CoroutineContext 4 | import kotlinx.coroutines.CancellableContinuation 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Delay 8 | import kotlinx.coroutines.DisposableHandle 9 | import kotlinx.coroutines.InternalCoroutinesApi 10 | import kotlinx.coroutines.Job 11 | import kotlinx.coroutines.Runnable 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flowOn 14 | import kotlinx.coroutines.launch 15 | import platform.darwin.DISPATCH_TIME_NOW 16 | import platform.darwin.dispatch_after 17 | import platform.darwin.dispatch_async 18 | import platform.darwin.dispatch_get_main_queue 19 | import platform.darwin.dispatch_queue_t 20 | import platform.darwin.dispatch_time 21 | 22 | actual val ApplicationDispatcher: CoroutineContext = 23 | NsQueueDispatcher(dispatch_get_main_queue()) 24 | 25 | internal class NsQueueDispatcher( 26 | private val dispatchQueue: dispatch_queue_t 27 | ) : CoroutineDispatcher() { 28 | override fun dispatch(context: CoroutineContext, block: Runnable) { 29 | dispatch_async(dispatchQueue) { 30 | block.run() 31 | } 32 | } 33 | } 34 | 35 | actual fun CoroutineScope.launchInMain(block: suspend CoroutineScope.() -> Unit): Job = 36 | launch(MainDispatcher) { block() } 37 | 38 | actual fun Flow.flowOnBackground(): Flow = flowOn(MainDispatcher) 39 | 40 | actual fun runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T = 41 | kotlinx.coroutines.runBlocking(context, block) 42 | 43 | @UseExperimental(InternalCoroutinesApi::class) 44 | internal object MainDispatcher : CoroutineDispatcher(), Delay { 45 | override fun dispatch(context: CoroutineContext, block: Runnable) { 46 | dispatch_async(dispatch_get_main_queue()) { 47 | block.run() 48 | } 49 | } 50 | 51 | @InternalCoroutinesApi 52 | override fun scheduleResumeAfterDelay( 53 | timeMillis: Long, 54 | continuation: CancellableContinuation 55 | ) { 56 | dispatch_after( 57 | dispatch_time(DISPATCH_TIME_NOW, timeMillis * 1_000_000), 58 | dispatch_get_main_queue() 59 | ) { 60 | try { 61 | with(continuation) { 62 | resumeUndispatched(Unit) 63 | } 64 | } catch (err: Throwable) { 65 | logError("UNCAUGHT", err.message ?: "", err) 66 | throw err 67 | } 68 | } 69 | } 70 | 71 | @InternalCoroutinesApi 72 | override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { 73 | val handle = object : DisposableHandle { 74 | var disposed = false 75 | private set 76 | 77 | override fun dispose() { 78 | disposed = true 79 | } 80 | } 81 | dispatch_after( 82 | dispatch_time(DISPATCH_TIME_NOW, timeMillis * 1_000_000), 83 | dispatch_get_main_queue() 84 | ) { 85 | try { 86 | if (!handle.disposed) { 87 | block.run() 88 | } 89 | } catch (err: Throwable) { 90 | logError("UNCAUGHT", err.message ?: "", err) 91 | throw err 92 | } 93 | } 94 | 95 | return handle 96 | } 97 | } -------------------------------------------------------------------------------- /iOSApp/iOSApp/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Mobile Multiplatform App (Android & iOS) 2 | 3 | One Code To Rule Them All. Application example using Kotlin Multiplatform and MVVM pattern for both platforms. 4 | 5 | 6 | 7 | Is used: 8 | - layered clean architecture 9 | - DI (Kodein) 10 | - coroutines 11 | - livedata 12 | - ktor 13 | - serialization 14 | - mockk 15 | - detekt, ktlint 16 | - unit tests and jacoco 17 | 18 | ### Presentation layer (Android & iOS) 19 | 20 | On both platforms (Android and iOS), we only need to implement an observer in which to process states. Further implementation on Android (Kotlin) and iOS (Swift): 21 | 22 | #### Android 23 | ```kotlin 24 | class MainActivity : AppCompatActivity() { 25 | 26 | private lateinit var viewModel: IndexesViewModel 27 | private var adapter: IndexesAdapter = IndexesAdapter { viewModel.getQuote(it.ticker) } 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | setContentView(R.layout.activity_main) 32 | 33 | recycler.adapter = adapter 34 | 35 | viewModel = ViewModelProviders.of(this).get(IndexesViewModel::class.java) 36 | observeViewState() 37 | 38 | viewModel.getMajorIndexes() 39 | } 40 | 41 | private fun observeViewState() { 42 | viewModel.getViewData.addObserver { updateViewState(it) } 43 | } 44 | 45 | private fun updateViewState(state: IndexesViewState) = runOnUiThread { 46 | when (state) { 47 | is Loading -> { 48 | Toast.makeText(this, "Loading...", Toast.LENGTH_SHORT).show() 49 | } 50 | is Error -> { 51 | Toast.makeText(this, state.message, Toast.LENGTH_LONG).show() 52 | } 53 | is ShowMajorIndexes -> { 54 | adapter.items = state.indexes 55 | } 56 | is ShowQuote -> { 57 | Toast.makeText(this, state.quote.dayLow + " - " + state.quote.dayHigh, Toast.LENGTH_LONG).show() 58 | } 59 | } 60 | } 61 | } 62 | ``` 63 | #### iOS 64 | ```swift 65 | class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 66 | 67 | @IBOutlet weak var tableView: UITableView! 68 | 69 | private var viewModel: IndexesViewModel! 70 | 71 | internal var indexes: [Index] = [] 72 | 73 | override func viewDidLoad() { 74 | super.viewDidLoad() 75 | 76 | tableView.dataSource = self 77 | tableView.delegate = self 78 | 79 | viewModel = IndexesViewModel() 80 | observeViewState() 81 | 82 | viewModel.getMajorIndexes() 83 | } 84 | 85 | func observeViewState() { 86 | viewModel.getViewData.addObserver { (state) in 87 | self.updateViewState(state: state as! IndexesViewState) 88 | } 89 | } 90 | 91 | func updateViewState(state: IndexesViewState) { 92 | switch state { 93 | case is Loading: 94 | view.displayToast("Loading...") 95 | case is Error: 96 | view.displayToast("Error") 97 | case is ShowMajorIndexes: 98 | let successState = state as! ShowMajorIndexes 99 | update(list: successState.indexes) 100 | case is ShowQuote: 101 | let successState = state as! ShowQuote 102 | view.displayToast(successState.quote.dayLow + " - " + successState.quote.dayHigh) 103 | default: break 104 | } 105 | } 106 | 107 | ... 108 | 109 | deinit { 110 | viewModel.onCleared() 111 | } 112 | } 113 | ``` 114 | 115 | ### Presentation Layer - ViewModels (Shared Code) 116 | 117 | This layer is shared by Android and iOS, and this is developed on Kotlin. Here is where we have to call the different use-cases of the domain layer. To make the call async we are using kotlin coroutines and flow. 118 | 119 | ```kotlin 120 | class IndexesViewModel : BaseViewModel() { 121 | 122 | private val getIndexesUseCase by Injector.instance() 123 | private val getQuoteUseCase by Injector.instance() 124 | 125 | var getViewData = MutableLiveData(Empty) 126 | 127 | fun getMajorIndexes() = launchInMain { 128 | getIndexesUseCase() 129 | .onStart { getViewData.postValue(Loading) } 130 | .flowOnBackground() 131 | .catch { getViewData.postValue(Error("Something went wrong")) } 132 | .collect { getViewData.postValue(ShowMajorIndexes(it)) } 133 | } 134 | 135 | fun getQuote(symbol: String) = launchInMain { 136 | getQuoteUseCase.invoke(symbol) 137 | .onStart { getViewData.postValue(Loading) } 138 | .flowOnBackground() 139 | .catch { getViewData.postValue(Error("Something went wrong")) } 140 | .collect { getViewData.postValue(ShowQuote(it)) } 141 | } 142 | } 143 | ``` 144 | 145 | ### Domain Layer — Models & UseCases (Shared Code) 146 | 147 | In this layer, we defining the models and all the use cases that we need for our application. 148 | 149 | ### Data Layer — Repository Pattern (Shared Code) 150 | 151 | For this layer we are using a repository pattern. We defining the entity models and all source of our data 152 | 153 | For networking we are using Ktor and for JSON deserialisation Kotlinx serialization. 154 | 155 | ### Running unit tests 156 | 157 | - Android test: ./gradlew testDebugUnitTest 158 | - Common test on iOS (need run Simulator iPhone 8): ./gradlew iosUnitTest 159 | 160 | ### Running the app 161 | 162 | To run the application use the same tools you use in Android and iOS. Just open the project with Intellj/Android Studio for the Android project and XCode for the iOS one. 163 | 164 | ## Screenshots 165 | 166 | |Android|iOS| 167 | |---|---| 168 | |![android-app](https://github.com/kernel0x/kmpapp/blob/master/images/Screenshot_android.png)|![ios-app](https://github.com/kernel0x/kmpapp/blob/master/images/Screenshot_ios.png)| 169 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /SharedCode/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin-multiplatform' 2 | apply plugin: 'kotlinx-serialization' 3 | apply plugin: 'com.android.library' 4 | 5 | android { 6 | compileSdkVersion(29) 7 | 8 | defaultConfig { 9 | minSdkVersion(21) 10 | targetSdkVersion(29) 11 | } 12 | 13 | // By default the android gradle plugin expects to find the kotlin source files in 14 | // the folder `main` and the test in the folder `test`. This is to be able place 15 | // the source code files inside androidMain and androidTest folders 16 | sourceSets { 17 | main { 18 | manifest.srcFile 'src/androidMain/AndroidManifest.xml' 19 | java.srcDirs = ['src/androidMain/kotlin'] 20 | res.srcDirs = ['src/androidMain/res'] 21 | } 22 | test { 23 | java.srcDirs = ['src/androidTest/kotlin'] 24 | res.srcDirs = ['src/androidTest/res'] 25 | } 26 | } 27 | 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | 33 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 34 | kotlinOptions { 35 | jvmTarget = "1.8" 36 | } 37 | } 38 | 39 | testOptions { 40 | unitTests.returnDefaultValues = true 41 | } 42 | } 43 | 44 | kotlin { 45 | targets { 46 | fromPreset(presets.android, 'android') 47 | 48 | def iosPreset = presets.iosX64 49 | fromPreset(iosPreset, 'ios') { 50 | binaries { 51 | framework { 52 | // Disable bitcode embedding for the simulator build. 53 | if (iosPreset == presets.iosX64) { 54 | embedBitcode("disable") 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | sourceSets { 62 | all { 63 | languageSettings { 64 | useExperimentalAnnotation('kotlin.Experimental') 65 | useExperimentalAnnotation('kotlin.time.ExperimentalTime') 66 | useExperimentalAnnotation('kotlinx.coroutines.FlowPreview') 67 | useExperimentalAnnotation('kotlinx.coroutines.ExperimentalCoroutinesApi') 68 | } 69 | } 70 | 71 | commonMain.dependencies { 72 | api 'org.jetbrains.kotlin:kotlin-stdlib-common' 73 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutine_version" 74 | implementation "dev.icerock.moko:mvvm:$moko_mvvm_version" 75 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serializer_version" 76 | implementation "org.kodein.di:kodein-di-core:$kodein_version" 77 | implementation "org.kodein.di:kodein-di-erased:$kodein_version" 78 | implementation "io.ktor:ktor-client-core:$ktor_version" 79 | implementation "io.ktor:ktor-client-logging:$ktor_version" 80 | } 81 | 82 | androidMain.dependencies { 83 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 84 | implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" 85 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" 86 | implementation "androidx.lifecycle:lifecycle-extensions:$androidx_lifecycle_version" 87 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serializer_version" 88 | implementation "io.ktor:ktor-client-android:$ktor_version" 89 | implementation "io.ktor:ktor-client-logging-jvm:$ktor_version" 90 | } 91 | 92 | iosMain.dependencies { 93 | implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" 94 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutine_version" 95 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serializer_version" 96 | implementation "io.ktor:ktor-client-ios:$ktor_version" 97 | implementation "io.ktor:ktor-client-logging-native:$ktor_version" 98 | } 99 | 100 | androidTest.dependencies { 101 | implementation kotlin('test') 102 | implementation kotlin('test-junit') 103 | implementation "io.mockk:mockk:$mockk_version" 104 | } 105 | 106 | commonTest.dependencies { 107 | implementation kotlin('test-common') 108 | implementation kotlin('test-annotations-common') 109 | } 110 | } 111 | } 112 | 113 | 114 | // This task attaches native framework built from ios module to Xcode project 115 | // (see IosApp directory). Don't run this task directly, 116 | // Xcode runs this task itself during its build process. 117 | // Before opening the project from iosApp directory in Xcode, 118 | // make sure all Gradle infrastructure exists (gradle.wrapper, gradlew). 119 | task packForXCode { 120 | def buildType = project.findProperty("kotlin.build.type") ?: "DEBUG" 121 | dependsOn "link${buildType.toLowerCase().capitalize()}FrameworkIos" 122 | //dependsOn "linkMainDebugFrameworkIOS" 123 | 124 | doLast { 125 | def srcFile = kotlin.targets.ios.binaries.getFramework(buildType).outputFile 126 | def targetDir = getProperty("configuration.build.dir") 127 | copy { 128 | from srcFile.parent 129 | into targetDir 130 | include 'SharedCode.framework/**' 131 | include 'SharedCode .framework.dSYM' 132 | } 133 | } 134 | } 135 | 136 | task iosUnitTest { 137 | def device = project.findProperty("iosDevice")?.toString() ?: "iPhone 8" 138 | dependsOn kotlin.targets.ios.binaries.getTest('DEBUG').linkTaskName 139 | group = JavaBasePlugin.VERIFICATION_GROUP 140 | description = "Runs iOS tests on a simulator" 141 | 142 | doLast { 143 | def binary = kotlin.targets.ios.binaries.getTest('DEBUG').outputFile 144 | exec { 145 | commandLine 'xcrun', 'simctl', 'spawn', device, binary.absolutePath 146 | } 147 | } 148 | } 149 | 150 | // check.dependsOn iosTest 151 | 152 | apply from: "../extras/jacoco.gradle" 153 | apply from: "../extras/ktlint.gradle" 154 | apply from: "../extras/detekt.gradle" 155 | -------------------------------------------------------------------------------- /AndroidApp/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /images/architecture.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | DataDomainPresen-tation
Data

Repositories, entities and sources data
Data...
Domain

Models and Use Cases
Domain...
Presentation

ViewModels
Presentation...
AndroidiOS
LiveData
LiveData
Android

Observer and state processing in Activities/Fragments
Android...
iOS

Observer and state processing in Controllers
iOS...
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /iOSApp/iOSApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 04475FBD23A0EB5700D4A7D4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04475FBC23A0EB5700D4A7D4 /* AppDelegate.swift */; }; 11 | 04475FBF23A0EB5700D4A7D4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04475FBE23A0EB5700D4A7D4 /* ViewController.swift */; }; 12 | 04475FC223A0EB5700D4A7D4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 04475FC023A0EB5700D4A7D4 /* Main.storyboard */; }; 13 | 04475FC423A0EB5800D4A7D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 04475FC323A0EB5800D4A7D4 /* Assets.xcassets */; }; 14 | 04475FC723A0EB5800D4A7D4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 04475FC523A0EB5800D4A7D4 /* LaunchScreen.storyboard */; }; 15 | 04475FD223A0EB5800D4A7D4 /* iOSAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04475FD123A0EB5800D4A7D4 /* iOSAppTests.swift */; }; 16 | 04475FDD23A0EB5800D4A7D4 /* iOSAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04475FDC23A0EB5800D4A7D4 /* iOSAppUITests.swift */; }; 17 | 1EE75F0A2438EF4500CD069F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE75F092438EF4500CD069F /* Extensions.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 04475FCE23A0EB5800D4A7D4 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 04475FB123A0EB5700D4A7D4 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 04475FB823A0EB5700D4A7D4; 26 | remoteInfo = iOSApp; 27 | }; 28 | 04475FD923A0EB5800D4A7D4 /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = 04475FB123A0EB5700D4A7D4 /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = 04475FB823A0EB5700D4A7D4; 33 | remoteInfo = iOSApp; 34 | }; 35 | /* End PBXContainerItemProxy section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 04475FB923A0EB5700D4A7D4 /* iOSApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 04475FBC23A0EB5700D4A7D4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 40 | 04475FBE23A0EB5700D4A7D4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 41 | 04475FC123A0EB5700D4A7D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 04475FC323A0EB5800D4A7D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 04475FC623A0EB5800D4A7D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 04475FC823A0EB5800D4A7D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | 04475FCD23A0EB5800D4A7D4 /* iOSAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 04475FD123A0EB5800D4A7D4 /* iOSAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSAppTests.swift; sourceTree = ""; }; 47 | 04475FD323A0EB5800D4A7D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 04475FD823A0EB5800D4A7D4 /* iOSAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 04475FDC23A0EB5800D4A7D4 /* iOSAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSAppUITests.swift; sourceTree = ""; }; 50 | 04475FDE23A0EB5800D4A7D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 1EE75F092438EF4500CD069F /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | 04475FB623A0EB5700D4A7D4 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | 04475FCA23A0EB5800D4A7D4 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | 04475FD523A0EB5800D4A7D4 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXFrameworksBuildPhase section */ 77 | 78 | /* Begin PBXGroup section */ 79 | 04475FB023A0EB5700D4A7D4 = { 80 | isa = PBXGroup; 81 | children = ( 82 | 04475FBB23A0EB5700D4A7D4 /* iOSApp */, 83 | 04475FD023A0EB5800D4A7D4 /* iOSAppTests */, 84 | 04475FDB23A0EB5800D4A7D4 /* iOSAppUITests */, 85 | 04475FBA23A0EB5700D4A7D4 /* Products */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | 04475FBA23A0EB5700D4A7D4 /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 04475FB923A0EB5700D4A7D4 /* iOSApp.app */, 93 | 04475FCD23A0EB5800D4A7D4 /* iOSAppTests.xctest */, 94 | 04475FD823A0EB5800D4A7D4 /* iOSAppUITests.xctest */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | 04475FBB23A0EB5700D4A7D4 /* iOSApp */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 04475FBC23A0EB5700D4A7D4 /* AppDelegate.swift */, 103 | 04475FBE23A0EB5700D4A7D4 /* ViewController.swift */, 104 | 04475FC023A0EB5700D4A7D4 /* Main.storyboard */, 105 | 04475FC323A0EB5800D4A7D4 /* Assets.xcassets */, 106 | 04475FC523A0EB5800D4A7D4 /* LaunchScreen.storyboard */, 107 | 04475FC823A0EB5800D4A7D4 /* Info.plist */, 108 | 1EE75F092438EF4500CD069F /* Extensions.swift */, 109 | ); 110 | path = iOSApp; 111 | sourceTree = ""; 112 | }; 113 | 04475FD023A0EB5800D4A7D4 /* iOSAppTests */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 04475FD123A0EB5800D4A7D4 /* iOSAppTests.swift */, 117 | 04475FD323A0EB5800D4A7D4 /* Info.plist */, 118 | ); 119 | path = iOSAppTests; 120 | sourceTree = ""; 121 | }; 122 | 04475FDB23A0EB5800D4A7D4 /* iOSAppUITests */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 04475FDC23A0EB5800D4A7D4 /* iOSAppUITests.swift */, 126 | 04475FDE23A0EB5800D4A7D4 /* Info.plist */, 127 | ); 128 | path = iOSAppUITests; 129 | sourceTree = ""; 130 | }; 131 | /* End PBXGroup section */ 132 | 133 | /* Begin PBXNativeTarget section */ 134 | 04475FB823A0EB5700D4A7D4 /* iOSApp */ = { 135 | isa = PBXNativeTarget; 136 | buildConfigurationList = 04475FE123A0EB5800D4A7D4 /* Build configuration list for PBXNativeTarget "iOSApp" */; 137 | buildPhases = ( 138 | 04475FEA23A0EB6900D4A7D4 /* ShellScript */, 139 | 04475FB523A0EB5700D4A7D4 /* Sources */, 140 | 04475FB623A0EB5700D4A7D4 /* Frameworks */, 141 | 04475FB723A0EB5700D4A7D4 /* Resources */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = iOSApp; 148 | productName = iOSApp; 149 | productReference = 04475FB923A0EB5700D4A7D4 /* iOSApp.app */; 150 | productType = "com.apple.product-type.application"; 151 | }; 152 | 04475FCC23A0EB5800D4A7D4 /* iOSAppTests */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = 04475FE423A0EB5800D4A7D4 /* Build configuration list for PBXNativeTarget "iOSAppTests" */; 155 | buildPhases = ( 156 | 04475FC923A0EB5800D4A7D4 /* Sources */, 157 | 04475FCA23A0EB5800D4A7D4 /* Frameworks */, 158 | 04475FCB23A0EB5800D4A7D4 /* Resources */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | 04475FCF23A0EB5800D4A7D4 /* PBXTargetDependency */, 164 | ); 165 | name = iOSAppTests; 166 | productName = iOSAppTests; 167 | productReference = 04475FCD23A0EB5800D4A7D4 /* iOSAppTests.xctest */; 168 | productType = "com.apple.product-type.bundle.unit-test"; 169 | }; 170 | 04475FD723A0EB5800D4A7D4 /* iOSAppUITests */ = { 171 | isa = PBXNativeTarget; 172 | buildConfigurationList = 04475FE723A0EB5800D4A7D4 /* Build configuration list for PBXNativeTarget "iOSAppUITests" */; 173 | buildPhases = ( 174 | 04475FD423A0EB5800D4A7D4 /* Sources */, 175 | 04475FD523A0EB5800D4A7D4 /* Frameworks */, 176 | 04475FD623A0EB5800D4A7D4 /* Resources */, 177 | ); 178 | buildRules = ( 179 | ); 180 | dependencies = ( 181 | 04475FDA23A0EB5800D4A7D4 /* PBXTargetDependency */, 182 | ); 183 | name = iOSAppUITests; 184 | productName = iOSAppUITests; 185 | productReference = 04475FD823A0EB5800D4A7D4 /* iOSAppUITests.xctest */; 186 | productType = "com.apple.product-type.bundle.ui-testing"; 187 | }; 188 | /* End PBXNativeTarget section */ 189 | 190 | /* Begin PBXProject section */ 191 | 04475FB123A0EB5700D4A7D4 /* Project object */ = { 192 | isa = PBXProject; 193 | attributes = { 194 | LastSwiftUpdateCheck = 1020; 195 | LastUpgradeCheck = 1020; 196 | ORGANIZATIONNAME = Kernel; 197 | TargetAttributes = { 198 | 04475FB823A0EB5700D4A7D4 = { 199 | CreatedOnToolsVersion = 10.2.1; 200 | }; 201 | 04475FCC23A0EB5800D4A7D4 = { 202 | CreatedOnToolsVersion = 10.2.1; 203 | TestTargetID = 04475FB823A0EB5700D4A7D4; 204 | }; 205 | 04475FD723A0EB5800D4A7D4 = { 206 | CreatedOnToolsVersion = 10.2.1; 207 | TestTargetID = 04475FB823A0EB5700D4A7D4; 208 | }; 209 | }; 210 | }; 211 | buildConfigurationList = 04475FB423A0EB5700D4A7D4 /* Build configuration list for PBXProject "iOSApp" */; 212 | compatibilityVersion = "Xcode 9.3"; 213 | developmentRegion = en; 214 | hasScannedForEncodings = 0; 215 | knownRegions = ( 216 | en, 217 | Base, 218 | ); 219 | mainGroup = 04475FB023A0EB5700D4A7D4; 220 | productRefGroup = 04475FBA23A0EB5700D4A7D4 /* Products */; 221 | projectDirPath = ""; 222 | projectRoot = ""; 223 | targets = ( 224 | 04475FB823A0EB5700D4A7D4 /* iOSApp */, 225 | 04475FCC23A0EB5800D4A7D4 /* iOSAppTests */, 226 | 04475FD723A0EB5800D4A7D4 /* iOSAppUITests */, 227 | ); 228 | }; 229 | /* End PBXProject section */ 230 | 231 | /* Begin PBXResourcesBuildPhase section */ 232 | 04475FB723A0EB5700D4A7D4 /* Resources */ = { 233 | isa = PBXResourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | 04475FC723A0EB5800D4A7D4 /* LaunchScreen.storyboard in Resources */, 237 | 04475FC423A0EB5800D4A7D4 /* Assets.xcassets in Resources */, 238 | 04475FC223A0EB5700D4A7D4 /* Main.storyboard in Resources */, 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | }; 242 | 04475FCB23A0EB5800D4A7D4 /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | 04475FD623A0EB5800D4A7D4 /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXShellScriptBuildPhase section */ 259 | 04475FEA23A0EB6900D4A7D4 /* ShellScript */ = { 260 | isa = PBXShellScriptBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | ); 264 | inputFileListPaths = ( 265 | ); 266 | inputPaths = ( 267 | ); 268 | outputFileListPaths = ( 269 | ); 270 | outputPaths = ( 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | shellPath = /bin/sh; 274 | shellScript = "\"$SRCROOT/../gradlew\" -p \"$SRCROOT/../SharedCode\" packForXCode \\\n-Pconfiguration.build.dir=\"$CONFIGURATION_BUILD_DIR\" \\\n-Pkotlin.build.type=\"$KOTLIN_BUILD_TYPE\" \\\n-Pdevice=\"$KOTLIN_DEVICE\"\n"; 275 | }; 276 | /* End PBXShellScriptBuildPhase section */ 277 | 278 | /* Begin PBXSourcesBuildPhase section */ 279 | 04475FB523A0EB5700D4A7D4 /* Sources */ = { 280 | isa = PBXSourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | 04475FBF23A0EB5700D4A7D4 /* ViewController.swift in Sources */, 284 | 04475FBD23A0EB5700D4A7D4 /* AppDelegate.swift in Sources */, 285 | 1EE75F0A2438EF4500CD069F /* Extensions.swift in Sources */, 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | }; 289 | 04475FC923A0EB5800D4A7D4 /* Sources */ = { 290 | isa = PBXSourcesBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | 04475FD223A0EB5800D4A7D4 /* iOSAppTests.swift in Sources */, 294 | ); 295 | runOnlyForDeploymentPostprocessing = 0; 296 | }; 297 | 04475FD423A0EB5800D4A7D4 /* Sources */ = { 298 | isa = PBXSourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 04475FDD23A0EB5800D4A7D4 /* iOSAppUITests.swift in Sources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | /* End PBXSourcesBuildPhase section */ 306 | 307 | /* Begin PBXTargetDependency section */ 308 | 04475FCF23A0EB5800D4A7D4 /* PBXTargetDependency */ = { 309 | isa = PBXTargetDependency; 310 | target = 04475FB823A0EB5700D4A7D4 /* iOSApp */; 311 | targetProxy = 04475FCE23A0EB5800D4A7D4 /* PBXContainerItemProxy */; 312 | }; 313 | 04475FDA23A0EB5800D4A7D4 /* PBXTargetDependency */ = { 314 | isa = PBXTargetDependency; 315 | target = 04475FB823A0EB5700D4A7D4 /* iOSApp */; 316 | targetProxy = 04475FD923A0EB5800D4A7D4 /* PBXContainerItemProxy */; 317 | }; 318 | /* End PBXTargetDependency section */ 319 | 320 | /* Begin PBXVariantGroup section */ 321 | 04475FC023A0EB5700D4A7D4 /* Main.storyboard */ = { 322 | isa = PBXVariantGroup; 323 | children = ( 324 | 04475FC123A0EB5700D4A7D4 /* Base */, 325 | ); 326 | name = Main.storyboard; 327 | sourceTree = ""; 328 | }; 329 | 04475FC523A0EB5800D4A7D4 /* LaunchScreen.storyboard */ = { 330 | isa = PBXVariantGroup; 331 | children = ( 332 | 04475FC623A0EB5800D4A7D4 /* Base */, 333 | ); 334 | name = LaunchScreen.storyboard; 335 | sourceTree = ""; 336 | }; 337 | /* End PBXVariantGroup section */ 338 | 339 | /* Begin XCBuildConfiguration section */ 340 | 04475FDF23A0EB5800D4A7D4 /* Debug */ = { 341 | isa = XCBuildConfiguration; 342 | buildSettings = { 343 | ALWAYS_SEARCH_USER_PATHS = NO; 344 | CLANG_ANALYZER_NONNULL = YES; 345 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 346 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 347 | CLANG_CXX_LIBRARY = "libc++"; 348 | CLANG_ENABLE_MODULES = YES; 349 | CLANG_ENABLE_OBJC_ARC = YES; 350 | CLANG_ENABLE_OBJC_WEAK = YES; 351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_COMMA = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 357 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INFINITE_RECURSION = YES; 361 | CLANG_WARN_INT_CONVERSION = YES; 362 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 364 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 366 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 367 | CLANG_WARN_STRICT_PROTOTYPES = YES; 368 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 369 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 370 | CLANG_WARN_UNREACHABLE_CODE = YES; 371 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 372 | CODE_SIGN_IDENTITY = "iPhone Developer"; 373 | COPY_PHASE_STRIP = NO; 374 | DEBUG_INFORMATION_FORMAT = dwarf; 375 | ENABLE_STRICT_OBJC_MSGSEND = YES; 376 | ENABLE_TESTABILITY = YES; 377 | GCC_C_LANGUAGE_STANDARD = gnu11; 378 | GCC_DYNAMIC_NO_PIC = NO; 379 | GCC_NO_COMMON_BLOCKS = YES; 380 | GCC_OPTIMIZATION_LEVEL = 0; 381 | GCC_PREPROCESSOR_DEFINITIONS = ( 382 | "DEBUG=1", 383 | "$(inherited)", 384 | ); 385 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 386 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 387 | GCC_WARN_UNDECLARED_SELECTOR = YES; 388 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 389 | GCC_WARN_UNUSED_FUNCTION = YES; 390 | GCC_WARN_UNUSED_VARIABLE = YES; 391 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 392 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 393 | MTL_FAST_MATH = YES; 394 | ONLY_ACTIVE_ARCH = YES; 395 | SDKROOT = iphoneos; 396 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 397 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 398 | }; 399 | name = Debug; 400 | }; 401 | 04475FE023A0EB5800D4A7D4 /* Release */ = { 402 | isa = XCBuildConfiguration; 403 | buildSettings = { 404 | ALWAYS_SEARCH_USER_PATHS = NO; 405 | CLANG_ANALYZER_NONNULL = YES; 406 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 407 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 408 | CLANG_CXX_LIBRARY = "libc++"; 409 | CLANG_ENABLE_MODULES = YES; 410 | CLANG_ENABLE_OBJC_ARC = YES; 411 | CLANG_ENABLE_OBJC_WEAK = YES; 412 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 413 | CLANG_WARN_BOOL_CONVERSION = YES; 414 | CLANG_WARN_COMMA = YES; 415 | CLANG_WARN_CONSTANT_CONVERSION = YES; 416 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 417 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 418 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 419 | CLANG_WARN_EMPTY_BODY = YES; 420 | CLANG_WARN_ENUM_CONVERSION = YES; 421 | CLANG_WARN_INFINITE_RECURSION = YES; 422 | CLANG_WARN_INT_CONVERSION = YES; 423 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 424 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 425 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 426 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 427 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 428 | CLANG_WARN_STRICT_PROTOTYPES = YES; 429 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 430 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 431 | CLANG_WARN_UNREACHABLE_CODE = YES; 432 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 433 | CODE_SIGN_IDENTITY = "iPhone Developer"; 434 | COPY_PHASE_STRIP = NO; 435 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 436 | ENABLE_NS_ASSERTIONS = NO; 437 | ENABLE_STRICT_OBJC_MSGSEND = YES; 438 | GCC_C_LANGUAGE_STANDARD = gnu11; 439 | GCC_NO_COMMON_BLOCKS = YES; 440 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 441 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 442 | GCC_WARN_UNDECLARED_SELECTOR = YES; 443 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 444 | GCC_WARN_UNUSED_FUNCTION = YES; 445 | GCC_WARN_UNUSED_VARIABLE = YES; 446 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 447 | MTL_ENABLE_DEBUG_INFO = NO; 448 | MTL_FAST_MATH = YES; 449 | SDKROOT = iphoneos; 450 | SWIFT_COMPILATION_MODE = wholemodule; 451 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 452 | VALIDATE_PRODUCT = YES; 453 | }; 454 | name = Release; 455 | }; 456 | 04475FE223A0EB5800D4A7D4 /* Debug */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 460 | CODE_SIGN_STYLE = Automatic; 461 | INFOPLIST_FILE = iOSApp/Info.plist; 462 | LD_RUNPATH_SEARCH_PATHS = ( 463 | "$(inherited)", 464 | "@executable_path/Frameworks", 465 | ); 466 | PRODUCT_BUNDLE_IDENTIFIER = com.kernel.iOSApp; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SWIFT_VERSION = 5.0; 469 | TARGETED_DEVICE_FAMILY = "1,2"; 470 | }; 471 | name = Debug; 472 | }; 473 | 04475FE323A0EB5800D4A7D4 /* Release */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 477 | CODE_SIGN_STYLE = Automatic; 478 | INFOPLIST_FILE = iOSApp/Info.plist; 479 | LD_RUNPATH_SEARCH_PATHS = ( 480 | "$(inherited)", 481 | "@executable_path/Frameworks", 482 | ); 483 | PRODUCT_BUNDLE_IDENTIFIER = com.kernel.iOSApp; 484 | PRODUCT_NAME = "$(TARGET_NAME)"; 485 | SWIFT_VERSION = 5.0; 486 | TARGETED_DEVICE_FAMILY = "1,2"; 487 | }; 488 | name = Release; 489 | }; 490 | 04475FE523A0EB5800D4A7D4 /* Debug */ = { 491 | isa = XCBuildConfiguration; 492 | buildSettings = { 493 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 494 | BUNDLE_LOADER = "$(TEST_HOST)"; 495 | CODE_SIGN_STYLE = Automatic; 496 | INFOPLIST_FILE = iOSAppTests/Info.plist; 497 | LD_RUNPATH_SEARCH_PATHS = ( 498 | "$(inherited)", 499 | "@executable_path/Frameworks", 500 | "@loader_path/Frameworks", 501 | ); 502 | PRODUCT_BUNDLE_IDENTIFIER = com.kernel.iOSAppTests; 503 | PRODUCT_NAME = "$(TARGET_NAME)"; 504 | SWIFT_VERSION = 5.0; 505 | TARGETED_DEVICE_FAMILY = "1,2"; 506 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSApp.app/iOSApp"; 507 | }; 508 | name = Debug; 509 | }; 510 | 04475FE623A0EB5800D4A7D4 /* Release */ = { 511 | isa = XCBuildConfiguration; 512 | buildSettings = { 513 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 514 | BUNDLE_LOADER = "$(TEST_HOST)"; 515 | CODE_SIGN_STYLE = Automatic; 516 | INFOPLIST_FILE = iOSAppTests/Info.plist; 517 | LD_RUNPATH_SEARCH_PATHS = ( 518 | "$(inherited)", 519 | "@executable_path/Frameworks", 520 | "@loader_path/Frameworks", 521 | ); 522 | PRODUCT_BUNDLE_IDENTIFIER = com.kernel.iOSAppTests; 523 | PRODUCT_NAME = "$(TARGET_NAME)"; 524 | SWIFT_VERSION = 5.0; 525 | TARGETED_DEVICE_FAMILY = "1,2"; 526 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSApp.app/iOSApp"; 527 | }; 528 | name = Release; 529 | }; 530 | 04475FE823A0EB5800D4A7D4 /* Debug */ = { 531 | isa = XCBuildConfiguration; 532 | buildSettings = { 533 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 534 | CODE_SIGN_STYLE = Automatic; 535 | INFOPLIST_FILE = iOSAppUITests/Info.plist; 536 | LD_RUNPATH_SEARCH_PATHS = ( 537 | "$(inherited)", 538 | "@executable_path/Frameworks", 539 | "@loader_path/Frameworks", 540 | ); 541 | PRODUCT_BUNDLE_IDENTIFIER = com.kernel.iOSAppUITests; 542 | PRODUCT_NAME = "$(TARGET_NAME)"; 543 | SWIFT_VERSION = 5.0; 544 | TARGETED_DEVICE_FAMILY = "1,2"; 545 | TEST_TARGET_NAME = iOSApp; 546 | }; 547 | name = Debug; 548 | }; 549 | 04475FE923A0EB5800D4A7D4 /* Release */ = { 550 | isa = XCBuildConfiguration; 551 | buildSettings = { 552 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 553 | CODE_SIGN_STYLE = Automatic; 554 | INFOPLIST_FILE = iOSAppUITests/Info.plist; 555 | LD_RUNPATH_SEARCH_PATHS = ( 556 | "$(inherited)", 557 | "@executable_path/Frameworks", 558 | "@loader_path/Frameworks", 559 | ); 560 | PRODUCT_BUNDLE_IDENTIFIER = com.kernel.iOSAppUITests; 561 | PRODUCT_NAME = "$(TARGET_NAME)"; 562 | SWIFT_VERSION = 5.0; 563 | TARGETED_DEVICE_FAMILY = "1,2"; 564 | TEST_TARGET_NAME = iOSApp; 565 | }; 566 | name = Release; 567 | }; 568 | /* End XCBuildConfiguration section */ 569 | 570 | /* Begin XCConfigurationList section */ 571 | 04475FB423A0EB5700D4A7D4 /* Build configuration list for PBXProject "iOSApp" */ = { 572 | isa = XCConfigurationList; 573 | buildConfigurations = ( 574 | 04475FDF23A0EB5800D4A7D4 /* Debug */, 575 | 04475FE023A0EB5800D4A7D4 /* Release */, 576 | ); 577 | defaultConfigurationIsVisible = 0; 578 | defaultConfigurationName = Release; 579 | }; 580 | 04475FE123A0EB5800D4A7D4 /* Build configuration list for PBXNativeTarget "iOSApp" */ = { 581 | isa = XCConfigurationList; 582 | buildConfigurations = ( 583 | 04475FE223A0EB5800D4A7D4 /* Debug */, 584 | 04475FE323A0EB5800D4A7D4 /* Release */, 585 | ); 586 | defaultConfigurationIsVisible = 0; 587 | defaultConfigurationName = Release; 588 | }; 589 | 04475FE423A0EB5800D4A7D4 /* Build configuration list for PBXNativeTarget "iOSAppTests" */ = { 590 | isa = XCConfigurationList; 591 | buildConfigurations = ( 592 | 04475FE523A0EB5800D4A7D4 /* Debug */, 593 | 04475FE623A0EB5800D4A7D4 /* Release */, 594 | ); 595 | defaultConfigurationIsVisible = 0; 596 | defaultConfigurationName = Release; 597 | }; 598 | 04475FE723A0EB5800D4A7D4 /* Build configuration list for PBXNativeTarget "iOSAppUITests" */ = { 599 | isa = XCConfigurationList; 600 | buildConfigurations = ( 601 | 04475FE823A0EB5800D4A7D4 /* Debug */, 602 | 04475FE923A0EB5800D4A7D4 /* Release */, 603 | ); 604 | defaultConfigurationIsVisible = 0; 605 | defaultConfigurationName = Release; 606 | }; 607 | /* End XCConfigurationList section */ 608 | }; 609 | rootObject = 04475FB123A0EB5700D4A7D4 /* Project object */; 610 | } 611 | --------------------------------------------------------------------------------