Mobius Tutorial for Android: Getting Started

Learn about Mobius, a functional reactive framework for managing state evolution and side effects and see how to connect it to your Android UIs. By Massimo Carli.

Leave a rating/review
Download materials
Save for later
Share

Choosing the proper architecture for your project is one of the most important steps in the development process. It impacts the maintainability and testability of the software you create and, therefore, its cost. In this tutorial, you’ll learn about Mobius, a unidirectional architecture 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.

A unidirectional architecture is simple because, as the name says, it allows the data to flow in a single direction from a single source of truth to the UI.

Note: If you want to learn more about the available architectures in Android, Real-World Android by Tutorials and Advanced Android App Architecture are the right books for you.

This definition contains some important functional programming concepts you’ll learn in this tutorial, along with:

  • 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 handles side effects.
  • How Mobius works with Android.

You’ll do this by creating a simple Mobius version of the popular game Memory. :]

Note: The app in this tutorial uses Dagger and Hilt for dependency injection and Jetpack Compose for the UI. If you want to learn all about the Android dependency injection framework, Dagger by Tutorials is the perfect book for you. By reading Jetpack Compose by Tutorials, you’ll learn everything you need to know about Jetpack Compose.

Getting Started

Download the materials for this tutorial using the Download Materials button at the top or bottom of this page. Open the project using Android Studio 2021.1.1 or above. Here’s the structure of the project:

Mobius Project Structure

Mobius Project Structure

The project has a very simple structure. In the previous image, you see three main packages:

  • di: with MobiusModule with all the main bindings Dagger needs.
  • mobius: with the Mobius-specific code for the app. This is where you’ll write most of the code.
  • ui: contains the UI code with composable functions and some reusable visual components.

Build and run the app, and after a splash screen, you’ll get the following:

Menu Screen with PLAY and CREDITS buttons

Menu Screen

If you click the PLAY or CREDITS buttons, nothing happens because you need to add some code. Before that, you need to understand how Mobius works and what it means to implement an application with this framework.

Understanding Mobius Principles and Concepts

To understand how Mobius works, just think about a typical mobile app. You have some UI that displays information. You usually interact with the UI by pressing buttons or providing input. This triggers 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 this:

The Mobius Loop

The Mobius Loop

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

When you launch your app, you 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 data. When the data changes, the UI usually changes. This is important because you can think of the data as the UI’s current state. The data you want to display with the UI is usually represented as the model.

Think about the app’s initial screen, which is the main menu. If what you see is the visual representation of a model, the model will probably contain a property that says what the current screen is. Rendering a screen in place of another with Jetpack Compose will simply consist of executing one composable function instead of another based on that property of the model.

As mentioned earlier, the user interacts with the UI by pressing buttons or providing data as input. Mobius models 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 a database. Using the same navigation example, tapping the PLAY or CREDITS buttons will trigger an event that changes the property of the model representing the current state, and so, the UI.

Mobius Update Function

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 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 the image 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.

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 function
  • Update function

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. The 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 app.

Modeling Your App

Models in Mobius can represent different concepts like:

  • The current state of the app.
  • Events generated by a user action.
  • External events.
  • Events resulting from a side effect’s execution.

Usually, the steps are the following:

  • Define external events.
  • Capture user interactions.
  • Define effects.
  • Define effects feedback events.

Defining External Events

In your case, the app doesn’t handle any external events, but it could. Imagine, for instance, if the game was multiplayer and you received an invitation from a friend. In that case, the app would display a dialog so you could accept or decline the invitation.

To track all the items Mobius needs to implement, you create a table like the following. This is the MoFlow table.

The initial MoFlow

The initial MoFlow

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

Capturing User Interactions

User interactions represent the possible events you need to handle when the user interacts with the app. In your case, you can start entering the name of the events related to navigation, which will allow you to move from the menu screen to credits or the game screen. When you navigate, you might also need to return to the previews screen. Upon adding these events, the MoFlow table becomes the following:

MoFlow chart with navigation events

Navigation Events

As you’ll see later, when you select PLAY, you’ll display a screen like the following with all the cards covered:

The Game Screen with 20 cards

The Game Screen

The game consists of flipping the cards in pairs and trying to find cards with the same value. If the values of the cards are the same, they remain flipped, changing their color. If the values are different, the program flips them back, and the game continues. The game ends when all the cards remain flipped. Every time the player flips a card, the number of moves increases by one. When the player finds a pair, the number of moves decreases by two. The goal is to find all the pairs in the fewest moves.

