Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gironnetd committed Jan 5, 2022
0 parents commit f390e0a
Show file tree
Hide file tree
Showing 1,619 changed files with 178,637 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
*/build
/captures
.externalNativeBuild
.cxx
local.properties
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Todo-Mvi-Kotlin-Multiplatform

### Summary

This project sets up the use of Kotlin Multiplatform using a SQLDelight database and the Kotlin Reaktive framework to create an Android and IOS application inspired by the sample from the Google Android Architecture Samples repository


![](art/ios-app.png) ![](art/android-app.png)


To start this project you need :

- Android Studio 4.2.2
- Gradle Distributions 7.0.1-bin
- Kotlin Gradle Plugin 1.5.21
- XCode 12.4


### Dependencies for iOS Application

* [Todo MVI RxSwift Swift](https://github.com/gironnetd/todo-mvi-rxswift-swift) by myself
* [RxSwift](https://github.com/ReactiveX/RxSwift)
* [Reaktive](https://github.com/badoo/Reaktive)
* [Material Component for iOS](https://github.com/material-components/material-components-ios)

### Dependencies for Android Application

* [Todo MVI RxKotlin Kotlin](https://github.com/oldergod/android-architecture) by oldergod
* [RxKotlin](https://github.com/ReactiveX/RxKotlin)
* [Reaktive](https://github.com/badoo/Reaktive)

### Dependencies for Android Application

* [SQLDelight](https://cashapp.github.io/sqldelight/)
* [Reaktive](https://github.com/badoo/Reaktive)
* [TouchLab Stately](https://github.com/touchlab/Stately)



41 changes: 41 additions & 0 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
}

dependencies {
implementation(project(":shared"))
implementation(Dependencies.AndroidX.constraintLayout)
implementation (Dependencies.AndroidX.appCompat)
implementation (Dependencies.AndroidX.cardView)
implementation (Dependencies.Com.Google.Android.material)
implementation (Dependencies.AndroidX.recyclerView)
implementation (Dependencies.AndroidX.legacy)
implementation(Dependencies.AndroidX.Test.Espresso.idlingResource)
implementation(Dependencies.Io.ReactiveX.RxJava2.rxJava)
implementation(Dependencies.Io.ReactiveX.RxJava2.rxAndroid)
implementation(Dependencies.Com.JakeWharton.RxBinding2.rxBinding)
implementation(Dependencies.AndroidX.LifeCycle.runtime)
implementation(Dependencies.AndroidX.LifeCycle.extensions)
implementation(Dependencies.Io.ReactiveX.RxJava2.rxKotlin)
implementation(Dependencies.Com.Badoo.Reaktive.rxjava2_interop)
implementation(Dependencies.Com.Badoo.Reaktive.reaktive)
kapt(Dependencies.AndroidX.LifeCycle.compiler)
}

android {
compileSdkVersion(31)
defaultConfig {
applicationId = "com.todo.mvi.kotlin.multiplatform.android"
minSdkVersion(21)
targetSdkVersion(31)
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
}
34 changes: 34 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http:https://schemas.android.com/apk/res/android"
xmlns:tools="http:https://schemas.android.com/tools"
package="com.todo.mvi.kotlin.multiplatform.android">

<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="com.todo.mvi.kotlin.multiplatform.android.tasks.TasksActivity"
android:exported="true"
android:theme="@style/AppTheme.OverlapSystemBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.todo.mvi.kotlin.multiplatform.android.taskdetail.TaskDetailActivity" />
<activity android:name="com.todo.mvi.kotlin.multiplatform.android.addedittask.AddEditTaskActivity" />
<activity
android:name="com.todo.mvi.kotlin.multiplatform.android.statistics.StatisticsActivity"
android:parentActivityName=".tasks.TasksActivity"
tools:ignore="UnusedAttribute">
<!-- Parent activity meta-data to support 4.0 and lower -->
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".tasks.TasksActivity" />
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.todo.mvi.kotlin.multiplatform.android

import android.content.Context
import com.todo.mvi.kotlin.multiplatform.android.util.schedulers.BaseSchedulerProvider
import com.todo.mvi.kotlin.multiplatform.android.util.schedulers.SchedulerProvider
import com.todo.mvi.kotlin.multiplatform.data.source.remote.TasksRemoteDataSource
import com.todo.mvi.kotlin.multiplatform.data.source.TasksDataSource
import com.todo.mvi.kotlin.multiplatform.data.source.TasksRepository
import com.todo.mvi.kotlin.multiplatform.data.source.local.DatabaseDriverFactory
import com.todo.mvi.kotlin.multiplatform.data.source.local.TasksLocalDataSource

/**
* Enables injection of mock implementations for
* [TasksDataSource] at compile time. This is useful for testing, since it allows us to use
* a fake instance of the class to isolate the dependencies and run a test hermetically.
*/
object Injection {

lateinit var tasksRepository: TasksRepository

fun provideTasksRepository(context: Context): TasksRepository {
if(!::tasksRepository.isInitialized) {
tasksRepository = TasksRepository(
TasksRemoteDataSource(),
TasksLocalDataSource(DatabaseDriverFactory(context))
)
}
return tasksRepository
}

fun provideSchedulerProvider(): BaseSchedulerProvider = SchedulerProvider
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.todo.mvi.kotlin.multiplatform.android.addedittask

import com.todo.mvi.kotlin.multiplatform.android.mvibase.MviAction

sealed class AddEditTaskAction : MviAction {
data class PopulateTaskAction(val taskId: String) : AddEditTaskAction()

data class CreateTaskAction(val title: String, val description: String) : AddEditTaskAction()

data class UpdateTaskAction(
val taskId: String,
val title: String,
val description: String
) : AddEditTaskAction()

object SkipMe : AddEditTaskAction()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.todo.mvi.kotlin.multiplatform.android.addedittask

import com.badoo.reaktive.rxjavainterop.asRxJava2Completable
import com.badoo.reaktive.rxjavainterop.asRxJava2Single
import com.todo.mvi.kotlin.multiplatform.android.addedittask.AddEditTaskAction.CreateTaskAction
import com.todo.mvi.kotlin.multiplatform.android.addedittask.AddEditTaskAction.PopulateTaskAction
import com.todo.mvi.kotlin.multiplatform.android.addedittask.AddEditTaskAction.UpdateTaskAction
import com.todo.mvi.kotlin.multiplatform.android.addedittask.AddEditTaskResult.CreateTaskResult
import com.todo.mvi.kotlin.multiplatform.android.addedittask.AddEditTaskResult.PopulateTaskResult
import com.todo.mvi.kotlin.multiplatform.android.addedittask.AddEditTaskResult.UpdateTaskResult
import com.todo.mvi.kotlin.multiplatform.data.source.TasksRepository
import com.todo.mvi.kotlin.multiplatform.android.mvibase.MviAction
import com.todo.mvi.kotlin.multiplatform.android.mvibase.MviResult
import com.todo.mvi.kotlin.multiplatform.android.mvibase.MviViewModel
import com.todo.mvi.kotlin.multiplatform.android.util.schedulers.BaseSchedulerProvider
import com.todo.mvi.kotlin.multiplatform.data.empty
import comtodomvikotlinmultiplatform.Task
import io.reactivex.Observable
import io.reactivex.ObservableTransformer
import java.util.*

/**
* Contains and executes the business logic for all emitted [MviAction]
* and returns one unique [Observable] of [MviResult].
*
*
* This could have been included inside the [MviViewModel]
* but was separated to ease maintenance, as the [MviViewModel] was getting too big.
*/
class AddEditTaskActionProcessorHolder(
private val tasksRepository: TasksRepository,
private val schedulerProvider: BaseSchedulerProvider
) {
private val populateTaskProcessor =
ObservableTransformer<PopulateTaskAction, PopulateTaskResult> { actions ->
actions.flatMap { action ->
tasksRepository.getTask(action.taskId).asRxJava2Single()
// Transform the Single to an Observable to allow emission of multiple
// events down the stream (e.g. the InFlight event)
.toObservable()
// Wrap returned data into an immutable object
.map(PopulateTaskResult::Success)
.cast(PopulateTaskResult::class.java)
// Wrap any error into an immutable object and pass it down the stream
// without crashing.
// Because errors are data and hence, should just be part of the stream.
.onErrorReturn(PopulateTaskResult::Failure)
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
// Emit an InFlight event to notify the subscribers (e.g. the UI) we are
// doing work and waiting on a response.
// We emit it after observing on the UI thread to allow the event to be emitted
// on the current frame and avoid jank.
.startWith(PopulateTaskResult.InFlight)
}
}

private val createTaskProcessor =
ObservableTransformer<CreateTaskAction, CreateTaskResult> { actions ->
actions
.map { action -> Task(id = UUID.randomUUID().toString(), title = action.title, description = action.description, completed = false) }
.publish { task ->
Observable.merge(
task.filter { it.empty }.map { CreateTaskResult.Empty },
task.filter { !it.empty }.flatMap {
tasksRepository.saveTask(it).asRxJava2Completable().andThen(Observable.just(CreateTaskResult.Success))
}
)
}
}

private val updateTaskProcessor =
ObservableTransformer<UpdateTaskAction, UpdateTaskResult> { actions ->
actions.flatMap { action ->
tasksRepository.saveTask(
Task(title = action.title, description = action.description, id = action.taskId, completed = false)
).asRxJava2Completable().andThen(Observable.just(UpdateTaskResult))
}
}

/**
* Splits the [Observable] to match each type of [MviAction] to
* its corresponding business logic processor. Each processor takes a defined [MviAction],
* returns a defined [MviResult]
* The global actionProcessor then merges all [Observable] back to
* one unique [Observable].
*
*
* The splitting is done using [Observable.publish] which allows almost anything
* on the passed [Observable] as long as one and only one [Observable] is returned.
*
*
* An security layer is also added for unhandled [MviAction] to allow early crash
* at runtime to easy the maintenance.
*/
internal var actionProcessor =
ObservableTransformer<AddEditTaskAction, AddEditTaskResult> { actions ->
actions.publish { shared ->
Observable.merge<AddEditTaskResult>(
// Match PopulateTasks to populateTaskProcessor
shared.ofType(AddEditTaskAction.PopulateTaskAction::class.java)
.compose(populateTaskProcessor),
// Match CreateTasks to createTaskProcessor
shared.ofType(AddEditTaskAction.CreateTaskAction::class.java)
.compose(createTaskProcessor),
// Match UpdateTasks to updateTaskProcessor
shared.ofType(AddEditTaskAction.UpdateTaskAction::class.java)
.compose(updateTaskProcessor))
.mergeWith(
// Error for not implemented actions
shared.filter { v ->
v !is AddEditTaskAction.PopulateTaskAction &&
v !is AddEditTaskAction.CreateTaskAction &&
v !is AddEditTaskAction.UpdateTaskAction
}
.flatMap { w ->
Observable.error<AddEditTaskResult>(
IllegalArgumentException("Unknown Action type: " + w))
})
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2016, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.todo.mvi.kotlin.multiplatform.android.addedittask

import android.os.Bundle
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import com.todo.mvi.kotlin.multiplatform.android.R
import com.todo.mvi.kotlin.multiplatform.android.util.addFragmentToActivity

/**
* Displays an add or edit task screen.
*/
class AddEditTaskActivity : AppCompatActivity() {

private lateinit var actionBar: ActionBar

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.addtask_act)

// Set up the toolbar.
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.run {
actionBar = this
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}

val taskId = intent.getStringExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID)
setToolbarTitle(taskId)

if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) {
val addEditTaskFragment = AddEditTaskFragment.invoke()

if (taskId != null) {
val args = Bundle()
args.putString(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId)
addEditTaskFragment.arguments = args
}

addFragmentToActivity(supportFragmentManager, addEditTaskFragment, R.id.contentFrame)
}
}

private fun setToolbarTitle(taskId: String?) {
actionBar.setTitle(if (taskId == null) R.string.add_task else R.string.edit_task)
}

override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}

companion object {
const val REQUEST_ADD_TASK = 1
}
}
Loading

0 comments on commit f390e0a

Please sign in to comment.