From Concept to Code: Crafting an Android Audio Book App with Jetpack Compose and MVI (P-2)

Part 2: Foundation for MVI & Building Blocks of the App

Waseem Abbas
5 min readJan 15, 2024

Welcome to Part 2 of the series, “From Concept to Code: Crafting an Android Audio Book App with Jetpack Compose and MVI”. In this installment, we’ll dive into the foundational elements that will shape our application's core structure and visual appeal. Let’s embark on setting up base classes for MVI, use cases, and crafting reusable UI components.

Understanding the MVI

Model-View-Intent (MVI) Architecture

Let’s take a quick overview of Model-View-Intent (MVI) before jumping to its implementation in our app. MVI is a design pattern that has gained popularity in modern Android development. It offers a structured and reactive approach to building user interfaces, clearly separating concerns and facilitating a unidirectional data flow.

The Components of MVI

Model (ViewState): The Model represents the current state of the application. It is often referred to as ViewState. This state is a unidirectional data flow that captures all the information needed to render the UI.

For example, to show the state of each item in the list of books on our home page, we can define the BooksListUiState model:

data class BooksListUiState(
val id: String,
val title: String,
val author: String,
val cover: String
)

Read more about defining UI state on Android official docs.

View (Composable functions): The View is responsible for rendering the UI based on the current state. In our case, Jetpack Compose is the perfect fit for building reactive UI components. Composables receive the ViewState and determine how to represent it on the screen.

For example, to present the BooksListUiState on UI we can write the following composable:

@Composable
private fun BookItem(book: BooksListUiState, onBookItemClick: (bookId: String) -> Unit) {
Column(
modifier = Modifier
.width(109.dp)
.clickable {
onBookItemClick(book.id)
}
) {
BookThumbnail(
url = book.cover,
modifier = Modifier.fillMaxWidth().height(160.dp)
)
Spacer(modifier = Modifier.height(10.dp))
Text(text = book.title, style = MaterialTheme.typography.labelLarge, maxLines = 1)
Text(
text = book.author, style = MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium
),
maxLines = 1
)
}
}

Intent: The Intent is a user actions or events that can trigger a state change. It serves as a bridge between the user interaction and the application’s logic.

For example in the case of our home screen, we may have “Load” and “BookItemClicked” intents.

sealed class HomeIntent {
object Load : HomeIntent()
data class BookItemClicked(val bookId: String) : HomeIntent()
}
//or
sealed class HomeAction {
object Load : HomeAction()
data class BookItemClicked(val bookId: String) : HomeAction()
}

I have used the Action term instead of Intent. So any class named with a suffix Action represents an intent.

The Flow of MVI

The diagram above explains how the MVI components communicate with each other. Let’s discuss it briefly.

User Interaction: When a user interacts with the UI (e.g., clicks a button), an Intent (Action) is generated.

@Composable
fun HomeScreen(
viewModel: HomeViewModel
) {
//user clicks this button and Load data action (intent) is generated.
Button(onClick = { viewModel.action(HomeAction.Load) }) {
Text(text = "Load Data")
}
}

ViewModel (Business Logic): This Action is passed to the ViewModel, where business logic is executed based on the received Intent.

class HomeViewModel : HomeViewModel() {
fun HomeAction.process(): HomeResult {
return when(this) {
HomeAction.Load -> TODO() send an api call to fetch data
HomeAction.ViewAllClicked -> TODO()
is HomeAction.BookItemClicked -> TODO() navigate to detail ui
}
}
}

An Action always generates a Result. In the above example, HomeResult is generated by the processor.

Reducer (state generator): The Reducer generates a new State one based on the previous state and the new result received from the processor.

State Update: The updated State is then emitted to the View, triggering a re-composition of the UI.

We’ll discuss the MVI flow in more detail in the next parts when designing a screen.

Setting Up MVI Foundation

To implement the MVI architecture, we’ll establish the base classes: MviViewState, BaseStateViewModel, and MviAction (Intent), MviResult, MviEvent, and MviStateReducer. Big thanks to Ryan Simon for crafting these foundational classes and being a lifesaver.

Base classes for MVI

We have placed these files under the core directory like this:

Let’s theme our app

Define Theme in Compose

theme directory

When we create a new Android project with Jetpack Compose, you can see these 3 files Color.kt, Theme.kt, and Type.kt. Read the material 3 guidelines for a better understanding of theming.

Avoid using hard-coded values while coding UI. Instead use MaterialTheme to consume the defined typography, colors, and shapes.

Text(
text = "Summary", style = MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium
),
maxLines = 1
)

Creating Shareable Composables

To maintain a consistent and visually appealing UI, we’ll create shareable Jetpack Compose components. These components will include buttons, cards, chips, and text elements that adhere to the theme of our application.

Composables for theming our app

Navigation Graph

The application incorporates nested navigation within the bottom app bar. Kindly take a moment to peruse my previously published Medium article “Implement Nested Navigation with Bottom Appbar in Jetpack Compose” for better understanding.

Under navigation directory, check these two files AppNavGraph.kt and Route.kt to see how to manage the navigation graph in compose.

navigation directory

Dependencies

For now, our Audio Book app has the following dependencies but new dependencies are expected to develop new features. Please stay vigilant regarding the build.gradle file to ensure up-to-date dependencies.

See README on the Github repo to see the Tech Stack of our application.

Conclusion

By the end of Part 2, You’ve set up the MVI architecture, created reusable UI components, styled your app with a custom theme, and integrated dependencies like Jetpack Compose and Hilt, etc. In the upcoming parts, we’ll delve into specific features, enhancing the functionality and user experience of your app.

For a comprehensive overview of the project setup and introduction to the series, check out “Part 1: Introduction and Project Setup”. Stay tuned for the next installment, where we’ll tackle the implementation of the Onboarding Screen. Happy coding!

Feel free to hit me up on Instagram or Facebook.

Know more about me on waseemabbs.com, GitHub, and Linkedin

--

--

Waseem Abbas

Mobile Software Engineer | Android | Flutter | Clean Architecture