Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lessons learned after successfully using Flux architectures on Android in the latest years

Lessons learned after successfully using Flux architectures on Android in the latest years

Watch the presentation: https://youtu.be/jOTO2VYaIpU

When Facebook proposed Flux ideas years ago, the world of web development changed forever.
However, in the mobile apps universe we stayed immutable (which in this case is not a really good thing), and well-established architectures like CLEAN or MVVM remained as the default ones to build new apps.
However, in the Android development team at BQ we decided to move on from these old friends to a new promising but unknown one: Flux.
The aim of this talk is to share the lessons learned after more than 3 years working with our own implementation of Flux architecture called Mini, which right now can be found as open-source projects for both Android (https://github.com/bq/mini-kotlin) and iOS (https://github.com/bq/mini-swift).
In this talk I cover not only the principles behind this architecture, how it has evolved and how we use it, but also the benefits we have achieved by using them in several Android BQ projects related to camera, IoT, 3D printing and back-end based products.

Daniel Gallego Vico

December 20, 2019
Tweet

More Decks by Daniel Gallego Vico

Other Decks in Programming

Transcript

  1. [email protected] @thanos_malkav • Mobile Software Engineering Manager at bq •

    Android lover since Eclair (2.1) • Zombies In The Lab founder • PhD on Telecom Engineering (Mobile RecSys) • Comic lover, board gamer, traveller & improv guy Who is in front of you? Daniel Gallego
  2. When CLEAN became dirty Hardware projects led to new challenges

    • By the end of 2016 we started bq camera from scratch • Initially we thought about using our well-known CLEAN implementation... • But many problems appeared due to managing complex state machines working at the same time in the app ◦ No proper “place” to model the states of the app ◦ Interactors orchestration was really difficult ◦ Bidirectional data flow is hell with HW → side effects Presented at Droidcon Madrid 2016
  3. Building blocks The pieces of the puzzle Unidirectional/Circular data flow

    to avoid side effects Logic is triggered by actions and views react to state changes Architecture layers totally decoupled Dispatcher Store State (b) View Action State(a) Reducer
  4. Reasons to use Flux State-centered architecture • The states contain

    the models of the application • They are plain and immutable objects • Every state change generates a new immutable object observed by the view • By using immutability this way, consistency in the business logic operations is ensured • The state can be recovered after a view is recreated • The history of state changes can be recorded for logging/debugging purposes • Reproducing an error is as easy as setting a suitable state • Useful when working with complex state machines
  5. Building blocks State data class PhotoState(val pictureType: PictureType = PictureType.NORMAL,

    val timerTicking: Boolean = false, val burstData: BurstData = BurstData(), val capturing: Boolean = false)
  6. Reasons to use Flux Stores listen to actions to change

    state • The state for each feature is saved only in one store instead of splitting it through different layers • Only defined actions trigger state changes • Every action must change the state of at least one store • Impossible to cause undesired effects as undefined actions are not supported • State mutation is done by reducer functions existing in the stores • Given a specific action (input), the state change (output) will always be the same ◦ Reducers are pure functions! ◦ Useful for unit testing • No side effects → remove common bugs
  7. Building blocks Action class TakePhotoAction : BaseAction() data class BatteryStatusChangedAction(val

    newStatus: BatteryStatus) : BaseAction() Represents a use case It is a plain immutable object Holds (when needed) the information to be performed
  8. Building blocks Store class PhotoStore constructor(val dispatcher: Dispatcher, val photoController:

    PhotoController) : Store<PhotoState>() { ... } A holder for state Can be observed Listens for actions to update state via Reducers
  9. Building blocks Subscribing to an action class PhotoStore constructor(val dispatcher:

    Dispatcher, val photoController: PhotoController) : Store<PhotoState>() { @Reducer fun onShutterButtonPressed(action: TakePhotoAction) { newState = state.copy(capturing = true) val options = CaptureOptions(pictureType = state.pictureType) photoController.takePicture(options) } } The store is subscribed to actions via @Reducer annotation The reducer code is in charge of changing the state And executes the associated business logic
  10. Reasons to use Flux UI components are not attached to

    the architecture • States are not bound to views • Views dispatch actions to change states • Views observe the state they want to represent • Removes common bugs by totally decoupling the view from the business logic
  11. Building blocks Dispatching an action after interacting with the UI

    class PhotoControlsView { private fun onShutterButtonClick() { when (sessionStore.state.config.mode) { SessionInputMode.PHOTO_AUTO, SessionInputMode.PHOTO_MANUAL, SessionInputMode.PORTRAIT -> { dispatcher.dispatch(TakePhotoAction()) } } } }
  12. Building blocks Observing state changes to update the UI class

    PhotoControlsView { private fun trackThumbnailChanges() { photoStore.flowable() .subscribe { //Logic to draw the thumbnail } .track() } }
  13. State(a) Action 1 State(b) State(c) Action 2 Update view Update

    view Reasons to use Flux The view cycle Reducer 1 Reducer 2
  14. Reasons to use Flux Dispatcher, the actions hub • Observe/Intercept

    actions → Interceptor pattern • Useful to “enforce” architecture: unified point where all actions go through • Single threaded on main thread • Actions can be dispatched in Views or Controllers • Stores are subscribed to specific actions through the Dispatcher via Reducers Action1 Action2 Dispatcher Store<A> Store<B> Store<C> View Controller
  15. PhotoControlsView bq Camera • First version released in May 2017

    • Since then: ◦ > 2M users ◦ > 20 smartphone models ◦ > 12 countries ◦ 99% of users free or errors • Let’s see Flux in action!
  16. PhotoControlsView Dispatcher PhotoStore PhotoState change state PhotoController take picture 1

    2 3 3 bq Camera Reducer 4 update view with “quick” picture Camera API 2 Take real picture (Android framework) 4 TakePhotoAction
  17. PhotoControlsView Dispatcher PhotoStore PhotoState PhotoController take picture 1 2 3

    bq Camera Camera API 2 Take real picture (Android framework) 4 5 TakePhotoAction PhotoAvailableAction
  18. PhotoControlsView Dispatcher PhotoStore PhotoState change state PhotoController take picture 1

    2 3 6 bq Camera Reducer Camera API 2 Take real picture (Android framework) 4 TakePhotoAction 5 PhotoAvailableAction
  19. PhotoControlsView Dispatcher PhotoStore PhotoState change state PhotoController take picture 1

    2 3 6 bq Camera Reducer Camera API 2 Take real picture (Android framework) 4 7 update view with real picture TakePhotoAction 5 PhotoAvailableAction
  20. • First IoT project where we applied Flux architecture •

    The printer has a big set of sensors providing info at the same time • Again, we had a complex state machine to model in the app • MQTT was selected as the protocol to communicate with the printer sensors • We developed a transformer to pass from MQTT messages to Flux actions • MQTT became a pure implementation detail Witbox Go! The first Qualcomm Android 3D printer in the market
  21. Zetup MQTT communications API interface CommunicationsController { fun connect(broker: String):

    Completable fun disconnect(): Completable fun publish(topic: String, message: String): Completable fun subscribeToTopic(topic: String): Completable fun getTopicMessagesFlowable(): Flowable<Response> fun getConnectionStatusFlowable() : Flowable<CommunicationsState.Status> } data class Response(val fromTopic: String, val messageReceived: String) data class CommunicationsState(val status: Status = Status.DISCONNECTED, val serverIp: String = BuildConfig.BROKER_IP) { enum class Status { DISCONNECTED, CONNECTING, CONNECTED } }
  22. Zetup Transforming MQTT messages to Flux actions class CommunicationsStore (private

    val dispatcher: Dispatcher) : Store<CommunicationsState>() { @Inject lateinit var communicationsController: CommunicationsController private fun trackTopicsChanges() { communicationsController.getTopicMessagesFlowable() .subscribeOn(Schedulers.io()) .subscribe( { (fromTopic, messageReceived) -> when (fromTopic) { // Dispatch the proper action including as payload the message received in the topic } }, { e -> Timber.e(e, "Message unsuccessfully retrieved") } ) } }
  23. Mini A minimal implementation of Flux architecture • Mini Kotlin

    ◦ Initial commit in Feb 2017 • Mini Swift ◦ Initial commit in March 2019 • Successfully used in several projects ◦ Camera ◦ 3D printing ◦ IoT ◦ Backend-based products https://github.com/bq/mini-kotlin https://github.com/bq/mini-swift
  24. Mini Kotlin Modules • mini-common ◦ All the core components:

    Store, Dispatcher, Action... • mini-processor ◦ Annotation processor logic and utils to generate code (e.g. Reducers) • mini-android ◦ Android utilities for using coroutine-based subscriptions in Activities/ViewModels/Fragments • mini-rx2 & mini-rx2-android ◦ Android utilities for using Rx-based subscriptions in Activities/ViewModels/Fragments ◦ Custom Rx operators to simplify common operations in Flux ▪ E.g. StateMerger to combine multiple state flowables into a single list • mini-kodein & mini-kodein-android ◦ Utilities for easy integration of Kodein in a Mini-based app
  25. Mini Kotlin: testing TestDispatcherRule @get:Rule val testDispatcher = testDispatcherRule() @Test

    fun login_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))) } Checking that an action is correctly dispatched by intercepting it when arrives to the Dispatcher
  26. Mini Kotlin: testing CleanStateRule @get:Rule val cleanState = cleanStateRule() @Test

    fun login_redirects_to_home_and_welcome_user_with_success_task() { //Set login state to success onUiSync { val loggedUser = User( email = MockModels.anyEmail, username = MockModels.anyUsername ) val state = SessionState().copy( loginRequestState = requestSuccess(), loggedIn = true, loggedUser = loggedUser ) sessionStore.setTestState(state) } //Check that the user name is shown after login onView(withId(R.id.welcome_text)).check(matches(withText(MockModels.anyUsername))) } Checking that a view correctly changes with an specific state
  27. • ViewModel ◦ A bridge between the core architecture (Flux)

    and the View side ◦ Contains the logic to dispatch actions and subscribe to state changes ◦ Transform the data model received from the stores into ViewData (UI-related data) objects ◦ Lifecycle-aware and resilient to configuration changes • LiveData ◦ Light observable holding the ViewData information ◦ Simplify the UI layers by avoiding Rx / Coroutines ◦ Lifecycle-aware • https://github.com/bq/android-architecture-example Flux gives you freedom to use any UI pattern MVVM based on Android Architecture Components
  28. Lessons learned Cons • Hiring is more complicated • Candidates

    are usually less familiar with concepts like Flux/Redux, MVI… • Understand how to properly use Mini was not easy for new members of the team • Exponential learning curve at the beginning... but the results deserve the effort
  29. Lessons learned Pros • Apps based on unidirectional data flows

    are much easier to understand • Flux makes defining decoupled use cases simpler • Easy to work at the same time in different verticals: Action + State + Store + Controller • <Actions, State> definition is the necessary contract to let people start working on the UI layer • What and how to test is clearer in every layer • Fast development (especially in big teams)