Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Mobile App Architecture: MVVM

By Kokil Thapa | Last reviewed: September 2026

Mobile App Architecture: MVVM keeps your screens dumb and your logic testable. Most production failures I see on API-backed mobile and web systems are not bad UI pixels. They are tangled state, duplicated validation, and network calls buried inside view controllers. MVVM—Model-View-ViewModel—fixes that split by giving each screen a ViewModel that owns presentation state while the View only renders and forwards user input. If you ship Android, iOS, Flutter, or a Progressive Web App, the pattern applies with different tooling but the same contract.

What is Mobile App Architecture: MVVM and how does it work?

MVVM is a presentation-layer pattern. The Model holds your domain data, validation rules, and persistence contracts. The View is the activity, fragment, SwiftUI view, or widget tree. The ViewModel sits between them. It exposes observable state and commands the View can bind to without knowing where data comes from.

On a legal-tech portal I built, the mobile companion needed offline-friendly document lists and secure uploads. We kept upload retry logic and token refresh in the ViewModel layer. The Android fragments only observed LiveData or StateFlow. That separation saved weeks when IRD-related form fields changed mid-project.

Mobile App Architecture: MVVMViewUI onlyObserves stateViewModelState + commandsNo Android APIsModelDomain + dataRepository layerbindsData sources below ModelREST API · Local DB · CacheLaravel Sanctum · Redis · Room
Mobile App Architecture: MVVM — View binds to ViewModel; ViewModel orchestrates Model and repositories

Core responsibilities in each layer

The View should not call HTTP clients directly. It should not format currency or compute eligibility rules. It renders UiState and emits events like onRetryClicked().

The ViewModel maps domain results into UI-friendly state. It handles loading flags, error messages, and pagination cursors. It survives configuration changes on Android when you use Jetpack ViewModel.

The Model layer is often a repository plus domain types. Your Laravel 13 API might return JSON that maps to Kotlin data classes or Swift structs. Validation that affects money or compliance stays on the server. The app repeats only what improves UX.

Data flow in one screen load

  1. View calls viewModel.loadOrders() on appear.
  2. ViewModel sets state to loading and calls OrderRepository.fetch().
  3. Repository hits your REST endpoint or local Room database.
  4. ViewModel maps success or failure into OrderUiState.
  5. View re-renders from the new observable state.

This loop matches how I structure modern Laravel backends for mobile clients. Thin controllers, validated Form Requests, and consistent JSON shapes make ViewModels predictable.

How does MVVM differ from MVC and MVP on mobile?

Teams often pick MVVM because binding libraries and lifecycle-aware components matured. MVC still appears in older iOS codebases where view controllers grew into god objects. MVP helps when you need explicit presenter interfaces for legacy Java Android code.

PatternWho owns UI logicTestabilityTypical mobile stackMain risk
MVCController / ViewControllerLow without heavy mockingLegacy UIKit, early AndroidMassive view controllers
MVPPresenter pushes to passive ViewHigh with interfacesOlder Android, some enterprise appsBoilerplate interfaces
MVVMViewModel exposes observable stateHigh with coroutines / CombineJetpack, SwiftUI, Flutter, .NET MAUILeaking Android context into ViewModel

For greenfield apps in 2026, MVVM—or MVVM plus Clean Architecture—is the default on Android. iOS teams often use MVVM with SwiftUI and ObservableObject. Cross-platform frameworks like Flutter mirror the same idea with Provider, Riverpod, or Bloc.

Your backend choice rarely forces the mobile pattern. A WooCommerce REST API for mobile apps works with any of these. MVVM just keeps your cart and checkout screens maintainable when product rules change every festival season in Nepal.

How do you implement MVVM in Android with Jetpack?

Android’s official guidance aligns with MVVM through ViewModel, LiveData or StateFlow, and Repository. Google documents this in the Guide to app architecture. Here is a minimal but production-shaped example.

Define UI state as a sealed class

sealed class BookingUiState {
    data object Loading : BookingUiState()
    data class Success(val items: List<BookingItem>) : BookingUiState()
    data class Error(val message: String) : BookingUiState()
}