Understanding how the game works allows you to define new events. In this case, only one is related to user interaction, which you call FlipCard. As you’ll see very soon, the game generates other types of events when you flip a pair of cards, depending on whether you’ve found a matching pair.

At this point, the MoFlow table looks like this:

MoFlow table of game events with FlipCard added

Game Events

Here, you have a clear idea of all the actions users can do and how the app should handle them.

Defining Effects

Now, you need to think about the effects the previous events can generate. When players flip a pair of cards, the game needs to understand what to do. If the cards are different, the game waits for a few seconds and flips the cards back to cover them. You can model this behavior with a DelayedWrongPair effect. In the same way, if the cards contain the same value, the game waits some time and sets the pair as found. You model this using a DelayedCompletedPair effect. In this case, the game also needs to check if all the pairs have been found and, if so, end the game. You can model this with the GameFinished effect.

As you’ll see later, you also need an ExitApplication effect to exit from the app when you go back to the initial menu.

With these effects, the MoFlow table becomes:

MoFlow table with Game Effects added

Game Effects

Note: Modeling network requests to an effect is quite intuitive. The case in this tutorial is different since an effect could be similar to an event. As a rule of thumb, you can think of an effect as a way to ask your app to run some task and then send another event to notify its completion. For instance, the GameFinished effect just makes the game wait and then send, as you’ll see later, an EndGame event to signal the end of the game.

Defining Your Model

In this step, you need to define the model as a representation of your app’s view. In the app for this tutorial, you basically need to handle two different aspects:

  1. Navigation
  2. Game State

To handle the navigation, you just need a property of the model that tells the UI what composable function to execute. The game’s state consists of which cards are available as well as which ones and how many of them the player has matched. If the app is big, you can decide to implement different models. In this case, you just have a single model that you implement in the CardGameModel class.

Now, the MoFlow table becomes:

MoFlow table with an entry in Models

The model for the app

As mentioned before, the view of your app will use all the data in the model to render the information it needs.

Defining Effects Feedback Events

Now, you have CardGameModel to keep track of the app’s current state. When the user flips a card, you generate a FlipCard event. If the card you flip is the first, you just need to update the model to update the UI. If the card you flip is the second, the system needs to check if the two cards have the same value. When the cards match, you generate a DelayedCompletedPair effect, which waits a few milliseconds and then flips the cards to a different color. If the cards have different values, you generate a DelayedWrongPair, which also waits a few milliseconds and then flips the cards back.

How do the effects notify the system that some time has elapsed and the cards need to change their state? Each effect has the option to notify its completion by sending an event called an effect feedback event.

In this app, when the cards are equal, the DelayedCompletedPair effect will notify its completion by sending a SetPairAsDone event. If the cards are different, the DelayedWrongPair effect will notify its completion by sending a RestorePair event. Finally, the GameFinished effect will send an EndGame to notify the completion of the game.

These are events, so the last version of the MoFlow for the app is the following:

MoFlow table showing feedback events

Feedback Events

Describing 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 works, you’ll now implement navigation for the app. But first, you have to look at the model. Open CardGameModel.kt in model, and you’ll get the following code:

data class CardGameModel(
  val screen: GameScreen = GameScreen.MENU, // 1
  val board: MutableList<PlayingCardModel> = mutableListOf(), // 2
  val moves: Int = 0, // 3
  val uncovered: MutableList<Pair<Int, Int>> = mutableListOf(), // 4
  val completed: Int = 0 // 5
)

enum class CardState {
  HIDDEN, VISIBLE, DONE;
}

data class PlayingCardModel(
  val cardId: Int,
  val value: Int,
  val state: CardState = CardState.HIDDEN
)

enum class GameScreen {
  MENU, BOARD, END, CREDITS
}

As you can see, CardGameModel is a simple data class whose properties define the app’s current state. In particular, you see that:

  1. screen represents the current screen to display. The GameScreen enum class at the end of the file contains all the possible values.
  2. board contains the state of the game as a mutable list of PlayingCardModel, which represents the state of each card.
  3. moves contains the current number of moves.
  4. uncovered contains the cards that are currently uncovered in each moment. You represent each uncovered card as a pair of its id and value.
  5. completed represents the number of pairs the user has successfully found.

