RoomRoster/Eiffel


Kotlin Android architecture library for view state handling with Jetpack Architecture Components.

Download


Step 1. Add the JitPack repository to your build file

Add it in your root settings.gradle at the end of repositories:

	dependencyResolutionManagement {
		repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
		repositories {
			mavenCentral()
			maven { url 'https://jitpack.io' }
		}
	}

Add it in your settings.gradle.kts at the end of repositories:

	dependencyResolutionManagement {
		repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
		repositories {
			mavenCentral()
			maven { url = uri("https://jitpack.io") }
		}
	}

Add to pom.xml

	<repositories>
		<repository>
		    <id>jitpack.io</id>
		    <url>https://jitpack.io</url>
		</repository>
	</repositories>

Add it in your build.sbt at the end of resolvers:

 
    resolvers += "jitpack" at "https://jitpack.io"
        
    

Add it in your project.clj at the end of repositories:

 
    :repositories [["jitpack" "https://jitpack.io"]]
        
    

Step 2. Add the dependency

	dependencies {
		implementation 'com.github.jordond:eiffel:5.0.6'
	}
	dependencies {
		implementation("com.github.jordond:eiffel:5.0.6")
	}
	<dependency>
	    <groupId>com.github.jordond</groupId>
	    <artifactId>eiffel</artifactId>
	    <version>5.0.6</version>
	</dependency>

                            
    libraryDependencies += "com.github.jordond" % "eiffel" % "5.0.6"
        
        

                            
    :dependencies [[com.github.jordond/eiffel "5.0.6"]]
        
        

Readme


Eiffel

Build Status JitPack Tweet

Logo

A Redux-inspired Android architecture library leveraging Architecture Components and Kotlin Coroutines.

Any questions or feedback? Feel free to contact me on Twitter @etiennelenhart.

Quick example

data class HelloEiffelState(val greeting: String = "Hello Eiffel") : State

sealed class HelloEiffelAction : Action {
    object NowInFrench : HelloEiffelAction()
    data class Greet(val name: String) : HelloEiffelAction()
}

val helloEiffelUpdate = update<HelloEiffelState, HelloEiffelAction> { action ->
    when (action) {
        is HelloEiffelAction.NowInFrench -> copy(greeting = greeting.replace("Hello", "Salut"))
        is HelloEiffelAction.Greet -> copy(greeting = "Salut ${action.name}")
    }
}

class HelloEiffelViewModel(initialState: HelloEiffelState) :
    EiffelViewModel<HelloEiffelState, HelloEiffelAction>(initialState) {
    override val update = helloEiffelUpdate
}

class HelloEiffelFragment : Fragment() {
    private val viewModel: HelloEiffelViewModel by eiffelViewModel()
    ...
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // example with View Bindings
        binding = FragmentHelloEiffelBinding.inflate(inflater, container, false)

        viewModel.state.observe(viewLifecycleOwner) { state ->
            binding.greetingText.text = state.greeting
        }
        binding.frenchButton.setOnClickListener { viewModel.dispatch(HelloEiffelAction.NowInFrench) }

        return binding.root
    }
    ...
}

Installation

build.gradle (project)

repositories {
    maven { url 'https://jitpack.io' }
}

build.gradle (module)

dependencies {
    implementation 'com.github.etiennelenhart.eiffel:eiffel:5.0.0'
    implementation 'com.github.etiennelenhart.eiffel:eiffel-test:5.0.0'
}

Features

Apart from providing Redux-like reactive ViewModels, Eiffel includes the following features to simplify common Android-related tasks and architecture plumbing:

  • First class support for Kotlin Coroutines and Flow
  • Powerful middleware functionality in the form of Interceptions with an easy-to-use DSL
  • Extended state observing for subscribing to specific state properties only
  • Convenient way to restore part or all of a state after process death
  • BindableState class to adapt one or more states for use with Data Binding
  • Simple option to pass Intent extras and Fragment arguments to a ViewModel's initial state
  • Implementation of a ViewEvent for one-off events inside of states
  • Resource wrapper to associate a status to LiveData
  • Delegated properties to lazily access a ViewModel indside an Activity or Fragment
  • A dedicated debug mode to trace all dispatched actions, interception calls and state updates
  • Separate testing module with JUnit rules to test async behavior and helpers to test a chain of Interceptions in isolation

Info on all of these and more can be found in the Wiki.

Interceptions DSL

Eiffel includes an easy-to-use Domain-specific language for creating a chain of Interceptions. This allows you to define the logic of your ViewModel domain in a simple and declarative way. Iterating on the quick example above, this is how you can define a set of interceptions in a few lines of code:

val helloEiffelInterceptions = interceptions<HelloEiffelState, HelloEiffelAction> {
    add(CustomInterception()) // your custom interception
    pipe { _, action -> Analytics.log("HelloEiffel", action) } // log something to analytics
    on<HelloEiffelAction.Greet> { // following will only react to 'Greet' action
        adapter("Upper case name") { _, action ->
            HelloEiffelAction.Greet(action.name.toUpperCase())
        }
        filter { state, action -> // ignore duplicate button presses and empty names
            !state.greeting.contains(action.name) || action.name.isNotBlank()
        }
    }
}

class HelloEiffelViewModel(initialState: HelloEiffelState) :
    EiffelViewModel<HelloEiffelState, HelloEiffelAction>(initialState) {
    override val update = helloEiffelUpdate
    override val interceptions = helloEiffelInterceptions
}

Migration

Migration guides for breaking changes: