├── .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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
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 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
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 |
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 |
--------------------------------------------------------------------------------