Flutter app development: architecture, setup and when to use it
Contents
Flutter app development means building iOS, Android, web and desktop apps from one Dart codebase, rendered by Flutter's own engine instead of native views. That choice drives most of the trade-offs: consistent UI and fast iteration on one side, a thinner native boundary and a smaller Dart talent pool on the other.
For a CTO or engineering manager, the useful question is whether those trade-offs fit your team's skills, UI requirements and native API needs. The answer differs for an MVP, a design-heavy consumer app and an enterprise tool.
What is Flutter app development?
Flutter app development is building iOS and Android apps from a single codebase written in Dart, using the Flutter SDK. Teams adopt it to stop maintaining two apps that drift apart in features and release timing. To see how this plays out in practice, look at real-world mobile apps built with Flutter.
The Flutter SDK bundles the framework, the Dart toolchain and flutter doctor, which checks that the Android and iOS toolchains are installed correctly.
We recommend Flutter when one team has to ship both platforms with a custom UI, because a single codebase removes duplicated feature work. Netguru designs and builds cross-platform mobile products, including Flutter apps with Firebase backends and enterprise integrations.
Apps that depend heavily on platform-specific APIs are a weaker fit. Many newcomers ask whether Flutter is a language of its own, but it is an SDK that relies on the Dart programming language.
Is Flutter good for app development? What stays platform-specific
Flutter is a good fit when one team owns a UI-heavy product on iOS and Android, because the single codebase covers screens, navigation, state, and business logic. It does not cover permissions, signing, store metadata, or OS version drift. Teams that need help with those platform-specific layers can lean on dedicated mobile app development services for signing, store releases, and OS compatibility.
The UI layer shares well because Flutter draws its own widget tree. The Impeller rendering engine, which replaced Skia as the default renderer on iOS and on recent Android versions, paints it, so the same widget code looks consistent across OS releases (Flutter Impeller documentation). That removes most per-platform UI forks.
The layers side by side:
| Layer | Shared in one codebase? | What stays per-platform |
|---|---|---|
| UI, navigation, state management | Yes | Review of platform conventions such as back gestures |
| Business logic | Yes | Nothing, unless it calls native APIs |
| Permissions | Partly | Info.plist usage strings, AndroidManifest entries, different runtime prompts |
| Signing and release | No | Apple certificates and provisioning profiles; Android keystore and Play App Signing |
| Store metadata | No | App Store Connect and Play Console listings, privacy declarations |
| OS version drift | Partly | New OS releases can break plugins until maintainers update them |
| Accessibility | Partly | Semantics map to VoiceOver and TalkBack, so test on both |
Xcode on a Mac is still required for iOS builds and signing.
We recommend budgeting platform-channel work for Bluetooth, background processing, and features that ship with a new OS release. Those areas have the thinnest plugin coverage and the most native code.
Why does Flutter use Dart?
Flutter uses Dart because one language supports JIT compilation for hot reload in debug builds and AOT compilation to native machine code for release builds.
Debug builds run on the Dart VM, which injects changed source into the running app and keeps widget state. Release builds are compiled ahead of time, so the shipped app carries no interpreter. Sound null safety, mandatory since Dart 3, lets the compiler reject null errors before the app runs.
Ramp-up depends on the language a developer already writes:
| Background | Transfers directly | Expect friction |
|---|---|---|
| Kotlin | Null-safety syntax (?, !), sealed classes, extension functions, async/await | Futures and Streams replace coroutines and Flow; smart casts do not apply to mutable fields |
| Swift | Optionals, ?. and ??, async/await, declarative UI as in SwiftUI | No value-type structs, so copy semantics need explicit handling |
| TypeScript | Syntax, generics, async/await, Futures that behave like Promises | Nominal typing, no union types, and isolates that share no memory |
Our view is that Kotlin and Swift developers feel productive in Dart sooner than they do in the widget tree. Composition-heavy UI code is the larger adjustment, and it is where most of the learning time goes.
Editor choice matters less than the language. If you are still deciding between editors, our comparison of the best Flutter IDE options covers the trade-offs in more detail.
Profile performance only in profile or release mode. Debug builds run under JIT and misrepresent frame times.
How the widget tree and Impeller render your app
Flutter builds your UI as a widget tree of immutable configuration objects, and the Impeller rendering engine draws the result straight to the GPU. Flutter keeps three parallel trees: widgets, elements, and render objects.

A rebuild creates new widgets cheaply, elements diff them, and only the long-lived render objects do layout and paint. Unlike React Native, which maps to native components, Flutter has no platform view in this path.
The widget categories you meet most often:
| Category | Examples | Rebuild behavior |
|---|---|---|
| Structural | Scaffold, MaterialApp | Rebuilt when the parent rebuilds |
| Layout | Center, Column, Padding | Cheap config; updates the render object |
| Leaf display | Text, Icon, Image | Rebuilt with the parent |
| Stateful | Checkbox, TextField | State object survives rebuilds |
| Inherited | Theme, MediaQuery | Rebuilds only dependents |
Scaffold(
body: Center(child: Text('Hello World')),
)
The Scaffold supplies the screen structure, Center handles layout, and the Text widget displays "Hello World".
Where Skia-era jank came from
Under Skia, shaders compiled at runtime the first time an animation needed them, which produced dropped frames on first run. Impeller precompiles its shaders when the engine is built, which removes that class of jank. Check the release notes for which platforms and OS versions use Impeller by default before assuming it.
Hot reload failure modes
The hot reload docs list the changes that need a full restart or lose state:
- Global and static field initializers
- Converting an enum to a class, or the reverse
- Changes to generic types
- Any native code change on the Android or iOS side
Where to take benchmarks from
Skip generic frame-rate claims. Profile your own screens in profile mode on physical mid-range devices with DevTools. Use the flutter/flutter GitHub issue tracker for renderer regressions on specific GPUs.
Flutter vs native vs React Native: Which fits your team?
Flutter fits teams that want a single Dart codebase with the same UI on every platform; React Native fits JavaScript teams; native or Kotlin Multiplatform fits apps dominated by platform APIs.
The Impeller rendering engine draws every pixel itself, so Flutter UI looks the same across devices and OS versions. React Native, an extension of the React library, maps to native components, so controls look and behave like each platform's own.
The options side by side:
| Option | Fits a team that has | UI fidelity | Native API depth | Main trade-off |
|---|---|---|---|---|
| Flutter | Mixed or junior mobile skills, willing to adopt Dart | Pixel-level control, same on every platform | Platform channels and Dart FFI for gaps | Platform look must be recreated in widgets |
| React Native | Strong JavaScript or React web engineers | Native components, platform look by default | Native modules, mature for common APIs | Bridge and dependency upkeep across upgrades |
| Kotlin Multiplatform | Kotlin or Android engineers, plus Swift for iOS | Fully native UI per platform | Direct access, shared business logic only | UI is written twice |
| Native (Swift, Kotlin) | Separate iOS and Android specialists | Highest, day-one access to new OS features | Complete | Two codebases and two release pipelines |
Our rule: pick by the constraint that is hardest to change. A brand-led UI with custom animation points to Flutter, a web team reusing React skills points to React Native, and heavy use of ARKit, HealthKit, or background services points to native or Kotlin Multiplatform. Team skills outweigh benchmark differences between the first two.
If you lack the skills in-house, a delivery partner can fill the gap. Netguru built the LANEXO lab inventory app in Flutter for Merck KGaA, Darmstadt, Germany, which made inventory management 97% faster.
Flutter app development tutorial: From install to a first build
Install the Flutter SDK, run flutter doctor until every check passes, then flutter create and flutter run give you a first build. Most lost time sits in the toolchain checks, not in Dart.