To understand how the specific page is rendered, open MainScreen.kt in ui, getting the following code:

@Composable
fun MainScreen(
  gameModel: CardGameModel, 
  eventConsumer: Consumer<CardGameEvent>
) {
  when (gameModel.screen) { // HERE
    GameScreen.MENU -> GameMenu(eventConsumer)
    GameScreen.BOARD -> GameBoard(gameModel, eventConsumer)
    GameScreen.END -> GameResult(gameModel, eventConsumer)
    GameScreen.CREDITS -> CreditsScreen(eventConsumer)
  }
}

It’s easy now to see how the value of the model’s screen property defines which screen to display by invoking the proper composable function.

Memory Game Update Function

From the description of the Mobius architecture, you understand that you’ll just need to update the screen property when a navigation event is triggered. Open CardGameLogic.kt in mobius.logic, and look at the following code:

val cardGameLogic: CardGameUpdate = object : CardGameUpdate {
  override fun update(
    model: CardGameModel,
    event: CardGameEvent
  ): Next<CardGameModel, CardGameEffect> = when (event) {
    is ShowCredits -> handleShowCredits(model, event)
    is BackPressed -> handleBack(model, event)
    is StartGame -> handleStartGame(model, event)
    is ShowMenu -> handleShowMenu(model, event)
    is FlipCard -> handleFlipCard(model, event)
    is SetPairAsDone -> handleSetPairAsDone(model, event)
    is RestorePair -> handleRestorePair(model, event)
    is EndGame -> handleEndGame(model, event)
  }
}

This is an Update function that handles each event, invoking a related function at the moment not completely implemented. Here, it’s very important to note how Update is a function of type (CardGameModel, CardGameEvent) -> Next<CardGameModel, CardGameEffect>. It accepts a CardGameModel and a CardGameEvent in input and returns a Next<CardGameModel, CardGameEffect>, which is a way to encapsulate the resulting CardGameModel and a set of optional CardGameEffects. Note that CardGameUpdate is a Mobius Update.

To handle navigation, start with the following function:

private fun handleShowCredits(
  model: CardGameModel, 
  event: ShowCredits
): Next<CardGameModel, CardGameEffect> = Next.noChange()

At the moment, this returns a value that tells Mobius to do nothing. To navigate to the credit screen, you just need to replace the previous code with the following:

private fun handleShowCredits(
  model: CardGameModel, 
  event: ShowCredits
): Next<CardGameModel, CardGameEffect> {
  return Next.next( // 1
    model.copy( // 2
      screen = GameScreen.CREDITS,
    )
  )
}

In this code, you:

  1. Return a new instance of Next you create using the next factory method.
  2. Pass to next a copy of the current model with a new value for the screen property.

That’s it! Mobius will handle all the updates for you. To see this, run the app and click the CREDITS button in the menu, landing on the following screen:

Credits Screen in app

Credits Screen

Congratulations! You just implemented the first logic in Mobius’s Update function.

Implementing Back-Out Functionality

You need to add another thing to navigation. If you press the back button, nothing happens. To handle this, replace the following code:

private fun handleBack(
  model: CardGameModel, 
  event: BackPressed
): Next<CardGameModel, CardGameEffect> = Next.noChange()

With:

private fun handleBack(
  model: CardGameModel, 
  event: BackPressed
): Next<CardGameModel, CardGameEffect> =
  when (model.screen) {
    GameScreen.BOARD -> Next.noChange() // 1
    GameScreen.MENU -> Next.next(model, setOf(ExitApplication)) // 2
    else -> Next.next(model.copy(screen = GameScreen.MENU)) // 3
  }

In this case, the logic is just a little bit more interesting because:

  1. You can’t exit from the board.
  2. When in the menu, back triggers the ExitApplication effect.
  3. In all other cases, you return to the menu.

Later, you’ll see how to handle the ExitApplication effect. Build and run the app, and check that you can actually go to the credits screen and go back. Unfortunately, you can’t play yet, but you already know how to fix it. Just replace:

private fun handleStartGame(
  model: CardGameModel, 
  event: StartGame
): Next<CardGameModel, CardGameEffect> = Next.noChange()

With:

private fun handleStartGame(
  model: CardGameModel, 
  event: StartGame
): Next<CardGameModel, CardGameEffect> {
  return Next.next(
    model.copy(
      screen = GameScreen.BOARD, // 1
      board = createRandomValues(), // 2
      moves = 0, // 3
      completed = 0, // 3
      uncovered = mutableListOf() // 3
    )
  )
}

