Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

technical: create sample application #14

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions build-logic/src/main/kotlin/android-app-convention.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import org.gradle.api.JavaVersion

plugins {
id("com.android.application")
id("android-base-convention")
id("org.jetbrains.kotlin.android")
}

android {
compileSdk = 33

defaultConfig {
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
multiDexEnabled = true
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.2"
}
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("org.jetbrains.kotlin.android")
id("android-base-convention")
}

Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ android.useAndroidX=true
kotlin.mpp.enableCInteropCommonization=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.native.binary.memoryModel=experimental
android.disableAutomaticComponentCreation=true
21 changes: 16 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
kotlinVersion = "1.8.10"
# kotlinx
coroutinesVersion = "1.6.4"

# multiplatform
kermitVersion = "1.2.2"

Expand All @@ -17,13 +16,25 @@ fragmentVersion = "1.5.4"
# kotlinx
mpp-kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" }
mpp-kermit = { module = "co.touchlab:kermit", version.ref = "kermitVersion" }
com-ionspin-kotlin-bignum = { module = "com.ionspin.kotlin:bignum", version = "0.3.7" }

#===========ANDROID===========#
#androidX
android-x-lifecycle-viewModel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleVersion" }
android-x-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleVersion" }
android-x-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleVersion" }
android-x-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentVersion" }
androidx-lifecycle-viewModel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleVersion" }
androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleVersion" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleVersion" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleVersion" }
androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentVersion" }

androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.9.0" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version = "1.6.1" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version = "2022.10.00" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }

#===========GRADLE PLUGINS ARTIFACTS===========#
plugin-kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinVersion" }
Expand Down
10 changes: 5 additions & 5 deletions mvi-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ dependencies {
commonMainImplementation(kermit)
}

with(libs.android) {
androidMainImplementation(x.lifecycle.livedata)
androidMainImplementation(x.lifecycle.viewModel)
androidMainImplementation(x.lifecycle.runtime)
androidMainImplementation(x.fragment)
with(libs.androidx) {
androidMainImplementation(lifecycle.livedata)
androidMainImplementation(lifecycle.viewModel)
androidMainImplementation(lifecycle.runtime)
androidMainImplementation(fragment)
}
}

Expand Down
1 change: 1 addition & 0 deletions mvi-sample/domain/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
9 changes: 9 additions & 0 deletions mvi-sample/domain/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
id("multiplatform-library-convention")
}

dependencies {
commonMainImplementation(libs.mpp.kotlinx.coroutines)
commonMainImplementation(libs.com.ionspin.kotlin.bignum)
commonMainImplementation(project(":mvi-core"))
}
2 changes: 2 additions & 0 deletions mvi-sample/domain/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="net.humans.kmm.mvi.sample.domain" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package net.humans.kmm.mvi.sample.domain

import com.ionspin.kotlin.bignum.decimal.BigDecimal
import net.humans.kmm.mvi.CoroutineEffectHandler
import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.Effect
import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.Message
import net.humans.kmm.mvi.sample.domain.model.usd
import net.humans.kmm.mvi.sample.domain.usecase.CalculateCashbackAndCommissionInput
import net.humans.kmm.mvi.sample.domain.usecase.CalculateCashbackAndCommissionResult
import net.humans.kmm.mvi.sample.domain.usecase.CalculateCashbackAndCommissionUseCase
import net.humans.kmm.mvi.sample.domain.usecase.GetBalanceUseCase
import net.humans.kmm.mvi.sample.domain.usecase.impl.RandomCalculateCashbackAndCommissionUseCase
import net.humans.kmm.mvi.sample.domain.usecase.impl.RandomGetBalanceUseCase

