惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
V2EX
小众软件
小众软件
MongoDB | Blog
MongoDB | Blog
Jina AI
Jina AI
G
Google Developers Blog
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
月光博客
月光博客
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
爱范儿
爱范儿
B
Blog
云风的 BLOG
云风的 BLOG
H
Hackread – Cybersecurity News, Data Breaches, AI and More
GbyAI
GbyAI
博客园 - 叶小钗
aimingoo的专栏
aimingoo的专栏
Blog — PlanetScale
Blog — PlanetScale
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
有赞技术团队
有赞技术团队
博客园_首页
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How to Structure a Scalable Flutter App Before It Structu...
Muhammad Zain Naseer · 2026-06-25 · via DEV Community

Every Flutter app starts the same way: one main.dart, a StatelessWidget, and a feeling of total control. Then features arrive. A login screen needs the same user object the profile screen mutates. A network call that lived happily inside a widget now needs to be reused in three places. Six weeks in, you're scrolling through a 1,400-line widget file looking for the one setState that's causing a rebuild storm, and "control" is the last word you'd use.

The good news: scalable Flutter architecture isn't a framework you bolt on later. It's a small number of boundaries you draw early, and then refuse to cross. This guide walks through the boundaries that matter, why each one exists, and how to apply them without turning a simple app into an enterprise cathedral.

The one rule that prevents most pain

If you take nothing else from this article, take this: your UI should never talk directly to your data sources.

A widget should not know whether the user's name came from a REST API, a local SQLite cache, or a hard-coded mock. The moment a widget knows that, you've welded your interface to your infrastructure. Swapping the API, adding a cache, or writing a test now means touching UI code — and UI code is the most volatile, most frequently rewritten code you own.

The fix is to put layers between them. Three is usually enough.

The three layers

1. Presentation — widgets, and the state objects that feed them. This layer's only job is to render state and forward user intent ("the user tapped Save"). It holds no business rules.

2. Domain — the logic that defines what your app actually does. "A user can't check out with an empty cart." "A draft auto-saves after 5 seconds of inactivity." This layer is pure Dart: no Flutter imports, no HTTP, no database. That purity is the point — it's the part of your app that's trivially testable and rarely needs to change just because a button moved.

3. Data — repositories and the data sources behind them (API clients, local storage, platform channels). This layer knows about JSON, status codes, and cache invalidation so that nothing above it has to.

Dependencies point in one direction only: Presentation → Domain → Data. The domain never imports the UI. The data layer never imports the domain's business logic. When you find yourself wanting to break that arrow, that's the signal you've put logic in the wrong place.

What this looks like in practice

Say you're building a screen that shows a user's profile. Here's the shape, layer by layer.

The data layer exposes a repository with an honest, source-agnostic interface:

abstract class UserRepository {
  Future<User> getUser(String id);
}

class UserRepositoryImpl implements UserRepository {
  UserRepositoryImpl(this._api, this._cache);
  final UserApi _api;
  final UserCache _cache;

  @override
  Future<User> getUser(String id) async {
    final cached = await _cache.read(id);
    if (cached != null) return cached;

    final user = await _api.fetchUser(id);
    await _cache.write(user);
    return user;
  }
}

Notice that the caching strategy lives here and nowhere else. The screen above will never know it exists — and if you later switch from a read-through cache to stale-while-revalidate, the UI doesn't change at all.

The presentation layer holds a state object that depends on the abstraction, not the implementation:

class ProfileNotifier extends ChangeNotifier {
  ProfileNotifier(this._users);
  final UserRepository _users;

  AsyncValue<User> state = const AsyncLoading();

  Future<void> load(String id) async {
    state = const AsyncLoading();
    notifyListeners();
    try {
      state = AsyncData(await _users.getUser(id));
    } catch (e) {
      state = AsyncError(e);
    }
    notifyListeners();
  }
}

And the widget just renders whatever state it's handed. Loading, error, and success are explicit states — not an afterthought you remember to handle after a crash report.

Choosing a state management approach (without the holy war)

Flutter's state management debate is exhausting because people argue about tools when the real question is about scope. Here's a pragmatic way to decide:

  • Ephemeral, single-widget state (a checkbox, an expanded/collapsed panel): just use setState. Reaching for a global solution here is over-engineering.
  • Shared, app-level state (the logged-in user, a shopping cart, feature flags): use a dependency-injected solution — Riverpod, Bloc, or provider with ChangeNotifier. Which one matters far less than using one consistently.

The trap isn't picking the "wrong" library. It's mixing three of them in the same codebase because each feature was built by someone with a different favorite. Pick one, write it in your team's README, and move on.

Organize by feature, not by type

A surprising amount of long-term maintainability comes from how you name folders. The instinct is to group by technical type:

lib/
  widgets/
  models/
  services/
  screens/

This works until you have forty screens, at which point every change to one feature means hopping across four directories. Group by feature instead:

lib/
  features/
    auth/
      data/
      domain/
      presentation/
    profile/
      data/
      domain/
      presentation/
  core/        // shared utilities, theming, networking

Now a feature is a vertical slice you can reason about — or delete — in one place. New developers can find things. And the layer boundaries from earlier show up right there in the folder names, which quietly enforces the discipline.

Where most teams actually go wrong

The failure mode is almost never "we didn't have enough architecture." It's the opposite: a team reads a clean-architecture article, generates eleven files and four abstractions to display a single string, and concludes that architecture is bureaucracy.

Scale your structure to your app. A weekend project doesn't need repositories. A two-screen utility might live happily with setState and a couple of service classes. The three-layer model earns its keep when you have real business logic, multiple data sources, or a team larger than one — and you'll feel the pull toward it naturally as those conditions appear.

The skill isn't applying every pattern. It's recognizing the moment a boundary will save you more than it costs, and drawing it then — one layer before the file gets to 1,400 lines, not one crisis after.