In this case, the new model adds some complexity related to the initialization of the game. To note that, you:

  1. Change the current screen to GameScreen.BOARD.
  2. Invoke createRandomValues to generate a random distribution of the cards.
  3. Reset all the counters related to the game state.

Build and run the app, and you can now open the board and start playing. Arg! Something’s wrong there. That’s because the effects you designed in the MoFlow aren’t implemented yet.

The Game Screen with 20 cards

The Game Screen

Handling Effects

To revise which effects you need to handle, it’s useful to look at the implementation of the game logic. Open CardGameLogic.kt and look at the following code:

fun handleFlipCard(
  model: CardGameModel, 
  event: FlipCard
): Next<CardGameModel, CardGameEffect> {
  val (pos, currentModel) = model.findModelById(event.cardId) // 1
  val newFlipState =
    if (currentModel.state == CardState.HIDDEN) {
      CardState.VISIBLE
    } else { CardState.HIDDEN } // 2
  val newModel = currentModel.copy(
    state = newFlipState
  )
  model.board[pos] = newModel
  val uncovered = model.uncovered // 3
  uncovered.add(currentModel.cardId to currentModel.value)
  val effects = mutableSetOf<CardGameEffect>() 
  if (uncovered.size == 2) {
    // Check they have the same value
    if (uncovered[0].second == uncovered[1].second) { // 4
      effects.add(DelayedCompletedPair(
        uncovered[0].first, 
        uncovered[1].first
      ))
    } else { // 5
      effects.add(DelayedWrongPair(
        uncovered[0].first, 
        uncovered[1].first
      ))
    }
  }
  return Next.next(
    model.copy(
      moves = model.moves + 1
    ), effects
  )
}

This function contains the logic for the game handling the FlipCard event. Here, you:

  1. Find the position of the current card.
  2. Update the model for the selected card.
  3. Handle the uncovered state.
  4. If you uncovered two cards with the same value, add DelayedCompletedPair as an effect to handle.
  5. If the values are different, add the DelayedWrongPair effect.

But how do you handle the DelayedCompletedPair and DelayedWrongPair effects? Open MobiusModule.kt in di and look at the following code:

  @Provides
  fun provideEffectHandler(
    gameHandler: GameEffectHandler,
  ): CardGameEffectHandler =
    RxMobius.subtypeEffectHandler<CardGameEffect, CardGameEvent>()
      .addTransformer(
        DelayedCompletedPair::class.java, 
        gameHandler::handlePairCompleted
      ) // HERE
      .addTransformer(
        DelayedWrongPair::class.java, 
        gameHandler::handleWrongPair
      ) // HERE
      .addTransformer(
        GameFinished::class.java, 
        gameHandler::handleGameFinished
      ) // HERE
      .addConsumer( // HERE
        ExitApplication::class.java,
        gameHandler::handleExitApp,
        AndroidSchedulers.mainThread()
      )
      .build();

Here, you basically register a function as the one Mobius executes when a specific effect needs to be handled. You do this in different ways because the ExitApplication effect just consumes an event and doesn’t trigger a feedback event.

Now, open GameEffectHandlerImpl.kt and replace the existing code with the following:

class GameEffectHandlerImpl @Inject constructor(
  @ActivityContext val context: Context
) : GameEffectHandler {

  override fun handlePairCompleted(
    request: Observable<DelayedCompletedPair>
  ): Observable<CardGameEvent> = // 1
    request
      .map { req -> // 2
        waitShort() // 3
        SetPairAsDone(req.firstId, req.secondId) // 4
      }

  override fun handleWrongPair(
    request: Observable<DelayedWrongPair>
  ): Observable<CardGameEvent> = // 1
    request
      .map { req -> // 2
        waitShort() // 3
        RestorePair(req.firstId, req.secondId) // 4
      }

  override fun handleGameFinished(
    request: Observable<GameFinished>
  ): Observable<CardGameEvent> = // 1
    request
      .map { req -> // 2
        waitShort() // 3
        EndGame // 4
      }

  override fun handleExitApp(extEffect: ExitApplication) {
    (context as Activity).finish()
  }

  private fun waitShort() = try {
    Thread.sleep(800)
  } catch (ie: InterruptedException) {
  }
}

