Home Android & Kotlin Books Functional Programming in Kotlin by Tutorials

18
Mobius — A Functional Reactive Framework Written by Massimo Carli

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

In Chapter 15, “Managing State”, you learned the importance of the concept of state. A state is usually defined as some value that can change over time. You also learned that what you consider a state defines side effects. A side effect is something that changes the state of the world, which is outside the context of a function.

Side effects aren’t harmful as long as you can control them. In Chapter 16, “Handling Side Effects”, you saw how important it is to separate the description of an effect from its actual execution.

These are all the fundamental principles used by Mobius, which is defined as: “a functional reactive framework for managing state evolution and side-effects, with add-ons for connecting to Android UIs and RxJava Observables. It emphasizes separation of concerns, testability, and isolating stateful parts of the code”.

In this chapter, you’ll learn:

  • The main concepts Mobius is based on.
  • What the Mobius loop is and how it works.
  • What the Mobius workflow is, and how to apply it to a real case.
  • How Mobius works with Android.
  • How Mobius handles side effects.

You’ll do this by creating a Mobius version of the RayTV app you met in Chapter 14, “Error Handling With Functional Programming”. You’ll call it Raybius. :]

Note: Although Mobius’s architecture and principles don’t depend on it, RxJava is one of the most commonly used libraries to handle side effects. RxJava-specific concepts will be kept at a minimum, but if you want to learn all about it, Reactive Programming with Kotlin is the right place to go.

Note: The Raybius app uses Dagger and Hilt. If you want to learn all about the Android dependency injection framework, Dagger by Tutorials is the perfect book for you.

Mobius principles and concepts

To understand how Mobius works, just think about a typical mobile app. You have some UI that displays some information. You usually interact with the UI by pressing some buttons or providing some input. This triggers some actions to access, for instance, a server, fetch some data and show it in the UI. This might look like an overly simplified description of what usually happens, but the reality isn’t far off. You can represent the flow like in Figure 18.1:

Figure 18.1: The Mobius loop
Figure 18.1: The Mobius loop

This image has numerous interesting concepts you can easily understand by following the flow described earlier.

When you launch your app, you can see a UI, which is usually a composition of views, like TextViews, Buttons and so on. You can think of a view as a way to represent some data. When the data changes, the UI usually changes. This is important because you can think of the data as the current state of the UI. The data you want to display with the UI is usually represented as the model.

As mentioned earlier, the user interacts with the UI by pressing some buttons or providing some data as input. Mobius represents these actions as events. An event is what makes an app interesting. Some events just update the UI, creating a new model to display. Others are more complicated because they trigger a request to the server or access to a database.

To handle both use cases, Mobius provides an Update function. It receives the current model and the input event, and returns the new model and an optional description of a side effect. It’s crucial to see how the Update function lives in the pure section of the diagram. The Update function is pure. This is because:

  • The new model just depends on the input model/state and event.
  • It returns a description of the side effects you need to execute eventually.

This makes the Update function very easy to test. Not represented in Figure 18.1 is the Init function. Init is a version of Update that generates the first state and, optionally, the first set of effects to generate.

Now, Mobius sends the new model to the UI and the optional effects to some effect handlers. These are what actually execute the side effects, usually in a background thread. It’s also interesting to see that effect handlers notify the outcome using events and how they go through the same flow you saw earlier for the events from the UI.

The Update function is invoked, and a new state/model is created along with other optional side effect descriptions.

This is an example of a unidirectional flow. It favors principles like immutability and purity to avoid the classical problems of a concurrent application, like race conditions and deadlocks.

Finally, an event source represents a generic component able to generate events even without a specific interaction from the user. Think about the events related to the battery level or your device going offline and then back online again.

The previous architecture is straightforward and gives each component a clear responsibility, as the separation of concerns principle suggests. This also allows the implementation of a process named the Mobius workflow.

The Mobius workflow

