All Projects → InventiDevelopment → Elmdroid

InventiDevelopment / Elmdroid

Licence: other
Minimalistic Android implementation of The Elm Architecture with android architecture components integration.

Programming Languages

kotlin
9241 projects

Projects that are alternatives of or similar to Elmdroid

Corerender
Moved to https://github.com/alexdrone/Render
Stars: ✭ 25 (+0%)
Mutual labels:  elm-architecture, unidirectional-data-flow
Teapot
Unidirectional Dataflow library for Android inspired by The Elm Architecture
Stars: ✭ 29 (+16%)
Mutual labels:  elm-architecture, unidirectional-data-flow
Reactor
🔄 Unidirectional data flow in Swift.
Stars: ✭ 174 (+596%)
Mutual labels:  elm-architecture, unidirectional-data-flow
Keemun
No description or website provided.
Stars: ✭ 13 (-48%)
Mutual labels:  elm-architecture, unidirectional-data-flow
Unidirectional Architecture On Mobile
Dive into 📱 Unidirectional Architecture!
Stars: ✭ 115 (+360%)
Mutual labels:  elm-architecture, unidirectional-data-flow
Reactivereswift
Unidirectional Data Flow in Swift via FRP - Inspired by Elm
Stars: ✭ 133 (+432%)
Mutual labels:  elm-architecture, unidirectional-data-flow
Render
UIKit a-là SwiftUI.framework [min deployment target iOS10]
Stars: ✭ 2,150 (+8500%)
Mutual labels:  elm-architecture, unidirectional-data-flow
Core
Starter kit for Core architecture.
Stars: ✭ 69 (+176%)
Mutual labels:  unidirectional-data-flow
gruid
Cross-platform grid-based UI and game framework.
Stars: ✭ 67 (+168%)
Mutual labels:  elm-architecture
software-systems-architecture
A collection of descriptions of the architecture that various systems use.
Stars: ✭ 24 (-4%)
Mutual labels:  architecture-components
android-kick-start-modular
Android Kick Start Project Template Framework FrogoBox || Admob, MVVM Archictecture || Clean Architecture Modularization
Stars: ✭ 16 (-36%)
Mutual labels:  architecture-components
Android-MVVM-News-App
MVVM News Application with clean code architecture & android jetpack components.
Stars: ✭ 38 (+52%)
Mutual labels:  architecture-components
trux
Unidirectional data layer for reactive user interfaces
Stars: ✭ 59 (+136%)
Mutual labels:  unidirectional-data-flow
tea-chess
A chess-themed tutorial on writing an SPA in Bucklescript-TEA
Stars: ✭ 28 (+12%)
Mutual labels:  elm-architecture
DaggerViewModel
An integration Module for injecting Google Architecture Components' ViewModel into Dagger2-injected Android components.
Stars: ✭ 67 (+168%)
Mutual labels:  architecture-components
MVI-Architecture-Android-Beginners
This repository contains a beginner sample app that implements MVI architecture
Stars: ✭ 199 (+696%)
Mutual labels:  architecture-components
GitMessengerBot-Android
타입스크립트, V8 엔진의 자바스크립트, 파이썬 그리고 Git을 지원하는 최첨단 메신저 봇!
Stars: ✭ 51 (+104%)
Mutual labels:  architecture-components
Simple-Notes-Kotlin-App
✍️ Simple Note Making App use mvvm architecture , dagger , coroutines and navigation component. Features includes 🗒️ create , edit and ❌ delete notes
Stars: ✭ 40 (+60%)
Mutual labels:  architecture-components
UniTEA
Implementation of The Elm Architecture for Unity3D
Stars: ✭ 31 (+24%)
Mutual labels:  elm-architecture
Android-Assignment
This assignment gives you basically a post list and its detail with comments.🚀
Stars: ✭ 32 (+28%)
Mutual labels:  architecture-components

Elmdroid

This library helps you implement The Elm Architecture(TEA) on Android. This well known unidirectional architecture is incredibly easy to use but not so simple to set up and that's why we created this library. To make things even simpler, we added integration with RxJava, LiveData, and optionally also ViewModel.

Basic concepts

This is not an introduction guide to TEA, if you are not familiar with it or with similar unidirectional architectures we strongly recommend to read the TEA docs.

Basically the only thing you have to implement is interface Component<STATE : State, MSG : Msg, CMD : Cmd> which is then wrapped inside ComponentRuntime. Look at the picture below to see how is your component integrated with rest of the app:

Runtime vs UI

