← BACK TO BLOG

AirADB Architecture Deep Dive

#CleanArchitecture #FlutterDesktop #Provider #SoftwareDesign

AirADB is built with Clean Architecture in Flutter for Windows Desktop. This post breaks down every architectural decision — why I made it, and what I'd do differently.

"Clean Architecture doesn't slow you down. Poor architecture does — just later."

Why Clean Architecture for a Desktop Utility?

When I started AirADB, a senior developer told me: "It's a small utility, just put everything in one file." I almost listened.

I'm glad I didn't. Six weeks into development, I needed to change how IP detection worked. Because the logic lived in the Domain layer — completely separate from the UI — the change took 20 minutes. In a monolithic approach, it would have taken hours of untangling.

The Four Layers

AirADB follows a strict four-layer separation:

Layer 1 of 4

Presentation Layer

Contains all Flutter widgets, screens, and the ConnectionProvider (state management via Provider package).

  • DashboardScreen — main app screen
  • DevicesScreen — device list
  • SettingsScreen — user preferences
  • ConnectionProvider — the single source of truth for app state

The Presentation layer knows nothing about ADB. It only calls repository methods and reacts to state changes. This means I could completely replace the backend — swap ADB for a different tool — without touching a single widget.

Layer 2 of 4

Domain Layer

The heart of the application. Contains business logic and entities with zero dependencies on Flutter or external packages.

  • DeviceRepository (interface) — defines what operations are possible
  • DeviceInfo, DeviceList, ConnectionResult — pure Dart entities
  • AdbStatus — represents ADB installation state

The Domain layer is the most important layer to get right. It defines the 'what' of your application, independent of the 'how'. If someone asked "what does AirADB do?" — the Domain layer is the answer.

Layer 3 of 4

Data Layer

Implements the Domain interfaces using real ADB commands.

  • DeviceRepositoryImpl — concrete implementation of DeviceRepository
  • AdbService — all ADB command execution lives here
  • CommandExecutor — runs system processes with timeout management

The Data layer is where Flutter and Dart's Process API come in. It's the only layer that knows ADB commands exist.

Layer 4 of 4

Core / Utils

  • ErrorMapper — translates technical exceptions to user-friendly messages
  • RetryHandler — configurable retry logic with backoff
  • LoggerService — timestamped logging with level control
  • AppConfig — environment configuration (dev/staging/production)
  • AppConstants — all magic strings and values in one place

State Management: Why Provider?

I evaluated Riverpod, BLoC, and Provider. For AirADB's complexity level, Provider was the right choice:

Why Provider

  • ConnectionProvider is a single ChangeNotifier — easy to reason about
  • The connection flow is sequential, not concurrent — BLoC's event streams would be overkill
  • Riverpod's added complexity wasn't justified for one primary flow

When Not Provider

  • If AirADB grows to manage multiple devices simultaneously, I'd reconsider Riverpod
  • For complex async flows, BLoC might be better

For V1, Provider is clean and sufficient.

The Connection Flow in Code

The 7-stage connection flow in AdbService.establishWirelessConnection() is the most important method in the codebase. Each stage emits a ConnectionProgress callback — this is what drives the real-time UI updates.

Stage 1 checkAdbInstallation()
Stage 2 getConnectedDevices() → firstAuthorizedUsbDevice
Stage 3 enableTcpIpMode(deviceSerial)
Stage 4 getDeviceIpAddress(deviceSerial)
Stage 5 connectWireless(ipAddress)
Stage 6 verifyWirelessConnection(ipAddress)
Stage 7 return ConnectionResult.success()

Every stage has early return on failure with a specific error message. The caller (ConnectionProvider) handles the result and updates UI state accordingly.

What I'd Do Differently

  • Add unit tests from day one — I wrote none for V1. This is my biggest regret.
  • Use Riverpod for multi-device V2 — the reactive graph will simplify concurrent device state.
  • Abstract CommandExecutor behind an interface — would make mocking for tests trivial.

View the Source Code

The full source code is on GitHub

View on GitHub