In the previous section, you learned that the main concepts in Mobius are:

Model your app

Models in Mobius can represent different concepts like:

Figure 18.2: The initial MoFlow
Xagabo 38.1: Ndu arupiay HoTqer

Figure 18.3: The MoFlow with user interaction events
Kefako 26.8: Qre QoNsom yevt isur epguqaqroos oreqds

Figure 18.4: Adding effects
Finezu 35.7: Eczevc ilyotvl

Figure 18.5: Define models
Rorihi 08.9: Xinefa buwomg

Figure 18.6: Define effects feedback events
Cusiji 71.9: Qoteta ayhetnz neektuxr ihiwvd

Figure 18.7: Additional effects
Doqafa 77.9: Aqvapoiluz oydoxvl

Describe your app

When you have the MoFlow for your app, you can start implementing the Update function. Update is a pure function that receives the event and the current model as input and generates the new model and the set of effects as output. In Mobius, an instance of the Next class encapsulates this information.

Figure 18.8: The Raybius app
Damuwu 15.7: Szi Garvuor igx

Figure 18.9: Some TV shows as results
Yowagi 03.9: Zixu PK hxobj er tigozyb

data class TvShowModel(
  val searchEnabled: Boolean = false, // 1
  val inputText: String = "", // 2
  val loading: Boolean = false, // 3
  val searchResults: List<ScoredShow> = emptyList(), // 4
  val error: Boolean = false // 5
)
fun FragmentSearchBinding.logic(model: TvShowModel) {
  if (model.loading) {
    showLoading()
  } else {
    hideLoading()
  }
  displayResults(model.searchResults)
  if (model.error) {
    errorMode()
  }
  searchButton.isEnabled = model.searchEnabled
}
fun FragmentSearchBinding.initUI(
  eventConsumer: Consumer<TvShowEvent>,
  onItemSelected: (Int) -> Unit
) {
  with(resultRecyclerView) {
    adapter = ScoredShowAdapter(onItemSelected)
    layoutManager = LinearLayoutManager(context)
    visibility = View.GONE
  }
  helpMessage.visibility = View.VISIBLE
  progressBar.visibility = View.GONE
  textUpdate { text ->
    eventConsumer.accept(InputTextChanged(text)) // 1
  }
  search {
    eventConsumer.accept(SearchButtonClicked) // 2
  }
}
sealed class TvShowEvent
data class InputTextChanged(val text: String) : TvShowEvent()
object SearchButtonClicked : TvShowEvent()
data class TvSearchSuccess(
  val results: List<ScoredShow>
) : TvShowEvent()
data class TvSearchFailure(val ex: Throwable) : TvShowEvent()
val tvShowLogic: TvShowUpdate = object : TvShowUpdate {
  override fun update(
    model: TvShowModel, event: TvShowEvent
    ): Next<TvShowModel, TvShowEffect> =
    when (event) {
      is InputTextChanged -> Next.next( // 1
        model.copy(
          searchEnabled = event.text.length >= 3,
          inputText = event.text
        )
      )
      is SearchButtonClicked -> Next.next( // 2
        model.copy(loading = true),
        setOf(SearchTvShow(model.inputText))
      )
      is TvSearchSuccess -> Next.next( // 3
        model.copy(
          searchResults = event.results,
          searchEnabled = true
        ), setOf(HideKeyboard)
      )
      is TvSearchFailure -> Next.next( // 4
        model.copy(
          error = true,
          searchEnabled = true
        ), setOf(
          HideKeyboard, DisplayErrorMessage(
            event.ex
          )
        )
      )
      else -> Next.noChange()
    }
}

Plan your app

In this step, you basically design how your app should do all the tasks you described in the Update function. The actual code should be part of the next step, but right now, you’ll look at how different effects are executed. You already know that an effect is basically the description of an operation that changes the external world. The component responsible for actually executing an effect is called an effect handler. Mobius provides different ways to implement an effect handler.

