KMMT : Kotlin Multiplatform Mobile Template

Overview

KMMT : Kotlin Multiplatform Mobile Template

Kotlin Multiplatform Mobile Development Simplified

Kotlin License: MIT Platform

KMMT is a KMM based project template designed to simplify the KMM development. It uses a simplified approach that can be shared both in android and iOS easily.

Primary objective of this project is to help KMM Developers & promote KMM technology

image

Credits : KaMP Kit

Android.mp4
iOS.mp4
IDE Requirements

IntelliJ/Android Studio - Android & Shared Module

Xcode - iOS Project

Features

1. Simple Networking API ( Ktor )

Create API Services using BaseAPI class. All network responses are wrapped in Either data type

, NetworkFailure> { return doGet { apiPath("comments?postId=$postId") } } suspend fun setPost(post: PostModel): Either { return doPost(post) { apiPath("comments") } } } ">
class JsonPlaceHolderServiceAPI : BaseAPI() {

    override val baseUrl: String
        get() = "https://jsonplaceholder.typicode.com/"

    suspend fun getPosts(postId: Int): Either<List<PostModel>, NetworkFailure> {
        return doGet {
            apiPath("comments?postId=$postId")
        }
    }

    suspend fun setPost(post: PostModel): Either<PostModel, NetworkFailure> {
        return doPost(post) {
            apiPath("comments")
        }
    }
}
, NetworkFailure> { return doGet { apiPath("api/breeds/list/all") }.flatMap { breedResult -> //Converting BreedResult to List Either.Success( breedResult.message.keys .sorted().toList() .map { TBreed(0L, name = it.toWordCaps(), false) } ) } } } ">
class BreedServiceAPI : BaseAPI() {
    override val baseUrl: String
        get() = "https://dog.ceo/"

    suspend fun getBreeds(): Either<List<TBreed>, NetworkFailure> {
        return doGet<BreedResult> {
            apiPath("api/breeds/list/all")
        }.flatMap { breedResult ->
            //Converting BreedResult to List
            Either.Success(
                breedResult.message.keys
                    .sorted().toList()
                    .map { TBreed(0L, name = it.toWordCaps(), false) }
            )
        }
    }
}

2. Async Task Helper ( Kotlinx.Coroutines )

Run code (Networking calls, Heavy calculations, Large dataSets from local DB, etc..) in Background thread and get the result in UI thread.

runOnBackground {
    //Code to execute in background
}

Return value from background

runOnBackgroundWithResult {
    //Code to execute in background with return
}.resultOnUI { result ->

}

or

runOnBackgroundWithResult {
    //Code to execute in background with return
}.resultOnBackground { result ->

}
class PostViewModel(view: LoginView) : BaseViewModel(view) {

    fun getPostsFromAPI() {

        runOnBackgroundWithResult {
            JsonPlaceHolderServiceAPI().getPosts(1)    //getPost returns data so return statement is not needed
            //or 
            // return@runOnBackgroundWithResult JsonPlaceHolderServiceAPI().getPosts(1)
        }.resultOnUI {
            getView()?.showPopUpMessage(
                "First Post Details",
                "Username : ${it.first().name}\n email : ${it.first().email}"
            )
        }
    }

    fun savePost() {

        val post = PostModel("Post Body", "[email protected]", 100, "Jitty", 6)

        runOnBackgroundWithResult {
            JsonPlaceHolderServiceAPI().setPost(post)
        }.resultOnUI {
            getView()?.showPopUpMessage("Saved Post Details", "Name : ${it.name}\n email : ${it.email}")
        }
    }
}

3. Multiplatform Bundle : Object Passing B/W Activities or ViewControllers

View Model can pass objects & values from Activity to Activity (Android) or ViewController to ViewController (iOS)

Send Values From 1st View Model
   // 1st View Model 

var userModel = UserModel("[email protected]", "Jitty", "Andiyan")

var bundle = Bundle {
    putStringExtra(HomeViewModel.USER_NAME, username.toString())
    putSerializableExtra(HomeViewModel.USER_OBJECT, userModel, UserModel.serializer())
}

getView()?.navigateToHomePage(bundle)


// 1st View 

fun navigateToHomePage(bundle: BundleX)


// 1st Activity : Android

override fun navigateToHomePage(bundle: BundleX) {
    openActivity(HomeActivity::class.java, bundle)
    finish()
}

// 1st ViewContoller : iOS

func navigateToHomePage (bundle: BundleX) {
    openViewController(newViewControllerName: "HomeViewController", bundle: bundle)
}
Retrieve Values From 2nd View Model
(USER_NAME)?.let { username -> } getBundleValue(USER_OBJECT)?.let { userModel -> } } } ">
   // 2nd View Model 

