Building two native apps for iOS and Android means two teams, two codebases, and double the cost. If your business needs a mobile app, you know the pain. At Meteora Web, we've seen too many SMEs stuck between tight budgets and long timelines. Flutter solves this: one Dart codebase compiles into performant apps for both platforms. It's not magic—it's engineering. And it works in production.
Why Flutter over React Native, Kotlin, or Swift
Flutter compiles to native code. It's not a WebView in disguise. Every pixel is drawn by the Skia engine directly on the GPU. The result: performance on par with native apps, but with a single codebase. React Native uses a JavaScript bridge that introduces latency. Flutter doesn't. Plus, Dart offers hot reload: you change code and see results in half a second. For developers, that means fast iteration. For clients, fewer billable hours.
Dart: a simple language with real power
If you know Java, C#, or JavaScript, Dart will feel familiar. It's typed, compiled both AOT (release) and JIT (debug). Installation is straightforward:
# Install Dart SDK (or Flutter SDK which includes it)
brew install dart
# Verify
dart --version
Starting a Flutter project:
flutter create my_app
cd my_app
flutter run
Hot reload changes the game. During development, you save the file and the app updates without losing state. We use it every day to tweak margins, colors, and logic on the fly—no full rebuild.
Dart basics: variables, functions, classes
void main() {
print('Hello, Meteora!');
}
int sum(int a, int b) => a + b;
class User {
final String name;
User(this.name);
}
Null safety built-in: String? name means it can be null. No more NullPointerException at runtime.
Sponsored Protocol
Widgets: everything is a widget
In Flutter everything is a widget. The button, the padding, the row, even the app itself. There are no Activities or ViewControllers. You build the UI by composing widgets inside widgets. This is the widget tree.
StatelessWidget vs StatefulWidget
- StatelessWidget: never changes. Icons, static text, fixed layouts.
- StatefulWidget: can change over time. A counter, a form, a list that updates.
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() {
setState(() { _count++; });
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(onPressed: _increment, child: Text('+1')),
],
);
}
}
Beware: overusing setState in large widgets will slow the app. That's where state management comes in.
State management: Provider, Riverpod, Bloc
A real app has dozens of states: logged-in user, cart, notifications. Managing them with setState is like paying taxes with receipts in a drawer. You need a method.
Provider (recommended to start)
Provider is the official solution, simple and stable. Wrap an object with ChangeNotifier and expose it with ChangeNotifierProvider. Widgets listen with context.watch.
Sponsored Protocol
class Cart extends ChangeNotifier {
int _quantity = 0;
int get quantity => _quantity;
void add() { _quantity++; notifyListeners(); }
}
// In main.dart
runApp(
ChangeNotifierProvider(create: (context) => Cart(), child: MyApp()),
);
// In a widget
final cart = context.watch<Cart>();
Text('${cart.quantity} items');
Riverpod (more testable and safe)
Riverpod removes dependency on context. Providers are global but type-safe. Great for large apps with automated tests.
Bloc (event-driven, for complex logic)
Bloc separates events and states using streams. Powerful but verbose. We use it for login forms with async validation or real-time search.
Navigation and routing
Navigation between screens is handled by Navigator. For simple apps, Navigator.push works. For complex apps, use GoRouter (official) which supports declarative routing, redirects, and deep links.
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (context, state) => HomePage()),
GoRoute(path: '/detail/:id', builder: (context, state) => DetailPage(id: state.pathParameters['id']!)),
],
);
Deep links are essential for marketing campaigns: a link opens the product detail directly. GoRouter handles them natively.
Flutter with Firebase: backend without servers
Firebase provides authentication, Firestore database, storage, and push notifications. We use it for apps with registered users and real-time data. Integration is straightforward:
Sponsored Protocol
// pubspec.yaml
dependencies:
firebase_core: ^2.24.0
firebase_auth: ^4.16.0
cloud_firestore: ^4.14.0
// main.dart
await Firebase.initializeApp();
// Email login
UserCredential user = await FirebaseAuth.instance.signInWithEmailAndPassword(
email: 'a@b.com',
password: '123456',
);
For push notifications, Firebase Cloud Messaging integrates natively. Pay attention to iOS permission handling.
UI: Material Design, Cupertino, and custom widgets
Flutter offers two widget sets: Material (Android) and Cupertino (iOS). You can use both for a native look. But we prefer custom widgets for the client's brand. Example: a button with shadow and gradient.
class MeteoraButton extends StatelessWidget {
final String label;
const MeteoraButton({required this.label});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue, Colors.purple]),
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(blurRadius: 4, color: Colors.grey)],
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Text(label, style: TextStyle(color: Colors.white)),
),
);
}
}
HTTP and API with Dio
Apps need to talk to servers. Dio is the best HTTP client for Dart. It supports interceptors, retries, timeouts, and error handling.
Sponsored Protocol
final dio = Dio(BaseOptions(baseUrl: 'https://api.meteoraweb.com'));
void fetchProducts() async {
try {
final response = await dio.get('/products');
final list = response.data as List;
// update state
} on DioException catch (e) {
// handle error
}
}
If you prefer a no-code backend, check our Bubble for SMEs guide.
Testing: unit, widget, and integration
If you don't test, you don't know if the app breaks. Flutter has native testing:
- Unit tests: test single functions and classes.
- Widget tests: test a widget in isolation with pumpWidget.
- Integration tests: test the full app on device or emulator.
test('sum should work', () {
expect(sum(2, 3), 5);
});
testWidgets('Counter increments', (tester) async {
await tester.pumpWidget(CounterApp());
expect(find.text('Count: 0'), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text('Count: 1'), findsOneWidget);
});
We write tests for every critical API call and checkout flow. It reduces production bugs by 70%.
Publishing to Play Store and App Store
Publishing a Flutter app is similar to a native one, but with some specifics.
Signing the app
For Android, generate a keystore:
keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
Configure android/app/build.gradle with credentials. For iOS, use Xcode and manage provisioning profiles.
Store optimization
Reduce APK size with flutter build appbundle. Use shrinking and obfuscation (--obfuscate --split-debug-info). For iOS, enable bitcode if required.
Sponsored Protocol
Performance: making the app smooth
A stuttering app won't be used. Flutter is fast, but you must avoid pitfalls.
Profiling with DevTools
Flutter DevTools includes frame rendering chart, memory profiler, CPU profiler. Open with flutter devtools while the app is running.
Best practices
- Use
constwidgets where possible (reduces rebuilds). - Avoid deep widget composition: extract into methods or separate widgets.
- Optimize images with
cached_network_imageand compression. - For long lists, use
ListView.builderinstead ofListView(children: []).
In summary — what to do now
- Install Flutter and follow the official course.
- Choose state management: start with Provider, move to Riverpod for larger apps.
- Integrate Firebase for authentication and real-time data.
- Write tests for critical flows before release.
- Publish to both stores with signed builds and optimized size.
- Monitor performance with DevTools during development.
If your company doesn't have resources for native or Flutter development, consider no-code solutions like Bubble — we covered it in our operational guide. But if you want a professional mobile app with a single team, Flutter is the right choice. At Meteora Web, we use it in production for clients who sell, manage inventory, and interact with customers in real time. And it works.