The sequence after unpacking the SDK and adding flutter/bin to your PATH:
flutter doctor
flutter doctor --android-licenses
flutter create hello_app
cd hello_app
flutter run
Flutter doctor audits the SDK, Android toolchain, Xcode, connected devices, and IDE plugins. Treat each red or yellow line as a blocker, because later build errors usually trace back to one of them.
Common environment failures
| Symptom | Cause | Fix |
|---|---|---|
| Android licenses not accepted | SDK licenses unsigned, or cmdline-tools missing | Install Android SDK Command-line Tools in SDK Manager, then run flutter doctor --android-licenses |
| CocoaPods not installed, or pod install fails | Missing or outdated CocoaPods, common on Apple Silicon Macs | Install via Homebrew, then run pod install in ios/ |
| Xcode incomplete or too old | Command-line tools not selected, or Xcode below the minimum for your Flutter release | Run sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer and sudo xcodebuild -runFirstLaunch, then check the minimum version on docs.flutter.dev |
VS Code or IntelliJ
The IntelliJ plugin has more built-in tooling, including launching an iOS simulator or Android emulator straight from the IDE. VS Code is lighter and is the better pick for developers without a Java background. We recommend VS Code for web and mobile teams starting out, and IntelliJ or Android Studio for teams already living in JetBrains tools.
A scaffold Hello World
Replace lib/main.dart with a minimal Dart app to confirm the pipeline works:
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(
home: Scaffold(body: Center(child: Text('Hello World'))),
));
With the app running, press r in the terminal for hot reload. It injects changed code and keeps state. Changes to main() or to initial state need a hot restart with R.
Open Flutter DevTools next. The widget inspector shows the widget tree, which is the quickest way to debug overflow errors and unexpected padding.
Beyond Hello World: Signing, store release and CI
A Flutter release needs a signed Android App Bundle, a provisioned iOS build, and a CI pipeline that reproduces both without anyone's laptop. Most tutorials stop at flutter run. The order of work below is where release weeks go missing.
- Pin the toolchain. Fix the Flutter SDK version (FVM or a .fvmrc file) and make flutter doctor -v the first CI step, so a drifted Xcode or Android SDK fails in seconds.
- Android signing. Generate an upload keystore, reference it from a git-ignored key.properties, and enroll in Play App Signing. Build with flutter build appbundle --release.
- iOS provisioning. Register the bundle ID, create a distribution certificate and an App Store provisioning profile, then run flutter build ipa --release. Upload to TestFlight with an App Store Connect API key.
- Store submission. Use internal testing on Play and TestFlight on Apple before any production review. Check Google Play's current target API level requirement before each release.
CI is where iOS signing breaks. A hosted runner starts with an empty keychain, so builds fail on a missing certificate or a profile that doesn't match the bundle ID. Install the certificate into a temporary keychain, or fetch signing assets through the App Store Connect API.
Caching is the other recurring failure. Cache ~/.gradle, the pub cache, and CocoaPods (Pods/ plus the spec repo), keyed on pubspec.lock, Podfile.lock and the Gradle wrapper version. A stale Pods cache after a plugin upgrade produces errors that disappear on a clean build.
For CI, keep the tool you already run for native projects: Bitrise, for example, builds Flutter alongside iOS and Android. Codemagic, which targets Flutter projects, is a reasonable alternative if you want managed signing with less setup.
Which state management should you pick: Riverpod, Bloc or provider?
Pick Riverpod for most teams of two to eight developers, Bloc when several squads need a strict event-and-state contract, and Provider only for small apps you expect to stay small. State management touches every screen, so it is the Flutter decision that costs most to reverse.
The trade-offs side by side. Team sizes are our illustrative judgement, not benchmarks:
| Provider | Riverpod | Bloc | |
|---|---|---|---|
| Best fit | 1-3 developers, few screens, prototypes | 2-8 developers, a growing feature set | 8+ developers or several squads |
| Test effort | ChangeNotifier classes unit-test easily, but anything reading BuildContext needs a pumped widget tree | Override providers in a ProviderContainer; most logic tests need no widget tree | bloc_test pattern: given events, expect states; most boilerplate to write |
| Cost to leave later | Low, because the surface is small | Medium | Highest: an event and state class pair per feature |
Riverpod suits the middle because provider overrides replace mocking frameworks in tests, and dependencies resolve without BuildContext. Bloc earns its boilerplate when many people touch one codebase: each feature's behavior reads off its event list, and code review stays mechanical.
Reversal cost depends less on the library than on where business logic lives. If rules sit in repositories and use cases, a migration swaps a thin layer. If they sit inside state classes, every feature gets rewritten.
Migrate feature by feature. Provider and Riverpod can run side by side in one app, and a Bloc feature can sit next to either. Write tests around a feature before touching it, start with a low-risk screen, and avoid a big-bang rewrite, which carries the most risk and the longest freeze on feature work.
Connecting a backend and reaching native features
Start with Firebase for the backend and pub.dev packages for native features, and write custom Dart-to-native code only when no maintained package covers the need.
The FlutterFire plugins expose Authentication, Cloud Firestore, and Cloud Messaging through one Dart API on iOS and Android. A small team gets login, data sync, and push notifications without writing a server. Firestore's query model and per-read pricing push relational or reporting-heavy products toward a custom REST or GraphQL API sooner.
The native boundary is where Flutter projects pick up hidden cost. Three failure modes are worth planning for:
- Platform channels: method names and arguments travel as untyped maps, so a mismatch between the Dart caller and the Kotlin or Swift handler surfaces at runtime. Pigeon generates typed interfaces and removes most of that risk.
- Dart FFI: calls go through the C ABI, so memory ownership and threading are yours to manage. A leaked pointer may only show up on a physical device.
- Plugin maintenance: a pub.dev package that lags behind a Flutter, Android Gradle Plugin, or iOS SDK upgrade blocks your own upgrade. Before adopting one, check its last publish date, open issues, and publisher verification.
Cost follows how much of this boundary you own. The comparison below reflects our judgement, not benchmark data:
| Cost driver | MVP | Enterprise |
|---|---|---|
| Backend | Managed Firebase services, one developer | Custom APIs, SSO, audit logging, several environments |
| Native features | Existing pub.dev packages | Custom channels or FFI for vendor and hardware SDKs |
| Maintenance | Package updates at each Flutter upgrade | Forked plugins and a per-platform test matrix |
The largest cost step comes when the team needs its own native engineers. That usually happens at the first vendor SDK with no Flutter plugin.
Flutter app development with AI: What helps and what to check
Coding assistants speed up Dart boilerplate, but they suggest APIs from older Flutter releases and pub.dev packages that have since been discontinued. Treat every generated change as unreviewed code written against a stale SDK.

