← Back to blog

Practical Mobile App Architecture for Developers: MVVM, MVI, Offline

October 1, 2026
Practical Mobile App Architecture for Developers: MVVM, MVI, Offline

For production mobile apps, favour Clean Architecture boundaries paired with MVVM as your default pattern, switching to MVI only for screens with genuinely complex state. This combination gives you testability, long-term maintainability, and close alignment with platform guidance from both Android Developers and Flutter. Add offline-first design wherever data availability matters to your users. These patterns have been applied across many client projects by experienced app developers, and the results back up the recommendation.


TL;DR:

  • Use a repository as the single source of truth and treat the local database as canonical for offline-first apps, with persistent queues for write operations.
  • Prefer constructor dependency injection, mainly on Android with Hilt, and split your project into clear modules to improve code quality and build efficiency.
  • Apply MVVM as the default pattern for simple screens, reserving MVI for complex state flows like multi-step forms, and use separate event channels for one-off events.
  • Enforce core principles such as separation of concerns, unidirectional data flow, and state ownership that considers limited memory and connectivity issues on mobile devices.
  • Start with a minimal architecture for MVPs and expand to layered, modular structures as feature complexity, team size, or long-term maintenance requirements grow.

Pocketapp
pocketapp.co.uk
Build A Mobile App That Lasts
Pocket App combines strategic discovery, UX/UI design, development, and cross-platform deployment for tailored mobile solutions.
Explore Pocket App

Table of Contents

Core principles every mobile app architecture must enforce

Whatever pattern you choose, four principles do the real work. Separation of concerns keeps your business rules independent of frameworks, so a change to your UI toolkit or database never forces a rewrite of your core logic. A single source of truth stops data from drifting out of sync across screens, while unidirectional data flow makes state changes predictable and easier to debug.

Mobile devices add constraints that server-side architecture rarely faces: limited memory, unpredictable connectivity, and a process lifecycle that can kill your app at any moment. State ownership has to account for all three.

  • Separation of concerns: business rules stay framework-free and testable in isolation.
  • Single source of truth: one authoritative store per data type, usually a repository.
  • Unidirectional data flow: state moves in one direction, making bugs traceable.
  • Modularisation: low coupling and high cohesion between feature areas.

These four principles recur through every pattern discussed below, and they are what Android's architecture guidance builds its recommendations around.

Clean Architecture: what it gives you and how to apply it pragmatically

The defining property of Clean Architecture is not a fixed count of layers. It is the dependency rule: business logic points inward, never outward towards frameworks, databases, or UI code. Get that direction right and you can swap a database or a UI toolkit without touching your domain rules.

A domain or use-case layer earns its cost on multi-team projects and apps expected to live for years, where business rules are substantial enough to warrant isolation. For a small app built by two developers over three months, a full domain layer often adds ceremony without a matching return.

  • Data layer: repositories and services, talking to APIs and local storage.
  • Domain layer: use cases and business rules, with no framework imports.
  • Presentation layer: view models and UI state, consuming the domain layer.

In long-lived projects, enforce the dependency rule with build-time checks: lint rules and module boundaries that stop domain code importing framework packages, rather than relying on developer discipline alone.

Pro Tip: Start with three folders (data, domain, presentation) and only split further when a module genuinely needs its own build target.

MVVM vs MVI: practical comparison and platform implementation notes

MVVM remains the sensible default because it maps naturally onto observable state in modern UI toolkits: SwiftUI, Jetpack Compose, and most Flutter setups all expect a view model exposing a stream of state that the view renders. It carries lower ceremony than MVI and most teams already understand it.

MVI earns its keep on screens with complex state transitions, such as a checkout flow or a multi-step form with validation, where explicit intents and a single state reducer make behaviour easier to reason about and test. The trade-off is more boilerplate: every user action becomes an explicit intent object rather than a direct method call.

  • MVVM: view model exposes state via StateFlow, Combine, or a StateHolder; view observes and renders.
  • MVI: user actions become intents, a reducer produces new state, the view renders that state only.
  • Mixed approach: MVVM for most screens, MVI reserved for the two or three screens where state genuinely gets tangled.

Command patterns for one-off events (navigation, snackbars) sit awkwardly in both, so most teams add a separate event channel alongside the state stream.

