Component-based Approach. Fighting Complexity in Android Applications

Artur Artikov
ITNEXT
Published in
11 min readNov 16, 2023

--

Imagine starting the development of a new Android application. At this stage, major problems are unlikely. You have implemented only the basic functionality. There are several simple screens. Navigating through the code is straightforward. Enthusiastically, you begin adding features one after another. However, as time passes, development becomes more complex. The codebase expands, the main screen gets cluttered with numerous UI elements and intricate logic, and the screen flow evolves into a complex chain of transitions. It becomes a headache to add something new without breaking anything old. Consequently, the pace of development slows down. Is this scenario familiar to you?

An effective strategy to combat this complexity is the component-based approach. At MobileUp, we’ve implemented this approach in three major Android applications, and it’s hard to imagine how we managed without it before.

I’m Artur Artikov, a team lead at MobileUp. I’m here to guide you in mastering the component-based approach. My aim is to make this journey as straightforward and engaging as possible.

A series of articles awaits you. This initial article is focused on theory. We will explore the complexities encountered in Android application development and discuss why MVVM and Clean Architecture aren’t a panacea. I’ll explain the component-based approach, outlining its advantages. Finally, at the end of the article, you’ll find links to resources for further, in-depth study.

Complexity in Android Applications

In Android application development, we typically encounter two types of complexity:

1. Complex screens
Consider the main screen in applications like banking, online shopping, or social networks. This screen displays crucial user information, featuring numerous UI elements, network requests, and complex logic.

2. Complex navigation
As the application grows and new screens are added, navigating between them becomes more complex. This leads to multi-step scenarios like authorization, registration, purchasing, and surveys, all interlinked. The application often integrates bottom navigation, a panel with buttons to switch between screens. For tablet users, there’s often a need for master-detail navigation, where the screen displays both a list of items and details about the selected item simultaneously. Additionally, navigation includes the use of bottom sheets and dialog boxes.

If nothing is done, the speed and quality of development will gradually decline.

Problems with MVVM and Clean Architecture

Many Android developers, including myself, rely on MVVM and Clean Architecture. While these methodologies help in organizing code better, they also have their shortcomings, which I’ll highlight in the following discussion.

It’s important to note that both MVVM and Clean Architecture can be interpreted and implemented in various ways. I’ll explain my understanding of these methods and the challenges I’ve faced using them. Your experience may vary, and I welcome you to share your insights in the comments.

Massive View Models

The MVVM pattern advises to extract a screen’s logic into a separate class, known as ViewModel. This class holds fields for all data shown on the screen and methods to handle all user actions.

UI elements of the screen correspond to fields and methods in the view model

Imagine we’re developing the main screen of a banking app and crafting a view model for it. At the top of the screen, we display the user’s name and avatar, requiring a profile field and an onAvatarClick method in the view model. In the top right corner, there’s a bell icon for notifications, so we add an isNotificationBadgeVisible field and an onNotificationIconClick method. We see a carousel of advertising banners, leading us to include a field advertisementItems and a method to handle banner clicks. As we continue, fields and methods are added for every feature — monthly expenses, bank cards, exchange rates, deposits, mortgages, and so on. You can see how quickly the view model becomes extremely complex.

Certainly, we’ll attempt to streamline the view model’s code, such as by delegating data loading to separate classes. However, this approach doesn’t fundamentally alter the situation. The fields and methods remain, and with each addition to the screen, the view model will continue to grow. This issue is inescapable in classical MVVM, where one screen is tied to a single view model.

Multi-Layer Architecture

Clean Architecture advocates for structuring the application into separate layers, each with its specific responsibilities. For example, one layer might be dedicated to retrieving data from external sources, another to executing business logic, and a third to presenting the user interface. The exact number and responsibilities of layers can vary depending on the project.

Let’s assume we’ve divided the application into three layers. Upon working with the code, we find it’s still complex. So, what’s the natural next step? Create more layers! And that’s exactly what we tend to do.

However, adding layers has its cost. The more layers we introduce, the more challenging the project becomes to maintain. We encounter additional abstractions and must manage interactions between layers. In pursuit of consistency, developers begin to implement even the simplest screens with numerous layers. Our intention was to simplify the code, but ironically, we end up complicating it.

The developer has added too many layers

Interactors are Not Use Cases

