Giter Club home page Giter Club logo

revolver's Introduction

Revolver

Immutable event based state management framework



how it works

Revolver is a Kotlin Multiplatform state management solution that enforces one single immutable state. Information is passed from clients to KMM by emitting RevolverEvents to a RevolverViewModel. This RevolverViewModel will have readonly RevolverState and RevolverEffect flows that the clients can subscribe to for updates.

The first thing we need are our sealed classes responsible for communicating data to and from kmm:

sealed class ExampleEvent : RevolverEvent {
    object Refresh : ExampleEvent()
}
sealed class ExampleState : RevolverState {
    class Loading : ExampleState()
    data class Loaded(val result: String) : ExampleState()
}

Avoid using object for sealed classes it might cause problems with state updates.

sealed class ExampleEffect : RevolverEffect {
    data class ShowToast(val message: String) : ExampleEffect()
}

with these 3 in place, we can create our own RevolverViewModel implementation. This RevolverViewModel always needs an initial state since all event handling happens asynchroniously.

class ExampleViewModel : RevolverViewModel<ExampleEvent, ExampleState, ExampleEffect>(
    initialState = ExampleState.Loading,
) 

In this ViewModel we can register one or more EventHandlers that are responsible for mapping an incomming event to one or more states.

class ExampleViewModel : RevolverViewModel<ExampleEvent, ExampleState, ExampleEffect>(
    initialState = ExampleState.Loading,
) {

    init {
        addEventHandler<ExampleEvent.Refresh>(::onRefresh)
    }

    suspend fun onRefresh(
        event: DealsEvent.Refresh,
        emit: Emitter<ExampleState, ExampleEffect>,
    ) {
        emit.state(ExampleState.Loading)
        val data = someDataFetchingOperation()
        
        emit.state(ExampleState.Loaded(data))
    }
}

As you can see, these event handlers have an Emitter used for emitting RevolverState changes or side RevolverEffect to the clients.

With these 4 things you have your basic state handling flow set up.

Error handling

A very important consept is that we don't want to expose any Kotlin Multiplatform errors to the clients directly. Therefore the RevolverViewModel will catch all exceptions that bubble up and allows you to handle them similarly to any other RevolverEvent. In every RevolverViewModel you should at least register one ErrorHandler.

If you want a quick solution you can register one event handler that catches the generic Exception. but you probably want to define multiple error handlers that catch your custom Errors

class ExampleViewModel : RevolverViewModel<ExampleEvent, ExampleState, ExampleEffect>(ExampleState.Loading) {

    init {
        addErrorHandler<IllegalStateException>(::onIllegalStateException)
        addErrorHandler<Exception>(::onException)
    }
    
    private fun onIllegalStateException(
        exception: IllegalStateException,
        emit: Emitter<ExampleState, ExampleEffect>,
    ) {
       // handle this specific error case
    }

    private fun onException(
        exception: Exception,
        emit: Emitter<ExampleState, ExampleEffect>,
    ) {
        // handle generic Exceptions
    }
}

Keep in mind that the order of handler registration matters, it wil match a thrown error in the order of handler registration, so if one exception type extends another, make sure the child type is registered first.

Reusing error handlers

It is possible that you don't want to rewrite the same error handling implementation for every viewModel, for example a toast should pop up every time a NoConnectionException is thrown.

To do this Revolver supports reusable error handling classes. If you don't want to worry about error handling you can use Revolvers build in error handling class to map any exception directly to a state.

class ExampleViewModel : RevolverViewModel<ExampleEvent, ExampleState, ExampleEffect>(ExampleState.Loading) {

    init {
        addErrorHandler(RevolverDefaultErrorHandler(ExampleState.Error))
    }
}

this will result in any exception thrown to emit an ExampleState.Error.

ofcourse you can implement your own RevolverErrorHandler to do more complex state mapping.

class ExampleErrorHandler<STATE, EFFECT> : RevolverErrorHandler<STATE, EFFECT, Throwable> {

    override suspend fun handleError(exception: Throwable, emit: Emitter<STATE, EFFECT>) {
        // log error, call some other external methods, map to reusable states ...
    }
}

which you can then register in your viewmodel like any other reused error handler.



Testing

if you implement your viewmodels as described above, you will end up with a completly immutable and defined state machine where all actions and responses are mapped. The great thing this allows us to do is test the complete state flow in Kotlin multiplatform unit tests without needing any link to a client application

There are a couple external tools we recommend when writing tests:

using these it's really easy to test your state machine's flow, take this example viewmodel

sealed class ExampleEvent : RevolverEvent {
    object Refresh : ExampleEvent()
}

sealed class ExampleState : RevolverState {
    object Loading : ExampleState()
    data class Loaded(val data: String) : ExampleState()
}

class ExampleViewModel(
    private val repository: ExampleRepository,
    initialState: ExampleState = ExampleState.Loading,
) : RevolverViewModel<ExampleEvent, ExampleState, EFFECT>(initialState) {

    init {
        addEventHandler<ExampleEvent.Refresh>(::onRefresh)
    }

    private suspend fun onRefresh(
        event: ExampleEvent.Refresh,
        emit: Emitter<ExampleState, EFFECT>,
    ) {
        emit.state(ExampleState.Loading)

        val result = repository.fetchData()
        emit.state(ExampleState.Loaded(data))
    }
}
@OptIn(ExperimentalCoroutinesApi::class)
internal class ExampleViewModelTests {

    @Mock
    private val exampleRepository = mock(classOf<ExampleRepository>())


    @BeforeTest
    fun setup() {
        Dispatchers.setMain(StandardTestDispatcher())
    }

    @AfterTest
    fun dispose() {
        Dispatchers.resetMain()
    }
    
    @Test
    fun onRefreshEmitsLoadingAndLoadedState() = runTest {
        // given
        given(exampleRepository).coroutine { fetchData() }.thenReturn("testData")
        
        val initialState = ExampleState.Loading
        val viewmodel = ExampleViewModel(exampleRepository, initialState)
        
        viewmodel.state.test {
            
            // when
            viewmodel.emit(ExampleEvent.Refresh)
            
            // then
            assertIs<ExampleState.Loading>(awaitItem())
            val loadedState = assertIs<ExampleState.Loaded>(awaitItem())
            assertEquals("testData", loadedState.data)
        }
        
    }
}

as you can see we use Mockative to mock our repository, so we can purely focus on testing the viewmodel. Then we use Turbine to test the viewmodel.state (you can also test viewmodel.effect).

Using in your project

to use this package in your kotlin multiplatform project you need to add our gradle server to you build.gradle.kts file:

dependencyResolutionManagement {
    repositories {
        maven {
            url = uri("https://maven.pkg.github.com/apegroup/revolver/")

            credentials {
                username = System.getenv("GH_USERNAME") ?: ""
                password = System.getenv("GH_TOKEN") ?: ""
            }
        }
    }
}

Then you can import it in your commonMain just like any other Kotlin multiplatform package

implementation("com.umain:revolver:{LATEST_VERSION}")

Contribution

This package is very much still in experimental mode and using this in a production environment is at your own risk. Bug reports, feature requests, or contributions are very much appreciated!

revolver's People

Contributors

miksto avatar casvanluijtelaar avatar dependabot[bot] avatar gsavvid avatar netaneleklind avatar alsedi avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.