In this code, you handle the effects with feedback events in the same way, and basically, you:

  1. Receive the effect through an Observable<CardGameEvent>.
  2. Use map to transform the CardGameEvent into the one to return to notify the completion or result of the effect.
  3. Wait a while.
  4. Return the feedback event.

handleExitApp is much simpler because it’s just a Consumer for the events, and it doesn’t need to return anything. In this case, you just use the context of the activity you receive from Dagger and invoke finish.

Now, Mobius sends the feedback event to the same Upate function you edited earlier. Build and run the app, and check how it works. Now, when you select two cards, depending on whether they have the same values, they’re either flipped to a yellow color or flipped back.

To see how you handle the end of the game, look at the following code in CardGameLogic.kt:

private fun handleSetPairAsDone(
  model: CardGameModel, 
  event: SetPairAsDone
): Next<CardGameModel, CardGameEffect> {
  val (pos1, model1) = model.findModelById(event.firstId)
  val (pos2, model2) = model.findModelById(event.secondId)
  val newBoard = model.board
  model.board[pos1] = model1.copy(state = CardState.DONE)
  model.board[pos2] = model2.copy(state = CardState.DONE)
  val effects = mutableSetOf<CardGameEffect>()
  val completed = model.completed + 2
  if (completed == 20) {
    effects.add(GameFinished) // HERE
  }
  return Next.next(
    model.copy(
      board = newBoard,
      completed = model.completed + 2,
      moves = model.moves - 2,
      uncovered = mutableListOf()
    ), effects
  )
}

Just note how you add the GameFinished effect, in case all the card pairs have been found.

Mobius and Android

Mobius is much more than what you’ve learned in this tutorial. As one last point, it’s interesting to look at how Mobius is connected to Android and Jetpack Compose. Open MainActivity.kt, and look at the following code:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

  @Inject
  lateinit var gameCardController: CardGameMobiusController // 1

  private var gameModel = mutableStateOf(CardGameModel())

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Switch to AppTheme for displaying the activity
    setTheme(R.style.AppTheme)
    gameCardController.connect(::connectViews) // 2
    setContent {
      MainScreen(gameModel.value, eventConsumer)
    }
  }

  override fun onDestroy() {
    super.onDestroy()
    gameCardController.disconnect() // 5
  }

  override fun onResume() {
    super.onResume()
    gameCardController.start() // 5
  }

  override fun onPause() {
    super.onPause()
    gameCardController.stop() // 5
  }

  override fun onBackPressed() {
    eventConsumer.accept(BackPressed) // 6
  }

  lateinit var eventConsumer: Consumer<CardGameEvent>

  private fun connectViews(
    eventConsumer: Consumer<CardGameEvent>
  ): Connection<CardGameModel> { // 2
    this.eventConsumer = eventConsumer
    return object : Connection<CardGameModel> {
      override fun accept(model: CardGameModel) {
        logic(eventConsumer, model) // 3
      }

      override fun dispose() {
      }
    }
  }

  var logic: (Consumer<CardGameEvent>, CardGameModel) -> Unit = 
    { _, cardModel ->
      gameModel.value = cardModel // 4
    }
}

Here, you:

  1. Declare a CardGameMobiusController, which is a typealias of MobiusLoop.Controller<CardGameModel, CardGameEvent>. This is basically the Mobius loop that handles all the concepts you learned in the introduction. This is the one responsible for handling events and triggering effects.
  2. In the onCreate, you connect the loop to the connectViews function, which returns an implementation of Connection<CardGameModel>. This is basically a callback for the lifecycle of the Mobius loop.
  3. Every time an event is sent, the accept function is invoked. This is where the magic happens, calling the logic function.
  4. The logic function updates the model used by Jetpack Compose.
  5. Note how the lifecycles of the Activity and the Mobius loop are connected.
  6. The Consumer<CardGameEvent> is the object you use to send events. How the back navigation event is handled is a good example of its usage.

You can spend some time seeing how all the dots are connected.

Where to Go From Here?

Congratulations! You learned how Mobius works by implementing a simple game. Use the Download Materials button at the top or bottom of the tutorial to access the final version of the code.

As mentioned, Mobius is much more than this. Besides the official documentation, a possible next step is the chapter about Mobius in Functional Programming in Kotlin by Tutorials.

If you have any questions or comments, please join the discussion below!