ViewModel exposes state and actions

class BookingViewModel(
    private val repository: BookingRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<BookingUiState>(BookingUiState.Loading)
    val uiState: StateFlow<BookingUiState> = _uiState.asStateFlow()

    fun loadBookings(userId: String) {
        viewModelScope.launch {
            _uiState.value = BookingUiState.Loading
            repository.getBookings(userId)
                .onSuccess { _uiState.value = BookingUiState.Success(it) }
                .onFailure { _uiState.value = BookingUiState.Error(it.message ?: "Unknown error") }
        }
    }
}

Repository talks to API and cache

class BookingRepository(
    private val api: BookingApi,
    private val dao: BookingDao
) {
    suspend fun getBookings(userId: String): Result<List<BookingItem>> = runCatching {
        val remote = api.fetchBookings(userId)
        dao.insertAll(remote)
        remote
    }
}

On a booking-heavy project like Adventure Third Pole Trek, this structure let us add supplier sync without rewriting fragments. The API layer matched a Laravel 12 backend with Sanctum tokens.

MVVM Request FlowUser tapViewCompose UIViewModelStateFlowRepositoryCache + APILaravel REST APIJSON · Auth · ValidationState update propagates upView recomposes · shows list or error
Mobile App Architecture: MVVM request flow — user action travels down; observable state travels up

Wire the View with Compose

@Composable
fun BookingScreen(viewModel: BookingViewModel = hiltViewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    when (state) {
        BookingUiState.Loading -> CircularProgressIndicator()
        is BookingUiState.Success -> BookingList(state.items)
        is BookingUiState.Error -> ErrorBanner(state.message) {
            viewModel.loadBookings(currentUserId)
        }
    }
}

Use Hilt or Koin for dependency injection. Keep the ViewModel free of Context except through an injected dispatcher or saved-state handle. That rule alone prevents half the memory leaks I audit in client codebases.

How does MVVM work on iOS and cross-platform apps?

Apple’s SwiftUI pairs naturally with MVVM. Microsoft’s original MVVM documentation for XAML remains a useful conceptual reference for .NET MAUI MVVM guidance. SwiftUI views observe an ObservableObject ViewModel.

SwiftUI ViewModel sketch

@MainActor
final class DocumentListViewModel: ObservableObject {
    @Published private(set) var state: LoadState = .idle
    private let repository: DocumentRepository

    init(repository: DocumentRepository) {
        self.repository = repository
    }

    func load(clientId: UUID) async {
        state = .loading
        do {
            let docs = try await repository.fetchDocuments(clientId: clientId)
            state = .loaded(docs)
        } catch {
            state = .failed(error.localizedDescription)
        }
    }
}

On client portals with document uploads, this pattern mirrors server-side policies. The ViewModel never stores raw passwords. It requests short-lived tokens from your API—the same approach we use on secure law-firm client portals.

Flutter teams implement MVVM with ChangeNotifier, Riverpod notifiers, or Bloc. Bloc adds explicit event reducers. It is closer to MVI than classic MVVM, but the separation goal is identical.

React Native projects often use hooks plus a service layer. That is MVVM in spirit even when nobody labels it. What matters is the boundary: screens do not import axios directly.

When should you choose MVVM over other mobile patterns?

Choose MVVM when your app has multiple screens sharing state, network-heavy workflows, or a long maintenance horizon. Skip it for a five-screen utility that will never change. The setup cost is real.

  • Complex forms and wizards: ViewModels centralise step validation and draft saving.
  • Offline-first: Repository sync logic stays out of UI. Pair with service worker patterns on web hybrids.
  • Role-based apps: Map JWT claims to UI flags in one place.
  • Apps backed by Laravel or Symfony APIs: Stable DTO mapping reduces mobile churn when endpoints evolve.
  • Teams with QA automation: ViewModels unit-test faster than Espresso alone.

Consider MVP or MVI when you need stricter unidirectional data flow for large Android teams. Consider plain MVC only for throwaway prototypes.

