DEV Community

Cover image for What Makes an Architecture "Clear" in Modern Android Apps?
GengWang Zhang
GengWang Zhang

Posted on

What Makes an Architecture "Clear" in Modern Android Apps?

When I first started my journey as a developer, I was constantly told to write "elegant" code. I heard that MVVM would be a staple interview question, so I diligently learned design patterns like Factory and Singleton, and adopted frameworks like Dagger Hilt.

However, I soon realized a harsh truth: all this knowledge didn't necessarily make my code clearer or my work more efficient. Sometimes, I was just rigidly forcing these patterns and frameworks into places where they didn't belong—using technology for the sake of using it.

Over time, I discovered that for real-world scenarios, the "best" architecture isn't always the most advanced one. It doesn't require overly complex design patterns or the bleeding-edge framework of the month. On the contrary, the most suitable architecture is the simplest, the clearest, and one that even a beginner can understand.

So, I want to discuss a fundamental question: In modern Android applications, what does a "clear" architecture actually look like?

  1. It Achieves Business Independence (Single Responsibility)

In the beginning, I tended to dump all business logic into Activity or Fragment classes. This inevitably led to God Classes that were massive and unmaintainable.

Later, I discovered the ViewModel. I started splitting different business logics into separate ViewModels. But as the product grew, I noticed a familiar pattern emerging: some ViewModels began to accumulate excessive business logic again. The problem wasn't solved; it was just shifted from the UI layer to the ViewModel layer.

Today, I've embraced the concept of Use Cases, and I believe this is the true solution.

  • One Use Case = One User Action: A Use Case represents a specific business action, such as "Login," "Place Order," or "Fetch Profile."

  • Stateless Functions: Ideally, a Use Case should act like a pure function. It shouldn't hold state. This ensures the purity of your business logic, making it easy to test and reason about.

By isolating logic into Use Cases, we ensure that each piece of business functionality stands on its own, independent of the UI lifecycle or data source specifics.

  1. It Embraces Dependency Inversion

The core mechanism of a clear architecture lies in the Domain Layer. By defining abstract interfaces, it isolates business logic from implementation details, effectively implementing the Dependency Inversion Principle.

Think of it like a power outlet:

  • The outlet (Abstraction) doesn't depend on the specific appliance (Detail) you plug in.

  • Yet, it can compatible with any device that meets the standard.

In our code:

  • The Domain Layer declares the contract via interfaces (e.g., UserRepository defines getUser() and saveUser()), but it knows nothing about how the data is fetched.

  • The Data Layer (implementing Room, Retrofit, or Firebase) is responsible for fulfilling these contracts. It handles the nitty-gritty of mapping data entities (like UserEntity) to domain models (User).

This separation ensures that your core business rules remain untouched even if you decide to switch your database from Room to SQLDelight, or your network library from Retrofit to OKHTTP.

  1. It Handles Exceptions Timely and Locally

A common debate in architecture is: Where should we handle errors?
My stance is firm: Data exceptions should be handled within the Repository (Data Layer), not wrapped in generic try-catch blocks in the Use Case layer.

Here is why:

  • Context Awareness: The Repository knows the true source of the exception best—is it a network timeout, a database constraint violation, or a JSON parsing error?

  • Translation: This is the perfect place to translate technical exceptions (e.g., SocketTimeoutException, HTTP 404) into meaningful Business Errors (e.g., NetworkUnavailable, UserNotFound).

  • Leakage Prevention: It prevents infrastructure details (like Retrofit annotations or HTTP status codes) from leaking into the clean Domain Layer.

  • Purity: It keeps the Domain Layer stable, pure, and focused solely on business rules.

The Use Case should simply receive a standardized result (like a Result sealed class) and decide what to do next, without worrying about the underlying technical failure modes.

Conclusion: The KFC Analogy

To summarize, a clear architecture in a modern Android app functions like an efficient KFC restaurant:

  • UI Layer (The Dining Area): This is where customers sit. It displays the menu and serves the food. It's concerned with presentation and user interaction, but it doesn't cook.

  • Domain Layer (The Waiters): They take orders and serve food. They handle specific customer requests independently. The process of taking an order is decoupled from the cooking process. They don't need to know how the fryer works; they just need to know the menu (interfaces).

  • Data Layer (The Kitchen): Busy and enclosed. They provide a unified interface (the service window) to the waiters. How the chicken is fried, stored, or sourced is their secret. The dining area never sees the chaos of the kitchen.

By adhering to these principles—Business Independence, Dependency Inversion, and Localized Exception Handling—we build apps that are not only robust and scalable but also clear enough for anyone on the team to understand and maintain.

Top comments (0)