ComponentRuntime just receives messages from UI and translates them into the from of new states, using Component.update(..) function. UI then simply observes state LiveData<State> and renders the changes.

Usage

Basic synchronous example

Let's say we want to implement a simple screen with two buttons for increment/decrement actions and plain TextView to keep track of current "score". You can find this example in official elm examples

First we have to define a state. That can be simply represented as a Kotlin data class. There is nothing interesting about it, it just holds the current "score" for our counter:

data class CounterState(val counter: Int) : State

Now let's define messages(Msg). You can think about it as defining all the possible events that can happen on a screen:

sealed class CounterMsg : Msg
object Increment : CounterMsg()
object Decrement : CounterMsg()

Now when we have all the basic building blocks defined, we can put everything together in CounterComponent:

class CounterComponent: SimpleComponent<CounterState, CounterMsg> {
    override fun initState(): CounterState = CounterState(0)

    override fun simpleUpdate(msg: CounterMsg, prevState: CounterState): CounterState = when(msg){
        is Increment -> CounterState(prevState.counter + 1)
        is Decrement -> CounterState(prevState.counter - 1)
    }
}

SimpleComponent is simplified version of Component created for purely synchronous usage. The most important part is simpleUpdate(msg, prevState) function. It takes incoming message with the previous state and defines a new state to render. initState() simply defines what should be the original state before any Msg arrives.

With this prepared component, we can simply use it in our activity or fragment:

class CounterActivity : AppCompatActivity() {

    private lateinit var runtime: ComponentRuntime<CounterState, CounterMsg>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_counter)
        supportActionBar?.title = getString(R.string.basic_counter)

        runtime = RuntimeFactory.create(CounterComponent())

        runtime.state().observe(this, Observer {
            it?.let { counter.text = "${it.counter}" }
        })

        increment.setOnClickListener { runtime.dispatch(Increment) }
        decrement.setOnClickListener { runtime.dispatch(Decrement) }
    }

    override fun onDestroy() {
        super.onDestroy()
        runtime.clear()
    }
}

We need to wrap our component in ComponentRuntime which gives you the ability to observe the current state as LiveData and to dispatch Increment and Decrement messages. Make sure you call clear() on runtime in onDestroy() to prevent memory leaks or you can pass LifecycleOwner to RuntimeFactory.create and runtime will handle the clear() call automatically.

    runtime = RuntimeFactory.create(CounterComponent(), this /* LifecycleOwner (for example your fragment or activity */ )

//  no need for manual clearing
//    override fun onDestroy() {
//        super.onDestroy()
//        runtime.clear()
//    }

You can go even further and use createRuntimeFor(component) helper method that also handles lifecycle for you.

    runtime = createRuntimeFor(CounterComponent())

If you want your component to survive configuration changes you have to handle it yourself or you can use ElmViewModel and pass in your component:

class CounterViewModel : ElmComponentViewModel<CounterState, CounterMsg, Nothing>(CounterComponent())

or extend ElmBaseViewModel and implement your component logic right inside the subclass. Either way, your component will survive configuration changes inside it's ViewModel and the ComponentRuntime.clear() will be called in ViewModel.onCleared() for you. You can then use your ViewModel in Activity/Fragment the same way we used runtime above because ElmComponentViewModel is essentially just an implementation of runtime:

class CounterActivity : AppCompatActivity() {

    private lateinit var viewModel: CounterViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_counter)
        supportActionBar?.title = getString(R.string.basic_counter)

        viewModel = ViewModelProviders.of(this).get(CounterViewModel::class.java)

        viewModel.state().observe(this, Observer {
            it?.let { counter.text = "${it.counter}" }
        })

        increment.setOnClickListener { runtime.dispatch(Increment) }
        decrement.setOnClickListener { runtime.dispatch(Decrement) }
    }
}

And because we want to make it even easier, we provide getViewModelFor(component) function which gives you the option not to define ElmComponentViewModel subclass and just ask for instance of ElmComponentViewModel wrapping given component.

    viewModel = getViewModelFor(CounterComponent())

Internally it uses ViewModelProviders.of() so there is no magic and no surprise behaviour. getViewModelFor(component) is just a huge shortcut where otherwise you would have to implement your ViewModelFactory etc.

Check out samples to explore even more.

Commands and Subscriptions

If you want to perform asynchronous an action you have two options: start one time async task with a Cmd that returns a single result Msg back to your update function or you can set up a Subscription and listen to continuous stream of messages.

Commands