Pick MVVM?New mobile app in 2026Tiny scopeMVC may sufficeAPI + offlineChoose MVVMLarge Android teamMVVM or MVIAdd Repository + DIUnit test ViewModels earlyAlign API with backend versioning
Decision guide for Mobile App Architecture: MVVM versus lighter patterns on new projects

Budget matters for Nepal startups. A full MVVM stack with CI, lint, and UI tests costs more upfront. It pays back when Dashain traffic doubles and you cannot afford emergency refactors. Plan for Rs 150,000–400,000 (~USD 1,100–3,000) extra engineering time on a mid-size app compared to a quick MVC spike.

What are common MVVM mistakes in production mobile apps?

MVVM is simple on slides and messy in production. These failures show up repeatedly when I review apps that consume APIs I built.

God ViewModels

One ViewModel for an entire checkout, profile, and settings flow becomes a new god object. Split by feature screen or use nested ViewModels with shared repositories.

Business logic duplicated on client and server

Mobile should not re-implement VAT rules or court-fee calculations that belong on the server. Use your API as the source of truth. Client validation is advisory only. For Nepal-specific calculators, deep-link to trusted web tools like the Nepal court fee calculator when accuracy beats offline convenience.

Ignoring lifecycle and cancellation

Coroutine jobs that keep running after navigate-away cause crashes and wasted battery. Always bind network work to viewModelScope or Swift task cancellation.

Leaking UI framework types into ViewModels

Importing androidx.compose or SwiftUI into ViewModels breaks unit tests and reuse. Map to plain data classes at the boundary.

Skipping contract tests on API boundaries

Mobile releases lag server deploys. Publish OpenAPI specs from your Laravel or Symfony app. Validate JSON fixtures in mobile CI. This pairs well with mobile CI/CD with Fastlane pipelines.

Before vs After MVVMBeforeAfterSingle ActivityHTTP in fragmentSQL in adapterUntestable 800 linesDeploy fearThin Compose ViewViewModel 120 linesRepository + APIUnit tests greenSafe releasesrefactor
Production Mobile App Architecture: MVVM reduces coupling compared to logic-heavy views

Testing strategy that actually helps

Unit-test ViewModels with fake repositories. Test repositories against mock web servers. Keep a small set of UI tests for critical flows—login, pay, submit. Run them in testing and optimization pipelines before store submission.

Security belongs in the stack too. Store tokens in EncryptedSharedPreferences or Keychain. Pin certificates only when you understand rotation cost. Read mobile app security basics before exposing sensitive client documents on mobile.

How do you connect MVVM mobile apps to Laravel and Symfony backends?

Most apps I deliver are not mobile-only. They are Laravel 12 or Symfony 8.1 APIs with optional native shells. MVVM on the client maps cleanly to layered backends.

Expose versioned JSON under /api/v1/. Use Sanctum personal access tokens or OAuth2 for mobile. Return problem-details style errors so ViewModels can show field-level messages.

// Laravel API Resource — stable mobile contract
class BookingResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->uuid,
            'title' => $this->title,
            'status' => $this->status,
            'starts_at' => $this->starts_at?->toIso8601String(),
        ];
    }
}

Push heavy work to queues. Mobile ViewModels should poll or subscribe via SSE/WebSocket only when necessary. Most listing screens work fine with pull-to-refresh against cache.

Event-driven updates on the server—see event-driven architecture with Laravel events—can invalidate Redis cache keys your mobile repository reads through ETag headers.

For eCommerce, product catalog APIs should paginate aggressively. A scalable catalog design on the server prevents mobile memory blowups. The same principle applies whether the storefront is WooCommerce 11.1 or a custom Laravel cart like Quick And Easy Nepalese Grocery.

Debug JSON payloads during integration with a JSON formatter before you hard-code DTOs. Small naming mismatches cause silent null fields in Kotlin and Swift.

