diff --git a/.gitignore b/.gitignore index a7ea2f0..edd7223 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,10 @@ fabric.properties # https://plugins.jetbrains.com/plugin/12206-codestream .idea/codestream.xml +# deploymentTargetDropDown +.idea/deploymentTargetDropDown.xml +.idea/assetWizardSettings.xml + ### Java ### # Compiled class file *.class diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2a5eb1..8ec2445 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,23 +1,45 @@ [versions] -kotlin = "1.9.22" +ksp = "1.9.23-1.0.19" +appcompat = "1.6.1" +koin = "3.5.3" +koin-annotations = "1.3.1" +koin-android = "3.5.3" +koin-compose = "3.5.3" +kotlin = "1.9.23" coroutines = "1.8.0" kotest = "5.8.1" -ktlint-lib = "1.2.1" +material = "1.6.5" mvi = "1.7.0" +mvi-compose = "0.0.3" +mvi-kotest = "0.0.2" activity = "1.8.2" lifecycle = "2.7.0" ktlint-gradle = "12.1.0" -android-library = "8.3.0" +android-library = "8.3.1" maven-publish = "0.25.2" +ui = "1.6.5" + +# its beeing used outside this file +ktlint-lib = "1.2.1" [libraries] -coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } -coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } -kotest-runner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } -kotlin-bom = { group = "org.jetbrains.kotlin", name = "kotlin-bom", version.ref = "kotlin" } -adidas-mvi = { group = "com.adidas.mvi", name = "mvi", version.ref = "mvi" } +appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +kotestRunner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } +kotlinBom = { group = "org.jetbrains.kotlin", name = "kotlin-bom", version.ref = "kotlin" } +mvi = { group = "com.adidas.mvi", name = "mvi", version.ref = "mvi" } +mviCompose = { group = "com.adidas.mvi", name = "mvi-compose", version.ref = "mvi-compose" } +mviKotest = { group = "com.adidas.mvi", name = "mvi-kotest", version.ref = "mvi-kotest" } activityCompose = { module = "androidx.activity:activity-compose", version.ref = "activity" } lifecycleRuntimeCompose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } +material = { module = "androidx.compose.material:material", version.ref = "material" } +ui = { module = "androidx.compose.ui:ui", version.ref = "ui" } +koinCore = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koinAndroid = { module = "io.insert-koin:koin-android", version.ref = "koin-android" } +koinAnnotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin-annotations" } +koinKspCompiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koin-annotations" } +koinCompose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin-compose" } [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } @@ -25,3 +47,4 @@ ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle" } mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } android-library = { id = "com.android.library", version.ref = "android-library" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/gradlew b/gradlew index 1b6c787..a6dd0e9 100755 --- a/gradlew +++ b/gradlew @@ -121,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java + JAVACMD=$JAVA_HOME/jre/sh/kotlin else - JAVACMD=$JAVA_HOME/bin/java + JAVACMD=$JAVA_HOME/bin/kotlin fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -132,8 +132,8 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=kotlin + which kotlin >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." @@ -154,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then esac fi -# Collect all arguments for the java command, stacking in reverse order: +# Collect all arguments for the kotlin command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath @@ -162,7 +162,7 @@ fi # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# For Cygwin or MSYS, switch paths to Windows format before running java +# For Cygwin or MSYS, switch paths to Windows format before running kotlin if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) @@ -193,7 +193,7 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; +# Collect all arguments for the kotlin command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and diff --git a/mvi-compose/build.gradle.kts b/mvi-compose/build.gradle.kts index 13a2d4c..f19e5ad 100644 --- a/mvi-compose/build.gradle.kts +++ b/mvi-compose/build.gradle.kts @@ -44,8 +44,8 @@ android { dependencies { - implementation(platform(libs.kotlin.bom)) - implementation(libs.adidas.mvi) + implementation(platform(libs.kotlinBom)) + implementation(libs.mvi) implementation(libs.activityCompose) implementation(libs.lifecycleRuntimeCompose) } diff --git a/mvi-kotest/build.gradle.kts b/mvi-kotest/build.gradle.kts index 605f868..3dc75f7 100644 --- a/mvi-kotest/build.gradle.kts +++ b/mvi-kotest/build.gradle.kts @@ -15,6 +15,6 @@ configure { } dependencies { - implementation(libs.kotest.runner) - implementation(libs.adidas.mvi) + implementation(libs.kotestRunner) + implementation(libs.mvi) } diff --git a/mvi-sample/.gitignore b/mvi-sample/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/mvi-sample/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/mvi-sample/build.gradle.kts b/mvi-sample/build.gradle.kts new file mode 100644 index 0000000..f1f8c7c --- /dev/null +++ b/mvi-sample/build.gradle.kts @@ -0,0 +1,86 @@ +import com.android.build.gradle.AppExtension +import com.android.build.gradle.TestedExtension +import com.android.build.gradle.api.BaseVariant + +plugins { + id("com.android.application") + id("kotlin-android") + id("kotlin-kapt") + alias(libs.plugins.ksp) +} + +android { + compileSdk = 34 + + defaultConfig { + minSdk = 24 + targetSdk = 34 + + versionCode = 1 + versionName = "1.0" + } + + namespace = "com.adidas.mvi.sample" + + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.11" + } + + buildFeatures { + compose = true + } + + extensions.findByType()?.let { + project.extensions.configure(AppExtension::class.java) { + applyKSP(this@configure, applicationVariants) + } + } +} + +private fun applyKSP( + extension: TestedExtension, + variants: DomainObjectSet, +) { + variants.all { + extension.sourceSets { + getByName(this@all.name) { + kotlin.srcDir("build/generated/ksp/$name/kotlin") + } + } + } +} + +kotlin { + explicitApi() +} + +dependencies { + implementation(libs.appcompat) + implementation(libs.activityCompose) + implementation(libs.ui) + implementation(libs.material) + + implementation(libs.mvi) + implementation(libs.mviCompose) + + implementation(libs.koinCore) + implementation(libs.koinAndroid) + implementation(libs.koinAnnotations) + implementation(libs.koinCompose) + ksp(libs.koinKspCompiler) +} \ No newline at end of file diff --git a/mvi-sample/src/main/AndroidManifest.xml b/mvi-sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bb30ff8 --- /dev/null +++ b/mvi-sample/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/MviSampleActivity.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/MviSampleActivity.kt new file mode 100644 index 0000000..04db2c7 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/MviSampleActivity.kt @@ -0,0 +1,17 @@ +package com.adidas.mvi.sample + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import com.adidas.mvi.sample.login.ui.LoginScreen + +public class MviSampleActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + LoginScreen() + } + } +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/app/MviSampleApplication.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/app/MviSampleApplication.kt new file mode 100644 index 0000000..ddb9a96 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/app/MviSampleApplication.kt @@ -0,0 +1,29 @@ +package com.adidas.mvi.sample.app + +import android.app.Application +import com.adidas.mvi.sample.app.di.AppModule +import com.adidas.mvi.sample.login.di.LoginModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin +import org.koin.core.logger.Level +import org.koin.ksp.generated.module + +public class MviSampleApplication : Application() { + + override fun onCreate() { + super.onCreate() + initializeKoin() + } + + private fun initializeKoin() { + startKoin { + androidLogger(Level.ERROR) + androidContext(this@MviSampleApplication) + modules( + AppModule().module, + LoginModule().module + ) + } + } +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/app/di/AppModule.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/app/di/AppModule.kt new file mode 100644 index 0000000..7ebaabd --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/app/di/AppModule.kt @@ -0,0 +1,15 @@ +package com.adidas.mvi.sample.app.di + +import com.adidas.mvi.Logger +import com.adidas.mvi.sample.app.logger.AppReduceLogger +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +public class AppModule { + + @Single + internal fun provideLogger(): Logger { + return AppReduceLogger() + } +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/app/logger/AppReduceLogger.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/app/logger/AppReduceLogger.kt new file mode 100644 index 0000000..5882c40 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/app/logger/AppReduceLogger.kt @@ -0,0 +1,55 @@ +package com.adidas.mvi.sample.app.logger + +import com.adidas.mvi.Loggable +import com.adidas.mvi.Logger + +internal class AppReduceLogger : Logger { + + override fun logIntent(intent: Loggable) { + println("Intent: ${getLoggableName(intent)}") + } + + override fun logFailedIntent( + intent: Loggable, + throwable: Throwable, + ) { + println("[Error]Intent: ${getLoggableName(intent)} Exception: ${throwable::class.qualifiedName}") + throwable.printStackTrace() + } + + override fun logTransformedNewState( + transform: Loggable, + previousState: Loggable, + newState: Loggable, + ) { + println( + "Reduce: Using: ${getLoggableName(transform)}, On: ${ + getLoggableName( + previousState + ) + }, To: ${getLoggableName(newState)}" + ) + } + + override fun logFailedTransformNewState( + transform: Loggable, + state: Loggable, + throwable: Throwable, + ) { + println( + "[Error]Reduce: Using: ${getLoggableName(transform)} On:${ + getLoggableName( + state + ) + } Exception: ${throwable::class.qualifiedName}" + ) + throwable.printStackTrace() + } + + private fun getLoggableName(loggable: Loggable): String? { + val qualifiedName = loggable::class.qualifiedName + val packageName = loggable.javaClass.`package`?.name + + return qualifiedName?.substringAfter("$packageName.") ?: "N/D" + } +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/di/LoginModule.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/di/LoginModule.kt new file mode 100644 index 0000000..2357f28 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/di/LoginModule.kt @@ -0,0 +1,19 @@ +package com.adidas.mvi.sample.login.di + +import com.adidas.mvi.Logger +import com.adidas.mvi.sample.login.viewmodel.LoginViewModel +import org.koin.android.annotation.KoinViewModel +import org.koin.core.annotation.Module + +@Module +public class LoginModule { + + @KoinViewModel + internal fun provideLoginViewModel( + logger: Logger, + ): LoginViewModel { + return LoginViewModel( + logger = logger + ) + } +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/ui/LoginScreen.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/ui/LoginScreen.kt new file mode 100644 index 0000000..e91a310 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/ui/LoginScreen.kt @@ -0,0 +1,193 @@ +package com.adidas.mvi.sample.login.ui + +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.adidas.mvi.compose.MviScreen +import com.adidas.mvi.sample.login.viewmodel.LoginIntent +import com.adidas.mvi.sample.login.viewmodel.LoginSideEffect +import com.adidas.mvi.sample.login.viewmodel.LoginState +import com.adidas.mvi.sample.login.viewmodel.LoginViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun LoginScreen( + viewModel: LoginViewModel = koinViewModel(), +) { + + val context = LocalContext.current + + MviScreen( + state = viewModel.state, + onSideEffect = { sideEffect -> + consumeSideEffect( + sideEffect = sideEffect, + context = context + ) + }, + onBackPressed = { }, + ) { view -> + LoginLandingContent( + state = view, + intentExecutor = viewModel::execute, + ) + } +} + + +@Composable +private fun LoginLandingContent( + state: LoginState, + intentExecutor: (intent: LoginIntent) -> Unit, +) { + Column( + modifier = + Modifier + .fillMaxSize() + ) { + + when (state) { + is LoginState.LoggedOut -> + LoginLoadedView( + isLoggingIn = state.isLoggingIn, + intentExecutor = intentExecutor, + ) + + is LoginState.LoggedIn -> WelcomeView(username = state.username, intentExecutor) + } + } +} + +@Composable +private fun LoginLoadedView( + intentExecutor: (intent: LoginIntent) -> Unit, + isLoggingIn: Boolean +) { + + if (isLoggingIn) { + LoginLoadingView() + } else { + LoginInputView(intentExecutor) + } +} + +@Composable +private fun LoginInputView(intentExecutor: (intent: LoginIntent) -> Unit) { + var username by remember { mutableStateOf("") } + + var password by remember { mutableStateOf("") } + + Surface( + color = MaterialTheme.colors.background, + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + Text( + text = "Login", + style = MaterialTheme.typography.h5, + color = Color.Black, + modifier = Modifier.padding(bottom = 16.dp) + ) + + TextField( + value = username, + onValueChange = { username = it }, + label = { Text("Username") }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) + + TextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ) + ) + + Button( + onClick = { intentExecutor(LoginIntent.Login(username, password)) }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Login") + } + } + } +} + +@Composable +private fun LoginLoadingView() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.padding(16.dp) + ) + } +} + +@Composable +private fun WelcomeView( + username: String, + intentExecutor: (intent: LoginIntent) -> Unit +) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Welcome $username!", modifier = Modifier.padding(16.dp)) + Button(onClick = { intentExecutor(LoginIntent.Logout) }) { + Text(text = "logout") + } + } +} + + +internal fun consumeSideEffect(sideEffect: LoginSideEffect, context: Context) { + + when (sideEffect) { + + LoginSideEffect.InvalidCredentials -> { + Toast.makeText(context, "Invalid credentials", Toast.LENGTH_SHORT).show() + } + } +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginIntent.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginIntent.kt new file mode 100644 index 0000000..e22e064 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginIntent.kt @@ -0,0 +1,10 @@ +package com.adidas.mvi.sample.login.viewmodel + +import com.adidas.mvi.Intent + +internal sealed class LoginIntent : Intent { + + data object Logout : LoginIntent() + + data class Login(val username: String, val password: String) : LoginIntent() +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginSideEffect.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginSideEffect.kt new file mode 100644 index 0000000..02aec78 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginSideEffect.kt @@ -0,0 +1,6 @@ +package com.adidas.mvi.sample.login.viewmodel + +internal sealed class LoginSideEffect { + + data object InvalidCredentials : LoginSideEffect() +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginState.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginState.kt new file mode 100644 index 0000000..2567a85 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginState.kt @@ -0,0 +1,10 @@ +package com.adidas.mvi.sample.login.viewmodel + +import com.adidas.mvi.LoggableState + +public sealed class LoginState : LoggableState { + + public data class LoggedOut(val isLoggingIn: Boolean) : LoginState() + + public data class LoggedIn(val username: String) : LoginState() +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginTransform.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginTransform.kt new file mode 100644 index 0000000..f152fc5 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginTransform.kt @@ -0,0 +1,35 @@ +package com.adidas.mvi.sample.login.viewmodel + +import com.adidas.mvi.sideeffects.SideEffects +import com.adidas.mvi.transform.SideEffectTransform +import com.adidas.mvi.transform.ViewTransform + +internal object LoginTransform { + + object SetShowLogin : ViewTransform() { + override fun mutate(currentState: LoginState): LoginState { + return LoginState.LoggedOut(isLoggingIn = false) + } + } + + data class SetIsLoggingIn(val isLoggingIn: Boolean) : + ViewTransform() { + override fun mutate(currentState: LoginState): LoginState { + return LoginState.LoggedOut(isLoggingIn = isLoggingIn) + } + } + + data class SetLoggedIn(val username: String) : ViewTransform() { + override fun mutate(currentState: LoginState): LoginState { + return LoginState.LoggedIn(username) + } + } + + data class AddSideEffect( + val sideEffect: LoginSideEffect, + ) : SideEffectTransform() { + override fun mutate(sideEffects: SideEffects): SideEffects { + return sideEffects.add(sideEffect) + } + } +} \ No newline at end of file diff --git a/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginViewModel.kt b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginViewModel.kt new file mode 100644 index 0000000..a5d6532 --- /dev/null +++ b/mvi-sample/src/main/kotlin/com/adidas/mvi/sample/login/viewmodel/LoginViewModel.kt @@ -0,0 +1,56 @@ +package com.adidas.mvi.sample.login.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.adidas.mvi.Logger +import com.adidas.mvi.MviHost +import com.adidas.mvi.State +import com.adidas.mvi.reducer.Reducer +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow + +internal class LoginViewModel( + logger: Logger, + coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default +) : ViewModel(), MviHost> { + + private val reducer = + Reducer( + coroutineScope = viewModelScope, + defaultDispatcher = coroutineDispatcher, + initialInnerState = LoginState.LoggedOut(isLoggingIn = false), + logger = logger, + intentExecutor = this::executeIntent, + ) + + override val state = reducer.state + + override fun execute(intent: LoginIntent) { + reducer.executeIntent(intent) + } + + private fun executeIntent(intent: LoginIntent) = + when (intent) { + is LoginIntent.Login -> executeLogin(intent) + LoginIntent.Logout -> executeLogout() + } + + private fun executeLogout() = flow { + emit(LoginTransform.SetShowLogin) + } + + private fun executeLogin(intent: LoginIntent.Login) = flow { + emit(LoginTransform.SetIsLoggingIn(isLoggingIn = true)) + + kotlinx.coroutines.delay(300) + + emit(LoginTransform.SetIsLoggingIn(isLoggingIn = false)) + + if (intent.username.isEmpty() || intent.password.isEmpty()) { + emit(LoginTransform.AddSideEffect(LoginSideEffect.InvalidCredentials)) + } else { + emit(LoginTransform.SetLoggedIn(intent.username)) + } + } +} \ No newline at end of file diff --git a/mvi-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mvi-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/mvi-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mvi-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mvi-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/mvi-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mvi-sample/src/main/res/mipmap-hdpi/ic_launcher.png b/mvi-sample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a0ba52e Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mvi-sample/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/mvi-sample/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..e1040b3 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/mvi-sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mvi-sample/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..3ee10d6 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/mvi-sample/src/main/res/mipmap-mdpi/ic_launcher.png b/mvi-sample/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..57f4689 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mvi-sample/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/mvi-sample/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..e3b425f Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/mvi-sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mvi-sample/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..d551e79 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/mvi-sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/mvi-sample/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..1e19e72 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mvi-sample/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/mvi-sample/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..aa8897f Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/mvi-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mvi-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..711b64e Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/mvi-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mvi-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..a292385 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mvi-sample/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mvi-sample/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4177e1a Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/mvi-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mvi-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..ab17d07 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/mvi-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mvi-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..ccd03d0 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mvi-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mvi-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..5bc36f3 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/mvi-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mvi-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..c555e55 Binary files /dev/null and b/mvi-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/mvi-sample/src/main/res/values/colors.xml b/mvi-sample/src/main/res/values/colors.xml new file mode 100644 index 0000000..8d2c8b6 --- /dev/null +++ b/mvi-sample/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #620CEE + #370CB3 + #03DCC5 + #FFFFFF + \ No newline at end of file diff --git a/mvi-sample/src/main/res/values/strings.xml b/mvi-sample/src/main/res/values/strings.xml new file mode 100644 index 0000000..b9523cf --- /dev/null +++ b/mvi-sample/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Adidas Mvi + \ No newline at end of file diff --git a/mvi-sample/src/main/res/values/styles.xml b/mvi-sample/src/main/res/values/styles.xml new file mode 100644 index 0000000..ecf8a69 --- /dev/null +++ b/mvi-sample/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/mvi/build.gradle.kts b/mvi/build.gradle.kts index 0c68413..9df3c47 100644 --- a/mvi/build.gradle.kts +++ b/mvi/build.gradle.kts @@ -24,8 +24,8 @@ tasks.getByName("test") { } dependencies { - implementation(libs.coroutines.core) + implementation(libs.coroutinesCore) - testImplementation(libs.kotest.runner) - testImplementation(libs.coroutines.test) + testImplementation(libs.kotestRunner) + testImplementation(libs.coroutinesTest) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 417ffd9..ec72b53 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,3 +17,4 @@ rootProject.name = "mvi" include(":mvi") include(":mvi-compose") include(":mvi-kotest") +include(":mvi-sample")