All commands start tasks. Task is a simple function that returns RxJava Single<Msg>. For example this login Task:

fun loginTask(email: String, password: String): Single<LoginMsg> {
    val repo = UserRepository()
    repo.loginUser(email, password)
    return Single.just(repo.getUser())
}

To start this task your update function needs to return some Cmd

override fun update(msg: LoginMsg, prevState: LoginState): Pair<LoginState, LoginCmd?> = when(msg) {
        is LoginClicked ->      prevState.copy(loadingVisible = true, loginEnabled = false, msgText = "") withCmd LoginAction(prevState.email, prevState.password)
        is LoginSuccess ->      prevState.copy(loadingVisible = false, msgText = "Login Successful, welcome ${msg.username}", email = "", password = "").updateLogin().noCmd()
}

Notice we use standard update function from normal Component interface and not the "simple" versions. The only difference is that our update function is now returning not just the new state but also possible Cmd to start some new async tasks.

After LoginClicked message arrives we return state with loading = true and we also use withCmd to return LoginAction Cmd with it. This is the way to specify immediate state change and async action that should follow. We need to define the last missing piece, the call() function that defines which Cmd starts which Task.

override fun call(cmd: LoginCmd): Single<LoginMsg> = when(cmd) {
        is LoginAction -> loginTask(cmd.email, cmd.password)
}

call() function specifies how to handle every Cmd that drops out of the update() function. Our loginTask returns LoginSuccess and this message is run through the update() function again to finally display some logged in state.

Subscriptions

You can define two types of subscriptions

  1. StatelessSub - Simply starts during ComponentRuntime creation and ends when ComponentRuntime is cleared. It is not affected by any state changes.
  2. StatefulSub - It has the same lifetime but it's given the opportunity to change anytime the state is changed. It can even define it's own filter policy, so it can be interested only in some particular state changes.

Example of Stateless subscription could be this LoginSubscription that notifies update function with a new message anytime a logged user changed. It takes the information about the logged user from the repository not from the current view state and that's why it's Stateless it has no interest in state and it's not influenced by it in any way. Here is how this subscription might look like:

class UserSubscription : StatelessSub<LoginState, LoginMsg>() {
    override fun invoke(): Observable<LoginMsg> = UserRepository().getUser().map {
        LoggedUserChanged(it.username)
    }
}

It is simply an object that extends StatelessSub and overrides invoke() function that returns stream of messages represented in a form of Observable.

Now let's say we want a similar sub that would emmit Tick event every second after some user logged in. This subscription will take choose the view state object as it's source of truth about the logged in user and that's why we call it Stateful

class CounterSubscription : StatefulSub<LoginState, LoginMsg>() {
    override fun invoke(state: LoginState): Observable<LoginMsg> = when {
        state.loggedUsername.isNotBlank() -> Observable.interval(1, TimeUnit.SECONDS).map { Tick }
        else -> Observable.empty()
    }
    override fun isDistinct(s1: LoginState, s2: LoginState) = s1.loggedUsername != s2.loggedUsername
}

As you can see, the super class is now called StatefulSub and the invoke(state) function now takes state as a parameter, so your stream of data will be restarted every time a new state comes out. The state is considered new if it's not equal (using equals() method) to the previous one, if you want to customize this behaviour just override isDistinct and choose your own rules for equality. As you can see in this example, we are only interested in new view states if they have different loggedUsername than the previous state.

Check out samples to explore even more complex solutions.

Logging

You can enable logging individually for every single ComponentRuntime

runtime = RuntimeFactory.create(LoginComponent(), LogLevel.BASIC)

or set it globally via RuntimeFactory

RuntimeFactory.defaultLogLevel = if (BuildConfig.DEBUG)  LogLevel.FULL else LogLevel.NONE

As you can see there are three different levels of LogLevel and the default one is set to LogLevel.NONE.

Testing

TEA is ideal for testing. Check out how we handle testing in sample testing code. For example we were able to test counter component like this:

verifyStates(runtime, observer,
    CounterState(0),
    Increment to CounterState(1),
    Decrement to CounterState(0),
    Decrement to CounterState(-1),
    Decrement to CounterState(-2),
    Increment to CounterState(-1),
    Increment to CounterState(0)
)

We simply compare messages to the expected result state.

Download

Download via jitpack for now

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

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

And add the dependency:

dependencies {
    implementation 'com.github.InventiDevelopment:Elmdroid:0.5.1'
}

jcenter and mavenCentral upload is in progress :)

License

Copyright 2017 INVENTI Development s.r.o.

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.
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].