Key Takeaways

  • Mobile App Architecture: MVVM separates UI rendering from presentation state so screens stay small and testable.
  • Use platform ViewModels—Jetpack on Android, ObservableObject on SwiftUI—with a Repository layer for API and local cache.
  • Keep business rules on the server; ViewModels format state and handle user commands only.
  • Prefer MVVM for multi-screen, API-heavy, or long-lived apps; skip heavy structure for disposable prototypes.
  • Avoid god ViewModels, lifecycle leaks, and untested API contracts—pair mobile CI with OpenAPI or fixture tests.
  • Align JSON contracts with your Laravel or Symfony backend so mobile releases decouple from server deploys.

People Also Ask

Is MVVM still relevant for mobile apps in 2026?

Yes. Google still recommends MVVM with Jetpack for Android. SwiftUI and cross-platform frameworks use the same observable-state model. Newer patterns like MVI extend MVVM rather than replace it.

Can MVVM work with Flutter or React Native?

Yes. Any stack that exposes observable state and keeps widgets or components thin can follow MVVM. Flutter uses ChangeNotifier or Riverpod; React Native uses hooks plus service modules.

Does MVVM replace Clean Architecture?

No. MVVM is a presentation pattern. Clean Architecture defines layer boundaries across domain, data, and UI. Production apps often combine both.

What is the best way to test an MVVM ViewModel?

Inject fake repositories, drive functions under test, and assert on state emissions. Use Turbine for Kotlin Flow and XCTest async for Swift. Keep Android framework classes out of the ViewModel.

Ship mobile apps with a backend that matches your architecture

Mobile App Architecture: MVVM pays off when your API, cache, and UI state tell the same story. Start with one feature slice—login or a single list screen—and prove the ViewModel plus Repository split before you scale. If you need a Laravel or Symfony API and a mobile-friendly contract designed together, review our enterprise application development and custom software development services. See delivered work on the portfolio, explore related guides on the blog, or contact us to plan your next release.

Frequently Asked Questions

MVVM splits apps into Model (data and rules), View (UI), and ViewModel (presentation state and commands). The View binds to the ViewModel, which talks to repositories and APIs—keeping business logic out of UI code and making unit tests practical.

In MVC, the controller or view controller owns UI logic, which often produces massive view controllers on legacy UIKit or early Android. MVP puts logic in a presenter that pushes to a passive view, giving high testability but more interface boilerplate on older Android codebases. MVVM exposes observable state from the ViewModel that the View binds to, which fits Jetpack, SwiftUI, Flutter, and .NET MAUI. The main MVVM risk is leaking Android context into the ViewModel. For greenfield apps in 2026, MVVM or MVVM plus Clean Architecture is the default on Android.

Google’s Guide to app architecture aligns with MVVM through ViewModel, LiveData or StateFlow, and a Repository layer. Define UI state as a sealed class such as Loading, Success, and Error. The ViewModel exposes StateFlow and launches coroutines in viewModelScope to call the repository. The repository hits your REST API and local Room cache. Wire the View with Jetpack Compose using collectAsStateWithLifecycle and inject dependencies with Hilt or Koin. Keep the ViewModel free of Context except through injected dispatchers or a saved-state handle to prevent memory leaks.

SwiftUI pairs naturally with MVVM using an ObservableObject ViewModel marked @MainActor, with @Published state and async repository calls. Flutter teams use ChangeNotifier, Riverpod notifiers, or Bloc—the last adds explicit event reducers closer to MVI but with the same separation goal. React Native projects often use hooks plus a service layer, which is MVVM in spirit even without the label. What matters across platforms is the boundary: screens do not call HTTP clients directly. On client portals with document uploads, the ViewModel requests short-lived tokens from your API rather than storing raw passwords.

Choose MVVM when your app has multiple screens sharing state, network-heavy workflows, or a long maintenance horizon. It suits complex forms and wizards, offline-first apps with repository sync logic, role-based apps mapping JWT claims to UI flags, and apps backed by Laravel or Symfony APIs where stable DTO mapping reduces churn. Skip heavy MVVM structure for a five-screen utility that will never change—the setup cost is real. Consider MVP or MVI when large Android teams need stricter unidirectional data flow. Plain MVC fits only throwaway prototypes.

