Chapters

Hide chapters

Functional Programming in Kotlin by Tutorials

First Edition · Android 12 · Kotlin 1.6 · IntelliJ IDEA 2022

Section I: Functional Programming Fundamentals

Section 1: 8 chapters
Show chapters Hide chapters

Appendix

Section 4: 13 chapters
Show chapters Hide chapters

18. Mobius — A Functional Reactive Framework
Written by Massimo Carli

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 in its documentation 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:

  • Models
  • Events
  • Effects
  • Init functions
  • Update functions

This implies that creating an app with Mobius means defining these same concepts in the domain of the app itself. This leads to a sequence of steps you can follow every time in the design and implementation of your app. This process has a name: the Mobius workflow. It consists of the following four steps:

  1. Model or MoFlow (Short for “Mobius Flow”)
  2. Describe
  3. Plan
  4. Build

It’s interesting now to see each of these in detail in the context of the Raybius app.

Model your app

Models in Mobius can represent different concepts like:

  • The current state of the app.
  • An event generated by a user action.
  • An external event.
  • An event result of a side effect’s execution.

Usually, the steps are the following:

External events definition: In your case, the Raybius app doesn’t handle any external events, but it could. For instance, you could display a message and disable the input if the device is offline and then restore that functionality when it reestablishes a connection. To track all the use cases, you create a table like the following. This is the MoFlow table.

Figure 18.2: The initial MoFlow
Figure 18.2: The initial MoFlow

Because you don’t have external events, the table is initially empty. No problem! You deal with user interactions next.

Capture user interactions: These represent the possible events you can generate when the user interacts with the app. In your case, you can enter the name of a TV show in the EditField and tap the button to perform the search. This leads to the following events:

Figure 18.3: The MoFlow with user interaction events
Figure 18.3: The MoFlow with user interaction events

When the user changes the text to use as input, the app generates an InputTextChanged with the new text. When the user clicks SEARCH, it triggers SearchButtonClicked.

Define effects: Here, you need to think about the effects the previous events can generate. In your case, you only have one effect — the one related to the request to the server for fetching the information about your TV show. Call this effect SearchTvShow, and add it to the table like this:

Figure 18.4: Adding effects
Figure 18.4: Adding effects

Define your model: In this step, you need to define the model as a representation of your app’s view. In Raybius, you enter text, so you probably want to disable the SEARCH button if the text field is empty or the text string is too short. So, the model should then contain some kind of property that enables or disables the SEARCH button. If you decide to handle the offline case, you also need a variable that disables all the features or displays some messages. For this, you can define TvShowModel, which makes your MoFlow table like the following:

Figure 18.5: Define models
Figure 18.5: Define models

As you’ll see later, TvShowModel will contain different types of information, like:

  • If you need to display a spinner.
  • If you have some results.
  • The current input text.

The view of your app will use all this data to render the information you need.

Define effects feedback events: Now, you have TvShowModel to keep track of the current state of the input text, and you know what events to generate when the user interacts with the app. You also know that you want the app to execute an effect for accessing the server to fetch the information about the input show. Hopefully, this effect will produce some results or — hopefully not — some errors. As you learned in Figure 18.1, effects notify some results using other events. In the case of Raybius, you can have a SearchSuccess containing the actual results and SearchFailure in case of errors. This leads to the following new version of the MoFlow table:

Figure 18.6: Define effects feedback events
Figure 18.6: Define effects feedback events

But what do you do in case of error? You probably need to display some error messages using, for instance, a Toast. To do this, you might need other events or, like in this case, a new effect. In case of success, you’ll probably want to select an item from a list and display the details for the show. This leads to the ItemClicked event and the GetTvShowDetail effect, which then leads to the TvShowDetailSuccess and TvShowDetailFailure events. Moving from the list to the detail allows you to define the NavigateToDetail effect. When you get results, you probably want to hide the keyboard — you can do this with the HideKeyboard effect. The DetailViewResumed event is useful to trigger the GetTvShowDetail effect when the detail screen is displayed.

As you’ll see later, the resulting MoFlow table is the following:

Figure 18.7: Additional effects
Figure 18.7: Additional effects

Now, you have a clear understanding of what your app should do and how it should behave when different events are triggered. You’d usually do MoFlow on a whiteboard and with the help of your designer or other stakeholders.

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.

To see how this is implemented in Raybius, open the starter project in this chapter’s material in Android Studio, and run the app. You’ll see the following screen:

Figure 18.8: The Raybius app
Figure 18.8: The Raybius app

Note how the SEARCH button is disabled. This is because the text field input is empty. When you insert a text string longer than three characters, you’ll see the button enabled.

When you click it, you’ll see the results in a list like this:

Figure 18.9: Some TV shows as results
Figure 18.9: Some TV shows as results

Also, note how the keyboard is now hidden. At the moment, you can’t select a show, but you’ll implement that feature later in the chapter. Now, look at the existing code. Open TvShowModel.kt in the raybius.mobius package, and look at the current implementation for the model:

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
)