Data layer and offline-first architecture: repositories, sync and conflict resolution

Offline cache sync and conflict resolution

Repositories should be the single source of truth for their data type, with services kept stateless and responsible only for wrapping external APIs, a split that both Flutter's official guidance and Android's architecture docs recommend. A repository that accretes extra responsibilities, caching logic here, mapping logic there, is a common production surprise that quietly erodes testability.

For apps where users expect the interface to work without a live connection, Android's offline-first guidance treats the local database as canonical. Reads come from local storage exposed as a Flow, writes go into a durable queue, and synchronisation happens when connectivity returns.

  • Treat the local database as canonical, not the network response.
  • Queue writes persistently so they survive process death, using a WorkManager-style pattern with retry and backoff.
  • Resolve conflicts with clear heuristics, such as last-write-wins or field-level merging, decided upfront rather than improvised at sync time.

A practical pattern worth adopting: Android's offline-first documentation recommends at least two data sources per repository that touches the network, one local and one remote, so the app keeps working when the remote source fails.

Test repositories with fake data sources rather than mocked network calls, since that exercises the actual sync logic rather than just the request shape.

Dependency injection and modularisation: wiring and build-time hygiene

Constructor injection should be your default: dependencies arrive explicitly through initialisers, which makes testing straightforward and avoids the hidden global state that service locators introduce. Service locators still have a place in smaller apps where the wiring overhead of a full DI framework outweighs the benefit, but the trade-off is weaker compile-time safety.

On Android, Hilt is the commonly recommended choice for projects with several feature modules and a non-trivial dependency graph. On iOS, a factory or container pattern handles most needs, with Swinject an option once the graph grows complex enough to justify a dedicated library.

  • Prefer constructor injection for testability and explicit dependencies.
  • Choose Hilt on Android once you have more than a handful of feature modules.
  • Split projects into feature, data, and core modules, following the Android modularisation guidance on low coupling and high cohesion.
  • Default to "implementation" rather than "api" visibility between modules to keep build times down.

Module boundaries pay off twice: they force cleaner interfaces between teams, and they cut incremental build times as the codebase grows, a concern covered in more depth in Pocket App's guide to app scalability.

State management and platform-specific practical notes

Each major UI toolkit now pushes state ownership out of view code and into dedicated holders, which reduces lifecycle coupling and simplifies testing, a shift Android's own recommendations call out directly.

  • Compose: pair a ViewModel with StateFlow, collect it with collectAsStateWithLifecycle, and keep a single Activity owning navigation.
  • SwiftUI: use @State for local view state, @StateObject or @ObservedObject for shared models, and the @Observable macro introduced at WWDC 2023 to cut wrapper boilerplate and track only the properties a view actually reads.
  • Flutter: organise by feature folder with an MVVM-like split, choosing Provider or Riverpod for dependency wiring and connecting view models to repositories rather than services directly.

Pro Tip: Whichever toolkit you use, keep navigation state and screen state in separate holders. Mixing the two is the most common source of state bugs teams report.

Pocket App's iOS design guidance covers how these state choices affect the interface layer in more detail.

Trade-offs and a compact decision rubric for choosing patterns

No single pattern fits every project, so weigh these axes before committing:

  1. Team size: solo or two-person teams rarely need a full domain layer; five or more engineers usually do.
  2. App lifetime: a prototype built to validate an idea does not need the same rigour as a banking app expected to run for a decade.
  3. State complexity: simple list-and-detail screens suit MVVM; multi-step flows with heavy validation suit MVI.
  4. Offline requirements: any app used in low-connectivity settings should treat local storage as canonical from day one.
  5. Regulatory or security needs: healthcare and financial apps justify stricter layering and audit trails regardless of team size.

Start minimal for an MVP: a thin MVVM layer over a single repository is often enough to validate a concept. Red flags that should trigger more structure include rapid feature growth that keeps touching the same files, a second team joining the codebase, or a compliance requirement landing on your backlog. At that point, the ceremony of Clean Architecture stops being overhead and starts paying for itself.

Implementation checklist: concrete steps, test targets and launch checks

