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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Mobius Tutorial for Android: Getting Started
30 mins
- Getting Started
- Understanding Mobius Principles and Concepts
- Mobius Update Function
- The Mobius Workflow
- Modeling Your App
- Defining External Events
- Capturing User Interactions
- Defining Effects
- Defining Your Model
- Defining Effects Feedback Events
- Describing Your App
- Memory Game Update Function
- Implementing Back-Out Functionality
- Handling Effects
- Mobius and Android
- Where to Go From Here?
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.
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. :]
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:
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:
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:
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 TextView
s, Button
s 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:
- Model or MoFlow (Short for “Mobius Flow”)
- Describe
- Plan
- 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.
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:
As you’ll see later, when you select PLAY, you’ll display a screen like the following with all the cards covered:
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:
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:
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:
- Navigation
- 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:
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:
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:
-
screen represents the current screen to display. The
GameScreen
enum class at the end of the file contains all the possible values. -
board contains the state of the game as a mutable list of
PlayingCardModel
, which represents the state of each card. - moves contains the current number of moves.
-
uncovered contains the cards that are currently uncovered in each moment. You represent each uncovered card as a pair of its
id
andvalue
. - 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 CardGameEffect
s. 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:
- Return a new instance of
Next
you create using thenext
factory method. - Pass to
next
a copy of the current model with a new value for thescreen
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:
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:
- You can’t exit from the board.
- When in the menu, back triggers the
ExitApplication
effect. - 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:
- Change the current screen to
GameScreen.BOARD
. - Invoke
createRandomValues
to generate a random distribution of the cards. - 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.
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:
- Find the position of the current card.
- Update the model for the selected card.
- Handle the uncovered state.
- If you uncovered two cards with the same value, add
DelayedCompletedPair
as an effect to handle. - 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:
- Receive the effect through an
Observable<CardGameEvent>
. - Use
map
to transform theCardGameEvent
into the one to return to notify the completion or result of the effect. - Wait a while.
- 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:
- Declare a
CardGameMobiusController
, which is a typealias ofMobiusLoop.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. - In the
onCreate
, you connect the loop to theconnectViews
function, which returns an implementation ofConnection<CardGameModel>
. This is basically a callback for the lifecycle of the Mobius loop. - Every time an event is sent, the
accept
function is invoked. This is where the magic happens, calling thelogic
function. - The
logic
function updates the model used by Jetpack Compose. - Note how the lifecycles of the
Activity
and the Mobius loop are connected. - 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!