One Code To Rule Them All. Application example using Kotlin Multiplatform and MVVM pattern for both platforms.
Is used:
- layered clean architecture
- DI (Kodein)
- coroutines
- livedata
- ktor
- serialization
- mockk
- detekt, ktlint
- unit tests and jacoco
On 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):
classMainActivity : AppCompatActivity() {
privatelateinitvar viewModel:IndexesViewModelprivatevar adapter:IndexesAdapter=IndexesAdapter { viewModel.getQuote(it.ticker) }
overridefunonCreate(savedInstanceState:Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recycler.adapter = adapter
viewModel =ViewModelProviders.of(this).get(IndexesViewModel::class.java)
observeViewState()
viewModel.getMajorIndexes()
}
privatefunobserveViewState() {
viewModel.getViewData.addObserver { updateViewState(it) }
}
privatefunupdateViewState(state:IndexesViewState) = runOnUiThread {
when (state) {
isLoading-> {
Toast.makeText(this, "Loading...", Toast.LENGTH_SHORT).show()
}
isError-> {
Toast.makeText(this, state.message, Toast.LENGTH_LONG).show()
}
isShowMajorIndexes-> {
adapter.items = state.indexes
}
isShowQuote-> {
Toast.makeText(this, state.quote.dayLow +" - "+ state.quote.dayHigh, Toast.LENGTH_LONG).show()
}
}
}
}classViewController:UIViewController,UITableViewDataSource,UITableViewDelegate{@IBOutlet weak vartableView:UITableView!privatevarviewModel:IndexesViewModel!internalvarindexes:[Index]=[]overridefunc viewDidLoad(){
super.viewDidLoad()
tableView.dataSource =self
tableView.delegate =self
viewModel =IndexesViewModel()observeViewState()
viewModel.getMajorIndexes()}func observeViewState(){
viewModel.getViewData.addObserver{(state)inself.updateViewState(state: state as!IndexesViewState)}}func updateViewState(state:IndexesViewState){switch state {case is Loading:
view.displayToast("Loading...")case is Error:
view.displayToast("Error")case is ShowMajorIndexes:letsuccessState= state as!ShowMajorIndexesupdate(list: successState.indexes)case is ShowQuote:letsuccessState= state as!ShowQuote
view.displayToast(successState.quote.dayLow +" - "+ successState.quote.dayHigh)default:break}}...deinit{
viewModel.onCleared()}}This 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.
classIndexesViewModel : BaseViewModel() {
privateval getIndexesUseCase by Injector.instance<GetIndexesUseCase>()
privateval getQuoteUseCase by Injector.instance<GetQuoteUseCase>()
var getViewData =MutableLiveData<IndexesViewState>(Empty)
fungetMajorIndexes() = launchInMain {
getIndexesUseCase()
.onStart { getViewData.postValue(Loading) }
.flowOnBackground()
.catch { getViewData.postValue(Error("Something went wrong")) }
.collect { getViewData.postValue(ShowMajorIndexes(it)) }
}
fungetQuote(symbol:String) = launchInMain {
getQuoteUseCase.invoke(symbol)
.onStart { getViewData.postValue(Loading) }
.flowOnBackground()
.catch { getViewData.postValue(Error("Something went wrong")) }
.collect { getViewData.postValue(ShowQuote(it)) }
}
}In this layer, we defining the models and all the use cases that we need for our application.
For this layer we are using a repository pattern. We defining the entity models and all source of our data
For networking we are using Ktor and for JSON deserialisation Kotlinx serialization.
- Android test: ./gradlew testDebugUnitTest
- Common test on iOS (need run Simulator iPhone 8): ./gradlew iosUnitTest
To 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.
| Android | iOS |
|---|---|
![]() | ![]() |