For Nepal startups, plan Rs 150,000–400,000 (~USD 1,100–3,000) extra engineering time on a mid-size app versus a quick MVC spike.

God ViewModels that own an entire checkout, profile, and settings flow become new god objects—split by feature screen or use nested ViewModels with shared repositories. Duplicating VAT rules or court-fee calculations on the client when the API should be source of truth is another repeat failure; client validation is advisory only. Ignoring lifecycle causes coroutine jobs to run after navigate-away, wasting battery and causing crashes—bind work to viewModelScope or Swift task cancellation. Importing Compose or SwiftUI into ViewModels breaks unit tests. Skipping OpenAPI or fixture contract tests lets silent null fields slip through when server deploys outpace mobile releases.

Inject fake repositories, drive ViewModel functions under test, and assert on state emissions. Use Turbine for Kotlin Flow and XCTest async for Swift. Keep Android framework classes out of the ViewModel so tests run fast without heavy mocking. Test repositories separately against mock web servers. Keep a small set of UI tests for critical flows like login, pay, and submit, run through testing pipelines before store submission. Pair mobile CI with OpenAPI specs or JSON fixture validation so API boundary changes fail in CI rather than in production.

Expose versioned JSON under /api/v1/ with Sanctum personal access tokens or OAuth2 for mobile auth. Return problem-details style errors so ViewModels can show field-level messages. Use Laravel API Resources for stable mobile contracts with consistent field names and ISO8601 dates. Push heavy work to server queues; mobile ViewModels should poll or subscribe via SSE or WebSocket only when necessary—most listing screens work with pull-to-refresh against cache. Paginate product catalog APIs aggressively to prevent mobile memory blowups. Debug JSON payloads with a formatter before hard-coding Kotlin or Swift DTOs—small naming mismatches cause silent null fields.

Yes. Google still recommends MVVM with Jetpack for Android, and SwiftUI plus cross-platform frameworks use the same observable-state model.

Yes. Any stack exposing observable state and keeping widgets or components thin can follow MVVM—Flutter uses ChangeNotifier or Riverpod; React Native uses hooks plus service modules.

No. MVVM is a presentation-layer pattern defining how View, ViewModel, and Model interact at the UI boundary. Clean Architecture defines broader layer boundaries across domain, data, and UI. Production apps often combine both—MVVM handles screen-level state while Clean Architecture keeps repositories, use cases, and domain rules separated underneath. On API-backed apps I deliver, the mobile ViewModel maps DTOs from a Laravel or Symfony backend without the View ever touching HTTP clients directly.

The View renders UiState and emits events like onRetryClicked() or calls viewModel.loadOrders() on appear. It should not call HTTP clients directly, format currency, or compute eligibility rules—that belongs in the ViewModel or on the server. On Android the View is an activity, fragment, or Compose screen observing LiveData or StateFlow. On iOS it is a SwiftUI view observing an ObservableObject. Keeping views dumb means when IRD-related form fields or festival-season product rules change, you update ViewModels and API contracts without rewriting entire UI files.

A god ViewModel is one class that owns checkout, profile, settings, and unrelated flows—it recreates the massive view-controller problem MVVM was meant to fix. Split ViewModels by feature screen or user journey, and share data through injected repositories rather than one mega-class. On a booking-heavy project, separate supplier sync from the booking list ViewModel so new API endpoints do not force rewrites across unrelated fragments. If a ViewModel file grows past what one screen needs, extract nested ViewModels or move cross-cutting state into a scoped repository.

The View calls viewModel.loadOrders() on appear. The ViewModel sets state to loading and calls OrderRepository.fetch(). The repository hits your REST endpoint or local Room database. The ViewModel maps success or failure into OrderUiState—a sealed class with Loading, Success, and Error variants. The View re-renders from the new observable state without knowing where data originated. User actions travel down to the ViewModel; observable state travels up to the View. This loop stays predictable when your Laravel backend returns thin controllers, validated Form Requests, and consistent JSON shapes.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: