No description
Find a file
2021-05-10 18:49:27 +02:00
.idea Initial commit 2021-05-10 18:49:27 +02:00
androidApp Initial commit 2021-05-10 18:49:27 +02:00
buildSrc Initial commit 2021-05-10 18:49:27 +02:00
gradle/wrapper Initial commit 2021-05-10 18:49:27 +02:00
iOS_App Initial commit 2021-05-10 18:49:27 +02:00
shared Initial commit 2021-05-10 18:49:27 +02:00
.gitignore Initial commit 2021-05-10 18:49:27 +02:00
build.gradle.kts Initial commit 2021-05-10 18:49:27 +02:00
gradle.properties Initial commit 2021-05-10 18:49:27 +02:00
gradlew Initial commit 2021-05-10 18:49:27 +02:00
gradlew.bat Initial commit 2021-05-10 18:49:27 +02:00
KMultiplatform.png Initial commit 2021-05-10 18:49:27 +02:00
LICENSE Initial commit 2021-05-10 18:49:27 +02:00
local.properties Initial commit 2021-05-10 18:49:27 +02:00
README.md Initial commit 2021-05-10 18:49:27 +02:00
settings.gradle.kts Initial commit 2021-05-10 18:49:27 +02:00

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

https://user-images.githubusercontent.com/9760688/116871405-d2815b80-ac31-11eb-89cd-cbc92dd42d8c.mp4

https://user-images.githubusercontent.com/9760688/116871609-24c27c80-ac32-11eb-93bb-387b4f5918c2.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

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")
        }
    }
}

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

runOnBackgroundAsFlow {
   //add a function that will return some result
}.resultOnUI { result ->
    
}

or

runOnBackgroundAsFlow {
   //add a function that will return some result
}.resultOnBackground { result ->

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

   fun getPostsFromAPI() {

      runOnBackgroundAsFlow {
         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", "jit@ccc.com", 100, "Jitty", 6)

      runOnBackgroundAsFlow {
         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("jittya@gmail.com", "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
   // 2nd View Model 
   
   class HomeViewModel(view: HomeView) : BaseViewModel<HomeView>(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("jittya@gmail.com", "Jitty", "Andiyan")
        
        var jsonString = userModel.toJsonString(UserModel.serializer())

String to Object Serialization

        var userModel = jsonString.toObject<UserModel>()
        
        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("jittya@gmail.com", "Jitty", "Andiyan")
        
        storeValue { 
            putString("Key1","Value")
            putBoolean("Key2",false)
            putSerializable("Key3",userModel,UserModel.serializer())
        }

Retrieve Value using Key

        var stringValue = getStoreValue<String>("Key1")
        
        or
        
        var stringValue:String? = getStoreValue("Key1")
        
        var boolValue = getStoreValue<Boolean>("Key2")
        
        var userModel = getStoreValue<UserModel>("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<String> {
           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()

class PremiumManager {
    private val premium = MutableLiveDataX<String>()
    fun premium(): LiveDataX<String> {
        return premium
    }

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

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

}

class PremiumManagerBoolean {
    private val premium = MutableLiveDataX<Boolean>()
    fun isPremium(): LiveDataX<Boolean> {
        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() {...}
   
}
class BreedViewModel(view: BreedView) : BaseViewModel<BreedView>(view) {

    lateinit var breedTableHelper: BreedTableHelper
    lateinit var breedLiveDataObservable: LiveDataObservable<List<TBreed>>
    val BREED_SYNC_TIME_KEY = "BREED_SYNC_TIME"

    override fun onStartViewModel() {
       
        breedTableHelper = BreedTableHelper()

        breedLiveDataObservable = observe { breedList ->
           //update UI on each value update from table
           getView()?.refreshBreedList(breedList)
        }

       observeBreedsTable()
       getBreedsFromAPIThenCache()
    }

   private fun getBreedsFromAPIThenCache() {
      if (isSyncExpired()) {
         //get Data from API and save to DB
         runOnBackground {
            BreedServiceAPI().getBreeds().let { breedResult ->
               breedResult.eitherAsync({
                  runOnUI {
                     getView()?.showPopUpMessage(it.message)
                     getView()?.stopRefreshing()
                  }
               }, { breedResult ->
                  storeValue { putLong(BREED_SYNC_TIME_KEY, DateTime.nowLocal().local.unixMillisLong) }
                  breedResult.message.keys
                     .sorted().toList()
                     .map { TBreed(0L, name = it.toWordCaps(), false) }
                     .let {
                        //This table insert will trigger data change and value will be available in observer
                        breedTableHelper.insertBreeds(it)
                     }
               })

            }
         }
      }
   }

   private fun isSyncExpired(): Boolean {
      ...
   }

   private fun observeBreedsTable() {
      //get Data from db with observe (Flow)
      runOnBackgroundBlock {
         breedTableHelper.getAllBreeds().collect {
            breedLiveDataObservable.setValue(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("jittya@gmail.com", "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")
   }
})

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.
class LoginViewModel(view: LoginView) :BaseViewModel<LoginView>(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())

         runOnBackgroundAsFlow {
            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

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