Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Minimal Flux architecture written in Kotlin.

License

NotificationsYou must be signed in to change notification settings

hyperdevs-team/mini-kotlin

Repository files navigation

Release

Mini is a minimal Flux architecture written in Kotlin that also adds a mix of useful features to build UIs fast.

Purpose

You should use this library if you aim to develop a reactive application with good performance (no reflection using code-gen).Feature development using Mini is fast compared to traditional architectures (like CLEAN or MVP), low boilerplate and state based models make feature integration and bugfixing easy as well as removing several families of problems like concurrency or view consistency across screens.

Setting Up

Import the library

First, add the following dependencies to your mainbuild.gradle so you can import the library dependencies:

Groovy
buildscript {    repositories {         maven { url"https://jitpack.io" }    }}
Kotlin
buildscript {    repositories {         maven("https://jitpack.io")    }}

Then, add the following dependencies to your module'sbuild.gradle:

Groovy
dependencies {def mini_version="<latest_version>"// Minimum working dependencies    implementation"com.github.hyperdevs-team.mini-kotlin:mini-android:$mini_version"// Use kapt as your annotation processor    kapt"com.github.hyperdevs-team.mini-kotlin:mini-processor:$mini_version"// Or ksp if you prefer using Kotlin Symbol Processing (requires extra dependencies)    ksp"com.github.hyperdevs-team.mini-kotlin:mini-processor:$mini_version"// Kodein helper libraries    implementation"com.github.hyperdevs-team.mini-kotlin:mini-kodein:$mini_version"    implementation"com.github.hyperdevs-team.mini-kotlin:mini-kodein-android:$mini_version"// Kodein helper library for view models scoped to the Navigation component's graph in Jetpack Compose    implementation"com.github.hyperdevs-team.mini-kotlin:mini-kodein-android-compose:$mini_version"// Android Testing helper libraries    androidTestImplementation"com.github.hyperdevs-team.mini-kotlin:mini-testing:$mini_version"}
Kotlin
dependencies {val miniVersion="<latest_version>"// Minimum working dependencies    implementation("com.github.hyperdevs-team.mini-kotlin:mini-android:$miniVersion")// Use kapt as your annotation processor    kapt("com.github.hyperdevs-team.mini-kotlin:mini-processor:$miniVersion")// Or ksp if you prefer using Kotlin Symbol Processing (requires extra dependencies)    ksp("com.github.hyperdevs-team.mini-kotlin:mini-processor:$miniVersion")// Kodein helper libraries    implementation("com.github.hyperdevs-team.mini-kotlin:mini-kodein:$miniVersion")    implementation("com.github.hyperdevs-team.mini-kotlin:mini-kodein-android:$miniVersion")// Kodein helper library for view models scoped to the Navigation component's graph in Jetpack Compose    implementation("com.github.hyperdevs-team.mini-kotlin:mini-kodein-android-compose:$miniVersion")// Android Testing helper libraries    androidTestImplementation("com.github.hyperdevs-team.mini-kotlin:mini-testing:$miniVersion")}

If you want, you can also useKotlin Symbol Processing (KSP) instead of KAPT. Keep in mind thatKSP may have some gotchas that can be worked around, so double check before using thisand report any issue that you find while working with KSP.

KSP extra dependencies
Groovy

Add this to your mainbuild.gradle:

buildscript {    ext {        ksp_version="<latest_ksp_version>"    }    dependencies {        classpath"com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$ksp_version"    }}

And this to your module'sbuild.gradle

applyplugin:"com.google.devtools.ksp"ksp"com.github.hyperdevs-team.mini-kotlin:mini-processor:$mini_version"
Kotlin

Add this to your mainbuild.gradle.kts:

buildscript {    dependencies {val kspVersion=<latest_ksp_version>        classpath("com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:${kspVersion}")    }}

And this to your module'sbuild.gradle.kts

plugins {    id"com.google.devtools.ksp"}ksp("com.github.hyperdevs-team.mini-kotlin:mini-processor:${miniVersion}")

JDK

Ensure that your project has compatibility with Java 17:

For Kotlin projects:

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {    kotlinOptions {        jvmTarget="17"    }}

For Android:

android {    compileOptions {        sourceCompatibility=JavaVersion.VERSION_17        targetCompatibility=JavaVersion.VERSION_17    }    kotlinOptions {        jvmTarget="17"    }}

Concepts

Actions

AnAction is a simple class that usually represents a use case. It can also contain a payload that includes data to perform said action. When an action is triggered, it will be delivered viadispatcher to thestores that are going to do something with the action to change their state.

For example, we may want to log in to a service. We would create an action like this one:

@Actiondata classLoginAction(valusername:String,valpassword:String)

When we receive the response from the server, we'll dispatch another action with the result:

@Actiondata classLoginCompletedAction(valloginTask:Task,valuser:User?)

Actions will usually be triggered fromViews,ViewModels orControllers, depending on the architecture of your choice.

Dispatcher

TheDispatcher is the hub that manages all data flow in a Flux application. It is basically a holder of store callbacks: each store registers itself and provides a callback for an action.

One important thing is that the dispatching is always performed in the same thread (usually the main thread) to avoid possible side-effects.

We can dispatch actions in the following ways:

// Dispatch an action in the main thread synchronouslydispatcher.dispatch(action=LoginAction(username="user", password="123"))// Dispatch an action with the given scopedispatcher.dispatchOn(action=LoginAction(username="user", password="123"), scope= coroutineScope)

Store

TheStores are holders for application state and state mutation logic. In order to do so they expose pure reducer functions that are later invoked by the dispatcher. AStore is a type of aStateContainer, which is exactly that: a container of states.

AState is a plain object (usually adata class) that holds all information needed to display the view. States should always be immutable. State classes should avoid using framework-specific elements (View, Camera, Cursor...) in order to ease testing.

Stores subscribe to actions to change the application state after adispatch. Mini generates the code that links dispatcher actions and stores using the@Reducer annotation over anon-private function that receives an Action as parameter.

data classSessionState(valloginTask:Task = taskIdle(),valloggedUser:User? =null)classSessionStore(valcontroller:SessionController) : Store<SessionState>() {    @Reducerfunlogin(action:LoginAction):SessionState {        controller.login(action.username, action.password)return state.copy(loginTask= taskRunning(), loggedUser=null)    }    @ReducerfunonLoginCompleted(action:LoginCompletedAction):SessionState {return state.copy(loginTask= action.loginTask, loggedUser= action.user)    }}

View changes

EachStateContainer exposes a KotlinFlow that emits changes produced on the state, allowing aView or aViewModel to listen to those changes and react accordingly to update the UI with the newStore state.

mainStore.flow()    .onEach { state->// Do whatever you want    }    .launchIn(coroutineScope)

Tasks

ATask is a basic object to represent an ongoing process. They should be used in the state of ourStateContainer (aStore, for example) to represent ongoing processes that must be represented in the UI.

You can also useTypedTask to save metadata related the current task.

IMPORTANT: Do not use TypedTask to hold values that must survive multiple task executions. Save them as a variable in the state instead.

Example

Given the exampleStores andActions explained before, the workflow would be:

  • View dispatchesLoginAction.
  • Store changes theloginTask of its state toloading (or running state) and call a function in aSessionController to perform the login asynchronously.
  • The view shows an Spinner whenloginTask is in running state.
  • The asynchronous call ends andLoginCompletedAction is dispatched, returning a nullUser and an error stateTask if the asynchronous work failed or a successTask and aUser if the work finished successfully.
  • The Store changes its state to the given values fromLoginCompletedAction.
  • The view will react (for example, redirecting to another home view) if the task was success or shows an error if not.

You can execute another sample in theapp package. It contains two different samples executing two types ofStateContainers:

  • StoreSampleActivity class uses aStore as aStateContainer.
  • ViewModelSampleActivity class uses aViewModel as aStateContainer.

How to use

Setting up Mini

You'll need to add the following snippet to the class that initializes your application (for example, in Android you would set this in yourApplication'sonCreate method).

val stores= listOf<Store<*>>()// Here you'll set-up you store list, you can retrieve it using your preferred DI frameworkval dispatcher=MiniGen.newDispatcher()// Create a new dispatcher// Initialize MinistoreSubscriptions=MiniGen.subscribe(dispatcher, stores)stores.forEach { store->    store.initialize()}// Optional: add logging middleware to log action eventsdispatcher.addMiddleware(LoggerMiddleware(stores)) { tag, msg->Log.d(tag, msg)}

As soon as you do this, you'll have Mini up and running. You'll then need to declare yourActions,Stores andState as mentioned previously. The sampleapp contains examples regarding app configuration.

Advanced usages

Kotlin Flow Utils

Mini includes some utility extensions over KotlinFlow to make easier listen state changes over theStateContainers.

  • select: Will emit only distinct values over the givenmap clause.
  • selectNotNull: Likeselect but also avoiding null values.
  • onEachChange: Emits a value when the values goes from one value to another.
  • onEachDisable: Emits when the value goes from true to false.
  • onEachEnable: Emits when the value goes from false to true.

You can see all extensions inStoreFlow.

Navigation and UI loops

In order to avoid loops when working with navigation based on a process result after dispatching anAction, you will need to do something like this

For example:

funlogin(username:String,password:String) {    dispatcher.dispatch(LoginAction(username, password))    sessionStore.flow()        .takeUntil { it.isTerminal }        .onEach {// Do your stuff        }        .launchIn(coroutineScope)}

Merging state from multiple stores

Sometimes we want to use get data from multiple stores at the same time. You can do this by usingmergeStates:

mergeStates<Any> {        merge(userStore) {this }        merge(downloadsStore) {this }    }.select { (userState, downloadsState)->CombinedState(userState, downloadState)    }        .onEach {// Do your stuff         }        .llaunchIn(coroutineScope)

Logging

Mini includes a customLoggerMiddleware to log any change in yourStateContainer states produced from anAction. This will allow you to keep track of your actions, changes and side-effects more easily.To add theLoggerMiddleware to your application you just need to add a single instance of it to yourDispatcher.

val loggerMiddleware=CustomLoggerMiddleware(stores().values)dispatcher.addMiddleware(loggerMiddleware)

Testing with Mini

Mini includes an extra library called mini-testing with a few methods andTestRules to simplify your UI tests with this framework.

  • TestDispatcherRule : This rule will intercept any action that arrives to theDispatcher, avoiding any call toStores. If we include this rule we will need to change the states manually in our tests.
  • CleanStateRule : This rule resets the state of theStores before and after each test.

Example of an Android test checking that an action is correctly dispatched:

@get:Ruleval testDispatcher= testDispatcherRule()@Testfunlogin_button_dispatch_login_action() {    onView(withId(R.id.username_edit_text)).perform(typeText("someUsername"))    onView(withId(R.id.password_edit_text)).perform(typeText("somePassword"))    onView(withId(R.id.login_button)).perform(click())        assertThat(testDispatcher.actions, contains(LoginAction(someUsername, somePassword)))}

Example of an Android test checking that aView correctly changes with an specific state:

@get:Ruleval cleanState= cleanStateRule()@Testfunlogin_redirects_to_home_with_success_task() {//Set login state to success     onUiSync {val loggedUser=User(email=MockModels.anyEmail, uid=MockModels.anyId, username=MockModels.anyUsername, photoUrl=MockModels.anyPhoto)val state=SessionState().copy(loginRequestState= requestSuccess(), verified=false, loggedIn=true, loggedUser= loggedUser)         sessionStore.setTestState(state)     }//Redirect to Email verification activity     intended(hasComponent(HomeActivity::class.java.name))}

Kodein support

Kodein is a very simple and yet very useful dependency retrieval container. it is very easy to use and configure.

The librarymini-kodein aims to ease working with Kodein and Mini by providing some utility methods to bind objects likeStores by relying on Kodein's retrieval capabilities.

object UserDIModule : BaseDIModule() {overrideval builder:DI.Builder.()->Unit= {        bindStore {UserStore(instance()) }// binds the store as a singleton and adds it to a seo of stores        bind<UserController>() with singleton {UserControllerImpl(instance()) }    }}

Android-specific features

Proguard/R8

Each of the libraries contain a sensible proguard file that your project can consume in order to run you app on Proguard or R8.No additional steps have to be done in order to use them apart from enabling minify in your project.

Kodein Android utils

The librarymini-kodein-android has some utility methods in order to inject an Android'sViewModel in aDIAwareActivity orFragment.In order to use these methods, bind the Android'sViewModelProvider.Factory instance with Kodein:

// Use any tag to differ between the injected `Context` or `Application` if you are binding also `Context` with Kodeinbind<Application>("appTag") with singleton { app }bind<ViewModelProvider.Factory>() with singleton {DIViewModelFactory(di.direct) }

To inject aViewModel without parameters, bind it as follows:

bindViewModel {MainViewModel(instance("appTag") }

And in yourDIAwareActivity orFragment:

privateval mainViewModel:MainViewModel by viewModel()

Kodein and Jetpack Compose utils

The librarymini-kodein-android-compose has some utility methods in order to inject an Android'sViewModel in the scope of a Navigation component graph. This is useful as in Jetpack Compose it is common to have only one or fewActivities and noFragments so, in order to scope the lifecycle of theViewModel not for all the life of theActivity, we can scope it to any route existing in theNavBackStackEntry.

In order to use it, do the same as above, but instead of injecting the ViewModel scoped to a route of the Navigation, theNavHost composable must be inside anDIAware Activity, and then do as follows:

composable(route="home") { navBackStackEntry->val homeViewModel:HomeViewModel by navBackStackEntry.viewModel(contextDI())HomeScreen(homeViewModel,...)}

In case you want to pass an argument to a ViewModel, you need to bind the factory of that kind of Android's ViewModel.You can do this in bothmini-kodein-android, andmini-kodein-android-compose.For example, given aViewModel that you want to pass aString param, it would be:

bindViewModelFactory<HomeViewModelWithParameter,ViewModelProvider.Factory> { param->TypedViewModelFactory(HomeViewModelWithParameter::class, instance("appTag"), paramasString)}

And to retrieve it with the given param in its constructor:

val param="Hello World!"val homeViewModelWithParameter:HomeViewModelWithParameter by navBackStackEntry.viewModel(contextDI(), param)

Tips and tricks

Improve compilation speed

In order to speed up the compilation process forkapt, it is recommended to add the following settings inyourgradle.properties:

##Improves kapt speed with parallel annotation processing tasks, may impactin memory usagekapt.use.worker.api=true##EnablesGradle build cacheorg.gradle.caching=true

Known issues

KSP gotchas

KSP code is not recognized by the IntelliJ IDEs

You may find that KSP generated sources are not indexed by IntelliJ IDEs. You can solve this bydeclaring the proper source sets in your build.gradle:

For Android apps:

applicationVariants.all {variant->    kotlin.sourceSets {def flavors= variant.productFlavors.indexed().collect {index,item->def flavorName= item.nameif (index>0)return flavorName.capitalize()else flavorName        }.join("")        debug {            getByName(flavors) {                kotlin.srcDirs+="build/generated/ksp/${flavors}Debug/kotlin"            }        }        release {            getByName(flavors) {                kotlin.srcDirs+="build/generated/ksp/${flavors}Release/kotlin"            }        }    }}

KSP code generates code for test source sets

You may encounter that KSP also runs over test source sets, so if you set any code related to Miniin test sources, it will generate code that may override your main source set generated code.

A workaround to avoid this is to disable any KSP task for test source sets:

afterEvaluate {    tasks.matching {        it.name.startsWith("ksp")&& it.name.endsWith("TestKotlin")    }.configureEach { it.enabled=false }}

Acknowledgements

The work in this repository up to April 30th, 2021 was done bybq.Thanks for all the work!!

License

This project is licensed under the Apache Software License, Version 2.0.

   Copyright 2021 HyperDevs      Copyright 2019 BQ   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://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.

About

Minimal Flux architecture written in Kotlin.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors8


[8]ページ先頭

©2009-2025 Movatter.jp