Clean Architecture in Flutter with Riverpod — A Real-World Deep Dive

Building apps that work is easy.
Building apps that last — that’s where things get tricky.

At our studio, we work with startups and small teams where speed matters, but we can’t afford a mess. That’s why we use a mix of Clean Architecture + Riverpod in Flutter. It keeps things organised, testable, and easy to maintain — even six months down the line.

Let’s break it down. 

🎯 What Problem Are We Solving?

You start a Flutter app, and everything goes in main.dart.
Next comes a service or two. Then, some state handling.
Soon, business logic sneaks into widgets… and boom — your app is a spaghetti plate.

You can't test it easily. Onboarding new devs is painful. Features take longer. Bugs creep in.

Solution: Clean up the layers. Separate what the app does from how it looks. Make logic testable. Make code readable.

That’s what Clean Architecture does.

🧱 Layered Thinking (In Simple Terms)

We break our app into three main parts:

  • Presentation Layer — Widgets, UI, Screens.

  • Application Layer — State, use-cases, logic.

  • Data Layer — APIs, DBs, services.

Each layer only talks to the one below it. The UI doesn’t know how data is fetched. It only knows what to show. The logic doesn’t care where the data comes from. It just works with the result.

This keeps everything modular, reusable, and much easier to test.

  

🌿 Why We Use Riverpod

We’ve tried BLoC, Provider, and even GetX. But for most real-world projects, Riverpod strikes the perfect balance between simplicity and power.

  • It’s declarative — your state updates re-actively.

  • It’s safe — compile-time errors catch mistakes.

  • It’s test-friendly — mocks are easy.

  • It plays well with clean separation of concerns.

💡 Let’s Build a Mini-Example

We’ll build a very simple "Quotes App" that fetches a random quote.

🧰 Data Layer: The Quote Repository

class QuoteRepository {

  Future<String> fetchRandomQuote() async {

    await Future.delayed(Duration(seconds: 1));

    return "Clean code always wins.";

  }

}


class QuoteRepository {
  Future<String> fetchRandomQuote() async {
    await Future.delayed(Duration(seconds: 1));
    return "Clean code always wins.";
  }
}

class QuoteRepository {
  Future<String> fetchRandomQuote() async {
    await Future.delayed(Duration(seconds: 1));
    return "Clean code always wins.";
  }
}
🌿 Riverpod Provider

final quoteRepoProvider = Provider<QuoteRepository>((ref) {
  return QuoteRepository();
});

⚙️ Application Layer: Use Case

class GetRandomQuote {
  final QuoteRepository repo;

  GetRandomQuote(this.repo);

  Future<String> execute() => repo.fetchRandomQuote();
}

final getQuoteProvider = Provider<GetRandomQuote>((ref) {
  final repo = ref.watch(quoteRepoProvider);
  return GetRandomQuote(repo);
});

🧠 State Notifier (for UI state)
class QuoteState extends StateNotifier<AsyncValue<String>> {
  final GetRandomQuote getQuote;

  QuoteState(this.getQuote) : super(const AsyncValue.loading()) {
    loadQuote();
  }

  Future<void> loadQuote() async {
    state = const AsyncValue.loading();
    try {
      final quote = await getQuote.execute();
      state = AsyncValue.data(quote);
    } catch (e) {
      state = AsyncValue.error(e);
    }
  }
}

final quoteNotifierProvider =
    StateNotifierProvider<QuoteState, AsyncValue<String>>((ref) {
  final useCase = ref.watch(getQuoteProvider);
  return QuoteState(useCase);
});

📱 UI Layer (Presentation)

class QuoteScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final quoteState = ref.watch(quoteNotifierProvider);

    return Scaffold(
      appBar: AppBar(title: Text("Quotes")),
      body: Center(
        child: quoteState.when(
          loading: () => CircularProgressIndicator(),
          data: (quote) => Text(quote),
          error: (e, _) => Text("Oops! $e"),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(quoteNotifierProvider.notifier).loadQuote(),
        child: Icon(Icons.refresh),
      ),
    );
  }
}

👨‍🔧 Final Thoughts

This architecture may feel like “too much” for very small apps, but once your app starts growing, you’ll thank yourself. Or your future teammates will.

With Riverpod, clean layers  we build fast without losing control.

🔗 Explore our site, ask us anything, or start your digital journey today at flangotech.com 



Comments

Popular posts from this blog

Why Flutter Is Our First Choice for MVPs (Minimum Viable Product or Model View Presenter)

Why choose Python and Django?