Work through this sequence when building or refactoring an architecture:

  1. Split each feature into repository and service responsibilities, with the repository as SSOT.
  2. Wire dependencies through constructor injection, adding a DI framework once the graph grows past a handful of classes.
  3. Give each screen its own state holder, exposing a single observable state stream.
  4. Confirm the local database is canonical for any data the app needs offline.
  5. Write unit tests for use cases and business rules first, since they carry the highest logic density.
  6. Add integration tests for repositories, exercising both local and remote data sources.
  7. Cover state flows with UI tests that assert on rendered state, not implementation details.
  8. Check cold start time, memory use under load, and sync behaviour when connectivity drops mid-write.

Pro Tip: Run your offline-write queue tests with the network disabled mid-test, not just before the test starts. That catches the failures real users actually hit.

Pocket App evidence: how experience from real projects maps to these recommendations

Experience from numerous projects spanning retail, charity, healthcare, and B2B platforms shows a common pattern: apps with clear repository boundaries and offline-first data layers cause far fewer support tickets after launch than apps where the UI talks to the network directly.

That experience informs the structuring of discovery and architecture reviews for new client projects, helping catch state management issues and modularisation gaps before they become expensive to fix mid-build.

  • Discovery workshops surface offline and scale requirements before a single screen is built.
  • Architecture reviews check dependency direction and repository boundaries against the patterns covered here.
  • Teams across different sectors have used this process to avoid late-stage rewrites.

Teams unsure whether their current architecture will hold up can request a review through a Mobile App Clinic.

Author's perspective on architecture choices in 2026

AI-assisted coding tools generate more code than ever, but they do not remove the need for architectural discipline. If anything, clear module boundaries and explicit contracts matter more, since generated code needs a well-defined place to slot into rather than a loose pile of files.

My practical rule for migrations: start small, secure your single source of truth first, and only add a domain layer once the business rules genuinely justify the extra indirection. Architectures that new team members can understand within a day tend to outlast the clever ones.

— Paul

Pocket App services: discovery, architecture review and delivery options

Getting the architecture right from the start saves the cost of a rewrite later, and that is exactly what Pocket App's process is built to catch early. Rather than handing you generic advice, Pocket App's Discover, Design, Develop and Deploy process walks through your specific requirements, including offline needs, team size, and expected growth, before a single line of code gets written.

Pocketapp

  • Discover: a strategic workshop that surfaces requirements, including data availability and scale, before design starts.
  • Design: UX/UI work informed by the state and navigation patterns the architecture supports.
  • Develop: build cycles that apply layering and modularisation principles.
  • Deploy: cross-platform release management once the app is ready.
  • Mobile App Clinics: focused sessions for teams who already have an app and want a second opinion on its architecture.

If your team is weighing up a rewrite, a refactor, or a fresh build, a Mobile App Clinic is a practical way to get an outside view on your repository boundaries, state management, and offline strategy before you commit engineering time.

Sources

For readers who want to go deeper than this article covers, these sources carry the detail:

FAQ

What are the three types of mobile apps?

Mobile apps generally fall into native, cross-platform, and hybrid categories. Native apps are built specifically for one platform using its own tools, such as SwiftUI for iOS or Compose for Android, giving the closest fit with platform guidance. Cross-platform frameworks like Flutter share most of the codebase across iOS and Android, while hybrid apps wrap web technology inside a native shell.

What is the best architecture app for Android?

There is no single approved architecture app: Android's own developer guidance recommends a layered structure combining repositories as the single source of truth, ViewModels as state holders, and Jetpack Compose for the UI layer. Following these official Android architecture recommendations gives you a solid foundation without locking you into a specific vendor tool.

How do I design a mobile app?

Start by separating your business logic from your UI and data layers, then choose MVVM as your default pattern unless a specific screen has genuinely complex state that justifies MVI. Build your data layer around repositories as the source of truth, add offline support if your users need it, and modularise once the codebase grows past what a single team can comfortably navigate.

Which architecture is best for Android?

For most production Android apps, a layered architecture combining Clean Architecture's dependency rule with MVVM and repositories as the single source of truth is the pattern endorsed by Android's official guidance. Teams building screens with complex state transitions, such as multi-step forms, often add MVI for those specific screens while keeping MVVM elsewhere.