This is a simple data class containing the properties that tells the view:

  1. If the search button is enabled.
  2. What the current input text is.
  3. Whether the app is loading some data. This is useful for displaying a spinner.
  4. What the existing results to display are.
  5. If there’s some error.

To see where this logic is actually used, open FragmentSearchBindingExt.kt in raybius.ui. This file contains extension functions for the FragmentSearchBinding class that the binding library creates for the fragment_search.xml layout file. In this file, you can see the following function:

fun FragmentSearchBinding.logic(model: TvShowModel) {
  if (model.loading) {
    showLoading()
  } else {
    hideLoading()
  }
  displayResults(model.searchResults)
  if (model.error) {
    errorMode()
  }
  searchButton.isEnabled = model.searchEnabled
}

This is quite simple and describes how the view changes when a new model needs to be rendered.

To see what happens when the user taps SEARCH, look at the initUI function in the same file.

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
  }
}

TvSearchFragment invokes this function to initialize the UI. It’s basically UI code, but it uses an object of type Consumer<TvShowEvent> to generate Mobius events from the UI. Note how you use eventConsumer to fire:

  1. InputTextChanged events when the user updates the text input.
  2. SearchButtonClicked when the user taps SEARCH.

Later, you’ll see where the Consumer<TvShowEvent> comes from, but for now, you can think of it as the tool to interact with the Mobius loop you saw in Figure 18.1.

To see the events, just open TvShowEvent.kt in mobius, and see they’re very simple data classes or objects:

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()

Now, you’ve seen how to send events and how to change the UI based on the current model. But the core of the Mobius architecture is the Update function. Open TvShowLogic.kt in the mobius package, and look at the following code:

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()
    }
}

This is a simple function that translates what you’ve designed in the MoFlow table into code. For instance, you can see that:

  1. If you receive an InputTextChanged event, you check if the text in input has at least 3 characters and generate a new TvShowModel with the searchEnabled property set to true and the inputText with the new text. In this case, you don’t trigger any effects.
  2. When the user taps SEARCH, it generates a SearchButtonClicked. Here, you set the loading property to true and trigger a SearchTvShow effect.
  3. If access to the server is successful, you receive a TvSearchSuccess. In this case, you update SearchTvShow with the results and trigger the HideKeyboard effect.
  4. In case of error, you receive a TvSearchFailure, and you then trigger the DisplayErrorMessage effect to display an error message along with the HideKeyboard effect.

This function is easy to read and pretty straightforward to write. All good, but what about the effects?

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.

In the case of the Raybius app, you have two of them. Open TvShowEffect.kt and look at the following effects’ definitions:

sealed interface TvShowEffect
data class SearchTvShow(val query: String) : TvShowEffect
data class DisplayErrorMessage(
  val error: Throwable
) : TvShowEffect
object HideKeyboard : TvShowEffect

For each of them, you need to define an effect handler. Some are very simple and just need to consume the information into the effect class. Others are more complicated and need to generate some events as the result of the effect. DisplayErrorMessage is in the first category. Open UIEffectHandlerImpl.kt in mobius.handlers, and look at the following code:

  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()
  }

Here, you just consume the DisplayErrorMessage and use the Context you inject to display a Toast.

An example of an effect that also generates some events is SearchTvShow. In this case, you need an RxJava transformer, like the following you find in ApiRequestHandlerImpl.kt.

  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
        )
      }

Note how you:

  1. Use the fetchAndParseTvShowResult you implemented in the previous chapters.
  2. Return an Observable<TvSearchSuccess> in the case of success.
  3. Return an Observable<TvSearchFailure> in the case of failure.

Remember that TvSearchSuccess and TvSearchFailure are TvShowEvents. Mobius sends them to the Mobius loop when the effect has completed.

OK, but how do you tell Mobius what effect handler to use for every effect? In the Raybius app, this is done in MobiusModule.kt in di with the following code:

  @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();

In this code, you bind:

  1. The SearchTvShow event to the apiRequestHandler::handleSearchTvShow function.
  2. DisplayErrorMessage to uiHandler::handleErrorMessage.

Besides some implementation details you can see directly in the project or the Mobius official documentation, this is basically all you need to design and implement your app with Mobius. So, now it’s time to implement some code yourself.

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:

  1. Update the TvShowModel with the data you need to display the ShowDetail.
  2. Create the events you need to display the ShowDetail information.
  3. Define the effects you need to access the TV show detail.
  4. Update the Update function (pun intended :]) to handle the new use case.
  5. Implement the new effect handler.
  6. Update UI-related code.

It’s time to write some code!

Model update

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

val detailResult: ShowDetail? = null

This will contain a ShowDetail, which has the detailed information for a TV show.

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

This allows you to:

  1. Trigger the request for the TV show detail.
  2. Handle the selection of an item in the list result for the first query.
  3. Handle a successful response for the detail.
  4. Handle errors when accessing the detail information.

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

These are the effects for:

  1. Navigating to the detail fragment.
  2. Triggering the request to the server to get the TV show detail.

Now, you need to bind all the logic together.

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))
)

Here, you:

  1. Trigger the NavigateToDetail effect when selecting an item in the list and then receive an ItemClicked event.
  2. Launch a GetTvShowDetail effect when the NavigateToDetail completes and a DetailViewResumed is sent.
  3. Update the TvShowModel with the detail information in the case of TvShowDetailSuccess.
  4. Display an error message triggering a DisplayErrorMessage in the case of TvShowDetailFailure.

Now, you’ve defined some new effects and bound them to some events. At the moment, Mobius doesn’t actually know how to execute them. It’s time to implement the effect handlers.

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>

This operation defines how to execute a NavigateToDetail, generating a TvShowEvent as result. This is the interface, and you need to provide an implementation as well. Open UIEffectHandlerImpl.kt, and add the following code:

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)
    }

Besides some implementation details related to the actual navigation, what’s important here is the DetailViewResumed you send to the Mobius loop on the last line. This is to notify the workflow that the effect has been executed. Remember, this event triggers the GetTvShowDetail effect for access to the network to fetch the TV show details, which needs an effect handler. Open ApiRequestHandler.kt, and add the following definition:

fun handleTvShowDetail(
  request: Observable<GetTvShowDetail>
): Observable<TvShowEvent>

As before, you’re just defining the operation for an effect handler that consumes a GetTvShowDetail and generates a TvShowEvent. For the implementation, open ApiRequestHandlerImpl.kt, and add the following code, which should be quite familiar:

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)) }
      )
    }

Now, you’ve implemented the code for the new effect handlers, but Mobius doesn’t know about them yet. Next, open MobiusModule.kt, and add the following transformers to provideEffectHandler below the existing transformer:

.addTransformer(
  GetTvShowDetail::class.java, // 1
  apiRequestHandler::handleTvShowDetail
)
.addTransformer(
  NavigateToDetail::class.java, // 2
  uiHandler::handleNavigateToDetail
)

In this code, you use addTransformer to tell Mobius what effect handler to execute for the effects:

  1. GetTvShowDetail
  2. NavigateToDetail

Now, you just need to update the UI logic for this new feature.

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:

  • Enable the selection of an item in the list.
  • Display ShowDetail in the TvShowDetailFragment.

Open TvSearchFragment.kt, and update the content of onCreateView to insert this code before the return statement:

searchBinding.initUI(eventConsumer) { showId ->
  eventConsumer.accept(ItemClicked(showId))
}

In this code, you use eventConsumer to send an ItemClicked event to the Mobius loop when the user selects an item in the result list. Now, you need to display the ShowDetail in the case of success.

Open FragmentDetailBindingExt.kt, and add the following to the bottom of logic:

if (model.detailResult != null) { // 1
  displayResult(model.detailResult) // 2
}

Here, you just:

  1. Check if details are available.
  2. Display them in the UI.

Now, you can build and run the app. When you select an item in the list of results, you can see how the detail screen is displayed, like in Figure 18.10:

Figure 18.10: The detail screen
Figure 18.10: The detail screen

Great job! As a last note, it’s worthwhile to have a quick look at how you set up the Mobius loop in Android.

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:

  • Delivering the events to the Update function.
  • Triggering the effects, invoking the configured effect handler.
  • Helping with concurrency in the execution of side effects.
  • Handling logging and monitoring.

Usually, there’s an instance of the Mobius loop per surface when a surface is basically a screen of the app. You can decide to use a single Mobius loop or create multiple ones depending on the dimension of the app. In the case of Raybius, a single Mobius loop is shared between all the Fragments through the MainActivity class. To bind the lifecycle of the Mobius loop to the Activity one, Mobius provides a MobiusLoop.Controller. If you look at MainActivity, removing the unrelated things, you’ll get the following:

@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
  }
}

In this code:

  1. You inject the object of type TvShowMobiusController, which is an alias for MobiusLoop.Controller<TvShowModel, TvShowEvent>.
  2. You invoke connect, passing the reference to a function of type (Consumer<TvShowEvent>) -> Connection<TvShowModel>. This function returns an object of type Connection<TvShowModel>. The object Connection<TvShowModel> needs to override accept and dispose.
  3. You start the Mobius loop, invoking start on the TvShowMobiusController. This happens in onResume.
  4. You stop the Mobius loop, invoking stop on the TvShowMobiusController. This happens in onPause.
  5. accept is invoked every time the Mobius loop needs to deliver a new model. This is where you bind the logic specific to your app.
  6. dispose is invoked when you stop the loop.
  7. disconnect removes the binding between the MainActivity and the TvShowMobiusController in onDestroy.
  8. This is a function each Fragment can invoke to set its specific logic. This doesn’t allow more Fragments to be visible at the same time, but for this app, this is an acceptable trade-off.

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.
© 2026 Kodeco Inc.