sealed interface TvShowEffect
data class SearchTvShow(val query: String) : TvShowEffect
data class DisplayErrorMessage(
  val error: Throwable
) : TvShowEffect
object HideKeyboard : TvShowEffect
  override fun handleErrorMessage(effect: DisplayErrorMessage) {
    val errorMessage = effect.error.localizedMessage
      ?: activityContext.getString(R.string.generic_error_message)
    Toast.makeText(
      activityContext, errorMessage, Toast.LENGTH_SHORT
    ).show()
  }
  override fun handleSearchTvShow(
    request: Observable<SearchTvShow>
  ): Observable<TvShowEvent> =
    request
      .flatMap { request ->
        fetchAndParseTvShowResult(request.query).fold( // 1
          onSuccess = {
            Observable.just(
              TvSearchSuccess(it.filter(removeIncompleteFilter))
            )
          },  // 2
          onFailure = {
            Observable.just(TvSearchFailure(it))
           } // 3
        )
      }
  @Provides
  fun provideEffectHandler(
    uiHandler: UIEffectHandler,
    apiRequestHandler: ApiRequestHandler
  ): TvShowEffectHandler =
    RxMobius.subtypeEffectHandler<TvShowEffect, TvShowEvent>()
      .addTransformer(
        SearchTvShow::class.java,
        apiRequestHandler::handleSearchTvShow
      ) // 1
      .addConsumer(
        DisplayErrorMessage::class.java,
        uiHandler::handleErrorMessage, // 2
        AndroidSchedulers.mainThread()
      )
      .addConsumer(
        HideKeyboard::class.java,
        uiHandler::handleHideKeyboardMessage,
        AndroidSchedulers.mainThread()
      )
      .build();

Build your app

In this step, you’ll finish implementing the MoFlow table you described earlier. It’s time for you to use Mobius to implement the feature to display TV show details.

Implementing the TvShowDetail feature

Now, you’ll add the show detail feature. To do this, you need to:

Model update

Open TvShowModel.kt, and add the following constructor property:

val detailResult: ShowDetail? = null

Adding new events

Now, open TvShowEvent.kt, and add the following events:

data class DetailViewResumed(
  val id: Int
) : TvShowEvent() // 1
data class ItemClicked(
  val id: Int
) : TvShowEvent() // 2
data class TvShowDetailSuccess(
  val results: ShowDetail
) : TvShowEvent() // 3
data class TvShowDetailFailure(
  val ex: Throwable
) : TvShowEvent() // 4

Adding effects

Add new effects. Open TvShowEffect.kt, and add the following code:

data class NavigateToDetail(val showId: Int) : TvShowEffect // 1
data class GetTvShowDetail(val showId: Int) : TvShowEffect // 2

Update the update function

To bind events and effects together, you need to open TvShowLogic.kt and add the following code to the update when block:

is ItemClicked -> Next.next( // 1
  model, setOf(NavigateToDetail(event.id))
)
is DetailViewResumed -> Next.next( // 2
  model.copy(loading = true), setOf(GetTvShowDetail(event.id))
)
is TvShowDetailSuccess -> Next.next( // 3
  model.copy(loading = false, detailResult = event.results)
)
is TvShowDetailFailure -> Next.next( // 4
  model.copy(loading = false),
  setOf(DisplayErrorMessage(event.ex))
)

Add new effect handlers

In the previous code, you defined the new NavigateToDetail and GetTvShowDetail effects. It’s now time to tell Mobius how to execute them. They’re both effects that need to generate some events as a result. Open UIEffectHandler.kt, and add the following definition to the interface:

fun handleNavigateToDetail(
  request: Observable<NavigateToDetail>
): Observable<TvShowEvent>
override fun handleNavigateToDetail(
  request: Observable<NavigateToDetail>
): Observable<TvShowEvent> = request
    .observeOn(AndroidSchedulers.mainThread())
    .map { request ->
      val activity = activityContext as AppCompatActivity
      activity.supportFragmentManager.beginTransaction()
        .replace(R.id.anchor, TvShowDetailFragment())
        .addToBackStack("Detail")
        .commit()

      DetailViewResumed(request.showId)
    }