class HomeViewModel(view: HomeView) : BaseViewModel(view) {

    companion object BundleKeys {
        const val USER_NAME = "USERNAME"
        const val USER_OBJECT = "USEROBJ"
    }

    override fun onStartViewModel() {

        getBundleValue<String>(USER_NAME)?.let { username ->

        }
        getBundleValue<UserModel>(USER_OBJECT)?.let { userModel ->

        }
    }
}

4. Platform Blocks

Execute anything specific to a particular platform using Platform Blocks

runOnAndroid {

}

runOniOS {

}

5. Object Serialization Helper ( Kotlinx.Serialization )

Use toJsonString and toObject functions for instant serialization.

Objects to String Serialization

        var userModel = UserModel("[email protected]", "Jitty", "Andiyan")
        
        var jsonString = userModel.toJsonString(UserModel.serializer())

String to Object Serialization

        var userModel = jsonString.toObject()
        
        or
        
        var userModel:UserModel = jsonString.toObject()
        
        or
        
        var userModel = jsonString.toObject(UserModel.serializer())

6. Key Value Store ( Multiplatform Settings )

Use storeValue and getStoreValue functions for storing and retrieving Key-Value respectively

Storing Key-Value pair

        var userModel = UserModel("[email protected]", "Jitty", "Andiyan")
        
        storeValue { 
            putString("Key1","Value")
            putBoolean("Key2",false)
            putSerializable("Key3",userModel,UserModel.serializer())
        }

Retrieve Value using Key

("Key2") var userModel = getStoreValue("Key3",UserModel.serializer()) ">
        var stringValue = getStoreValue("Key1")
        
        or
        
        var stringValue:String? = getStoreValue("Key1")
        
        var boolValue = getStoreValue("Key2")
        
        var userModel = getStoreValue("Key3",UserModel.serializer())

7. LiveData & LiveDataObservable ( LiveData )

LiveData follows the observer pattern. LiveData notifies Observer objects when underlying data changes. You can consolidate your code to update the UI in these Observer objects. That way, you don't need to update the UI every time the app data changes because the observer does it for you.

        //Sources
        var premiumManager = PremiumManager()
        var premiumManagerBoolean = PremiumManagerBoolean()

        //Create Observer & Observe
        var subscriptionLiveDataObservable = observe {
           getView()?.setSubscriptionLabel(it)
        }
        
        //Adding Sources
        subscriptionLiveDataObservable.addSource(premiumManager.premium())

        or
        
        //Adding Sources with converter (Boolean to String)
        subscriptionLiveDataObservable.addSource(premiumManagerBoolean.isPremium()){
            if (it)
            {
                return@addSource "Premium"
            }else{
                return@addSource "Free"
            }

        }

        //Update source states
        premiumManager.becomePremium()

        premiumManagerBoolean.becomeFree()

        premiumManager.becomeFree()

        premiumManagerBoolean.becomePremium()

() fun isPremium(): LiveDataX { return premium } fun becomePremium() { premium.value = true } fun becomeFree() { premium.value = false } } ">
class PremiumManager {
    private val premium = MutableLiveDataX()
    fun premium(): LiveDataX {
        return premium
    }

    fun becomePremium() {
        premium.value = "premium"
    }

    fun becomeFree() {
        premium.value = "free"
    }

}

class PremiumManagerBoolean {
    private val premium = MutableLiveDataX()
    fun isPremium(): LiveDataX {
        return premium
    }

    fun becomePremium() {
        premium.value = true
    }

    fun becomeFree() {
        premium.value = false
    }

}

