{"id":22509477,"url":"https://github.com/kernel0x/kmpapp","last_synced_at":"2025-08-03T13:31:24.584Z","repository":{"id":258721801,"uuid":"254937536","full_name":"kernel0x/kmpapp","owner":"kernel0x","description":"👨‍💻 Kotlin Mobile Multiplatform App (Android \u0026 iOS). One Code To Rule Them All. MVVM, DI (Kodein), coroutines, livedata, ktor, serialization, mockk, detekt, ktlint, jacoco","archived":false,"fork":false,"pushed_at":"2020-04-14T11:30:37.000Z","size":666,"stargazers_count":39,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-10-20T10:17:27.497Z","etag":null,"topics":["android","couroutine","ios","kodein","kotlin","kotlin-mpp","kotlin-multiplatform","ktlint","ktor","livedata","mock","mvvm-architecture","swift","unit-test"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kernel0x.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-11T19:13:14.000Z","updated_at":"2024-10-18T11:10:43.000Z","dependencies_parsed_at":"2024-10-20T10:17:47.200Z","dependency_job_id":"65bf66f4-2a96-4e4c-95ff-d20d9cb65c99","html_url":"https://github.com/kernel0x/kmpapp","commit_stats":null,"previous_names":["kernel0x/kmpapp"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kernel0x%2Fkmpapp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kernel0x%2Fkmpapp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kernel0x%2Fkmpapp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kernel0x%2Fkmpapp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kernel0x","download_url":"https://codeload.github.com/kernel0x/kmpapp/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228548406,"owners_count":17935204,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","couroutine","ios","kodein","kotlin","kotlin-mpp","kotlin-multiplatform","ktlint","ktor","livedata","mock","mvvm-architecture","swift","unit-test"],"created_at":"2024-12-07T01:29:13.101Z","updated_at":"2024-12-07T01:29:13.551Z","avatar_url":"https://github.com/kernel0x.png","language":"Kotlin","readme":"# Kotlin Mobile Multiplatform App (Android \u0026 iOS)\n\nOne Code To Rule Them All. Application example using Kotlin Multiplatform and MVVM pattern for both platforms. \n\n\u003cimg src=\"https://github.com/kernel0x/kmpapp/blob/master/images/architecture.svg\"\u003e\n\nIs used:\n- layered clean architecture\n- DI (Kodein)\n- coroutines\n- livedata\n- ktor\n- serialization\n- mockk\n- detekt, ktlint\n- unit tests and jacoco\n\n### Presentation layer (Android \u0026 iOS)\n\nOn both platforms (Android and iOS), we only need to implement an observer in which to process states. Further implementation on Android (Kotlin) and iOS (Swift):\n\n#### Android\n```kotlin\nclass MainActivity : AppCompatActivity() {\n\n  private lateinit var viewModel: IndexesViewModel\n  private var adapter: IndexesAdapter = IndexesAdapter { viewModel.getQuote(it.ticker) }\n\n  override fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    setContentView(R.layout.activity_main)\n\n    recycler.adapter = adapter\n\n    viewModel = ViewModelProviders.of(this).get(IndexesViewModel::class.java)\n    observeViewState()\n\n    viewModel.getMajorIndexes()\n  }\n\n  private fun observeViewState() {\n    viewModel.getViewData.addObserver { updateViewState(it) }\n  }\n\n  private fun updateViewState(state: IndexesViewState) = runOnUiThread {\n    when (state) {\n      is Loading -\u003e {\n        Toast.makeText(this, \"Loading...\", Toast.LENGTH_SHORT).show()\n      }\n      is Error -\u003e {\n        Toast.makeText(this, state.message, Toast.LENGTH_LONG).show()\n      }\n      is ShowMajorIndexes -\u003e {\n        adapter.items = state.indexes\n      }\n      is ShowQuote -\u003e {\n        Toast.makeText(this, state.quote.dayLow + \" - \" + state.quote.dayHigh, Toast.LENGTH_LONG).show()\n      }\n    }\n  }\n}\n```\n#### iOS\n```swift\nclass ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {\n    \n    @IBOutlet weak var tableView: UITableView!\n    \n    private var viewModel: IndexesViewModel!\n    \n    internal var indexes: [Index] = []\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        tableView.dataSource = self\n        tableView.delegate = self\n        \n        viewModel = IndexesViewModel()\n        observeViewState()\n        \n        viewModel.getMajorIndexes()\n    }\n    \n    func observeViewState() {\n        viewModel.getViewData.addObserver { (state) in\n            self.updateViewState(state: state as! IndexesViewState)\n        }\n    }\n    \n    func updateViewState(state: IndexesViewState) {\n        switch state {\n        case is Loading:\n            view.displayToast(\"Loading...\")\n        case is Error:\n            view.displayToast(\"Error\")\n        case is ShowMajorIndexes:\n            let successState = state as! ShowMajorIndexes\n            update(list: successState.indexes)\n        case is ShowQuote:\n            let successState = state as! ShowQuote\n            view.displayToast(successState.quote.dayLow + \" - \" + successState.quote.dayHigh)\n        default: break\n        }\n    }\n    \n    ...\n\n    deinit {\n        viewModel.onCleared()\n    }\n}\n```\n\n### Presentation Layer - ViewModels (Shared Code)\n\nThis layer is shared by Android and iOS, and this is developed on Kotlin. Here is where we have to call the different use-cases of the domain layer. To make the call async we are using kotlin coroutines and flow.\n\n```kotlin\nclass IndexesViewModel : BaseViewModel() {\n\n  private val getIndexesUseCase by Injector.instance\u003cGetIndexesUseCase\u003e()\n  private val getQuoteUseCase by Injector.instance\u003cGetQuoteUseCase\u003e()\n\n  var getViewData = MutableLiveData\u003cIndexesViewState\u003e(Empty)\n\n  fun getMajorIndexes() = launchInMain {\n    getIndexesUseCase()\n      .onStart { getViewData.postValue(Loading) }\n      .flowOnBackground()\n      .catch { getViewData.postValue(Error(\"Something went wrong\")) }\n      .collect { getViewData.postValue(ShowMajorIndexes(it)) }\n  }\n\n  fun getQuote(symbol: String) = launchInMain {\n    getQuoteUseCase.invoke(symbol)\n      .onStart { getViewData.postValue(Loading) }\n      .flowOnBackground()\n      .catch { getViewData.postValue(Error(\"Something went wrong\")) }\n      .collect { getViewData.postValue(ShowQuote(it)) }\n  }\n}\n```\n\n### Domain Layer — Models \u0026 UseCases (Shared Code)\n\nIn this layer, we defining the models and all the use cases that we need for our application.\n\n### Data Layer — Repository Pattern (Shared Code)\n\nFor this layer we are using a repository pattern. We defining the entity models and all source of our data\n\nFor networking we are using Ktor and for JSON deserialisation Kotlinx serialization.\n\n### Running unit tests\n\n- Android test: ./gradlew testDebugUnitTest\n- Common test on iOS (need run Simulator iPhone 8): ./gradlew iosUnitTest\n\n### Running the app\n\nTo run the application use the same tools you use in Android and iOS. Just open the project with Intellj/Android Studio for the Android project and XCode for the iOS one.\n\n## Screenshots\n\n|Android|iOS|\n|---|---|\n|![android-app](https://github.com/kernel0x/kmpapp/blob/master/images/Screenshot_android.png)|![ios-app](https://github.com/kernel0x/kmpapp/blob/master/images/Screenshot_ios.png)|\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkernel0x%2Fkmpapp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkernel0x%2Fkmpapp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkernel0x%2Fkmpapp/lists"}