fun handleTvShowDetail(
  request: Observable<GetTvShowDetail>
): Observable<TvShowEvent>
override fun handleTvShowDetail(
  request: Observable<GetTvShowDetail>
): Observable<TvShowEvent> = request
    .flatMap { request ->
      fetchAndParseTvShowDetailResult(request.showId).fold(
        onSuccess = { Observable.just(TvShowDetailSuccess(it)) },
        onFailure = { Observable.just(TvShowDetailFailure(it)) }
      )
    }
.addTransformer(
  GetTvShowDetail::class.java, // 1
  apiRequestHandler::handleTvShowDetail
)
.addTransformer(
  NavigateToDetail::class.java, // 2
  uiHandler::handleNavigateToDetail
)

Update UI logic

You still need to do a few last things to handle user events and use the data you receive from the new effects. You basically need to:

searchBinding.initUI(eventConsumer) { showId ->
  eventConsumer.accept(ItemClicked(showId))
}
if (model.detailResult != null) { // 1
  displayResult(model.detailResult) // 2
}
Figure 18.10: The detail screen
Niputi 13.19: Gvi ziqiah jqriak

Mobius loop in Android

In Figure 18.1, you learned that Mobius’s architecture is based on the creation of the Mobius loop, which is responsible for:

@AndroidEntryPoint
class MainActivity :
  AppCompatActivity(), MobiusHost<TvShowModel, TvShowEvent> {

  @Inject
  lateinit var tvShowController: TvShowMobiusController // 1

  override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    tvShowController.connect(::connectViews) // 2
    // ...
  }

  override fun onResume() {
    super.onResume()
    tvShowController.start() // 3
  }

  override fun onPause() {
    super.onPause()
    tvShowController.stop() // 4
  }

  override fun onDestroy() {
    super.onDestroy()
    tvShowController.disconnect() // 7
  }

  lateinit var eventConsumer: Consumer<TvShowEvent>

  private fun connectViews(
    eventConsumer: Consumer<TvShowEvent>
  ): Connection<TvShowModel> {
    this.eventConsumer = eventConsumer
    return object : Connection<TvShowModel> {
      override fun accept(model: TvShowModel) { // 5
        logic(eventConsumer, model)
      }

      override fun dispose() { // 6
      }
    }
  }

  var logic: (
    Consumer<TvShowEvent>, TvShowModel
  ) -> Unit = { _, _ -> }
  override fun injectLogic(
    logic: (Consumer<TvShowEvent>, TvShowModel) -> Unit
  ) { // 8
    this.logic = logic
  }
}

Key points

  • Mobius is a functional reactive framework for managing state evolution and side effects, with add-ons for connecting to Android UIs and RxJava Observables.
  • Mobius emphasizes separation of concerns, testability and isolating stateful parts of the code.
  • Mobius is an example of unidirectional flow architecture.
  • The MoFlow is a process that allows you to design your app in terms of models, events and effects.
  • The model represents the current state of the UI.
  • You can use events to represent a user interaction or the result of an effect.
  • An effect is the description of a task that might change the state of the world.
  • Models, events and effects are immutable.
  • The Update function is a pure function, receiving the current model and the event as input, and returning the new model and the optional effects.
  • The purity of the Update function makes it very easy to test.
  • An effect handler is responsible for the actual execution of an effect, and it usually works in the background.

Where to go from here?

Congratulations! In this chapter, you learned how the Mobius framework works as an example of a unidirectional architecture that uses many of the principles you learned about functional programming. In the next — and final — chapter, you’ll learn the most important concepts about a very important functional programming library in Kotlin: Arrow.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

© 2022 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.