The goal of this Template is to be our starting point for new projects, following the best development practices. It's our interpretation and adaptation of years in mobile development that we have implemented in our internal codebases for all kind of Mobile Projects.
Module
shared
: data and domain layeriosApp
: ios presentation layerandroidApp
: android presentation layer
- Introduction
- Architecture
- Features
- Installation
- Libraries
- Domain to Presentation
- Expect and Actual
- Project Structure
- Build Config
This template includes:
-
Expect actual implementation
- Cryptography SHA 256
-
Networking :
- HTTP GET
- HTTP POST
- HTTP POST Multipart/Form-Data
- Authenticator
- HTTP Error Handler
-
Database
- Create
- Read
- Delete
-
Preference
- Read
- Update
- Follow the KMM Guide by Jetbrains for getting started building a project with KMM.
- Install Kotlin Multiplatform Mobile plugin in Android Studio
- Clone or Download the repo
- Rebuild Project
- To run in iOS, Open Xcode, select
.xcworkspace
, thenpod install
insideiosApp
folder to install shared module and ios dependencies
In Android, Because both shared
and androidApp
written in Kotlin, we can simply collect flow :
fun getRocketLaunches() = viewModelScope.launch {
_rocketLaunchResults.value = Resource.loading()
proceed(_rocketLaunchResults) {
rocketLaunchUseCase.getRocketLaunches()
}
}
But in iOS, we have to deal with swift, here i'm using createPublisher()
from KMPNativeCoroutines
to collect flow as Publisher in Combine
:
func getRocketLaunches() {
rocketLaunch = .loading
viewStatePublisher(
for: rocketLaunchUseCase.getRocketLaunchesNative(),
in: &cancellables
) { self.rocketLaunch = $0 }
}
both proceed()
and viewStatePublisher(for: , in:)
are the same logic under the hood, to handle
general error, reactively retrying the function, etc.
learn more: https://github.com/rickclephas/KMP-NativeCoroutines
in KMM, there is a negative case when there's no support to share code for some feature in both ios and android, and it's expensive to write separately in each module
so the solution is ✨expect
and actual
✨, we can write expect
inside commonMain
and write "
actual" implementation with actual
inside androidMain
and iosMain
and then each module will use expect
example:
expect fun getRequestHash(): String
actual fun getRequestHash(): String {
val key: String = "NBS KMM Sample"
val timestamp = (System.currentTimeMillis() / 1000).toString()
val algorithm: String = "HmacSHA256"
val charset: Charset = Charset.forName("UTF-8")
val sha256Hmac: Mac = Mac.getInstance(algorithm)
val secretKeySpec = SecretKeySpec(key.toByteArray(charset), algorithm)
sha256Hmac.init(secretKeySpec)
val hash: String = bytesToHex(sha256Hmac.doFinal(timestamp.toByteArray(charset))).orEmpty()
logging { "HASH ANDROID $hash" }
return hash
}
actual fun getRequestHash(): String {
val key = "NBS KMM Sample"
val timestamp = NSDate().timeIntervalSince1970.toLong().toString()
val hash = (timestamp as NSString).sha256Hmac(key = key)
logging { "HASH IOS $hash" }
return hash
}
@OptIn(ExperimentalUnsignedTypes::class)
fun NSString.sha256Hmac(algorithm: CCHmacAlgorithm = kCCHmacAlgSHA256, key: String): String {
val string = this.cStringUsingEncoding(encoding = NSUTF8StringEncoding)
val stringLength = this.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
val digestLength = CC_SHA256_DIGEST_LENGTH
var result = UByteArray(size = digestLength)
val keyString = (key as NSString).cStringUsingEncoding(encoding = NSUTF8StringEncoding)
val keyLength = key.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
CCHmac(algorithm, keyString, keyLength, string, stringLength, result.refTo(0))
return stringFromResult(result, digestLength)
}
yes, we can use Foundation
, CoreCrypto
, CoreFoundation
same as what we use in Xcode
shared
:
base
cache
data
sample
model
response
request
remote
SampleApi
SampleApiClient
di
ios
feature
domain
sample
model
mapper
SampleInteractor
SampleUseCase
utils
enum
eventbus
ext
androidApp
:
base
di
sample
theme
utils
iosApp
:
Dependency
App
Main
Resources
ReusableView
Extensions
Utils
Features
Sample
Navigator
Views
ViewModel
You can setup Build Config for multiple Environment, Just add your build configuration at
build.gradle on shared
in the buildkonfig
section like this:
buildkonfig {
packageName = "com.nbs.kmm.sample"
objectName = "NbsKmmSharedConfig"
exposeObjectWithName = "NbsKmmSharedPublicConfig"
// default config is required
defaultConfigs {
buildConfigField(STRING, "BASE_URL", "story-api.dicoding.dev")
buildConfigField(STRING, "BASE_URL_SPACEX", "api.spacexdata.com")
}
// config for staging
defaultConfigs("staging") {
buildConfigField(STRING, "BASE_URL", "story-api.dicoding.dev")
buildConfigField(STRING, "BASE_URL_SPACEX", "api.spacexdata.com")
}
// config for release
defaultConfigs("release") {
buildConfigField(STRING, "BASE_URL", "story-api.dicoding.dev")
buildConfigField(STRING, "BASE_URL_SPACEX", "api.spacexdata.com")
}
}
And for changing the Environment just set it on gradle.properties
with buildkonfig.flavor
and assign the value with the Environment name that you want to use, for
default config just let the buildkonfig.flavor
value to be empty