├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── android-ios-screenshot.png └── kmp-showcase.png ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml └── inspectionProfiles │ └── Project_Default.xml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PRIVACY-POLICY.md ├── README.md ├── androidApp ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ └── xml │ │ └── network_security_config.xml │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── me │ │ └── moallemi │ │ └── kmpshowcase │ │ ├── app │ │ └── KmpShowCaseApp.kt │ │ ├── di │ │ └── appModule.kt │ │ ├── ui │ │ ├── base │ │ │ └── adapter │ │ │ │ ├── IdentifiableAdapter.kt │ │ │ │ └── IdentifiableDiffCallback.kt │ │ ├── home │ │ │ ├── AppsListAdapter.kt │ │ │ ├── HomeFragment.kt │ │ │ ├── MainActivity.kt │ │ │ └── OnAppItemClickListener.kt │ │ └── splash │ │ │ └── SplashActivity.kt │ │ └── util │ │ ├── ContextExt.kt │ │ ├── FragmentExt.kt │ │ └── ImageViewExt.kt │ └── res │ ├── drawable-xhdpi │ └── ic_kotlin_multiplatform_logo.png │ ├── drawable-xxxhdpi │ └── ic_kotlin_multiplatform_logo.png │ ├── drawable │ ├── branded_splash_bg.xml │ ├── ic_404_error_art.xml │ ├── ic_apple_app_store_icon.xml │ ├── ic_google_play_icon.xml │ ├── ic_kmp_large_art.xml │ ├── ic_kmp_small_art.xml │ ├── ic_launcher_foreground.xml │ └── ic_web.xml │ ├── font │ ├── noto_sans.xml │ ├── noto_sans_bold.ttf │ └── noto_sans_regular.ttf │ ├── layout │ ├── activity_main.xml │ ├── fragment_home.xml │ └── item_app_item.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.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 │ ├── values-night │ └── colors.xml │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── network_security_config.xml ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── Dependencies.kt ├── deploy ├── Procfile ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── me │ │ └── moallemi │ │ └── kmpshowcase │ │ └── webrunner │ │ └── WebRunnerApp.kt │ └── resources │ ├── application.conf │ └── logback.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── KmpShowcase.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── KmpShowcase │ ├── App │ │ ├── AppDelegate.swift │ │ └── SceneDelegate.swift │ ├── DI │ │ └── Koin.swift │ ├── Extensions │ │ └── Shared Views │ │ │ ├── ChipButtonStyle.swift │ │ │ └── ImageView.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── icon-40.png │ │ │ │ ├── icon-40@2x.png │ │ │ │ ├── icon-40@3x.png │ │ │ │ ├── icon-60@2x.png │ │ │ │ ├── icon-60@3x.png │ │ │ │ ├── icon-72.png │ │ │ │ ├── icon-72@2x.png │ │ │ │ ├── icon-76.png │ │ │ │ ├── icon-76@2x.png │ │ │ │ ├── icon-83.5@2x.png │ │ │ │ ├── icon-small-50.png │ │ │ │ ├── icon-small-50@2x.png │ │ │ │ ├── icon-small.png │ │ │ │ ├── icon-small@2x.png │ │ │ │ ├── icon-small@3x.png │ │ │ │ ├── icon.png │ │ │ │ ├── icon@2x.png │ │ │ │ ├── ios-marketing.png │ │ │ │ ├── notification-icon@2x.png │ │ │ │ ├── notification-icon@3x.png │ │ │ │ ├── notification-icon~ipad.png │ │ │ │ └── notification-icon~ipad@2x.png │ │ │ ├── Contents.json │ │ │ ├── appstore.symbolset │ │ │ │ ├── Contents.json │ │ │ │ └── appstore.svg │ │ │ └── kmp-artwork.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── kmp-artwork.svg │ │ └── en.lproj │ │ │ └── Localizable.strings │ ├── Supporting Files │ │ └── Info.plist │ └── UI │ │ └── AppList │ │ ├── AppListView.swift │ │ ├── AppListViewModelWrapper.swift │ │ └── AppRowView.swift ├── KmpShowcaseTests │ ├── Info.plist │ └── iosAppTests.swift └── KmpShowcaseUITests │ ├── Info.plist │ └── iosAppUITests.swift ├── server ├── DEPLOY.md ├── Procfile ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── me │ │ │ └── moallemi │ │ │ └── kmpshowcase │ │ │ └── server │ │ │ ├── Configuration.kt │ │ │ ├── Routes.kt │ │ │ ├── ServerApp.kt │ │ │ ├── model │ │ │ ├── App.kt │ │ │ ├── AppsResponse.kt │ │ │ └── Links.kt │ │ │ └── utils │ │ │ ├── ApplicationCallExt.kt │ │ │ └── TopLevelFunctions.kt │ └── resources │ │ ├── application.conf │ │ ├── images │ │ └── apps │ │ │ ├── cache-app.png │ │ │ ├── careem.png │ │ │ ├── chalk.png │ │ │ ├── eneco.png │ │ │ ├── fastwork.png │ │ │ ├── golchin.png │ │ │ ├── hue-essentials.png │ │ │ ├── netflix.png │ │ │ ├── plangrid.png │ │ │ ├── quizlet.png │ │ │ ├── studyo.png │ │ │ ├── target.png │ │ │ └── vmware.png │ │ ├── logback.xml │ │ └── response │ │ └── apps.json │ └── test │ └── kotlin │ └── me │ └── moallemi │ └── kmpshowcase │ └── server │ └── ApplicationTest.kt ├── settings.gradle.kts ├── shared ├── build.gradle.kts ├── buildKonfig.gradle └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── me │ │ └── moallemi │ │ └── kmpshowcase │ │ └── shared │ │ ├── Platform.kt │ │ ├── di │ │ └── KoinAndroid.kt │ │ ├── network │ │ └── api │ │ │ └── KmpShowcaseApi.kt │ │ ├── presentation │ │ └── BaseViewModel.kt │ │ └── utils │ │ ├── ApplicationDispatcher.kt │ │ └── logger.kt │ ├── commonMain │ └── kotlin │ │ └── me │ │ └── moallemi │ │ └── kmpshowcase │ │ └── shared │ │ ├── Platform.kt │ │ ├── di │ │ ├── globalDomain.kt │ │ ├── koin.kt │ │ ├── network.kt │ │ ├── qualifires.kt │ │ └── viewModels.kt │ │ ├── domain │ │ ├── mapper │ │ │ ├── AppDtoToApp.kt │ │ │ ├── LinksDtoToLinks.kt │ │ │ └── Mapper.kt │ │ └── model │ │ │ ├── App.kt │ │ │ ├── Identifiable.kt │ │ │ └── Links.kt │ │ ├── network │ │ ├── api │ │ │ └── KmpShowcaseApi.kt │ │ └── response │ │ │ ├── AppDto.kt │ │ │ ├── AppsResponseDto.kt │ │ │ └── LinksDto.kt │ │ ├── presentation │ │ ├── AppListViewModel.kt │ │ ├── AppListViewModelNavigation.kt │ │ └── BaseViewModel.kt │ │ ├── repository │ │ └── AppRepository.kt │ │ └── utils │ │ ├── ApplicationDispatcher.kt │ │ ├── GenericExt.kt │ │ └── logger.kt │ ├── iosMain │ └── kotlin │ │ └── me │ │ └── moallemi │ │ └── kmpshowcase │ │ └── shared │ │ ├── Platform.kt │ │ ├── di │ │ └── koinIOS.kt │ │ ├── network │ │ └── api │ │ │ └── KmpShowcaseApi.kt │ │ ├── presentation │ │ └── BaseViewModel.kt │ │ └── utils │ │ ├── ApplicationDispatcher.kt │ │ └── logger.kt │ └── jsMain │ └── kotlin │ └── me │ └── moallemi │ └── kmpshowcase │ └── shared │ ├── Platform.kt │ ├── di │ └── KoinJs.kt │ ├── network │ └── api │ │ └── KmpShowcaseApi.kt │ ├── presentation │ └── BaseViewModel.kt │ └── utils │ ├── applicationDispatcher.kt │ └── logger.kt ├── system.properties ├── web ├── DEPLOY.md ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ ├── Application.kt │ └── main.kt │ └── resources │ ├── index.html │ ├── style.css │ └── top-banner.svg └── webReact ├── build.gradle.kts └── src └── main ├── kotlin ├── Application.kt ├── Styles.kt ├── component │ ├── AppList.kt │ ├── Banner.kt │ └── Card.kt └── main.kt └── resources └── index.html /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/android-ios-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/.github/android-ios-screenshot.png -------------------------------------------------------------------------------- /.github/kmp-showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/.github/kmp-showcase.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/java,kotlin,linux,macos,windows,xcode,androidstudio,intellij,gradle 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=java,kotlin,linux,macos,windows,xcode,androidstudio,intellij,gradle 3 | 4 | ### Intellij ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # Generated files 16 | .idea/**/contentModel.xml 17 | 18 | # Sensitive or high-churn files 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | 27 | # Gradle 28 | .idea/**/gradle.xml 29 | .idea/**/libraries 30 | 31 | # Gradle and Maven with auto-import 32 | # When using Gradle or Maven with auto-import, you should exclude module files, 33 | # since they will be recreated, and may cause churn. Uncomment if using 34 | # auto-import. 35 | # .idea/artifacts 36 | # .idea/compiler.xml 37 | # .idea/jarRepositories.xml 38 | # .idea/modules.xml 39 | # .idea/*.iml 40 | # .idea/modules 41 | # *.iml 42 | # *.ipr 43 | 44 | # CMake 45 | cmake-build-*/ 46 | 47 | # Mongo Explorer plugin 48 | .idea/**/mongoSettings.xml 49 | 50 | # File-based project format 51 | *.iws 52 | 53 | # IntelliJ 54 | out/ 55 | 56 | # mpeltonen/sbt-idea plugin 57 | .idea_modules/ 58 | 59 | # JIRA plugin 60 | atlassian-ide-plugin.xml 61 | 62 | # Cursive Clojure plugin 63 | .idea/replstate.xml 64 | 65 | # Crashlytics plugin (for Android Studio and IntelliJ) 66 | com_crashlytics_export_strings.xml 67 | crashlytics.properties 68 | crashlytics-build.properties 69 | fabric.properties 70 | 71 | # Editor-based Rest Client 72 | .idea/httpRequests 73 | 74 | # Android studio 3.1+ serialized cache file 75 | .idea/caches/build_file_checksums.ser 76 | 77 | ### Intellij Patch ### 78 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 79 | 80 | # *.iml 81 | # modules.xml 82 | # .idea/misc.xml 83 | # *.ipr 84 | 85 | # Sonarlint plugin 86 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 87 | .idea/**/sonarlint/ 88 | 89 | # SonarQube Plugin 90 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 91 | .idea/**/sonarIssues.xml 92 | 93 | # Markdown Navigator plugin 94 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 95 | .idea/**/markdown-navigator.xml 96 | .idea/**/markdown-navigator-enh.xml 97 | .idea/**/markdown-navigator/ 98 | 99 | # Cache file creation bug 100 | # See https://youtrack.jetbrains.com/issue/JBR-2257 101 | .idea/$CACHE_FILE$ 102 | 103 | # CodeStream plugin 104 | # https://plugins.jetbrains.com/plugin/12206-codestream 105 | .idea/codestream.xml 106 | 107 | ### Java ### 108 | # Compiled class file 109 | *.class 110 | 111 | # Log file 112 | *.log 113 | 114 | # BlueJ files 115 | *.ctxt 116 | 117 | # Mobile Tools for Java (J2ME) 118 | .mtj.tmp/ 119 | 120 | # Package Files # 121 | *.jar 122 | *.war 123 | *.nar 124 | *.ear 125 | *.zip 126 | *.tar.gz 127 | *.rar 128 | 129 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 130 | hs_err_pid* 131 | 132 | ### Kotlin ### 133 | # Compiled class file 134 | 135 | # Log file 136 | 137 | # BlueJ files 138 | 139 | # Mobile Tools for Java (J2ME) 140 | 141 | # Package Files # 142 | 143 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 144 | 145 | ### Linux ### 146 | *~ 147 | 148 | # temporary files which can be created if a process still has a handle open of a deleted file 149 | .fuse_hidden* 150 | 151 | # KDE directory preferences 152 | .directory 153 | 154 | # Linux trash folder which might appear on any partition or disk 155 | .Trash-* 156 | 157 | # .nfs files are created when an open file is removed but is still being accessed 158 | .nfs* 159 | 160 | ### macOS ### 161 | # General 162 | .DS_Store 163 | .AppleDouble 164 | .LSOverride 165 | 166 | # Icon must end with two \r 167 | Icon 168 | 169 | # Thumbnails 170 | ._* 171 | 172 | # Files that might appear in the root of a volume 173 | .DocumentRevisions-V100 174 | .fseventsd 175 | .Spotlight-V100 176 | .TemporaryItems 177 | .Trashes 178 | .VolumeIcon.icns 179 | .com.apple.timemachine.donotpresent 180 | 181 | # Directories potentially created on remote AFP share 182 | .AppleDB 183 | .AppleDesktop 184 | Network Trash Folder 185 | Temporary Items 186 | .apdisk 187 | 188 | ### Windows ### 189 | # Windows thumbnail cache files 190 | Thumbs.db 191 | Thumbs.db:encryptable 192 | ehthumbs.db 193 | ehthumbs_vista.db 194 | 195 | # Dump file 196 | *.stackdump 197 | 198 | # Folder config file 199 | [Dd]esktop.ini 200 | 201 | # Recycle Bin used on file shares 202 | $RECYCLE.BIN/ 203 | 204 | # Windows Installer files 205 | *.cab 206 | *.msi 207 | *.msix 208 | *.msm 209 | *.msp 210 | 211 | # Windows shortcuts 212 | *.lnk 213 | 214 | ### Xcode ### 215 | # Xcode 216 | # 217 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 218 | 219 | ## User settings 220 | xcuserdata/ 221 | 222 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 223 | *.xcscmblueprint 224 | *.xccheckout 225 | 226 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 227 | build/ 228 | DerivedData/ 229 | *.moved-aside 230 | *.pbxuser 231 | !default.pbxuser 232 | *.mode1v3 233 | !default.mode1v3 234 | *.mode2v3 235 | !default.mode2v3 236 | *.perspectivev3 237 | !default.perspectivev3 238 | 239 | ## Gcc Patch 240 | /*.gcno 241 | 242 | ### Xcode Patch ### 243 | *.xcodeproj/* 244 | !*.xcodeproj/project.pbxproj 245 | !*.xcodeproj/xcshareddata/ 246 | !*.xcworkspace/contents.xcworkspacedata 247 | **/xcshareddata/WorkspaceSettings.xcsettings 248 | 249 | ### Gradle ### 250 | .gradle 251 | 252 | # Ignore Gradle GUI config 253 | gradle-app.setting 254 | 255 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 256 | !gradle-wrapper.jar 257 | 258 | # Cache of project 259 | .gradletasknamecache 260 | 261 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 262 | # gradle/wrapper/gradle-wrapper.properties 263 | 264 | ### Gradle Patch ### 265 | **/build/ 266 | 267 | ### AndroidStudio ### 268 | # Covers files to be ignored for android development using Android Studio. 269 | 270 | # Built application files 271 | *.apk 272 | *.ap_ 273 | 274 | # Files for the ART/Dalvik VM 275 | *.dex 276 | 277 | # Java class files 278 | 279 | # Generated files 280 | bin/ 281 | gen/ 282 | 283 | # Gradle files 284 | .gradle/ 285 | 286 | # Signing files 287 | .signing/ 288 | 289 | # Local configuration file (sdk path, etc) 290 | local.properties 291 | 292 | # Proguard folder generated by Eclipse 293 | proguard/ 294 | 295 | # Log Files 296 | 297 | # Android Studio 298 | /*/build/ 299 | /*/local.properties 300 | /*/out 301 | /*/*/build 302 | /*/*/production 303 | captures/ 304 | .navigation/ 305 | *.ipr 306 | *.swp 307 | 308 | # Android Patch 309 | gen-external-apklibs 310 | 311 | # External native build folder generated in Android Studio 2.2 and later 312 | .externalNativeBuild 313 | 314 | # NDK 315 | obj/ 316 | 317 | # IntelliJ IDEA 318 | *.iml 319 | /out/ 320 | 321 | # User-specific configurations 322 | .idea/caches/ 323 | .idea/libraries/ 324 | .idea/shelf/ 325 | .idea/workspace.xml 326 | .idea/tasks.xml 327 | .idea/.name 328 | .idea/compiler.xml 329 | .idea/copyright/profiles_settings.xml 330 | .idea/encodings.xml 331 | .idea/misc.xml 332 | .idea/modules.xml 333 | .idea/scopes/scope_settings.xml 334 | .idea/dictionaries 335 | .idea/vcs.xml 336 | .idea/jsLibraryMappings.xml 337 | .idea/datasources.xml 338 | .idea/dataSources.ids 339 | .idea/sqlDataSources.xml 340 | .idea/dynamic.xml 341 | .idea/uiDesigner.xml 342 | .idea/assetWizardSettings.xml 343 | 344 | # OS-specific files 345 | .DS_Store? 346 | 347 | # Legacy Eclipse project files 348 | .classpath 349 | .project 350 | .cproject 351 | .settings/ 352 | 353 | # Mobile Tools for Java (J2ME) 354 | 355 | # Package Files # 356 | 357 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 358 | 359 | ## Plugin-specific files: 360 | 361 | # mpeltonen/sbt-idea plugin 362 | 363 | # JIRA plugin 364 | 365 | # Mongo Explorer plugin 366 | .idea/mongoSettings.xml 367 | 368 | # Crashlytics plugin (for Android Studio and IntelliJ) 369 | 370 | ### AndroidStudio Patch ### 371 | 372 | !/gradle/wrapper/gradle-wrapper.jar 373 | 374 | # End of https://www.toptal.com/developers/gitignore/api/java,kotlin,linux,macos,windows,xcode,androidstudio,intellij,gradle 375 | 376 | .idea/markdown-navigator 377 | .idea/markdown-navigator-enh.xml 378 | .idea/jarRepositories.xml 379 | .idea/artifacts 380 | *.hprof -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 19 | 23 | 24 | 26 | 27 | 28 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | xmlns:android 37 | 38 | ^$ 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | xmlns:.* 48 | 49 | ^$ 50 | 51 | 52 | BY_NAME 53 | 54 |
55 |
56 | 57 | 58 | 59 | .*:id 60 | 61 | http://schemas.android.com/apk/res/android 62 | 63 | 64 | 65 |
66 |
67 | 68 | 69 | 70 | .*:name 71 | 72 | http://schemas.android.com/apk/res/android 73 | 74 | 75 | 76 |
77 |
78 | 79 | 80 | 81 | name 82 | 83 | ^$ 84 | 85 | 86 | 87 |
88 |
89 | 90 | 91 | 92 | style 93 | 94 | ^$ 95 | 96 | 97 | 98 |
99 |
100 | 101 | 102 | 103 | .* 104 | 105 | ^$ 106 | 107 | 108 | BY_NAME 109 | 110 |
111 |
112 | 113 | 114 | 115 | .* 116 | 117 | http://schemas.android.com/apk/res/android 118 | 119 | 120 | ANDROID_ATTRIBUTE_ORDER 121 | 122 |
123 |
124 | 125 | 126 | 127 | .* 128 | 129 | .* 130 | 131 | 132 | BY_NAME 133 | 134 |
135 |
136 |
137 |
138 | 139 | 141 |
142 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at reza@moallemi.me. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Contributions of all types are welcome. 3 | We use GitHub as our bug and feature tracker both for code and for other aspects of the project (documentation, the wiki, etc.). 4 | 5 | ## Contributing code 6 | Pull requests are welcome for all parts of the codebase. we tried to keep code as simple as possible 7 | You can find instructions on building the project in [README.md][1]. 8 | 9 | This project uses Kotlin Coding Conventions, provided via the bundled project IntelliJ codestyle. 10 | If you find that one of your pull reviews does not pass the CI server check due to a code style conflict, you can review the problems and fix it by running: `./gradlew spotlessApply`, and run IntelliJ / Android Studio's code formatter. 11 | If you'd like to submit code, but can't get the style checks to pass, feel free to put up your pull request anyway and we can help you fix the style issues. 12 | 13 | ## PR Guideline 14 | 1. Your PR must contain only one commit including all of your changes. 15 | 2. Please use appropriate format for you commit message like: `[AND] Handle screen orientation`. We have these labels for different sub projects in repository. 16 | * [AND] - for all changes related to `androidApp` project 17 | * [iOS] - for all changes related to `iosApp` project 18 | * [SERVER] - for all changes related to `server` project 19 | * [WEB] - for all changes related to `web` project 20 | * [WEB-REACT] - for all changes related to `webReact` project 21 | 22 | 23 | [1]: https://github.com/moallemi/kotlin-multiplatform-showcase -------------------------------------------------------------------------------- /PRIVACY-POLICY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Last Revised: *September 25, 2020* 4 | 5 | **KMP Showcase** does not use or collect your data. it only uses internet connection to get the list of the apps that are developed by Kotlin Multiplatform. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Multiplatform Showcase 2 | 3 | 4 | 5 | A minimal app illustrating **Kotlin Multiplatform**. Currently running on 6 | * Android (available on [Google Play](https://play.google.com/store/apps/details?id=me.moallemi.kmpshowcase)) 7 | * iOS (Rejected by Apple!) 8 | * Web (view [Demo](https://kmp-showcase-web.herokuapp.com)) 9 | * Web-React (in progress) 10 | * macOS (TODO) 11 | * CLI (TODO) 12 | 13 | 14 | 15 | ## Project Structure 16 | 17 | This project consists of several gradle modules as well as an xcode project. 18 | 19 | ### Shared 20 | 21 | This is the central module which contains shared client code. It includes an Api class with a method to query the apps endpoint using the Ktor http client. 22 | 23 | ### Android 24 | 25 | This is an Android project consuming the `:shared` module. It contains a single activity which calls the `viewModel.load()` function and displays its output in UI. 26 | 27 | ### iOS 28 | 29 | The iOS app code is in the `iosApp` directory. It uses SwiftUI to render output to screen. 30 | 31 | ### Server 32 | 33 | This is a simple Ktor server running on the Netty engine with a single endpoint `/api/v1/apps`, which outputs a list of `Apps` object serialized to JSON. 34 | 35 | Note that this repository may have not used the best practices on android or iOS implementation to help more clearly illustrate key parts of a Kotlin 36 | Multiplatform project and also to help someone just starting to explore KMP for the first time. 37 | 38 | ### Libraries and tools used 39 | 40 | * [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) 41 | * [Kotlinx Serialization](https://github.com/Kotlin/kotlinx.serialization) 42 | * [Ktor client and server library](https://github.com/ktorio/ktor) 43 | * [Android Architecture Components](https://developer.android.com/topic/libraries/architecture/index.html) 44 | * [Koin](https://github.com/InsertKoinIO/koin) 45 | * [SwiftUI](https://developer.apple.com/documentation/swiftui) 46 | 47 | ### Development setup 48 | * **Android**: For development, the latest version of Android Studio 4.0+ is required. 49 | * **iOS**: The iOS code can be modified or built by opening `iosApp/KmpShowcase.xcodeproj` in Xcode. 50 | * **Server**: Running `./gradlew server:run` will deploy the server to localhost on port 9090. 51 | * **Web**: Running `./gradlew web:run -t` will run web client on localhost. 52 | * **WebReact**: Running `./gradlew webReact:run -t` will run web React client on localhost. 53 | 54 | Once you setup the project, you might want to change server address in your `~/.gradle/gradle.properties`: 55 | 56 | ``` 57 | KMP_SHOWCASE_API_BASE_URL_DEFAULT= // default: http://localhost:9090 58 | KMP_SHOWCASE_API_BASE_URL_ANDROID= // default: http://10.0.2.2:9090 59 | KMP_SHOWCASE_API_BASE_URL_IOS_X64= // default: http://localhost:9090 60 | KMP_SHOWCASE_API_BASE_URL_IOS_ARM64= // default: http://localhost:9090 61 | ``` 62 | 63 | ## Contributions 64 | 65 | If you've found an error in this sample or you want to improve the project, please read the [Contributing Guide](CONTRIBUTING.md) first. 66 | 67 | Patches are encouraged, and may be submitted by forking this project and 68 | submitting a pull request. Since this project is still in its very early stages, 69 | if your change is substantial, please raise an issue first to discuss it. 70 | 71 | 72 | -------------------------------------------------------------------------------- /androidApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | id("kotlin-android-extensions") 5 | } 6 | group = "me.moallemi.kmpshowcase" 7 | version = "1.0-SNAPSHOT" 8 | 9 | repositories { 10 | gradlePluginPortal() 11 | google() 12 | jcenter() 13 | mavenCentral() 14 | } 15 | dependencies { 16 | implementation(project(":shared")) 17 | 18 | implementation(Dependencies.recyclerView) 19 | implementation(Dependencies.materialDesign) 20 | implementation(Dependencies.appCompat) 21 | implementation(Dependencies.constraintLayout) 22 | implementation(Dependencies.LifeCycle.runtime) 23 | 24 | implementation(Dependencies.Koin.core) 25 | implementation(Dependencies.Koin.android) 26 | 27 | implementation(Dependencies.Coroutines.common) 28 | implementation(Dependencies.Coroutines.android) 29 | 30 | implementation(Dependencies.Glide.glide) 31 | } 32 | android { 33 | compileSdkVersion(Versions.Android.compileSdk) 34 | buildToolsVersion(Versions.Android.buildToolsVersion) 35 | 36 | defaultConfig { 37 | minSdkVersion(Versions.Android.minSdk) 38 | targetSdkVersion(Versions.Android.targetSdk) 39 | versionCode = 1 40 | versionName = "1.0.0" 41 | } 42 | 43 | buildFeatures { 44 | viewBinding = true 45 | } 46 | 47 | buildTypes { 48 | getByName("release") { 49 | isMinifyEnabled = true 50 | isShrinkResources = true 51 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 52 | } 53 | } 54 | 55 | kotlinOptions { 56 | jvmTarget = "1.8" 57 | } 58 | 59 | packagingOptions { 60 | packagingOptions { 61 | exclude("META-INF/AL2.0") 62 | exclude("META-INF/LGPL2.1") 63 | exclude("META-INF/*.kotlin_module") 64 | } 65 | } 66 | } 67 | 68 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach { 69 | kotlinOptions { 70 | jvmTarget = "1.8" 71 | freeCompilerArgs = listOf( 72 | *kotlinOptions.freeCompilerArgs.toTypedArray(), 73 | "-Xallow-jvm-ir-dependencies", 74 | "-Xskip-prerelease-check" 75 | ) 76 | } 77 | } -------------------------------------------------------------------------------- /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 | 23 | -keepattributes *Annotation*, InnerClasses 24 | -dontnote kotlinx.serialization.SerializationKt 25 | -keep,includedescriptorclasses class me.moallemi.kmpshowcase.**$$serializer { *; } 26 | -keepclassmembers class me.moallemi.kmpshowcase.** { 27 | *** Companion; 28 | } 29 | -keepclasseswithmembers class me.moallemi.kmpshowcase.** { 30 | kotlinx.serialization.KSerializer serializer(...); 31 | } 32 | 33 | -keep class me.moallemi.kmpshowcase.shared.domain.** { *;} 34 | 35 | # Remove all logs 36 | -assumenosideeffects class android.util.Log { 37 | public static *** v(...); 38 | public static *** d(...); 39 | public static *** i(...); 40 | public static *** w(...); 41 | public static *** e(...); 42 | } -------------------------------------------------------------------------------- /androidApp/src/debug/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0.2.2 5 | 6 | -------------------------------------------------------------------------------- /androidApp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /androidApp/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/app/KmpShowCaseApp.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.app 2 | 3 | import android.app.Application 4 | import me.moallemi.kmpshowcase.di.appModule 5 | import me.moallemi.kmpshowcase.shared.di.initKoinAndroid 6 | import org.koin.android.ext.koin.androidContext 7 | 8 | class KmpShowCaseApp : Application() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | 13 | initializeKoin() 14 | } 15 | 16 | private fun initializeKoin() { 17 | initKoinAndroid { 18 | androidContext(this@KmpShowCaseApp) 19 | modules(appModule) 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/di/appModule.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.di 2 | 3 | import me.moallemi.kmpshowcase.shared.presentation.AppListViewModel 4 | import org.koin.androidx.viewmodel.dsl.viewModel 5 | import org.koin.dsl.module 6 | 7 | val appModule = module { 8 | viewModel { AppListViewModel(get()) } 9 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/ui/base/adapter/IdentifiableAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.ui.base.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import androidx.viewbinding.ViewBinding 9 | import me.moallemi.kmpshowcase.shared.domain.model.Identifiable 10 | 11 | abstract class IdentifiableAdapter( 12 | private val layoutResId: Int 13 | ) : ListAdapter(IdentifiableDiffCallback()) { 14 | 15 | abstract fun onBindData(itemView: View, model: Item, position: Int) 16 | 17 | override fun onCreateViewHolder( 18 | parent: ViewGroup, 19 | viewType: Int 20 | ): ItemViewHolder { 21 | val dataBinding = LayoutInflater.from(parent.context).inflate( 22 | layoutResId, 23 | parent, 24 | false 25 | ) 26 | return ItemViewHolder(dataBinding) 27 | } 28 | 29 | override fun onBindViewHolder( 30 | holder: ItemViewHolder, 31 | position: Int 32 | ) { 33 | onBindData(holder.itemView, getItem(position), position) 34 | } 35 | 36 | class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) 37 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/ui/base/adapter/IdentifiableDiffCallback.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.ui.base.adapter 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import me.moallemi.kmpshowcase.shared.domain.model.Identifiable 5 | 6 | class IdentifiableDiffCallback : DiffUtil.ItemCallback() { 7 | 8 | override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.id === newItem.id 9 | 10 | override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem == newItem 11 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/ui/home/AppsListAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.ui.home 2 | 3 | import android.view.View 4 | import androidx.core.view.isVisible 5 | import me.moallemi.kmpshowcase.R 6 | import me.moallemi.kmpshowcase.databinding.ItemAppItemBinding 7 | import me.moallemi.kmpshowcase.shared.domain.model.App 8 | import me.moallemi.kmpshowcase.ui.base.adapter.IdentifiableAdapter 9 | import me.moallemi.kmpshowcase.util.load 10 | 11 | class AppsListAdapter( 12 | private val onAppItemClickListener: OnAppItemClickListener 13 | ) : IdentifiableAdapter(R.layout.item_app_item) { 14 | 15 | override fun onBindData(itemView: View, model: App, position: Int) { 16 | with(ItemAppItemBinding.bind(itemView)) { 17 | title.text = model.name 18 | summary.text = model.summary 19 | banner.load(model.bannerUrl) 20 | 21 | googlePlay.isVisible = model.links?.googlePlay != null 22 | appStore.isVisible = model.links?.appStore != null 23 | website.isVisible = model.links?.website != null 24 | 25 | googlePlay.setOnClickListener { 26 | onAppItemClickListener.onGooglePlayLinkClicked(model.links?.googlePlay) 27 | } 28 | appStore.setOnClickListener { 29 | onAppItemClickListener.onAppStoreLinkClicked(model.links?.appStore) 30 | } 31 | website.setOnClickListener { 32 | onAppItemClickListener.onWebsiteLinkClicked(model.links?.website) 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/ui/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.ui.home 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.core.content.ContextCompat 6 | import androidx.core.graphics.ColorUtils 7 | import androidx.core.view.isVisible 8 | import androidx.fragment.app.Fragment 9 | import androidx.recyclerview.widget.ConcatAdapter 10 | import com.google.android.material.appbar.AppBarLayout 11 | import me.moallemi.kmpshowcase.R 12 | import me.moallemi.kmpshowcase.databinding.FragmentHomeBinding 13 | import me.moallemi.kmpshowcase.shared.domain.model.App 14 | import me.moallemi.kmpshowcase.shared.presentation.AppListViewModel 15 | import me.moallemi.kmpshowcase.shared.presentation.AppListViewModelNavigation 16 | import me.moallemi.kmpshowcase.shared.presentation.AppListViewModelNavigation.NavigateToUrl 17 | import me.moallemi.kmpshowcase.util.collect 18 | import me.moallemi.kmpshowcase.util.openUrl 19 | import org.koin.androidx.viewmodel.ext.android.viewModel 20 | import kotlin.math.abs 21 | 22 | class HomeFragment : Fragment(R.layout.fragment_home), OnAppItemClickListener { 23 | 24 | private val appListViewModel: AppListViewModel by viewModel() 25 | 26 | private var fragmentBinding: FragmentHomeBinding? = null 27 | 28 | private val adapter = ConcatAdapter( 29 | ConcatAdapter.Config.Builder() 30 | .setIsolateViewTypes(true) 31 | .build() 32 | ) 33 | 34 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 35 | super.onViewCreated(view, savedInstanceState) 36 | 37 | fragmentBinding = FragmentHomeBinding.bind(view) 38 | fragmentBinding?.rootRecyclerView?.adapter = adapter 39 | 40 | appListViewModel.apply { 41 | collect(apps, ::collectApps) 42 | collect(navigation, ::collectNavigation) 43 | } 44 | 45 | appListViewModel.load() 46 | 47 | fragmentBinding?.appBarLayout?.addOnOffsetChangedListener( 48 | AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> 49 | val percentage = abs(verticalOffset).toFloat() / appBarLayout.totalScrollRange 50 | 51 | val textColor = ColorUtils.setAlphaComponent( 52 | ContextCompat.getColor(requireContext(), R.color.onPrimaryColor), 53 | (percentage * 255).toInt() 54 | ) 55 | fragmentBinding?.toolbar?.setTitleTextColor(textColor) 56 | } 57 | ) 58 | 59 | } 60 | 61 | override fun onDestroy() { 62 | fragmentBinding = null 63 | super.onDestroy() 64 | } 65 | 66 | private fun collectApps(items: List) { 67 | val appsAdapter = AppsListAdapter(this).apply { 68 | this.submitList(items) 69 | } 70 | adapter.addAdapter(appsAdapter) 71 | 72 | if (items.isNotEmpty()) { 73 | fragmentBinding?.progressBar?.isVisible = false 74 | } 75 | } 76 | 77 | private fun collectNavigation(navigation: AppListViewModelNavigation?) { 78 | if (navigation is NavigateToUrl) { 79 | appListViewModel.onNavigated() 80 | context?.openUrl(navigation.url) 81 | } 82 | } 83 | 84 | override fun onGooglePlayLinkClicked(url: String?) { 85 | appListViewModel.onGooglePlayLinkClicked(url) 86 | } 87 | 88 | override fun onAppStoreLinkClicked(url: String?) { 89 | appListViewModel.onAppStoreLinkClicked(url) 90 | } 91 | 92 | override fun onWebsiteLinkClicked(url: String?) { 93 | appListViewModel.onWebsiteLinkClicked(url) 94 | } 95 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/ui/home/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.ui.home 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import me.moallemi.kmpshowcase.R 5 | 6 | class MainActivity : AppCompatActivity(R.layout.activity_main) 7 | -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/ui/home/OnAppItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.ui.home 2 | 3 | interface OnAppItemClickListener { 4 | 5 | fun onGooglePlayLinkClicked(url: String?) 6 | 7 | fun onAppStoreLinkClicked(url: String?) 8 | 9 | fun onWebsiteLinkClicked(url: String?) 10 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/ui/splash/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.ui.splash 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import me.moallemi.kmpshowcase.ui.home.MainActivity 7 | 8 | class SplashActivity : AppCompatActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | startActivity(Intent(this, MainActivity::class.java)) 14 | finish() 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/util/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.Intent.ACTION_VIEW 6 | import android.net.Uri 7 | 8 | fun Context.openUrl(url: String?) { 9 | val intent = Intent(ACTION_VIEW, Uri.parse(url)) 10 | startActivity(intent) 11 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/util/FragmentExt.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.util 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.lifecycle.lifecycleScope 5 | import kotlinx.coroutines.flow.StateFlow 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.launch 8 | 9 | fun Fragment.collect( 10 | item: StateFlow, 11 | block: (T) -> Unit 12 | ) { 13 | viewLifecycleOwner.lifecycleScope.launch { 14 | item.collect { 15 | block(it) 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /androidApp/src/main/java/me/moallemi/kmpshowcase/util/ImageViewExt.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.util 2 | 3 | import android.widget.ImageView 4 | import com.bumptech.glide.Glide 5 | 6 | fun ImageView.load(urlStr: String?) { 7 | Glide.with(this) 8 | .load(urlStr) 9 | .into(this) 10 | } -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable-xhdpi/ic_kotlin_multiplatform_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/drawable-xhdpi/ic_kotlin_multiplatform_logo.png -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable-xxxhdpi/ic_kotlin_multiplatform_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/drawable-xxxhdpi/ic_kotlin_multiplatform_logo.png -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/branded_splash_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/ic_apple_app_store_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/ic_google_play_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/ic_kmp_large_art.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 12 | 13 | 16 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 38 | -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/ic_kmp_small_art.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 12 | 15 | 18 | 21 | 22 | 25 | 28 | 31 | 34 | 35 | 37 | 40 | 43 | 46 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/ic_web.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /androidApp/src/main/res/font/noto_sans.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /androidApp/src/main/res/font/noto_sans_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/font/noto_sans_bold.ttf -------------------------------------------------------------------------------- /androidApp/src/main/res/font/noto_sans_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/font/noto_sans_regular.ttf -------------------------------------------------------------------------------- /androidApp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /androidApp/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 23 | 24 | 32 | 33 | 43 | 44 | 45 | 46 | 47 | 48 | 58 | 59 | 64 | 65 | 73 | 74 | 80 | 81 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /androidApp/src/main/res/layout/item_app_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 31 | 32 | 39 | 40 | 47 | 48 | 54 | 55 | 62 | 63 | 70 | 71 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3c30d2 4 | #4c49eb 5 | #ffffff 6 | #212121 7 | #ea0328 8 | #ff414a 9 | #212121 10 | #ffffff 11 | #212121 12 | #ffffff 13 | #ffb300 14 | #212121 15 | @android:color/black 16 | 17 | #ffffff 18 | #999999 19 | #808080 20 | 21 | #191919 22 | #434447 23 | -------------------------------------------------------------------------------- /androidApp/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3c30d2 4 | #4c49eb 5 | #ffffff 6 | #ffffff 7 | #ea0328 8 | #ff414a 9 | #212121 10 | #212121 11 | #ffffff 12 | #212121 13 | #ffb300 14 | #212121 15 | @color/primaryVariantColor 16 | 17 | 18 | #FFFFFF 19 | 20 | #000000 21 | #555555 22 | #808080 23 | 24 | #26000000 25 | 26 | #EAEAEA 27 | #eee 28 | -------------------------------------------------------------------------------- /androidApp/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 8dp 5 | 16dp 6 | 32dp 7 | @dimen/space_4dp 8 | @dimen/space_8dp 9 | @dimen/space_16dp 10 | @dimen/space_32dp 11 | 12 | 150dp 13 | -------------------------------------------------------------------------------- /androidApp/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | No items artwork 4 | Nothing to show yet! 5 | KMP Showcase 6 | Google Play 7 | App Store 8 | Mac App Store 9 | Website 10 | Kotlin Multiplatform Showcase 11 | -------------------------------------------------------------------------------- /androidApp/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | 26 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /androidApp/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0.2.2 5 | 6 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath(Dependencies.kotlinGradlePlugin) 9 | classpath(Dependencies.androidGradlePlugin) 10 | classpath(Dependencies.kotlinSerialization) 11 | } 12 | } 13 | group = "me.moallemi.kmpshowcase" 14 | version = "1.0-SNAPSHOT" 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | mavenCentral() 20 | maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | jcenter() 7 | } 8 | 9 | kotlinDslPluginOptions { 10 | experimentalWarning.set(false) 11 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Dependencies.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | const val kotlin = "1.5.10" 3 | const val androidxTest = "1.2.0" 4 | const val androidxTestExt = "1.1.1" 5 | const val androidGradlePlugin = "7.0.2" 6 | const val constraintLayout = "2.0.0" 7 | const val appCompat = "1.2.0" 8 | const val coreKtx = "1.2.0" 9 | const val materialDesign = "1.2.0" 10 | const val junit = "4.13" 11 | const val koin = "3.1.2" 12 | const val coroutines = "1.3.9-native-mt" 13 | const val kotlinxSerialization = "1.0.0-RC" 14 | const val lifecycle = "2.2.0" 15 | const val ktor = "1.4.0" 16 | const val recyclerView = "1.2.0-alpha05" 17 | const val glide = "4.11.0" 18 | const val logback = "1.2.3" 19 | const val react = "17.0.2-pre.213-kotlin-${kotlin}" 20 | const val styled = "5.3.0-pre.211-kotlin-${kotlin}" 21 | 22 | object Android { 23 | const val minSdk = 21 24 | const val targetSdk = 30 25 | const val compileSdk = 30 26 | const val buildToolsVersion = "30.0.2" 27 | } 28 | } 29 | 30 | object Dependencies { 31 | const val appCompat = "androidx.appcompat:appcompat:${Versions.appCompat}" 32 | const val materialDesign = "com.google.android.material:material:${Versions.materialDesign}" 33 | const val recyclerView = "androidx.recyclerview:recyclerview:${Versions.recyclerView}" 34 | const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}" 35 | const val constraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.constraintLayout}" 36 | const val androidGradlePlugin = "com.android.tools.build:gradle:${Versions.androidGradlePlugin}" 37 | const val kotlinSerialization = "org.jetbrains.kotlin:kotlin-serialization:${Versions.kotlin}" 38 | const val kotlinxSerialization = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.kotlinxSerialization}" 39 | const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" 40 | const val junit = "junit:junit:${Versions.junit}" 41 | 42 | object LifeCycle { 43 | const val extensions = "androidx.lifecycle:lifecycle-extensions:${Versions.lifecycle}" 44 | const val runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}" 45 | } 46 | 47 | object Koin { 48 | const val core = "io.insert-koin:koin-core:${Versions.koin}" 49 | const val android = "io.insert-koin:koin-android:${Versions.koin}" 50 | const val test = "io.insert-koin:koin-test-junit4:${Versions.koin}" 51 | } 52 | 53 | object AndroidXTest { 54 | const val core = "androidx.test:core:${Versions.androidxTest}" 55 | const val junit = "androidx.test.ext:junit:${Versions.androidxTestExt}" 56 | const val runner = "androidx.test:runner:${Versions.androidxTest}" 57 | const val rules = "androidx.test:rules:${Versions.androidxTest}" 58 | } 59 | 60 | object KotlinTest { 61 | const val common = "org.jetbrains.kotlin:kotlin-test-common:${Versions.kotlin}" 62 | const val annotations = "org.jetbrains.kotlin:kotlin-test-annotations-common:${Versions.kotlin}" 63 | const val jvm = "org.jetbrains.kotlin:kotlin-test:${Versions.kotlin}" 64 | const val junit = "org.jetbrains.kotlin:kotlin-test-junit:${Versions.kotlin}" 65 | } 66 | 67 | object Coroutines { 68 | const val common = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}" 69 | const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}" 70 | const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines}" 71 | } 72 | 73 | object Glide { 74 | const val glide = "com.github.bumptech.glide:glide:${Versions.glide}" 75 | const val compiler = "com.github.bumptech.glide:compiler:${Versions.glide}" 76 | } 77 | 78 | object Ktor { 79 | const val commonCore = "io.ktor:ktor-client-core:${Versions.ktor}" 80 | const val commonJson = "io.ktor:ktor-client-json:${Versions.ktor}" 81 | const val commonLogging = "io.ktor:ktor-client-logging:${Versions.ktor}" 82 | const val jvmCore = "io.ktor:ktor-client-core-jvm:${Versions.ktor}" 83 | const val androidCore = "io.ktor:ktor-client-okhttp:${Versions.ktor}" 84 | const val jvmJson = "io.ktor:ktor-client-json-jvm:${Versions.ktor}" 85 | const val jvmLogging = "io.ktor:ktor-client-logging-jvm:${Versions.ktor}" 86 | const val ios = "io.ktor:ktor-client-ios:${Versions.ktor}" 87 | const val iosCore = "io.ktor:ktor-client-core-native:${Versions.ktor}" 88 | const val iosJson = "io.ktor:ktor-client-json-native:${Versions.ktor}" 89 | const val iosLogging = "io.ktor:ktor-client-logging-native:${Versions.ktor}" 90 | const val commonSerialization = "io.ktor:ktor-client-serialization:${Versions.ktor}" 91 | const val androidSerialization = "io.ktor:ktor-client-serialization-jvm:${Versions.ktor}" 92 | } 93 | 94 | object KotlinWrappers { 95 | // Kotlin wrappers for popular JavaScript libraries 96 | const val react = "org.jetbrains.kotlin-wrappers:kotlin-react:${Versions.react}" 97 | const val reactDom = "org.jetbrains.kotlin-wrappers:kotlin-react-dom:${Versions.react}" 98 | const val styled = "org.jetbrains.kotlin-wrappers:kotlin-styled:${Versions.styled}" 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /deploy/Procfile: -------------------------------------------------------------------------------- 1 | web: deploy/build/install/deploy/bin/deploy -------------------------------------------------------------------------------- /deploy/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kotlin-platform-jvm") 3 | application 4 | kotlin("plugin.serialization") 5 | } 6 | 7 | group = "me.moallemi.kmpshowcase.weebrunner" 8 | version = "1.0.0" 9 | 10 | dependencies { 11 | 12 | implementation("io.ktor:ktor-server-core:${Versions.ktor}") 13 | implementation("io.ktor:ktor-server-netty:${Versions.ktor}") 14 | implementation("ch.qos.logback:logback-classic:${Versions.logback}") 15 | } 16 | 17 | application { 18 | mainClassName = "io.ktor.server.netty.EngineMain" 19 | } 20 | 21 | tasks.create("copyDistributionDirectory") { 22 | dependsOn(tasks.getByPath(":web:build")) 23 | doLast { 24 | copy { 25 | from("../web/build/distributions") 26 | into("src/main/resources") 27 | } 28 | } 29 | } 30 | 31 | tasks.create("stage") { 32 | dependsOn(tasks.getByName("installDist")) 33 | } 34 | 35 | tasks.getByPath(":deploy:processResources").dependsOn(tasks.getByPath("copyDistributionDirectory")) -------------------------------------------------------------------------------- /deploy/src/main/kotlin/me/moallemi/kmpshowcase/webrunner/WebRunnerApp.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.webrunner 2 | 3 | import io.ktor.application.Application 4 | import io.ktor.application.call 5 | import io.ktor.http.HttpStatusCode 6 | import io.ktor.http.content.resources 7 | import io.ktor.http.content.static 8 | import io.ktor.response.respond 9 | import io.ktor.response.respondRedirect 10 | import io.ktor.routing.get 11 | import io.ktor.routing.routing 12 | 13 | fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) 14 | 15 | @JvmOverloads 16 | fun Application.module(testing: Boolean = false) { 17 | 18 | routing { 19 | get("/") { 20 | call.respondRedirect("index.html") 21 | } 22 | static("/") { 23 | resources("") 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /deploy/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 8080 4 | port = ${?PORT} 5 | } 6 | 7 | application { 8 | modules = [ me.moallemi.kmpshowcase.webrunner.WebRunnerAppKt.module ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /deploy/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.mpp.enableGranularSourceSetsMetadata=true 3 | kotlin.native.enableDependencyPropagation=false 4 | 5 | xcodeproj=./iosApp 6 | 7 | android.useAndroidX=true 8 | android.enableJetifier=true 9 | 10 | org.gradle.jvmargs=-Xmx2048m 11 | org.gradle.parallel=true 12 | org.gradle.daemon=true 13 | org.gradle.configureondemand=true 14 | org.gradle.caching=true 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Sep 11 20:34:45 CEST 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-7.0.2-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "FetchImage", 6 | "repositoryURL": "https://github.com/kean/FetchImage", 7 | "state": { 8 | "branch": null, 9 | "revision": "b9ea4f9f97a4bc9fde452cea277d25d0a4f8f6df", 10 | "version": "0.2.1" 11 | } 12 | }, 13 | { 14 | "package": "Nuke", 15 | "repositoryURL": "https://github.com/kean/Nuke", 16 | "state": { 17 | "branch": null, 18 | "revision": "a2a56211cd80614e1ac90bf2e76c5329f4f6b3d0", 19 | "version": "9.1.2" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | 10 | startKoin() 11 | 12 | return true 13 | } 14 | 15 | // MARK: UISceneSession Lifecycle 16 | 17 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 18 | // Called when a new scene session is being created. 19 | // Use this method to select a configuration to create the new scene with. 20 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 21 | } 22 | 23 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 24 | // Called when the user discards a scene session. 25 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 26 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 27 | } 28 | 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | 9 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 10 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 11 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 12 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 13 | 14 | // Create the SwiftUI view that provides the window contents. 15 | let contentView = AppListView() 16 | 17 | // Use a UIHostingController as window root view controller. 18 | if let windowScene = scene as? UIWindowScene { 19 | let window = UIWindow(windowScene: windowScene) 20 | window.rootViewController = UIHostingController(rootView: contentView) 21 | self.window = window 22 | window.makeKeyAndVisible() 23 | } 24 | } 25 | 26 | func sceneDidDisconnect(_ scene: UIScene) { 27 | // Called as the scene is being released by the system. 28 | // This occurs shortly after the scene enters the background, or when its session is discarded. 29 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 30 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 31 | } 32 | 33 | func sceneDidBecomeActive(_ scene: UIScene) { 34 | // Called when the scene has moved from an inactive state to an active state. 35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 36 | } 37 | 38 | func sceneWillResignActive(_ scene: UIScene) { 39 | // Called when the scene will move from an active state to an inactive state. 40 | // This may occur due to temporary interruptions (ex. an incoming phone call). 41 | } 42 | 43 | func sceneWillEnterForeground(_ scene: UIScene) { 44 | // Called as the scene transitions from the background to the foreground. 45 | // Use this method to undo the changes made on entering the background. 46 | } 47 | 48 | func sceneDidEnterBackground(_ scene: UIScene) { 49 | // Called as the scene transitions from the foreground to the background. 50 | // Use this method to save data, release shared resources, and store enough scene-specific state information 51 | // to restore the scene back to its current state. 52 | } 53 | 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/DI/Koin.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import shared 3 | 4 | func startKoin() { 5 | let koinApplication = KoinIOSKt.doInitKoinIos() 6 | _koin = koinApplication.koin 7 | } 8 | 9 | private var _koin: Koin_coreKoin? = nil 10 | var koin: Koin_coreKoin { 11 | return _koin! 12 | } 13 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Extensions/Shared Views/ChipButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChipButtonStyle.swift 3 | // KmpShowcase 4 | // 5 | // Created by Saeed Taheri on 9/21/20. 6 | // Copyright © 2020 orgName. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ChipButtonStyle: ButtonStyle { 12 | func makeBody(configuration: Self.Configuration) -> some View { 13 | configuration.label 14 | .padding(.vertical, 8) 15 | .padding(.horizontal) 16 | .background(Color.accentColor) 17 | .foregroundColor(.white) 18 | .clipShape(Capsule()) 19 | .scaleEffect(configuration.isPressed ? 0.95 : 1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Extensions/Shared Views/ImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageView.swift 3 | // KmpShowcase 4 | // 5 | // Created by Saeed Taheri on 9/21/20. 6 | // Copyright © 2020 orgName. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import FetchImage 11 | 12 | public struct ImageView: View { 13 | @ObservedObject var image: FetchImage 14 | let maxWidth: CGFloat 15 | 16 | public var body: some View { 17 | ZStack(alignment: .center) { 18 | Rectangle() 19 | .fill(Color(.systemFill)) 20 | .cornerRadius(12.0) 21 | image.view? 22 | .resizable() 23 | .aspectRatio(contentMode: .fit) 24 | .frame(maxWidth: maxWidth, alignment: .center) 25 | .padding() 26 | } 27 | .animation(.default) 28 | .onAppear { 29 | image.priority = .normal 30 | image.fetch() 31 | } 32 | .onDisappear { 33 | image.priority = .low 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "notification-icon@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "notification-icon@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "icon-small.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "icon-small@2x.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "icon-small@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "icon-40@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "icon-40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "icon.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "icon@2x.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "icon-60@2x.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "icon-60@3x.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "notification-icon~ipad.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "20x20" 74 | }, 75 | { 76 | "filename" : "notification-icon~ipad@2x.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "20x20" 80 | }, 81 | { 82 | "filename" : "icon-small.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "29x29" 86 | }, 87 | { 88 | "filename" : "icon-small@2x.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "29x29" 92 | }, 93 | { 94 | "filename" : "icon-40.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "40x40" 98 | }, 99 | { 100 | "filename" : "icon-40@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "40x40" 104 | }, 105 | { 106 | "filename" : "icon-small-50.png", 107 | "idiom" : "ipad", 108 | "scale" : "1x", 109 | "size" : "50x50" 110 | }, 111 | { 112 | "filename" : "icon-small-50@2x.png", 113 | "idiom" : "ipad", 114 | "scale" : "2x", 115 | "size" : "50x50" 116 | }, 117 | { 118 | "filename" : "icon-72.png", 119 | "idiom" : "ipad", 120 | "scale" : "1x", 121 | "size" : "72x72" 122 | }, 123 | { 124 | "filename" : "icon-72@2x.png", 125 | "idiom" : "ipad", 126 | "scale" : "2x", 127 | "size" : "72x72" 128 | }, 129 | { 130 | "filename" : "icon-76.png", 131 | "idiom" : "ipad", 132 | "scale" : "1x", 133 | "size" : "76x76" 134 | }, 135 | { 136 | "filename" : "icon-76@2x.png", 137 | "idiom" : "ipad", 138 | "scale" : "2x", 139 | "size" : "76x76" 140 | }, 141 | { 142 | "filename" : "icon-83.5@2x.png", 143 | "idiom" : "ipad", 144 | "scale" : "2x", 145 | "size" : "83.5x83.5" 146 | }, 147 | { 148 | "filename" : "ios-marketing.png", 149 | "idiom" : "ios-marketing", 150 | "scale" : "1x", 151 | "size" : "1024x1024" 152 | } 153 | ], 154 | "info" : { 155 | "author" : "xcode", 156 | "version" : 1 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-72.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small-50.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/icon@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/iosApp/KmpShowcase/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/appstore.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "appstore.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/kmp-artwork.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "kmp-artwork.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "original" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/Assets.xcassets/kmp-artwork.imageset/kmp-artwork.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | KmpShowcase 4 | 5 | Created by Saeed Taheri on 9/22/20. 6 | Copyright © 2020 orgName. All rights reserved. 7 | */ 8 | "loading" = "Loading…"; 9 | "app_list_page_title" = "KMP Showcase"; 10 | "app_store" = "App Store"; 11 | "google_play" = "Google Play"; 12 | "website" = "Website"; 13 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | KMP Showcase 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsLocalNetworking 28 | 29 | 30 | UIApplicationSceneManifest 31 | 32 | UIApplicationSupportsMultipleScenes 33 | 34 | UISceneConfigurations 35 | 36 | UIWindowSceneSessionRoleApplication 37 | 38 | 39 | UISceneConfigurationName 40 | Default Configuration 41 | UISceneDelegateClassName 42 | $(PRODUCT_MODULE_NAME).SceneDelegate 43 | 44 | 45 | 46 | 47 | UILaunchScreen 48 | 49 | UIRequiredDeviceCapabilities 50 | 51 | armv7 52 | 53 | UISupportedInterfaceOrientations 54 | 55 | UIInterfaceOrientationPortrait 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | UISupportedInterfaceOrientations~ipad 60 | 61 | UIInterfaceOrientationPortrait 62 | UIInterfaceOrientationPortraitUpsideDown 63 | UIInterfaceOrientationLandscapeLeft 64 | UIInterfaceOrientationLandscapeRight 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/UI/AppList/AppListView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AppListView: View { 4 | @StateObject private var viewModel = AppListViewModelWrapper() 5 | 6 | var body: some View { 7 | NavigationView { 8 | if viewModel.apps.isEmpty { 9 | ProgressView("loading") 10 | } else { 11 | List { 12 | Image("kmp-artwork") 13 | .resizable() 14 | .aspectRatio(contentMode: .fit) 15 | .padding() 16 | .frame(height: 200) 17 | .frame(maxWidth: .infinity) 18 | .background(Color.accentColor) 19 | .listRowInsets(EdgeInsets()) 20 | ForEach(viewModel.apps) { app in 21 | AppRowView(app: app) 22 | .listRowInsets(EdgeInsets()) 23 | } 24 | .padding(.vertical) 25 | } 26 | .listStyle(InsetGroupedListStyle()) 27 | .navigationBarTitle("app_list_page_title") 28 | } 29 | } 30 | .onAppear(perform: viewModel.load) 31 | .navigationViewStyle(StackNavigationViewStyle()) 32 | } 33 | } 34 | 35 | struct ContentView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | AppListView() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/UI/AppList/AppListViewModelWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppListViewModelWrapper.swift 3 | // KmpShowcase 4 | // 5 | // Created by Saeed Taheri on 9/20/20. 6 | // Copyright © 2020 orgName. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import shared 11 | 12 | final class AppListViewModelWrapper: ObservableObject { 13 | 14 | @Published private(set) var apps = [App]() 15 | 16 | private let viewModel: AppListViewModel 17 | 18 | init() { 19 | viewModel = koin.get(objCClass: AppListViewModel.self, qualifier: nil) as! AppListViewModel 20 | viewModel.apps.collect(collector: self) { _, _ in } 21 | } 22 | 23 | func load() { 24 | viewModel.load() 25 | } 26 | } 27 | 28 | extension AppListViewModelWrapper: Kotlinx_coroutines_coreFlowCollector { 29 | public func emit(value: Any?, completionHandler: @escaping (KotlinUnit?, Error?) -> Void) { 30 | defer { 31 | completionHandler(nil, nil) 32 | } 33 | 34 | guard let items = value as? [App] else { 35 | return 36 | } 37 | 38 | apps = items 39 | } 40 | } 41 | 42 | extension App: Swift.Identifiable { 43 | } 44 | -------------------------------------------------------------------------------- /iosApp/KmpShowcase/UI/AppList/AppRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppRowView.swift 3 | // KmpShowcase 4 | // 5 | // Created by Saeed Taheri on 9/22/20. 6 | // Copyright © 2020 orgName. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import FetchImage 11 | import shared 12 | 13 | struct AppRowView: View { 14 | let app: shared.App 15 | 16 | var body: some View { 17 | VStack(alignment: .leading, spacing: 8) { 18 | if let urlStr = app.bannerUrl, 19 | let url = URL(string: urlStr) { 20 | ImageView( 21 | image: FetchImage(url: url), 22 | maxWidth: 300) 23 | .frame(height: 150) 24 | .clipped() 25 | .padding(.horizontal) 26 | } 27 | 28 | Text(app.name) 29 | .font(.title2) 30 | .fontWeight(.semibold) 31 | .padding(.horizontal) 32 | Text(app.summary) 33 | .foregroundColor(.secondary) 34 | .font(.body) 35 | .padding(.horizontal) 36 | 37 | makeLinksButtons(using: app.links) 38 | } 39 | } 40 | 41 | @ViewBuilder 42 | private func makeLinksButtons(using links: Links?) -> some View { 43 | if let links = links { 44 | ScrollView(.horizontal, showsIndicators: false) { 45 | HStack(spacing: 8) { 46 | links.appStore.map { link in 47 | Button(action: { 48 | didClickOn(link) 49 | }) { 50 | Label("app_store", image: "appstore") 51 | } 52 | } 53 | links.googlePlay.map { link in 54 | Button(action: { 55 | didClickOn(link) 56 | }) { 57 | Label("google_play", systemImage: "g.circle") 58 | } 59 | } 60 | links.website.map { link in 61 | Button(action: { 62 | didClickOn(link) 63 | }) { 64 | Label("website", systemImage: "globe") 65 | } 66 | } 67 | } 68 | .padding([.horizontal, .top]) 69 | .buttonStyle(ChipButtonStyle()) 70 | } 71 | } else { 72 | EmptyView() 73 | } 74 | } 75 | 76 | private func didClickOn(_ link: String) { 77 | guard let url = URL(string: link) else { 78 | return 79 | } 80 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 81 | } 82 | } 83 | 84 | struct AppRowView_Previews: PreviewProvider { 85 | static var previews: some View { 86 | AppRowView( 87 | app: shared.App( 88 | id: UUID().uuidString, 89 | name: "Golchin", 90 | summary: "Golchin is the best app possible", 91 | links: Links(appStore: "", 92 | googlePlay: "", 93 | website: ""), 94 | bannerUrl: "") 95 | ) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /iosApp/KmpShowcaseTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /iosApp/KmpShowcaseTests/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 | -------------------------------------------------------------------------------- /iosApp/KmpShowcaseUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /iosApp/KmpShowcaseUITests/iosAppUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class appNameUITests: 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 | // 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. 12 | } 13 | 14 | override func tearDown() { 15 | // Put teardown code here. This method is called after the invocation of each test method in the class. 16 | } 17 | 18 | func testExample() { 19 | // UI tests must launch the application that they test. 20 | let app = XCUIApplication() 21 | app.launch() 22 | 23 | // Use recording to get started writing UI tests. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testLaunchPerformance() { 28 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 29 | // This measures how long it takes to launch your application. 30 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { 31 | XCUIApplication().launch() 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Deploy to heroku 2 | 3 | Create a new project on heroku 4 | 5 | ### Setup Environment Variables 6 | ``` 7 | KTOR_CORS_ALLOWED_HOSTS= default: * 8 | ``` 9 | 10 | ### Add Buildpacks 11 | Add these buildpacks to your project: 12 | * https://github.com/heroku/heroku-buildpack-multi-procfile 13 | * heroku/gradle 14 | 15 | ### Configure Procfile 16 | You have to point to correct Procfile in repo which is `server/Procfile` 17 | 18 | ```bash 19 | heroku config:set PROCFILE=server/Procfile 20 | ``` 21 | 22 | ### Configure Build Task 23 | You have to change default task in order to ask heroku to run server project: 24 | 25 | ```bash 26 | heroku config:set GRADLE_TASK="server:stage" 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /server/Procfile: -------------------------------------------------------------------------------- 1 | web: server/build/install/server/bin/server -------------------------------------------------------------------------------- /server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kotlin-platform-jvm") 3 | application 4 | kotlin("plugin.serialization") 5 | } 6 | 7 | group = "me.moallemi.kmpshowcase.server" 8 | version = "1.0.0" 9 | 10 | dependencies { 11 | 12 | implementation("io.ktor:ktor-server-core:${Versions.ktor}") 13 | implementation("io.ktor:ktor-server-netty:${Versions.ktor}") 14 | implementation("io.ktor:ktor-serialization:${Versions.ktor}") 15 | 16 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.kotlinxSerialization}") 17 | implementation("ch.qos.logback:logback-classic:${Versions.logback}") 18 | 19 | testImplementation("io.ktor:ktor-server-tests:${Versions.ktor}") 20 | } 21 | 22 | application { 23 | mainClassName = "io.ktor.server.netty.EngineMain" 24 | } 25 | 26 | tasks.create("stage") { 27 | dependsOn(tasks.getByName("installDist")) 28 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/me/moallemi/kmpshowcase/server/Configuration.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.server 2 | 3 | import io.ktor.application.Application 4 | 5 | private val Application.envType 6 | get() = environment.config.property("ktor.environment").getString() 7 | 8 | val Application.isDev get() = envType == "dev" 9 | val Application.isProd get() = envType == "prod" 10 | 11 | val Application.corsAllowedHosts 12 | get() = environment.config.property("ktor.corsAllowedHosts").getString().split(",") -------------------------------------------------------------------------------- /server/src/main/kotlin/me/moallemi/kmpshowcase/server/Routes.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.server 2 | 3 | import io.ktor.application.call 4 | import io.ktor.http.HttpStatusCode 5 | import io.ktor.response.respond 6 | import io.ktor.routing.Route 7 | import io.ktor.routing.get 8 | import io.ktor.routing.route 9 | import kotlinx.serialization.decodeFromString 10 | import kotlinx.serialization.json.Json 11 | import me.moallemi.kmpshowcase.server.model.AppsResponse 12 | import me.moallemi.kmpshowcase.server.utils.baseUrl 13 | import me.moallemi.kmpshowcase.server.utils.getResourceContent 14 | 15 | fun Route.home() { 16 | get("/") { 17 | call.respond(HttpStatusCode.ServiceUnavailable) 18 | } 19 | } 20 | 21 | fun Route.apiV1() { 22 | route("/api/v1") { 23 | apps() 24 | } 25 | } 26 | 27 | fun Route.apps() { 28 | get("/apps") { 29 | val fileContent = getResourceContent("response/apps.json") 30 | val appsResponse = Json.decodeFromString(fileContent) 31 | appsResponse.apps.onEach { app -> 32 | app.bannerUrl = "${call.baseUrl()}/images/apps/${app.id}.png" 33 | } 34 | call.respond(appsResponse) 35 | } 36 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/me/moallemi/kmpshowcase/server/ServerApp.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.server 2 | 3 | import io.ktor.application.Application 4 | import io.ktor.application.install 5 | import io.ktor.features.CORS 6 | import io.ktor.features.ContentNegotiation 7 | import io.ktor.http.content.resources 8 | import io.ktor.http.content.static 9 | import io.ktor.routing.routing 10 | import io.ktor.serialization.json 11 | 12 | fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) 13 | 14 | @JvmOverloads 15 | fun Application.module(testing: Boolean = false) { 16 | 17 | install(ContentNegotiation) { 18 | json() 19 | } 20 | 21 | install(CORS) { 22 | corsAllowedHosts.onEach { allowedHost -> 23 | host(host = allowedHost, schemes = listOf("http", "https")) 24 | } 25 | } 26 | 27 | routing { 28 | home() 29 | apiV1() 30 | static("/") { 31 | resources("") 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/me/moallemi/kmpshowcase/server/model/App.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.server.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class App( 7 | val id: String, 8 | val name: String, 9 | val summary: String, 10 | val links: Links?, 11 | var bannerUrl: String? = null 12 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/me/moallemi/kmpshowcase/server/model/AppsResponse.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.server.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AppsResponse( 7 | val lastUpdate: String, 8 | val apps: List 9 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/me/moallemi/kmpshowcase/server/model/Links.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.server.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Links( 7 | val appStore: String? = null, 8 | val googlePlay: String? = null, 9 | val website: String? = null, 10 | val article: String? = null, 11 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/me/moallemi/kmpshowcase/server/utils/ApplicationCallExt.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.server.utils 2 | 3 | import io.ktor.application.ApplicationCall 4 | import io.ktor.features.origin 5 | import io.ktor.request.host 6 | import io.ktor.request.port 7 | 8 | fun ApplicationCall.baseUrl() = 9 | if (request.host() !in listOf("0.0.0.0", "localhost", "10.0.2.2")) { 10 | "https://${request.host()}" 11 | } else { 12 | "${request.origin.scheme}://${request.host()}:${request.port()}" 13 | } 14 | -------------------------------------------------------------------------------- /server/src/main/kotlin/me/moallemi/kmpshowcase/server/utils/TopLevelFunctions.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.server.utils 2 | 3 | fun getResourceContent(filePath: String) = object {}.javaClass.classLoader.getResource(filePath).readText() -------------------------------------------------------------------------------- /server/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 9090 4 | port = ${?PORT} 5 | } 6 | 7 | environment = dev 8 | environment = ${?KTOR_ENV} 9 | 10 | corsAllowedHosts = "*" //comma separated list of hosts 11 | corsAllowedHosts = ${?KTOR_CORS_ALLOWED_HOSTS} 12 | 13 | application { 14 | modules = [ me.moallemi.kmpshowcase.server.ServerAppKt.module ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/cache-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/cache-app.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/careem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/careem.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/chalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/chalk.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/eneco.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/eneco.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/fastwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/fastwork.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/golchin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/golchin.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/hue-essentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/hue-essentials.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/netflix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/netflix.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/plangrid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/plangrid.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/quizlet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/quizlet.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/studyo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/studyo.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/target.png -------------------------------------------------------------------------------- /server/src/main/resources/images/apps/vmware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moallemi/kotlin-multiplatform-showcase/69e5b3e68e6d9d0dc86f8adb19a12b315479026f/server/src/main/resources/images/apps/vmware.png -------------------------------------------------------------------------------- /server/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /server/src/main/resources/response/apps.json: -------------------------------------------------------------------------------- 1 | { 2 | "lastUpdate": "2020-10-04T11:53:11.133Z", 3 | "apps": [ 4 | { 5 | "name": "Quizlet", 6 | "id": "quizlet", 7 | "summary": "Kotlin Multiplatform Mobile powers the Quizlet app, a global learning platform with 100 million active installs that provides engaging study tools to help people practice and master whatever they are learning.", 8 | "bannerUrl": "", 9 | "links": { 10 | "appStore": "https://apps.apple.com/us/app/quizlet-flashcards-study-tools/id546473125", 11 | "googlePlay": "https://play.google.com/store/apps/details?id=com.quizlet.quizletandroid", 12 | "website": "https://quizlet.com/", 13 | "article": "https://kotlinlang.org/lp/mobile/case-studies/quizlet/" 14 | } 15 | }, 16 | { 17 | "name": "VMware", 18 | "id": "vmware", 19 | "summary": "VMware uses Kotlin Multiplatform Mobile in various modules across their Workspace ONE productivity app portfolio.", 20 | "links": { 21 | "appStore": "https://apps.apple.com/us/app/plangrid-construction-collaboration/id498795789", 22 | "googlePlay": "https://play.google.com/store/apps/details?id=com.plangrid.android&hl=en", 23 | "website": "https://www.plangrid.com", 24 | "article": "https://kotlinlang.org/lp/mobile/case-studies/vmware/" 25 | } 26 | }, 27 | { 28 | "name": "PlanGrid", 29 | "id": "plangrid", 30 | "summary": "Autodesk introduced Kotlin Multiplatform Mobile to their PlanGrid app to ship a single source of truth for offline sync logic and data models on 3 mobile platforms: iOS, Android, and Windows.", 31 | "links": { 32 | "appStore": "https://apps.apple.com/us/app/plangrid-construction-collaboration/id498795789", 33 | "googlePlay": "https://play.google.com/store/apps/details?id=com.plangrid.android&hl=en", 34 | "website": "https://www.plangrid.com", 35 | "article": "https://kotlinlang.org/lp/mobile/case-studies/autodesk/" 36 | } 37 | }, 38 | { 39 | "name": "Golchin", 40 | "id": "golchin", 41 | "summary": "As millions of people create videos every day on YouTube, finding what you love to watch, in your own language, could be an issue. What if there was an app which offered curated Persian videos for you. Here it comes Golchin.", 42 | "bannerUrl": "", 43 | "links": { 44 | "appStore": "https://apps.apple.com/us/app/id1530458228", 45 | "googlePlay": "https://play.google.com/store/apps/details?id=io.golchin", 46 | "website": "https://golchin.io" 47 | } 48 | }, 49 | { 50 | "name": "Cash App", 51 | "id": "cache-app", 52 | "summary": "Cash App is a mobile payment service developed by Square, Inc., allowing users to transfer money to one another using a mobile phone app. A lot of app business logic, including an ability to search through all transactions, is implemented with Kotlin Multiplatform Mobile.", 53 | "bannerUrl": "", 54 | "links": { 55 | "appStore": "https://apps.apple.com/us/app/square-cash/id711923939", 56 | "googlePlay": "https://play.google.com/store/apps/details?id=com.squareup.cash", 57 | "website": "https://cash.app", 58 | "article": "https://kotlinconf.com/2019/talks/video/2019/116027/" 59 | } 60 | }, 61 | { 62 | "name": "Chalk", 63 | "id": "chalk", 64 | "summary": "The UI for each of the Chalk.com apps is native to the platform, but other than that, almost everything else for their apps can be shared with Kotlin Multiplatform Mobile.", 65 | "bannerUrl": "", 66 | "links": { 67 | "website": "https://www.chalk.com/", 68 | "article": "https://kotlinlang.org/lp/mobile/case-studies/chalk/" 69 | } 70 | }, 71 | { 72 | "name": "Fastwork", 73 | "id": "fastwork", 74 | "summary": "Fastwork is one of the largest professional freelancing platforms in Southeast Asia in terms of both the number of users it has and the number of projects completed.", 75 | "bannerUrl": "", 76 | "links": { 77 | "appStore": "https://apps.apple.com/us/app/fastwork-hire-freelancers-at-your-fingertips/id1154830520?ls=1", 78 | "googlePlay": "https://play.google.com/store/apps/details?id=com.fastwork.app&hl=en", 79 | "website": "https://fastwork.co/", 80 | "article": "https://kotlinlang.org/lp/mobile/case-studies/fastwork/" 81 | } 82 | }, 83 | { 84 | "name": "Careem", 85 | "id": "careem", 86 | "summary": "Careem is a transportation network company with operations in the Middle East, Africa, and South Asia. Careem driver applications are built using Kotlin Multiplatform Mobile.", 87 | "bannerUrl": "", 88 | "links": { 89 | "appStore": "https://apps.apple.com/us/app/careem-rides-food-delivery/id592978487", 90 | "googlePlay": "https://play.google.com/store/apps/details?id=com.careem.acma", 91 | "website": "https://www.careem.com", 92 | "article": "https://kotlinconf.com/2019/talks/video/2019/127172/" 93 | } 94 | }, 95 | { 96 | "name": "Target", 97 | "id": "target", 98 | "summary": "Now the Target app can help you have a more rewarding Target run! Introducing Target Circle, which gives you access to hundreds of deals, a birthday gift and the chance to support your community", 99 | "bannerUrl": "", 100 | "links": { 101 | "appStore": "https://apps.apple.com/app/apple-store/id297430070?mt=8", 102 | "googlePlay": "https://play.google.com/store/apps/details?id=com.target.ui", 103 | "website": "https://www.target.com/", 104 | "article": "https://tech.target.com/2019/04/02/gift-registry-kotlin-multiplatform.html" 105 | } 106 | }, 107 | { 108 | "name": "Studyo", 109 | "id": "studyo", 110 | "summary": "Get ready for college and the world by learning to get organized. Students absolutely love Studyo and use it many times a day. Schools have adopted Studyo to better prepare students for College and for life, replacing paper planners and other solutions, complementing their admin systems and LMS.", 111 | "bannerUrl": "", 112 | "links": { 113 | "appStore": "https://apps.apple.com/us/app/studyo/id981974458", 114 | "googlePlay": "https://play.google.com/store/apps/details?id=app.studyo.prod.android.react&hl=en", 115 | "website": "https://studyo.co/" 116 | } 117 | }, 118 | { 119 | "name": "Eneco", 120 | "id": "eneco", 121 | "summary": "Do you use more energy with washing, heating or cooking? And how can you save? The Eneco app gives you insight and useful tips. You also arrange all your energy matters there. Download it now.", 122 | "bannerUrl": "", 123 | "links": { 124 | "appStore": "https://apps.apple.com/nl/app/eneco/id827734058", 125 | "googlePlay": "https://play.google.com/store/apps/details?id=com.afrogleap.eneco.myeneco", 126 | "website": "https://www.eneco.nl" 127 | } 128 | }, 129 | { 130 | "name": "Hue Essentials", 131 | "id": "hue-essentials", 132 | "summary": "Discover ways to get the most out of your smart lighting.", 133 | "bannerUrl": "", 134 | "links": { 135 | "appStore": "https://apps.apple.com/app/id1462943921", 136 | "googlePlay": "https://play.google.com/store/apps/details?id=com.superthomaslab.hueessentials", 137 | "website": "https://www.hueessentials.com/" 138 | } 139 | }, 140 | { 141 | "name": "Netflix", 142 | "id": "netflix", 143 | "summary": "KMM helps tech giant Netflix optimize product reliability and speed of delivery, crucial for serving their customers' constantly evolving needs.", 144 | "bannerUrl": "", 145 | "links": { 146 | "website": "https://netflix.com/", 147 | "article": "https://netflixtechblog.com/netflix-android-and-ios-studio-apps-kotlin-multiplatform-d6d4d8d25d23" 148 | } 149 | } 150 | ] 151 | } -------------------------------------------------------------------------------- /server/src/test/kotlin/me/moallemi/kmpshowcase/server/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.server 2 | 3 | import io.ktor.http.HttpMethod 4 | import io.ktor.http.HttpStatusCode 5 | import io.ktor.server.testing.handleRequest 6 | import io.ktor.server.testing.withTestApplication 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | class ApplicationTest { 11 | @Test 12 | fun testRoot() { 13 | withTestApplication({ module(testing = true) }) { 14 | handleRequest(HttpMethod.Get, "/").apply { 15 | assertEquals(HttpStatusCode.OK, response.status()) 16 | assertEquals("HELLO WORLD!", response.content) 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | rootProject.name = "kmp-showcase" 9 | 10 | 11 | include(":androidApp") 12 | include(":shared") 13 | include(":server") 14 | include(":web") 15 | include(":webReact") 16 | 17 | if (System.getenv("KMP_SHOWCASE_DEPLOY_PROJECT")?.contains("web") == true) { 18 | include(":deploy") 19 | } -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | id("com.android.library") 6 | id("kotlin-android-extensions") 7 | id("kotlinx-serialization") 8 | id("com.codingfeline.buildkonfig") version "0.7.0" 9 | } 10 | group = "me.moallemi.kmpshowcase" 11 | version = "1.0-SNAPSHOT" 12 | 13 | repositories { 14 | gradlePluginPortal() 15 | google() 16 | jcenter() 17 | mavenCentral() 18 | } 19 | kotlin { 20 | android() 21 | ios { 22 | binaries { 23 | framework { 24 | baseName = "shared" 25 | } 26 | } 27 | } 28 | js { 29 | browser { 30 | } 31 | } 32 | 33 | sourceSets { 34 | all { 35 | languageSettings.apply { 36 | useExperimentalAnnotation("kotlin.RequiresOptIn") 37 | useExperimentalAnnotation("kotlinx.coroutines.ExperimentalCoroutinesApi") 38 | useExperimentalAnnotation("kotlinx.serialization.InternalSerializationApi") 39 | } 40 | } 41 | val commonMain by getting { 42 | dependencies { 43 | implementation(Dependencies.Koin.core) 44 | implementation(Dependencies.Coroutines.common) 45 | 46 | implementation(Dependencies.Ktor.commonCore) 47 | implementation(Dependencies.Ktor.commonJson) 48 | implementation(Dependencies.Ktor.commonLogging) 49 | implementation(Dependencies.Ktor.commonSerialization) 50 | 51 | implementation(Dependencies.kotlinxSerialization) 52 | } 53 | } 54 | val commonTest by getting { 55 | dependencies { 56 | implementation(kotlin("test-common")) 57 | implementation(kotlin("test-annotations-common")) 58 | } 59 | } 60 | val androidMain by getting { 61 | dependencies { 62 | api(Dependencies.coreKtx) 63 | implementation(Dependencies.Coroutines.test) 64 | 65 | implementation(Dependencies.Ktor.jvmCore) 66 | implementation(Dependencies.Ktor.jvmJson) 67 | implementation(Dependencies.Ktor.jvmLogging) 68 | implementation(Dependencies.Ktor.androidSerialization) 69 | implementation(Dependencies.Ktor.androidCore) 70 | 71 | implementation(Dependencies.LifeCycle.extensions) 72 | } 73 | } 74 | val androidTest by getting { 75 | dependencies { 76 | implementation(kotlin("test-junit")) 77 | implementation(Dependencies.junit) 78 | } 79 | } 80 | val iosMain by getting { 81 | dependencies { 82 | implementation(Dependencies.Koin.core) 83 | implementation(Dependencies.Coroutines.common) { 84 | version { 85 | strictly(Versions.coroutines) 86 | } 87 | } 88 | implementation(Dependencies.Ktor.ios) 89 | } 90 | } 91 | val iosTest by getting 92 | 93 | val jsMain by getting { 94 | dependencies { 95 | api(Dependencies.Koin.core) 96 | api(Dependencies.Coroutines.common) 97 | } 98 | } 99 | } 100 | } 101 | android { 102 | compileSdkVersion(Versions.Android.compileSdk) 103 | buildToolsVersion(Versions.Android.buildToolsVersion) 104 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 105 | defaultConfig { 106 | minSdkVersion(Versions.Android.minSdk) 107 | targetSdkVersion(Versions.Android.targetSdk) 108 | } 109 | buildTypes { 110 | getByName("release") { 111 | isMinifyEnabled = false 112 | } 113 | } 114 | } 115 | val packForXcode by tasks.creating(Sync::class) { 116 | group = "build" 117 | val mode = System.getenv("CONFIGURATION") ?: "DEBUG" 118 | val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator" 119 | val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64" 120 | val framework = 121 | kotlin.targets.getByName(targetName).binaries.getFramework(mode) 122 | inputs.property("mode", mode) 123 | dependsOn(framework.linkTask) 124 | val targetDir = File(buildDir, "xcode-frameworks") 125 | from({ framework.outputDirectory }) 126 | into(targetDir) 127 | } 128 | tasks.getByName("build").dependsOn(packForXcode) 129 | 130 | apply(from = "buildKonfig.gradle") -------------------------------------------------------------------------------- /shared/buildKonfig.gradle: -------------------------------------------------------------------------------- 1 | buildkonfig { 2 | packageName = "me.moallemi.kmpshowcase.shared" 3 | objectName = "SharedConfig" 4 | 5 | // default config is required 6 | defaultConfigs { 7 | buildConfigField 'STRING', 'API_BASE_URL', getProperty('KMP_SHOWCASE_API_BASE_URL_DEFAULT', 'http://localhost:9090') 8 | } 9 | 10 | targetConfigs { 11 | android { 12 | buildConfigField 'STRING', 'API_BASE_URL', getProperty('KMP_SHOWCASE_API_BASE_URL_ANDROID', 'http://10.0.2.2:9090') 13 | } 14 | 15 | iosX64 { 16 | buildConfigField 'STRING', 'API_BASE_URL', getProperty('KMP_SHOWCASE_API_BASE_URL_IOS_X64', 'http://localhost:9090') 17 | } 18 | 19 | iosArm64 { 20 | buildConfigField 'STRING', 'API_BASE_URL', getProperty('KMP_SHOWCASE_API_BASE_URL_IOS_ARM64', 'http://localhost:9090') 21 | } 22 | } 23 | } 24 | 25 | Object getProperty(String propertyName, Object defaultValue) { 26 | def propertyValue = project.properties[propertyName] 27 | def envValue = System.getenv(propertyName) 28 | return propertyValue != null ? propertyValue : (envValue != null ? envValue : defaultValue) 29 | } -------------------------------------------------------------------------------- /shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/me/moallemi/kmpshowcase/shared/Platform.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared 2 | 3 | actual class Platform actual constructor() { 4 | actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}" 5 | } -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/me/moallemi/kmpshowcase/shared/di/KoinAndroid.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.di 2 | 3 | import io.ktor.client.engine.okhttp.OkHttp 4 | import org.koin.core.module.Module 5 | import org.koin.dsl.KoinAppDeclaration 6 | import org.koin.dsl.module 7 | 8 | fun initKoinAndroid(appDeclaration: KoinAppDeclaration) = initKoin { 9 | appDeclaration() 10 | } 11 | 12 | actual val platformModule: Module = module { 13 | single { 14 | OkHttp.create() 15 | } 16 | } -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/me/moallemi/kmpshowcase/shared/network/api/KmpShowcaseApi.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.network.api 2 | 3 | import me.moallemi.kmpshowcase.shared.SharedConfig 4 | 5 | actual val API_BASE_URL: String = SharedConfig.API_BASE_URL -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/me/moallemi/kmpshowcase/shared/presentation/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.presentation 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.CompletableJob 5 | import kotlinx.coroutines.CoroutineExceptionHandler 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Job 8 | import me.moallemi.kmpshowcase.shared.utils.log 9 | import kotlin.coroutines.CoroutineContext 10 | 11 | actual abstract class BaseViewModel actual constructor( 12 | viewModelContext: CoroutineContext, 13 | ) : ViewModel(), CoroutineScope { 14 | 15 | private val context: CoroutineContext = viewModelContext 16 | 17 | private var _job: CompletableJob? = null 18 | private val job: CompletableJob 19 | get() { 20 | if (_job == null) { 21 | _job = Job() 22 | } 23 | return _job ?: Job() 24 | } 25 | 26 | private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> 27 | log(throwable.message) 28 | } 29 | 30 | actual override val coroutineContext: CoroutineContext 31 | get() = context + job + exceptionHandler 32 | 33 | override fun onCleared() { 34 | super.onCleared() 35 | _job?.cancel() 36 | _job = null 37 | } 38 | } -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/me/moallemi/kmpshowcase/shared/utils/ApplicationDispatcher.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.utils 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlin.coroutines.CoroutineContext 5 | 6 | internal actual val applicationDispatcher: CoroutineContext = Dispatchers.IO -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/me/moallemi/kmpshowcase/shared/utils/logger.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.utils 2 | 3 | import android.util.Log 4 | 5 | actual fun log(message: String?, tag: String, level: LogLevel) { 6 | if (message == null) { 7 | return 8 | } 9 | 10 | when (level) { 11 | LogLevel.DEBUG -> Log.d(tag, message) 12 | LogLevel.WARN -> Log.w(tag, message) 13 | LogLevel.ERROR -> Log.e(tag, message) 14 | }.exhaustive 15 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/Platform.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared 2 | 3 | expect class Platform() { 4 | val platform: String 5 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/di/globalDomain.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.di 2 | 3 | import me.moallemi.kmpshowcase.shared.domain.mapper.AppDtoToApp 4 | import me.moallemi.kmpshowcase.shared.domain.mapper.LinksDtoToLinks 5 | import me.moallemi.kmpshowcase.shared.repository.AppRepository 6 | import org.koin.dsl.module 7 | 8 | internal val domainModule = module { 9 | single { 10 | LinksDtoToLinks() 11 | } 12 | single { 13 | AppDtoToApp(get()) 14 | } 15 | single { 16 | AppRepository(get(), get()) 17 | } 18 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/di/koin.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.di 2 | 3 | import org.koin.core.context.startKoin 4 | import org.koin.core.module.Module 5 | import org.koin.dsl.KoinAppDeclaration 6 | 7 | internal fun initKoin(appDeclaration: KoinAppDeclaration = {}) = startKoin { 8 | appDeclaration() 9 | modules( 10 | networkModule, 11 | domainModule, 12 | platformModule 13 | ) 14 | } 15 | 16 | expect val platformModule: Module -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/di/network.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.di 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.features.json.JsonFeature 5 | import io.ktor.client.features.json.serializer.KotlinxSerializer 6 | import io.ktor.client.features.logging.LogLevel 7 | import io.ktor.client.features.logging.Logger 8 | import io.ktor.client.features.logging.Logging 9 | import kotlinx.serialization.json.Json 10 | import me.moallemi.kmpshowcase.shared.network.api.KmpShowcaseApi 11 | import org.koin.dsl.module 12 | import me.moallemi.kmpshowcase.shared.utils.log as kmpLog 13 | 14 | internal val networkModule = module { 15 | single { 16 | HttpClient(this@single.get()) { 17 | install(JsonFeature) { 18 | serializer = KotlinxSerializer(this@single.get()) 19 | } 20 | install(Logging) { 21 | logger = object : Logger { 22 | override fun log(message: String) { 23 | kmpLog(message = message, tag = "ktor") 24 | } 25 | } 26 | level = LogLevel.INFO 27 | } 28 | } 29 | } 30 | single { 31 | Json { 32 | isLenient = true 33 | ignoreUnknownKeys = true 34 | } 35 | } 36 | single { 37 | KmpShowcaseApi(get()) 38 | } 39 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/di/qualifires.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.di 2 | 3 | internal const val QUALIFIER_NAME_MAPPER_LINKSDTO_TOLINKS = "qn_mapper_linksdto_tolinks" 4 | internal const val QUALIFIER_NAME_MAPPER_APPDTO_TO_APP = "qn_mapper_appdto_to_app" -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/di/viewModels.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.di 2 | 3 | import me.moallemi.kmpshowcase.shared.presentation.AppListViewModel 4 | import org.koin.dsl.module 5 | 6 | internal val viewModelsModule = module { 7 | factory { AppListViewModel(get()) } 8 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/domain/mapper/AppDtoToApp.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.domain.mapper 2 | 3 | import me.moallemi.kmpshowcase.shared.domain.model.App 4 | import me.moallemi.kmpshowcase.shared.network.response.AppDto 5 | 6 | class AppDtoToApp( 7 | private val linksDtoToLinks: LinksDtoToLinks, 8 | ) { 9 | 10 | fun map(from: AppDto) = with(from) { 11 | App( 12 | id = name, 13 | name = name, 14 | summary = summary, 15 | links = linksDtoToLinks.map(links), 16 | bannerUrl = bannerUrl 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/domain/mapper/LinksDtoToLinks.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.domain.mapper 2 | 3 | import me.moallemi.kmpshowcase.shared.domain.model.Links 4 | import me.moallemi.kmpshowcase.shared.network.response.LinksDto 5 | 6 | class LinksDtoToLinks { 7 | 8 | fun map(from: LinksDto?) = from?.let { linksDto -> 9 | Links( 10 | appStore = linksDto.appStore, 11 | googlePlay = linksDto.googlePlay, 12 | website = linksDto.website 13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/domain/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.domain.mapper 2 | 3 | // FIXME due to inconsistency we can not use generic interfaces with Koin 4 | // https://github.com/InsertKoinIO/koin/issues/75#issuecomment-474405908 5 | interface Mapper { 6 | fun map(from: F): T 7 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/domain/model/App.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.domain.model 2 | 3 | data class App( 4 | override val id: String, 5 | val name: String, 6 | val summary: String, 7 | val links: Links?, 8 | val bannerUrl: String? 9 | ) : Identifiable -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/domain/model/Identifiable.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.domain.model 2 | 3 | interface Identifiable { 4 | val id: String 5 | override fun equals(other: Any?): Boolean 6 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/domain/model/Links.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.domain.model 2 | 3 | data class Links( 4 | val appStore: String?, 5 | val googlePlay: String?, 6 | val website: String? 7 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/network/api/KmpShowcaseApi.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.network.api 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.request.get 5 | import me.moallemi.kmpshowcase.shared.network.response.AppsResponseDto 6 | 7 | expect val API_BASE_URL: String 8 | 9 | class KmpShowcaseApi( 10 | private val httpClient: HttpClient, 11 | ) { 12 | suspend fun getApps(): AppsResponseDto = httpClient.get("$API_BASE_URL/api/v1/apps") 13 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/network/response/AppDto.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.network.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AppDto( 7 | val name: String, 8 | val summary: String, 9 | val links: LinksDto?, 10 | val bannerUrl: String? 11 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/network/response/AppsResponseDto.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.network.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AppsResponseDto( 7 | val lastUpdate: String, 8 | val apps: List 9 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/network/response/LinksDto.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.network.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class LinksDto( 7 | val appStore: String?, 8 | val googlePlay: String?, 9 | val website: String? 10 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/presentation/AppListViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.presentation 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.coroutines.flow.StateFlow 5 | import kotlinx.coroutines.flow.collect 6 | import kotlinx.coroutines.launch 7 | import me.moallemi.kmpshowcase.shared.domain.model.App 8 | import me.moallemi.kmpshowcase.shared.presentation.AppListViewModelNavigation.NavigateToUrl 9 | import me.moallemi.kmpshowcase.shared.repository.AppRepository 10 | import me.moallemi.kmpshowcase.shared.utils.applicationDispatcher 11 | 12 | class AppListViewModel( 13 | private val appRepository: AppRepository 14 | ) : BaseViewModel(applicationDispatcher) { 15 | 16 | private val _apps = MutableStateFlow>(emptyList()) 17 | val apps: StateFlow> = _apps 18 | 19 | private val _navigation = 20 | MutableStateFlow(null) 21 | val navigation: StateFlow = _navigation 22 | 23 | fun load() { 24 | launch { 25 | appRepository.getAllAppsAsFlow().collect { 26 | _apps.value = it 27 | } 28 | } 29 | } 30 | 31 | fun onGooglePlayLinkClicked(url: String?) { 32 | _navigation.value = NavigateToUrl(url) 33 | } 34 | 35 | fun onAppStoreLinkClicked(url: String?) { 36 | _navigation.value = NavigateToUrl(url) 37 | } 38 | 39 | fun onWebsiteLinkClicked(url: String?) { 40 | _navigation.value = NavigateToUrl(url) 41 | } 42 | 43 | fun onNavigated() { 44 | _navigation.value = null 45 | } 46 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/presentation/AppListViewModelNavigation.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.presentation 2 | 3 | sealed class AppListViewModelNavigation { 4 | class NavigateToUrl(val url: String?) : AppListViewModelNavigation() 5 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/presentation/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.presentation 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlin.coroutines.CoroutineContext 5 | 6 | expect abstract class BaseViewModel( 7 | viewModelContext: CoroutineContext, 8 | ) : CoroutineScope { 9 | override val coroutineContext: CoroutineContext 10 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/repository/AppRepository.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.flow 5 | import me.moallemi.kmpshowcase.shared.domain.mapper.AppDtoToApp 6 | import me.moallemi.kmpshowcase.shared.domain.mapper.Mapper 7 | import me.moallemi.kmpshowcase.shared.domain.model.App 8 | import me.moallemi.kmpshowcase.shared.network.api.KmpShowcaseApi 9 | import me.moallemi.kmpshowcase.shared.network.response.AppDto 10 | 11 | class AppRepository( 12 | private val kmpShowcaseApi: KmpShowcaseApi, 13 | private val appDtoToApp: AppDtoToApp, 14 | ) { 15 | fun getAllAppsAsFlow(): Flow> = flow { 16 | kmpShowcaseApi.getApps() 17 | .apps 18 | .map(appDtoToApp::map) 19 | .let { 20 | emit(it) 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/utils/ApplicationDispatcher.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.utils 2 | 3 | import kotlin.coroutines.CoroutineContext 4 | 5 | internal expect val applicationDispatcher: CoroutineContext -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/utils/GenericExt.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.utils 2 | 3 | val T.exhaustive: T 4 | get() = this -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/me/moallemi/kmpshowcase/shared/utils/logger.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.utils 2 | 3 | enum class LogLevel { 4 | DEBUG, WARN, ERROR 5 | } 6 | 7 | expect fun log(message: String?, tag: String = "KmpShowcase", level: LogLevel = LogLevel.DEBUG) -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/me/moallemi/kmpshowcase/shared/Platform.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared 2 | 3 | 4 | import platform.UIKit.UIDevice 5 | 6 | actual class Platform actual constructor() { 7 | actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion 8 | } -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/me/moallemi/kmpshowcase/shared/di/koinIOS.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.di 2 | 3 | import io.ktor.client.engine.ios.Ios 4 | import kotlinx.cinterop.ObjCClass 5 | import kotlinx.cinterop.getOriginalKotlinClass 6 | import org.koin.core.Koin 7 | import org.koin.core.KoinApplication 8 | import org.koin.core.parameter.parametersOf 9 | import org.koin.core.qualifier.Qualifier 10 | import org.koin.dsl.module 11 | 12 | fun initKoinIos(): KoinApplication = initKoin { 13 | modules( 14 | viewModelsModule 15 | ) 16 | } 17 | 18 | actual val platformModule = module { 19 | single { 20 | Ios.create() 21 | } 22 | } 23 | 24 | fun Koin.get(objCClass: ObjCClass, qualifier: Qualifier?, parameter: Any): Any { 25 | val kClazz = getOriginalKotlinClass(objCClass)!! 26 | return get(kClazz, qualifier) { parametersOf(parameter) } 27 | } 28 | 29 | fun Koin.get(objCClass: ObjCClass, parameter: Any): Any { 30 | val kClazz = getOriginalKotlinClass(objCClass)!! 31 | return get(kClazz, null) { parametersOf(parameter) } 32 | } 33 | 34 | fun Koin.get(objCClass: ObjCClass, qualifier: Qualifier?): Any { 35 | val kClazz = getOriginalKotlinClass(objCClass)!! 36 | return get(kClazz, qualifier, null) 37 | } 38 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/me/moallemi/kmpshowcase/shared/network/api/KmpShowcaseApi.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.network.api 2 | 3 | import me.moallemi.kmpshowcase.shared.SharedConfig 4 | 5 | actual val API_BASE_URL: String = SharedConfig.API_BASE_URL -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/me/moallemi/kmpshowcase/shared/presentation/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.presentation 2 | 3 | import kotlinx.coroutines.CompletableJob 4 | import kotlinx.coroutines.CoroutineExceptionHandler 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Job 7 | import me.moallemi.kmpshowcase.shared.utils.log 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | actual abstract class BaseViewModel actual constructor( 11 | viewModelContext: CoroutineContext, 12 | ) : CoroutineScope { 13 | 14 | private val context: CoroutineContext = viewModelContext 15 | 16 | private var _job: CompletableJob? = null 17 | private val job: CompletableJob 18 | get() { 19 | if (_job == null) { 20 | _job = Job() 21 | } 22 | return _job ?: Job() 23 | } 24 | 25 | private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> 26 | log(throwable.message) 27 | } 28 | 29 | actual override val coroutineContext: CoroutineContext 30 | get() = context + job + exceptionHandler 31 | } -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/me/moallemi/kmpshowcase/shared/utils/ApplicationDispatcher.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.utils 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Runnable 5 | import platform.darwin.dispatch_async 6 | import platform.darwin.dispatch_get_main_queue 7 | import platform.darwin.dispatch_queue_t 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | internal actual val applicationDispatcher: CoroutineContext = 11 | NSQueueDispatcher(dispatch_get_main_queue()) 12 | 13 | internal class NSQueueDispatcher(private val dispatchQueue: dispatch_queue_t) : 14 | CoroutineDispatcher() { 15 | 16 | override fun dispatch(context: CoroutineContext, block: Runnable) { 17 | dispatch_async(dispatchQueue) { 18 | block.run() 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/me/moallemi/kmpshowcase/shared/utils/logger.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.utils 2 | 3 | import platform.Foundation.NSLog 4 | 5 | actual fun log(message: String?, tag: String, level: LogLevel) { 6 | if (message == null) { 7 | return 8 | } 9 | NSLog("$tag: $message") 10 | } -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/me/moallemi/kmpshowcase/shared/Platform.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared 2 | 3 | actual class Platform actual constructor() { 4 | actual val platform: String = "Web" 5 | } -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/me/moallemi/kmpshowcase/shared/di/KoinJs.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.di 2 | 3 | import io.ktor.client.engine.js.Js 4 | import org.koin.core.module.Module 5 | import org.koin.dsl.module 6 | 7 | fun initKoinJs() = initKoin { 8 | modules( 9 | viewModelsModule 10 | ) 11 | } 12 | 13 | actual val platformModule: Module = module { 14 | single { 15 | Js.create() 16 | } 17 | } -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/me/moallemi/kmpshowcase/shared/network/api/KmpShowcaseApi.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.network.api 2 | 3 | import me.moallemi.kmpshowcase.shared.SharedConfig 4 | 5 | actual val API_BASE_URL: String = SharedConfig.API_BASE_URL -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/me/moallemi/kmpshowcase/shared/presentation/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.presentation 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlin.coroutines.CoroutineContext 5 | 6 | actual abstract class BaseViewModel actual constructor( 7 | viewModelContext: CoroutineContext 8 | ) : CoroutineScope { 9 | actual override val coroutineContext: CoroutineContext = viewModelContext 10 | } -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/me/moallemi/kmpshowcase/shared/utils/applicationDispatcher.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.utils 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlin.coroutines.CoroutineContext 5 | 6 | internal actual val applicationDispatcher: CoroutineContext = Dispatchers.Main -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/me/moallemi/kmpshowcase/shared/utils/logger.kt: -------------------------------------------------------------------------------- 1 | package me.moallemi.kmpshowcase.shared.utils 2 | 3 | import me.moallemi.kmpshowcase.shared.utils.LogLevel.DEBUG 4 | import me.moallemi.kmpshowcase.shared.utils.LogLevel.ERROR 5 | import me.moallemi.kmpshowcase.shared.utils.LogLevel.WARN 6 | 7 | actual fun log( 8 | message: String?, 9 | tag: String, 10 | level: LogLevel 11 | ) { 12 | when (level) { 13 | DEBUG -> console.log(message) 14 | WARN -> console.warn(message) 15 | ERROR -> console.error(message) 16 | }.exhaustive 17 | } -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=11 -------------------------------------------------------------------------------- /web/DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Deploy to heroku 2 | 3 | Create a new project on heroku 4 | 5 | ### Setup Environment Variables 6 | ``` 7 | KMP_SHOWCASE_API_BASE_URL_DEFAULT= 8 | KMP_SHOWCASE_DEPLOY_PROJECT=web 9 | ``` 10 | 11 | ### Add Buildpacks 12 | Add these buildpacks to your project: 13 | * https://github.com/heroku/heroku-buildpack-multi-procfile 14 | * heroku/gradle 15 | 16 | ### Configure Procfile 17 | You have to point to correct Procfile in repo which is `deploy/Procfile` 18 | 19 | ```bash 20 | heroku config:set PROCFILE=deploy/Procfile 21 | ``` 22 | 23 | ### Configure Build Task 24 | You have to change default task in order to ask heroku to build web project: 25 | 26 | ```bash 27 | heroku config:set GRADLE_TASK="deploy:stage" 28 | ``` 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("js") 3 | } 4 | 5 | dependencies { 6 | implementation(kotlin("stdlib-js")) 7 | implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.7.2") 8 | implementation(project(":shared")) 9 | } 10 | 11 | kotlin { 12 | js { 13 | useCommonJs() 14 | browser { 15 | runTask { 16 | outputFileName = "web.js" 17 | } 18 | } 19 | binaries.executable() 20 | } 21 | } -------------------------------------------------------------------------------- /web/src/main/kotlin/Application.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.GlobalScope 2 | import kotlinx.coroutines.flow.collect 3 | import kotlinx.coroutines.launch 4 | import kotlinx.html.TagConsumer 5 | import kotlinx.html.h5 6 | import kotlinx.html.js.a 7 | import kotlinx.html.js.div 8 | import kotlinx.html.js.img 9 | import kotlinx.html.js.p 10 | import me.moallemi.kmpshowcase.shared.domain.model.App 11 | import me.moallemi.kmpshowcase.shared.domain.model.Links 12 | import me.moallemi.kmpshowcase.shared.presentation.AppListViewModel 13 | import org.koin.core.KoinComponent 14 | import org.koin.core.inject 15 | import org.w3c.dom.HTMLElement 16 | 17 | class Application : KoinComponent { 18 | 19 | private val appListViewModel: AppListViewModel by inject() 20 | 21 | fun load(result: (List) -> Unit) { 22 | GlobalScope.launch { 23 | appListViewModel.apps.collect { 24 | result(it) 25 | } 26 | } 27 | appListViewModel.load() 28 | } 29 | } 30 | 31 | fun TagConsumer.createCard(app: App) { 32 | div(classes = "card kmp-card") { 33 | div(classes = "kmp-banner") { 34 | img(src = app.bannerUrl, classes = "card-img-top") 35 | } 36 | div(classes = "card-body") { 37 | h5(classes = "card-title") { +app.name } 38 | p(classes = "card-text") { +app.summary } 39 | div { 40 | createCardLinks(app.links) 41 | } 42 | } 43 | } 44 | } 45 | 46 | fun TagConsumer.createCardLinks(links: Links?) = links?.let { 47 | links.website?.let { link -> 48 | a(href = link, target = "_blank", classes = "btn btn-primary") { +"Website" } 49 | } 50 | links.googlePlay?.let { link -> 51 | a(href = link, target = "_blank", classes = "btn btn-primary") { +"Google Play" } 52 | } 53 | links.appStore?.let { link -> 54 | a(href = link, target = "_blank", classes = "btn btn-primary") { +"App Store" } 55 | } 56 | } -------------------------------------------------------------------------------- /web/src/main/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.browser.document 2 | import kotlinx.html.dom.append 3 | import me.moallemi.kmpshowcase.shared.di.initKoinJs 4 | 5 | fun main() { 6 | initKoinJs() 7 | 8 | val app = Application() 9 | app.load { apps -> 10 | if (apps.isNotEmpty()) { 11 | document.getElementById("loader")?.remove() 12 | } 13 | document.getElementById("root") 14 | ?.append { 15 | apps.onEach(::createCard) 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /web/src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Kotlin Multiplatform Showcase 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /web/src/main/resources/style.css: -------------------------------------------------------------------------------- 1 | .top-banner { 2 | width: 100%; 3 | height: 150px; 4 | padding-top: 20px; 5 | padding-bottom: 20px; 6 | background-color: #4c49eb; 7 | } 8 | 9 | .kmp-card { 10 | border-top-left-radius: 10px; 11 | border-top-right-radius: 10px; 12 | border-bottom-left-radius: 10px; 13 | border-bottom-right-radius: 10px; 14 | } 15 | .kmp-card img { 16 | padding: 20px; 17 | align-self: center; 18 | width: 80%; 19 | height: 100%; 20 | object-fit: scale-down; 21 | } 22 | 23 | .kmp-banner { 24 | background: #eee; 25 | height: 120px; 26 | display: flex; 27 | flex-direction: column; 28 | border-top-left-radius: 10px; 29 | border-top-right-radius: 10px; 30 | } 31 | 32 | #loader { 33 | position: absolute; 34 | left: 50%; 35 | top: 50%; 36 | z-index: 1; 37 | width: 150px; 38 | height: 150px; 39 | margin: -75px 0 0 -75px; 40 | border: 8px solid #f3f3f3; 41 | border-radius: 50%; 42 | border-top: 8px solid #3498db; 43 | width: 75px; 44 | height: 75px; 45 | -webkit-animation: spin 2s linear infinite; 46 | animation: spin 2s linear infinite; 47 | } 48 | 49 | @-webkit-keyframes spin { 50 | 0% { -webkit-transform: rotate(0deg); } 51 | 100% { -webkit-transform: rotate(360deg); } 52 | } 53 | 54 | @keyframes spin { 55 | 0% { transform: rotate(0deg); } 56 | 100% { transform: rotate(360deg); } 57 | } -------------------------------------------------------------------------------- /web/src/main/resources/top-banner.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webReact/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("js") 3 | } 4 | 5 | dependencies { 6 | implementation(kotlin("stdlib-js")) 7 | implementation(project(":shared")) 8 | 9 | //React, React DOM + Kotlin Wrappers 10 | implementation(Dependencies.KotlinWrappers.react) 11 | implementation(Dependencies.KotlinWrappers.reactDom) 12 | implementation(npm("react", "17.0.2")) 13 | implementation(npm("react-dom", "17.0.2")) 14 | 15 | //Styled + Kotlin Wrappers 16 | implementation(Dependencies.KotlinWrappers.styled) 17 | implementation(npm("styled-components", "~5.1.1")) 18 | implementation(npm("inline-style-prefixer", "~6.0.0")) 19 | 20 | } 21 | 22 | kotlin { 23 | js { 24 | useCommonJs() 25 | browser { 26 | runTask { 27 | outputFileName = "app.js" 28 | } 29 | } 30 | binaries.executable() 31 | } 32 | } -------------------------------------------------------------------------------- /webReact/src/main/kotlin/Application.kt: -------------------------------------------------------------------------------- 1 | import component.AppList 2 | import kotlinx.coroutines.MainScope 3 | import kotlinx.coroutines.flow.launchIn 4 | import kotlinx.coroutines.flow.onEach 5 | import me.moallemi.kmpshowcase.shared.presentation.AppListViewModel 6 | import org.koin.core.KoinComponent 7 | import org.koin.core.inject 8 | import react.RProps 9 | import react.child 10 | import react.functionalComponent 11 | import react.useEffect 12 | import react.useState 13 | 14 | class Application : KoinComponent { 15 | 16 | private val appListViewModel: AppListViewModel by inject() 17 | 18 | private val scope = MainScope() 19 | 20 | fun createRootComponent() = functionalComponent { _ -> 21 | 22 | val (apps, setApps) = useState(appListViewModel.apps.value) 23 | appListViewModel.load() 24 | useEffect(dependencies = listOf()) { 25 | appListViewModel.apps.onEach { setApps(it) }.launchIn(scope) 26 | } 27 | 28 | child(AppList) { attrs.apps = apps } 29 | 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /webReact/src/main/kotlin/Styles.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.css.Align 2 | import kotlinx.css.CSSBuilder 3 | import kotlinx.css.Color 4 | import kotlinx.css.Display 5 | import kotlinx.css.FlexDirection 6 | import kotlinx.css.FlexWrap 7 | import kotlinx.css.JustifyContent 8 | import kotlinx.css.ObjectFit 9 | import kotlinx.css.alignSelf 10 | import kotlinx.css.backgroundColor 11 | import kotlinx.css.body 12 | import kotlinx.css.borderRadius 13 | import kotlinx.css.borderTopLeftRadius 14 | import kotlinx.css.borderTopRightRadius 15 | import kotlinx.css.color 16 | import kotlinx.css.display 17 | import kotlinx.css.flexDirection 18 | import kotlinx.css.flexWrap 19 | import kotlinx.css.fontFamily 20 | import kotlinx.css.fontSize 21 | import kotlinx.css.height 22 | import kotlinx.css.justifyContent 23 | import kotlinx.css.lineHeight 24 | import kotlinx.css.margin 25 | import kotlinx.css.objectFit 26 | import kotlinx.css.padding 27 | import kotlinx.css.pct 28 | import kotlinx.css.properties.boxShadow 29 | import kotlinx.css.properties.lh 30 | import kotlinx.css.px 31 | import kotlinx.css.width 32 | 33 | object Styles { 34 | 35 | val global: CSSBuilder.() -> Unit = { 36 | body { 37 | margin(0.px) 38 | padding(0.px) 39 | } 40 | } 41 | 42 | val banner: CSSBuilder.() -> Unit = { 43 | width = 100.pct 44 | height = 120.px 45 | padding(vertical = 30.px) 46 | backgroundColor = Color("#4c49eb") 47 | } 48 | 49 | val appList: CSSBuilder.() -> Unit = { 50 | display = Display.flex 51 | flexWrap = FlexWrap.wrap 52 | justifyContent = JustifyContent.center 53 | } 54 | 55 | object Card { 56 | 57 | val root: CSSBuilder.() -> Unit = { 58 | width = 200.px 59 | borderRadius = 5.px 60 | margin(vertical = 16.px, horizontal = 16.px) 61 | boxShadow( 62 | Color("#0000000d"), 63 | 0.px, 64 | 4.px, 65 | 8.px, 66 | 0.px, 67 | ) 68 | } 69 | 70 | val header: CSSBuilder.() -> Unit = { 71 | display = Display.flex 72 | flexDirection = FlexDirection.column 73 | width = 100.pct 74 | height = 100.px 75 | borderTopLeftRadius = 5.px 76 | borderTopRightRadius = 5.px 77 | backgroundColor = Color("#eaeaea") 78 | } 79 | 80 | val headerImage: CSSBuilder.() -> Unit = { 81 | alignSelf = Align.center 82 | width = 80.pct 83 | height = 100.pct 84 | objectFit = ObjectFit.scaleDown 85 | } 86 | 87 | val content: CSSBuilder.() -> Unit = { 88 | padding(vertical = 2.px, horizontal = 16.px) 89 | } 90 | 91 | val contentTitle: CSSBuilder.() -> Unit = { 92 | fontFamily = "sans-serif" 93 | fontSize = 16.px 94 | color = Color("#000000") 95 | padding(vertical = 10.px) 96 | } 97 | 98 | val contentDescription: CSSBuilder.() -> Unit = { 99 | fontFamily = "sans-serif" 100 | fontSize = 14.px 101 | color = Color("#888") 102 | lineHeight = 18.px.lh 103 | } 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /webReact/src/main/kotlin/component/AppList.kt: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import me.moallemi.kmpshowcase.shared.domain.model.App 4 | import react.RProps 5 | import react.child 6 | import react.functionalComponent 7 | import styled.css 8 | import styled.styledDiv 9 | 10 | external interface AppListProps : RProps { 11 | var apps: List 12 | } 13 | 14 | val AppList = functionalComponent { props -> 15 | styledDiv { 16 | css(Styles.appList) 17 | child(Banner) 18 | props.apps.forEach { item -> 19 | child(card) { 20 | attrs.name = item.name 21 | attrs.summary = item.summary 22 | attrs.bannerUrl = item.bannerUrl 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /webReact/src/main/kotlin/component/Card.kt: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import react.RProps 4 | import react.functionalComponent 5 | import styled.css 6 | import styled.styledDiv 7 | import styled.styledImg 8 | 9 | external interface CardProps : RProps { 10 | var name: String 11 | var summary: String 12 | var bannerUrl: String? 13 | } 14 | 15 | val card = functionalComponent { props -> 16 | styledDiv { 17 | css(Styles.Card.root) 18 | styledDiv { 19 | css(Styles.Card.header) 20 | styledImg(src = props.bannerUrl, alt = "HeaderImage") { 21 | css(Styles.Card.headerImage) 22 | } 23 | } 24 | styledDiv { 25 | css(Styles.Card.content) 26 | styledDiv { 27 | css(Styles.Card.contentTitle) 28 | +props.name 29 | } 30 | styledDiv { 31 | css(Styles.Card.contentDescription) 32 | +props.summary 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /webReact/src/main/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.browser.document 2 | import me.moallemi.kmpshowcase.shared.di.initKoinJs 3 | import react.dom.render 4 | import react.child 5 | import styled.injectGlobal 6 | 7 | fun main() { 8 | initKoinJs() 9 | injectGlobal(Styles.global) 10 | val app = Application() 11 | render(document.getElementById("root")) { 12 | child(app.createRootComponent()) 13 | } 14 | } -------------------------------------------------------------------------------- /webReact/src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kotlin Multiplatform Showcase 6 | 7 | 8 |
9 | 10 | 11 | --------------------------------------------------------------------------------