8. Observe with DBHelper ( Local Database : SQLite - SQLDelight )

Use 'asFlow()' extension from DBHelper class to observe a query data

class BreedTableHelper : DBHelper() {

    fun getAllBreeds(): Flow<List<TBreed>> =
        localDB.tBreedQueries
            .selectAll()
            .asFlow()
            .mapToList()
            .flowOn(Dispatchers_Default)


    suspend fun insertBreeds(breeds: List<TBreed>) {
        ...
    }

    fun selectById(id: Long): Flow<List<TBreed>> =
        localDB.tBreedQueries
            .selectById(id)
            .asFlow()
            .mapToList()
            .flowOn(Dispatchers_Default)

    suspend fun deleteAll() {
        ...
    }

    suspend fun updateFavorite(breedId: Long, favorite: Boolean) {
        localDB.transactionWithContext(Dispatchers_Default) {
            localDB.tBreedQueries.updateFavorite(favorite, breedId)
        }
    }

}
class BreedViewModel(view: BreedView) : BaseViewModel(view) {

    private lateinit var breedTableHelper: BreedTableHelper
    private lateinit var breedLiveDataObservable: LiveDataObservable<Either<List<TBreed>, Failure>>
    private lateinit var breedListCache: BreedListCache

    override fun onStartViewModel() {

        breedTableHelper = BreedTableHelper()
        breedListCache = BreedListCache(getBackgroundCoroutineScope())

        breedLiveDataObservable = observe { breedList ->
            breedList.either({
                getView()?.showPopUpMessage(it.message)
                getView()?.stopRefreshing()
            }, {
                getView()?.refreshBreedList(it)
                getView()?.stopRefreshing()
            })

        }

        refreshBreedListCache(forceRefresh = false)

        observeBreedsTable()

    }

    private fun observeBreedsTable() {
        //get Data from db with observe (Flow)
        runOnBackground {
            //Each refreshBreedListCache will trigger collect 
            breedTableHelper.getAllBreeds().collect {
                breedLiveDataObservable.setValue(Either.Success(it))
            }
        }
    }

    private fun refreshBreedListCache(forceRefresh: Boolean) {
        breedListCache.cacheData(Unit, forceRefresh)
        { cachedResult ->
            cachedResult.either({
                breedLiveDataObservable.setValue(Either.Failure(it))
            }, {
                println("Cache Table updated : $it")
            })
        }
    }
}

9. Useful Functional Programming

use Either data type to represent a value of one of two possible types (a disjoint union). Instances of Either are either an instance of Failure or Success

 Either<SuccessType, FailureType>

convert or map SuccessType using flatMap or map

var result = doGet<List<UserModel>> {
    apiPath("jittya/jsonserver/users?username=${credentails.username}&password=${credentails.password}")
}

return result.flatMap {
    // convert List to Boolean
    Either.Success(it.any { it.username == credentails.username && it.password == credentails.password })
}

use either blocks( either or eitherAsync [for suspended method support] ) to define failure & success functionalities

 authenticatedResult.either({
    //Failure
    getView()?.showPopUpMessage(it.message)

}, { isAuthenticated ->
    //Success
    if (isAuthenticated) {

        var userModel = UserModel("[email protected]", "Jitty", "Andiyan")

        var bundle = Bundle {
            putStringExtra(HomeViewModel.USER_NAME, username.toString())
            putSerializableExtra(HomeViewModel.USER_OBJECT, userModel, UserModel.serializer())
        }

        getView()?.navigateToHomePage(bundle)
    } else {
        getView()?.showPopUpMessage("Login Failed")
    }
})

10. Data Cache Helper

Use BaseDataCache to implement data caching (remote to local). call cacheData function to get and save data