Typical failures include WillPopScope where current code uses PopScope, Color.withOpacity where newer SDKs expect withValues, textScaleFactor in place of textScaler, and snippets that predate null safety. Suggested pub.dev packages may be abandoned or may not support your target platforms.
Four checks catch most of this:
- Run flutter analyze and treat deprecated_member_use as a build failure. dart fix --apply migrates many of these automatically.
- Run dart pub outdated, then check each suggested package's discontinued flag, publisher, and last release on pub.dev before adding it.
- Write widget tests and golden tests for generated UI. They catch layout regressions that compile cleanly.
- Profile in Flutter DevTools. The widget inspector and performance view expose excess rebuilds, and the memory view shows leaks from controllers that generated code never disposes.
On-device AI usually arrives through pub.dev packages such as ML Kit wrappers or TensorFlow Lite bindings. Check platform support and model size before committing. None of these checks catch wrong business logic, so human review of state management and data handling stays mandatory.
When should you not use Flutter?
Avoid Flutter when the app leans on native platform APIs, when install size is tight, when new iOS features must ship on day one, or when you can't hire Dart developers.
- Native-API-heavy apps: Bluetooth stacks, background processing, and camera or AR pipelines push most of the work into platform channels or Dart FFI. You end up maintaining Swift and Kotlin anyway, with a Dart layer on top.
- Tight size budgets: AOT compilation bundles your Dart code with the Flutter engine, so a minimal app starts larger than its native equivalent, which affects emerging-market installs and app-clip-style experiences.
- Day-one Apple features: Live Activities and WidgetKit home screen widgets need native code, and plugin support on pub.dev packages tends to follow the OS release.
- Thin Dart talent: The hiring pool is smaller than JavaScript's. A JavaScript-heavy team may ship faster with React Native.
Before you commit, audit the pub.dev packages your roadmap depends on for maintenance status and platform coverage. The benefits of Flutter overview covers the other side of the trade-off.
None of this rules out enterprise use. Regulated, multi-platform products with standard UI needs are a good fit. The Flutter app Netguru built for METRO BRAZIL grew daily active users by 70% in its first three months. For a food ordering example, see the Lunching.pl delivery app.
Flutter app development FAQ
Is Flutter used for app development?
Is Flutter difficult to learn?
Is Flutter better than React?
Can I learn Flutter in 3 months?
Plan your Flutter project with Netguru
A Flutter project plan with Netguru starts with a short discovery phase that fixes scope, architecture, and a realistic estimate before any build begins.
We decide how much of the product fits one single codebase, where Firebase covers authentication and push notifications, and which features need platform channels. Our Flutter app development services draw on a pool of 3,000+ specialists, and our wider software development team covers backend and AI work.
Share your scope and constraints, and we will return a scoped estimate.
