# How to Apply Layered Architecture (UI, Logic, Data) in Flutter Apps

> Master layered architecture in Flutter apps. Separate UI, Logic, and Data layers for independent testing and easy component replacement. Enhance your app development.

- Repository: [Flutter/skills](https://github.com/flutter/skills)
- Tags: architecture
- Published: 2026-05-09

---

**Layered architecture in Flutter separates your app into UI (widgets/events), Logic (use cases/BLoC), and Data (repositories/services) layers, enabling independent testing and replacement of components.**

The `flutter/skills` repository demonstrates this pattern through its CLI-driven skill-generation tool, where clear boundaries between presentation, domain, and infrastructure concerns allow the same business logic to run identically across command-line and GUI environments. By implementing this three-layer structure in your Flutter applications, you create a codebase where UI changes never break business rules and data sources can be swapped without touching widget code.

## What Is Layered Architecture in Flutter?

A layered (or "clean") architecture divides your application into three distinct responsibilities:

- **UI / Presentation Layer**: Renders widgets, collects user input, and forwards events to business logic using `StatelessWidget`, `StatefulWidget`, or state management solutions like `BlocBuilder`
- **Logic / Domain Layer**: Contains pure business rules, coordinates data flow, and exposes clean APIs through use-case classes, BLoC/Cubit implementations, and repository interfaces
- **Data / Infrastructure Layer**: Accesses external resources (REST APIs, local storage, files) and translates them into plain Dart models via services and data providers

This isolation ensures each layer can be **tested independently**, **replaced** (e.g., swapping a network implementation for a mock), and **evolved** without ripple effects across the codebase.

## UI Layer: Handling Presentation and User Input

The UI layer's sole responsibility is parsing input and dispatching commands to the logic layer. In `flutter/skills`, the CLI entry point serves this role by parsing arguments and forwarding them to domain commands.

```dart
// File: tool/generator/bin/skills.dart
import 'package:args/args.dart';
import 'package:skills/tool/generator/lib/src/commands/generate_skill_command.dart';

void main(List<String> arguments) {
  final parser = ArgParser()..addFlag('help', abbr: 'h', negatable: false);
  final result = parser.parse(arguments);

  if (result['help'] as bool) {
    print('Usage: skills <command> [options]');
    return;
  }

  // UI layer: dispatch to a command (logic layer)
  GenerateSkillCommand().run(result.rest);
}

```

This pattern translates directly to Flutter UI code. Widgets should only handle `ArgParser`-like input parsing and instantiate command objects, never performing business logic or direct network calls.

## Logic Layer: Implementing Business Rules

Commands and use cases orchestrate data retrieval, validation, and coordination while remaining agnostic to *how* data is fetched. The `GenerateSkillCommand` in `flutter/skills` demonstrates this by processing arguments, delegating network calls to services, and executing pure domain logic.

```dart
// File: tool/generator/lib/src/commands/generate_skill_command.dart
class GenerateSkillCommand extends BaseSkillCommand {
  @override
  Future<void> run(List<String> args) async {
    final params = SkillParams.fromArgs(args);
    final markdown = await ResourceFetcherService()
        .fetchMarkdown(params.sourceUrl);          // calls Data layer
    final skill = Skill.fromMarkdown(markdown);    // pure domain logic
    await skill.writeToFile(params.outputPath);    // pure domain logic
  }
}

```

Key characteristics of the logic layer:

- **Pure Dart implementation** (`Skill.fromMarkdown`, `writeToFile`) containing no UI or platform code
- Delegation of data fetching to service abstractions (`ResourceFetcherService`)
- Processing of `SkillParams` models that travel between layers as plain objects

## Data Layer: Managing External Resources

Services hide implementation details of HTTP requests, file I/O, and external APIs. The UI and Logic layers never call `http.get` directly; they request data through repository abstractions.

The `ResourceFetcherService` handles network requests:

```dart
// File: tool/generator/lib/src/services/resource_fetcher_service.dart
import 'package:http/http.dart' as http;

class ResourceFetcherService {
  Future<String> fetchMarkdown(String url) async {
    final response = await http.get(Uri.parse(url));
    if (response.statusCode != 200) {
      throw Exception('Failed to fetch resource');
    }
    return response.body;
  }
}

```

Similarly, the `GeminiService` encapsulates AI API communication:

```dart
// File: tool/generator/lib/src/services/gemini_service.dart
class GeminiService {
  final String apiKey; // injected from the environment or config

  Future<String> generateSkillDescription(String prompt) async {
    // Build request → call Gemini API → parse response
    // All of that lives in the Data layer.
  }
}

```

This layer centralizes **network error handling**, **authentication**, and **serialization**, ensuring the rest of your app works with plain `String` or model objects rather than `http.Response` instances or raw JSON.

## Mapping CLI Patterns to Flutter UI Applications

The same three-layer structure from the `flutter/skills` CLI tool maps directly to Flutter UI projects:

| UI (Flutter) | Logic (BLoC / ViewModel) | Data (Repository) |
|--------------|--------------------------|-------------------|
| `SkillPage` (a `StatelessWidget` that displays a list) | `SkillBloc` that receives UI events, calls the use-case, and emits states | `SkillRepository` that uses `ResourceFetcherService` and `GeminiService` underneath |
| UI → `SkillBloc.add(LoadSkills())` | `SkillBloc` → `GetSkillsUseCase.execute()` | `GetSkillsUseCase` → `SkillRepository.getAll()` |
| UI ← `BlocBuilder` shows `SkillLoaded(skills)` | `SkillRepository` → `ResourceFetcherService.fetchMarkdown()` | `ResourceFetcherService` → HTTP GET |

### UI Implementation Example

```dart
class SkillPage extends StatelessWidget {
  final SkillBloc bloc = SkillBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Skills')),
      body: BlocBuilder<SkillBloc, SkillState>(
        bloc: bloc,
        builder: (_, state) {
          if (state is SkillLoading) return const CircularProgressIndicator();
          if (state is SkillLoaded) {
            return ListView(
              children: state.skills.map((s) => ListTile(title: Text(s.title))).toList(),
            );
          }
          return const Text('Tap to load');
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => bloc.add(LoadSkills()),
        child: const Icon(Icons.refresh),
      ),
    );
  }
}

```

### Logic Implementation Example

```dart
class GetSkillsUseCase {
  final SkillRepository repository;

  GetSkillsUseCase(this.repository);

  Future<List<Skill>> call() async {
    final markdown = await repository.fetchSkillMarkdown('https://example.com/skill.md');
    return SkillParser.fromMarkdown(markdown);
  }
}

```

### Data Implementation Example

```dart
class SkillRepository {
  final ResourceFetcherService fetcher;

  SkillRepository(this.fetcher);

  Future<String> fetchSkillMarkdown(String url) => fetcher.fetchMarkdown(url);
}

```

Because each layer has a single responsibility, you can unit-test the **logic** without any Flutter framework dependencies, mock the **data** layer with `mockito`, and run widget tests for the **UI** in isolation.

## Summary

- **Separate concerns** into UI (widgets/events), Logic (use cases/commands), and Data (services/repositories) layers as demonstrated in `flutter/skills`
- **Keep the UI layer thin** by limiting it to input parsing and event dispatching, following the pattern in `tool/generator/bin/skills.dart`
- **Implement business rules** in pure Dart classes like `GenerateSkillCommand` that delegate external operations to service abstractions
- **Encapsulate external I/O** in the Data layer using services such as `ResourceFetcherService` and `GeminiService` to centralize error handling and serialization
- **Enable testability** by ensuring the Logic layer has no Flutter or platform dependencies, allowing unit tests to run without `flutter_test`

## Frequently Asked Questions

### What is the main benefit of layered architecture in Flutter?

Layered architecture allows you to modify, test, and replace individual components without affecting the rest of your application. For example, you can swap the HTTP implementation in `ResourceFetcherService` without changing any code in `GenerateSkillCommand` or your Flutter widgets, because the Logic layer depends only on abstract interfaces rather than concrete implementations.

### How does the UI layer communicate with the logic layer?

The UI layer dispatches events or command objects to the Logic layer and reacts to state changes or return values. In the `flutter/skills` CLI, `skills.dart` instantiates `GenerateSkillCommand` and calls `run()`. In a Flutter UI, widgets add events to a BLoC or call use-case methods directly, then rebuild based on emitted states or `Future` results, keeping presentation concerns separated from business rules.

### Can I use layered architecture with Riverpod or Provider instead of BLoC?

Yes. The architecture pattern is agnostic to your state management solution. Whether you use `ChangeNotifier` with Provider, `StateNotifier` with Riverpod, or `Cubit` with flutter_bloc, the key principle remains: your UI layer triggers actions in the Logic layer, which may be implemented as ViewModels, Notifiers, or Use Cases, while the Data layer remains hidden behind repository interfaces like `ResourceFetcherService`.

### Where should network error handling live in a Flutter layered architecture?

Network error handling belongs in the **Data layer**. Services like `ResourceFetcherService` catch HTTP exceptions, timeouts, and authentication failures, then translate them into domain-specific exceptions or result objects that the Logic layer can handle uniformly. The Logic layer should never see raw `http.Response` objects or deal with status codes directly, as implemented in `tool/generator/lib/src/services/resource_fetcher_service.dart`.