, Failure> { //get data from remote (using api) return BreedServiceAPI().getBreeds() } override suspend fun saveData(data: List): Either { //save remote data in Local database return try { BreedTableHelper().insertBreeds(data) Either.Success(true) } catch (e: Exception) { Either.Failure(DataBaseFailure(e)) } } } ">
class BreedListCache(backgroundCoroutineScope: CoroutineScope) :
    BaseDataCache<Unit, List<TBreed>>(backgroundCoroutineScope, "BREED_SYNC_TIME") {
    
    override suspend fun getData(param: Unit): Either<List<TBreed>, Failure> {
        //get data from remote (using api)
        return BreedServiceAPI().getBreeds()
    }

    override suspend fun saveData(data: List<TBreed>): Either<Boolean, Failure> {
        //save remote data in Local database
        return try {
            BreedTableHelper().insertBreeds(data)
            Either.Success(true)
        } catch (e: Exception) {
            Either.Failure(DataBaseFailure(e))
        }
    }
}
println("Cache updated : $success") }) } } ">
var breedListCache = BreedListCache(getBackgroundCoroutineScope())

private fun refreshBreedListCache(forceRefresh: Boolean) {
    
//    breedListCache.cacheData(Unit, forceRefresh)
//                or
    breedListCache.cacheData(Unit, forceRefresh)
    { cachedResult ->
        cachedResult.either({ failure ->
            println("Cache failed : $failure")
        }, { success ->
            println("Cache updated : $success")
        })
    }
}

How to use

Shared Module (Business Logics & UI Binding Methods) :

Step 1 : Define View
  • Create a View interface by extending from BaseView.
  • Define UI binding functions in View interface.
interface LoginView : BaseView {

    fun setLoginPageLabel(msg: String)
    fun setUsernameLabel(usernameLabel: String)
    fun setPasswordLabel(passwordLabel: String)
    fun setLoginButtonLabel(loginLabel: String)

    fun getEnteredUsername(): String
    fun getEnteredPassword(): String

    fun setLoginButtonClickAction(onLoginClick: KFunction0<Unit>)

    fun navigateToHomePage(bundle: BundleX)
}
Step 2 : Define ViewModel
  • Create a ViewModel class by extending from BaseViewModel with View as Type.
  • Define your business logic in ViewModel class.
getView()?.dismissLoading() authenticatedResult.either({ getView()?.showPopUpMessage(it.message) }, { isAuthenticated -> if (isAuthenticated) { var bundle = Bundle { putStringExtra(HomeViewModel.USER_NAME, username.toString()) } getView()?.navigateToHomePage(bundle) } else { getView()?.showPopUpMessage( "Login Failed" ) } }) } } else { getView()?.showPopUpMessage("Validation Failed", "Username or Password is empty") } } } ">
class LoginViewModel(view: LoginView) : BaseViewModel(view) {
    override fun onStartViewModel() {
        getView()?.setLoginPageLabel("Login : ${Platform().platform}")
        getView()?.setUsernameLabel("Enter Username")
        getView()?.setPasswordLabel("Enter Password")
        getView()?.setLoginButtonLabel("Login")
        getView()?.setLoginButtonClickAction(this::onLoginButtonClick)
    }

    fun onLoginButtonClick() {
        getView()?.showLoading("authenticating...")
        val username = getView()?.getEnteredUsername()
        val password = getView()?.getEnteredPassword()
        checkValidation(username, password)
    }

    fun checkValidation(username: String?, password: String?) {
        if (username.isNullOrBlank().not() && password.isNullOrBlank().not()) {
            val credentials = CredentialsModel(username.toString(), password.toString())

            runOnBackgroundWithResult {
                JsonPlaceHolderServiceAPI().authenticate(credentials)
            }.resultOnUI { authenticatedResult ->
                getView()?.dismissLoading()
                authenticatedResult.either({
                    getView()?.showPopUpMessage(it.message)
                }, { isAuthenticated ->
                    if (isAuthenticated) {
                        var bundle = Bundle {
                            putStringExtra(HomeViewModel.USER_NAME, username.toString())
                        }
                        getView()?.navigateToHomePage(bundle)
                    } else {
                        getView()?.showPopUpMessage(
                            "Login Failed"
                        )
                    }
                })

            }
        } else {
            getView()?.showPopUpMessage("Validation Failed", "Username or Password is empty")
        }
    }
}

Android Module UI Binding :

Step 3 : Define Android View
  • Create new activity by extending from KMMActivity with ViewModel as Type.
  • Implement created View interface in activity.
  • Implement all necessary methods from View & KMMActivity.