class CommissionCalculatorEffectHandler(
private val getBalanceUseCase: GetBalanceUseCase = RandomGetBalanceUseCase(),
private val calculateCashbackAndCommissionUseCase: CalculateCashbackAndCommissionUseCase =
RandomCalculateCashbackAndCommissionUseCase()
) : CoroutineEffectHandler<Effect, Message> {
override suspend fun handle(eff: Effect): Message = when (eff) {
is Effect.CalculateCommissionAndCashback -> calculateCommissionAndCashback(eff)
Effect.Initialize -> Message.UpdateState(
balance = getBalanceUseCase.execute().balance,
inputAmount = BigDecimal.ZERO.usd,
commission = BigDecimal.ZERO.usd,
cashback = BigDecimal.ZERO.usd,
)
}

private suspend fun calculateCommissionAndCashback(
eff: Effect.CalculateCommissionAndCashback
): Message {
val input = CalculateCashbackAndCommissionInput(
balance = eff.balance,
inputAmount = eff.inputAmount
)
return when (val result = calculateCashbackAndCommissionUseCase.execute(input = input)) {
CalculateCashbackAndCommissionResult.Failed.InsufficientBalance ->
Message.SetError(CommissionCalculatorRedux.State.Error.InsufficientBalance)

is CalculateCashbackAndCommissionResult.Success -> Message.UpdateCommissionAndCashback(
inputAmount = result.inputAmount,
commission = result.commission,
cashback = result.cashback,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package net.humans.kmm.mvi.sample.domain

import net.humans.kmm.mvi.ComplexReducer
import net.humans.kmm.mvi.Return
import net.humans.kmm.mvi.pure
import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.Effect
import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.Message
import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.State
import net.humans.kmm.mvi.withEffect

class CommissionCalculatorReducer : ComplexReducer<State, Message, Effect> {
override fun invoke(state: State, msg: Message): Return<State, Effect> = when (msg) {
is Message.UpdateCommissionAndCashback -> state.copy(
inputAmount = msg.inputAmount,
commission = msg.commission,
cashback = msg.cashback,
).pure()

is Message.UpdateInput -> state withEffect Effect.CalculateCommissionAndCashback(
balance = state.balance,
inputAmount = msg.inputAmount
)

is Message.UpdateState -> State(
balance = msg.balance,
inputAmount = msg.inputAmount,
commission = msg.commission,
cashback = msg.cashback,
).pure()

Message.ErrorHandled -> state.copy(error = null).pure()
is Message.SetError -> state.copy(error = msg.error).pure()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package net.humans.kmm.mvi.sample.domain

import com.ionspin.kotlin.bignum.decimal.BigDecimal
import net.humans.kmm.mvi.sample.domain.model.MoneyAmount
import net.humans.kmm.mvi.sample.domain.model.usd

object CommissionCalculatorRedux {
data class State(
val balance: MoneyAmount,
val inputAmount: MoneyAmount,
val commission: MoneyAmount,
val cashback: MoneyAmount,
val error: Error? = null,
) {

sealed class Error {
object InsufficientBalance : Error()
}

companion object {
val DEFAULT = State(
balance = BigDecimal.ZERO.usd,
inputAmount = BigDecimal.ZERO.usd,
commission = BigDecimal.ZERO.usd,
cashback = BigDecimal.ZERO.usd,
)
}
}

sealed class Message {
data class UpdateState(
val balance: MoneyAmount,
val inputAmount: MoneyAmount,
val commission: MoneyAmount,
val cashback: MoneyAmount,
) : Message()

data class UpdateInput(
val inputAmount: MoneyAmount,
) : Message()

data class UpdateCommissionAndCashback(
val inputAmount: MoneyAmount,
val commission: MoneyAmount,
val cashback: MoneyAmount,
) : Message()

data class SetError(
val error: State.Error,
) : Message()

object ErrorHandled : Message()
}

sealed class Effect {
object Initialize : Effect()

data class CalculateCommissionAndCashback(
val balance: MoneyAmount,
val inputAmount: MoneyAmount,
) : Effect()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.humans.kmm.mvi.sample.domain.model

enum class Currency {
USD,
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.humans.kmm.mvi.sample.domain.model

import com.ionspin.kotlin.bignum.decimal.BigDecimal

data class MoneyAmount(
val amount: BigDecimal,
val currency: Currency
) {
operator fun times(value: Float): MoneyAmount =
this.copy(amount = amount * BigDecimal.fromFloat(value))

operator fun compareTo(moneyAmount: MoneyAmount): Int {
check(this.currency == moneyAmount.currency) {
"Impossible to compare money amount. Non consistent currencies."
}
return this.amount.compareTo(moneyAmount.amount)
}
}

val BigDecimal.usd: MoneyAmount
get() = MoneyAmount(amount = this, currency = Currency.USD)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package net.humans.kmm.mvi.sample.domain.usecase

import net.humans.kmm.mvi.sample.domain.model.MoneyAmount

interface CalculateCashbackAndCommissionUseCase {
suspend fun execute(
input: CalculateCashbackAndCommissionInput
): CalculateCashbackAndCommissionResult
}

data class CalculateCashbackAndCommissionInput(
val balance: MoneyAmount,
val inputAmount: MoneyAmount,
)

sealed class CalculateCashbackAndCommissionResult {
data class Success(
val inputAmount: MoneyAmount,
val cashback: MoneyAmount,
val commission: MoneyAmount,
) : CalculateCashbackAndCommissionResult()

sealed class Failed : CalculateCashbackAndCommissionResult() {
object InsufficientBalance : Failed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.humans.kmm.mvi.sample.domain.usecase

import net.humans.kmm.mvi.sample.domain.model.MoneyAmount

interface GetBalanceUseCase {
suspend fun execute(): GetBalanceResult
}

data class GetBalanceResult(
val balance: MoneyAmount,
)
Loading