Another key element in Clean Architecture is the concept of interactors. However, before we get into that, it’s important to understand use cases.

Use cases is a term from requirements engineering. Use cases describe what a user can do within an application. For instance, in a ‘phone book’ app, use cases might include viewing a contact list, adding, editing, deleting a contact, and calling a contact.

Interactors originated from the programming world. Programmers write interactors to implement use cases. Each use case is typically matched with a distinct interactor. This seems practical, right?

But there’s a subtlety. In reality, an interactor doesn’t fully implement a use case. As per Clean Architecture guidelines, an interactor should not be aware of the user interface, focusing only on high-level logic, the so-called business rules. The specifics of user interaction with the app are outside its scope. Let’s explore the practical implications of this approach.

Consider the phone book application again, and let’s focus on a more complex use case: deleting multiple contacts simultaneously. For the user, the process looks like this:

  1. The user views the contact list.
  2. The user performs a long press on one of the contacts. The contact becomes selected. A ‘Delete’ button appears.
  3. The user presses on several more contacts. They also become selected.
  4. The user presses the ‘Delete’ button. A confirmation dialog appears.
  5. The user confirms the deletion. The selected contacts disappear.

And this is how the interactor turns out:

class RemoveContactsInteractor(
private val contactsRepository: ContactsRepository
) {

suspend fun execute(contactIds: Set<ContactId>) {
contactsRepository.removeContacts(contactIds)
}
}

See, it’s almost empty. There’s no logic for selecting multiple contacts or confirming their deletion. Such responsibilities typically fall within the view model, not the interactor. And since there are no business rules here, the interactor doesn’t do anything useful, just passes the call to the repository.

This situation is not rare. Most mobile apps have lots of user interactions but not many business rules.

Use case in enterprise development (left) and use case in mobile development (right)

How does this discussion of use cases and interactors tie into complex screens? The complexity of a screen arises from it managing multiple use cases simultaneously. Ideally, we’d encapsulate each use case in its own class. However, interactors don’t facilitate this.

What is the Component-Based Approach

Component-Based Approach in the Real World

The good news is that you’re already acquainted with the component-based approach, if not from programming, then definitely from the real world.

A human body is structured according to the component-based approach

Consider the human body, composed of tiny elements — cells. These cells aren’t randomly connected; there’s a clear hierarchical structure. Cells form tissues, tissues come together to create organs, organs make up organ systems, and these systems together constitute the entire organism.

In essence, simpler elements merge to form more complex structures, and this process repeats at multiple levels. We refer to this as the component-based approach.

It’s easy to find examples where this principle also applies:

  • the entire universe: planets and stars ➜ planetary systems ➜ galaxies ➜ galaxy clusters
  • a personal computer: from the tiniest transistors in the processor to large components like the system unit and monitor
  • Hogwarts castle, assembled from Lego
  • a book library
  • a large IT company
  • a house
  • a spacecraft

In general, any complex objects and systems are structured according to the component-based approach.

The component-based approach is effective in managing complexity. Thanks to its hierarchical structure, we can simplify it by skipping certain levels. Take the human body, for example: saying ‘Humans consist of organs, which form organ systems that make up the entire organism’ is accurate. Here, we’ve just bypassed the levels of tissues and cells. Even in our original description, there was a simplification, as cells themselves are complex structures.

This ability to adjust the level of detail based on the task at hand is a key advantage of the component-based approach, and it’s an aspect we’ll find very useful.

When applying the component-based approach, it is important to choose the appropriate level of detail

Component-Based Approach in Android Development

A mobile application can similarly be understood as a hierarchical structure, composed of elements known as components.

Component-based approach in mobile development

Mobile applications comprise following types of components:

  • UI elements: these are the simplest components, encompassing standard UI framework offerings like buttons, text fields, and checkboxes, as well as custom UI elements developed by programmers. Generally, UI elements are abstract and don’t independently address user tasks.
  • Functional blocks: these components are more autonomous. Each functional block handles specific functionality, effectively performing tasks from an end-user’s perspective.
  • Screens: as the name implies, these components are in charge of entire application screens. A complex screen, performing multiple functions, is typically composed of several functional blocks.
  • Flows: these represent sequences of screens dedicated to a common functionality. Examples of flows include authorization, registration, purchasing, and survey processes.
  • An application: this is also viewed as a component, overseeing the full range of functions accessible to the user, and is made up of multiple flows.