Implement LoginView & Bind UI Controls

class LoginActivity : KMMActivity<LoginViewModel, ActivityMainBinding>(), LoginView {

    //Generated Methods from KMMActivity based on LoginViewModel
    override fun initializeViewModel(): LoginViewModel {
        return LoginViewModel(this)
    }

    override fun viewBindingInflate(): ActivityMainBinding {
        return ActivityMainBinding.inflate(layoutInflater)
    }

    //Generated Methods from LoginView
    override fun setLoginPageLabel(msg: String) {
        binding.textView.text = msg
    }

    override fun setUsernameLabel(usernameLabel: String) {
        binding.usernameET.hint = usernameLabel
    }

    override fun setPasswordLabel(passwordLabel: String) {
        binding.passwordET.hint = passwordLabel
    }

    override fun getEnteredUsername(): String {
        return binding.usernameET.text.toString()
    }

    override fun getEnteredPassword(): String {
        return binding.passwordET.text.toString()
    }

    override fun setLoginButtonClickAction(onLoginClick: KFunction0<Unit>) {
        binding.loginBtn.setClickAction(onLoginClick)
    }

    override fun setLoginButtonLabel(loginLabel: String) {
        binding.loginBtn.text = loginLabel
    }

    override fun navigateToHomePage(bundle: BundleX) {
        openActivity(HomeActivity::class.java, bundle)
        finish()
    }
}

iOS Module UI Binding (Xcode) :

Step 4 : Define iOS View
  • Create new viewcontroller by extending from KMMUIViewController.
  • Implement created View interface in viewcontroller.
  • Implement all necessary methods from View & KMMUIViewController.

Implement LoginView & Bind UI Controls

String { return passwordTF.text ?? "" } func setLoginButtonClickAction(onLoginClick: @escaping() -> KotlinUnit) { loginBtn.setClickAction(action: onLoginClick) } func setLoginButtonLabel(loginLabel: String) { loginBtn.setTitle(loginLabel, for: UIControl.State.normal) } //Generated Methods from KMMUIViewController override func initializeViewModel() -> BaseViewModel { return LoginViewModel(view: self).getViewModel() } func navigateToHomePage(bundle: BundleX) { openViewController(newViewControllerName: "HomeViewController", bundle: bundle) } } ">
class LoginViewController : KMMUIViewController, LoginView {

    @IBOutlet weak
    var usernameTF: UITextFieldX!
    @IBOutlet weak
    var passwordTF: UITextFieldX!
    @IBOutlet weak
    var textlabel: UILabel!
    @IBOutlet weak
    var loginBtn: UIButton!

    override func viewDidLoad()
    {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    //Generated Methods from LoginView
    func setLoginPageLabel(msg: String)
    {
        textlabel.text = msg
    }

    func setUsernameLabel(usernameLabel: String)
    {
        usernameTF.placeholder = usernameLabel
    }

    func setPasswordLabel(passwordLabel: String)
    {
        passwordTF.placeholder = passwordLabel
    }

    func getEnteredUsername() -> String
    {
        usernameTF.errorMessage = ""
        return usernameTF.text ?? ""
    }

    func getEnteredPassword() -> String
    {
        return passwordTF.text ?? ""
    }

    func setLoginButtonClickAction(onLoginClick: @escaping() -> KotlinUnit)
    {
        loginBtn.setClickAction(action: onLoginClick)
    }

    func setLoginButtonLabel(loginLabel: String)
    {
        loginBtn.setTitle(loginLabel, for: UIControl.State.normal)
    }

    //Generated Methods from KMMUIViewController
    override func initializeViewModel() -> BaseViewModel<BaseView>
    {
        return LoginViewModel(view: self).getViewModel()
    }

    func navigateToHomePage(bundle: BundleX)
    {
        openViewController(newViewControllerName: "HomeViewController", bundle: bundle)
    }
}
Subscribe for upcoming details and features...

Views

You might also like...
My personal template for a Spring Boot REST reactive webapp

My personal spring boot kotlin reactive template Features Spring Security implementation with JWT access and refresh token MongoDB database Project Co

Here is a ready to use JAICF bot template that utilises

JAICF Spring Bot template Here is a ready to use JAICF bot template that utilises Spring MongoDB Docker Prometheus Grafana Graylog How to use Please r

Used to generate the template code of GetX framework
Used to generate the template code of GetX framework

Language: English | 中文简体 statement some fast code snippet prompt come from getx-snippets-intelliJ Description install Plugin effect Take a look at the

A basic template ecommerce application with payment integration made using Android Architechture componets
A basic template ecommerce application with payment integration made using Android Architechture componets

ShopIt ShopIt is a basic template ecommerce application with payment integration(RazorPay), made using Android Architechture componets and Material Co

Team management service is a production ready and fully tested service that can be used as a template for a microservices development.
Team management service is a production ready and fully tested service that can be used as a template for a microservices development.

team-mgmt-service Description Team management service is a production ready and fully tested service that can be used as a template for a microservice

Template for a modern spring web service.

Spring Service Scaffold A scaffold for a web service operating with a Spring Framework backend, reactjs as frontend and a continuous testing and build

Template to accelerate the creation of new apps using Spring Boot 3, MongoDB & GraphQL.

Template to accelerate the creation of new apps using Spring Boot 3, MongoDB & GraphQL.

This is a GitHub template repository intended to kickstart development on an Android application.

Android App Template This is a GitHub template repository intended to kickstart development on an Android application. This project comes set with a h

A Hello World and Template for the KorGe game engine

Korge Hello World and Template This is a Hello World and Template for the KorGe game engine. Using gradle with kotlin-dsl. You can open this project i

Owner
Jitty Andiyan
Kotlin Multiplatform Mobile App Developer. Looking for challenging opportunities.
Jitty Andiyan
HelloKMM - Hello World in Kotlin Multiplatform Mobile (new empty project)

Hello KMM! Hello World in Kotlin Multiplatform Mobile (new empty project) Gettin

Blake Barrett 1 Feb 2, 2022
This is a template to help you get started building amazing Kotlin applications and libraries.

Welcome to the Starter This is a template to help you get started building amazing Kotlin applications and libraries. Over time, examples will be comp

Backbone 8 Nov 4, 2022
Template for building CLI tool in Kotlin and producing native binary

Kotlin command-line native tool template This template allows you to quickly build command-line tool using Kotlin , Clikt and build a native binary fo

Ryszard Grodzicki 10 Dec 31, 2022
Hexagonal Architecture Kotlin Template

The purpose of this template is to avoid repeating, over and over again, the same basic packages structure, gradle and configurations.

Fabri Di Napoli 10 Dec 14, 2022
For Kotlin with SpringBoot project that have multi-module-structure template

Goals kotlin + spring-boot + gradle + multi-module building Module-Structure ---root |--- src.main.kotlin.KotlinSpringbootMultiModuleTemplateAppl

pguma 1 Jul 24, 2022
An Android template you can use to build your project with gradle kotlin dsl

Android Gradle KTS An Android template you can use to build your project with gradle kotlin dsl Build.gradle.kts You can use your project's build.grad

Deep 17 Sep 12, 2022
Minecraft Forge Kotlin Template

Minecraft Forge Kotlin Template Minecraft 1.12.2 で Forge と Kotlin を用いた Mod のテンプレートです。 Minecraft 1.12.2 Mod template using Forge and Kotlin. Getting St

null 0 Nov 27, 2021
Kotlin async server template with coroutines and zero deps

kotlin-server At attempt to very light-weight non-blocking http app template with support for Kotlin coroutines. Zero dependencies - Java built-in jdk

Anton Keks 62 Dec 31, 2022
A Simple Task Template With Kotlin

Tarefas - Kotlin Download do aplicativo Tecnologia MVVM Retrofit SQLite Navigation Fingerprint - Autenticação biométrica Tela de Login Tela de cadastr

Erick Weigner Freitas Cunha 1 Mar 3, 2022
Kotlin multi platform project template and sample app with everything shared except the UI. Built with clean architecture + MVI

KMMNewsAPP There are two branches Main News App Main The main branch is a complete template that you can clone and use to build the awesome app that y

Kashif Mehmood 188 Dec 30, 2022