This structure can be tailored to fit your application’s needs. For instance, in simple screens, you might bypass the functional block level and construct screens directly from UI elements. In a very basic application, there might not be multiple flows, resulting in the app consisting solely of individual screens. Conversely, for more complexity, you can further elaborate the structure by dividing blocks into sub-blocks and adding additional layers of navigation nesting.

Components in this approach are defined by program code, organized by functionality rather than the layer-based division typical of Clean Architecture. Often, a component spans multiple layers. Within a single component, there can be network data loading, some form of logic, and presenting data to the user. This rule is somewhat less applicable to UI elements, but even among them, there are examples that take on roles of the data layer, such as loading images from the internet.

A component doesn’t always equate to a single class; it typically comprises multiple classes. The organization of these classes is versatile: they can be divided by layers, grouped into packages, or even shared across different components.

Depending on the task at hand, we can adjust the level of detail we focus on. For instance, when constructing a screen using functional blocks, the specific UI elements within each block become less significant. We start to see these blocks as simple, cohesive units. This principle also applies to flows — we create transitions between screens without delving into the screens’ internal structures. Moreover, organizing navigation in a large application becomes more feasible. Instead of juggling a hundred screens, we manage around a dozen flows.

A Fresh Look at MVVM and Clean Architecture

The component-based approach doesn’t conflict with MVVM and Clean Architecture; instead, it complements them and opens up new possibilities. Having greater freedom of choice, we can take a fresh look at MVVM and Clean Architecture.

MVVM 2.0

The MVVM pattern is perfect for implementing screens and functional blocks. A complex screen can be structured with multiple view models: one parent and several children.

View models of the complex screen

The parent view model acts as a coordinator, managing interactions between child view models as needed.

Child view models handle individual functional blocks. Usually, each covers a limited scope of functionality, leading to simple view models. However, when a block’s functionality is too extensive, we can reapply the component-based approach. By dividing the block into smaller sub-blocks and assigning a view model to each, we completely solve the problem of massive view models.

Eliminating Artificial Complexity

Complex screens and navigation are inherent complexities, stemming from the application’s requirements. We can’t simply eliminate them, but we manage them using MVVM, Clean Architecture, and the component-based approach.

However, there’s also ‘artificial complexity’, which developers create by using inappropriate tools or techniques. It is also known as overengineering.

Overengineering is a common issue with Clean Architecture. It’s seen in the overuse of layers and abstractions. It arises as interactors that fail to implement any real business rules. I’ve even observed cases where on top of such ineffective interactors, there was an additional layer — a facade for the interactors.

Implementing the component-based approach naturally mitigates the risk of overengineering. The division into screens, functional blocks, and flows aligns with the application’s inherent structure. Rarely, if ever, have I seen a developer excessively subdivide a screen into too many functional blocks. Likewise, creating a flow typically only occurs when there’s a clear need, evidenced by the presence of at least two screens that fit into it.

Does this suggest that we should exclusively adopt the component-based approach and abandon Clean Architecture? Certainly not! Every tool has its specific purpose.

The component-based approach is great at dividing the application into loosely coupled functional units. Meanwhile, Clean Architecture is invaluable for implementing these units. However, the process should follow a specific order: initially, we break the code into components, and then, as required, we introduce layers, interactors, and other abstractions.

Adopting this approach, you’ll discover that for most tasks, a very lightweight version of Clean Architecture is sufficient. This involves fewer layers, often three or maybe even two. Use cases are integrated into distinct components, and interactors are employed specifically in areas where business rules are present.

The evolution of architectural approaches: no architecture ➔ Clean Architecture ➔ component-based approach + Clean Architecture

Further Reading

  • Robert C. Martin’s book, “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” — a comprehensive explanation of Clean Architecture.
  • Fernando Cejas articles (part1, part2, part3) —introducing Clean Architecture to Android development.
  • The talk “The Immense Benefits of Not Thinking in Screens” — talk about the advantages of the component-based approach.

To be continued

I hope this article has given you a clear understanding of the component-based approach and its advantages. If you have any questions, feel free to ask in the comments. In upcoming articles, I’ll go into more detail on how to practically apply the component-based approach.

--

--