Flutter #
Flutter is Google’s UI framework that lets you build mobile apps (iOS & Android), web, and desktop from a single Dart codebase. What sets Flutter apart from other frameworks isn’t just “one code, many platforms” — Flutter has its own rendering engine (Skia/Impeller) that draws every pixel directly to the screen, without depending on platform-native UI components. The result is consistent look and performance across all platforms, including smooth 60/120fps animations. For Dart developers new to Flutter, this article builds an understanding of Flutter’s way of thinking from a Dart perspective.
Why Does Flutter Matter for Dart Developers? #
Dart and Flutter are complementary ecosystems:
flowchart LR
DART["Dart SDK\n(programming language)"] --> SERVER["Server-side\ndart:io, shelf"]
DART --> CLI["Command-line\ndart compile"]
DART --> FLUTTER["Flutter SDK\n(UI framework)"]
FLUTTER --> MOBILE["Mobile\niOS & Android"]
FLUTTER --> WEB["Web\nFlutter Web"]
FLUTTER --> DESKTOP["Desktop\nmacOS, Windows, Linux"]The Dart knowledge you already have — null safety, async/await, classes, generics, mixins — is all directly applicable in Flutter. What you need to learn is Flutter’s way of thinking: everything is a widget.
Installing Flutter #
The Flutter SDK already includes the Dart SDK:
# macOS — via Homebrew
brew install flutter
# Or download manually from flutter.dev
# Extract and add to PATH
# Verify the installation
flutter doctor
# Output shows the status of each component:
# [✓] Flutter (Channel stable, 3.x.x)
# [✓] Android toolchain
# [✓] Xcode (for iOS/macOS)
# [✓] VS Code with the Flutter extension
Everything is a Widget #
The most fundamental concept in Flutter: everything visible on screen is a widget. Text is a widget. Buttons are widgets. Padding is a widget. Even the app itself is a widget.
Widgets in Flutter are immutable descriptions of UI pieces — not objects that can be changed directly, but blueprints Flutter uses to build and render the UI:
import 'package:flutter/material.dart';
// The simplest Flutter app
void main() {
runApp(
const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello, Flutter!'),
),
),
),
);
}
This structure is a widget tree — each widget is a parent of the widgets below it:
MaterialApp
└── Scaffold
└── Center
└── Text('Hello, Flutter!')
StatelessWidget vs StatefulWidget #
Two fundamental widget types to understand before anything else:
StatelessWidget — Widgets Without State #
Widgets whose appearance is entirely determined by the parameters they receive — no internal state that changes:
import 'package:flutter/material.dart';
// StatelessWidget — build() is called once, doesn't change unless the parent rebuilds
class KartuProduk extends StatelessWidget {
final String nama;
final double harga;
final String? urlGambar;
const KartuProduk({
super.key,
required this.nama,
required this.harga,
this.urlGambar,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (urlGambar != null)
Image.network(urlGambar!),
Text(
nama,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Rp ${harga.toStringAsFixed(0)}',
style: const TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
}
// Usage
KartuProduk(
nama: 'Laptop Gaming',
harga: 15000000,
urlGambar: 'https://example.com/laptop.jpg',
)
StatefulWidget — Widgets with State #
Widgets whose appearance can change based on user interaction or data that changes over time:
import 'package:flutter/material.dart';
// A StatefulWidget is always paired with its State
class KeranjangBelanja extends StatefulWidget {
const KeranjangBelanja({super.key});
@override
State<KeranjangBelanja> createState() => _KeranjangBelanjaState();
}
class _KeranjangBelanjaState extends State<KeranjangBelanja> {
// State — data that can change
final List<String> _items = [];
int _totalItem = 0;
// Lifecycle methods
@override
void initState() {
super.initState();
// Called once when the widget is first created
// Good for initialization: load data, set up controllers, subscribe to streams
_muatDataAwal();
}
@override
void dispose() {
// Called when the widget is removed from the tree
// Required: close controllers, cancel subscriptions, release resources
super.dispose();
}
Future<void> _muatDataAwal() async {
final items = await ambilKeranjangDariStorage();
setState(() {
_items.addAll(items);
_totalItem = items.length;
});
}
void _tambahItem(String item) {
// setState() tells Flutter that the state changed → rebuild the UI
setState(() {
_items.add(item);
_totalItem = _items.length;
});
}
void _hapusItem(int index) {
setState(() {
_items.removeAt(index);
_totalItem = _items.length;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Keranjang ($_totalItem item)'),
),
body: _items.isEmpty
? const Center(child: Text('Keranjang kosong'))
: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) => ListTile(
title: Text(_items[index]),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _hapusItem(index),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _tambahItem('Produk ${_items.length + 1}'),
child: const Icon(Icons.add),
),
);
}
}
Widget Lifecycle #
stateDiagram-v2
[*] --> createState: new StatefulWidget()
createState --> initState: State created
initState --> build: Widget ready to render
build --> didUpdateWidget: Parent rebuilds with new params
didUpdateWidget --> build: Rebuild with new data
build --> setState: State changed
setState --> build: Rebuild UI
build --> deactivate: Widget temporarily removed
deactivate --> dispose: Widget permanently removed
dispose --> [*]Basic Layout #
Flutter uses a widget-based layout system — no XML or CSS:
// Column — arrange widgets vertically
Column(
mainAxisAlignment: MainAxisAlignment.center, // position on the main axis (vertical)
crossAxisAlignment: CrossAxisAlignment.start, // position on the cross axis (horizontal)
children: [
Text('Baris 1'),
Text('Baris 2'),
Text('Baris 3'),
],
)
// Row — arrange widgets horizontally
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.star),
Text('Judul'),
Icon(Icons.more_vert),
],
)
// Stack — layer widgets on top of each other
Stack(
children: [
Image.network('https://example.com/bg.jpg'),
const Positioned(
bottom: 16,
left: 16,
child: Text('Overlay text', style: TextStyle(color: Colors.white)),
),
],
)
// Expanded and Flexible — space allocation in a Row/Column
Row(
children: [
Expanded(flex: 2, child: Container(color: Colors.red)), // 2/3 of the width
Expanded(flex: 1, child: Container(color: Colors.blue)), // 1/3 of the width
],
)
// Container — a versatile widget with decoration
Container(
width: 200,
height: 100,
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 8)],
),
child: const Text('Hello', style: TextStyle(color: Colors.white)),
)
Commonly Used Widgets #
// Text with styling
Text(
'Hello Flutter',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.deepPurple,
letterSpacing: 1.5,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
// Buttons
ElevatedButton(
onPressed: () => print('Pressed!'),
child: const Text('Click Me'),
)
TextButton(onPressed: () {}, child: const Text('Text Button'))
OutlinedButton(onPressed: () {}, child: const Text('Outlined Button'))
IconButton(icon: const Icon(Icons.favorite), onPressed: () {})
// Text input
TextField(
decoration: const InputDecoration(
labelText: 'Email',
hintText: '[email protected]',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
onChanged: (nilai) => print('Input: $nilai'),
)
// Images
Image.network('https://example.com/gambar.jpg')
Image.asset('assets/images/logo.png')
// Icons
Icon(Icons.home, size: 32, color: Colors.blue)
// ListView — a scrollable list
ListView.builder(
itemCount: 50,
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(child: Text('$index')),
title: Text('Item $index'),
subtitle: Text('Description of item $index'),
trailing: const Icon(Icons.chevron_right),
onTap: () => print('Tap item $index'),
),
)
// GridView
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: 20,
itemBuilder: (context, index) => Card(
child: Center(child: Text('Grid $index')),
),
)
Navigating Between Pages #
// Navigator.push — navigate to a new page
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HalamanDetail(id: '123'),
),
);
// Navigator.pop — go back to the previous page
Navigator.pop(context);
// With named routes (configured in MaterialApp)
MaterialApp(
routes: {
'/': (context) => const HalamanUtama(),
'/detail': (context) => const HalamanDetail(),
'/profil': (context) => const HalamanProfil(),
},
)
Navigator.pushNamed(context, '/detail', arguments: {'id': '123'});
State Management Overview #
For simple apps, setState in a StatefulWidget is enough. For larger apps, there are several state management solutions:
| Solution | Complexity | Best for |
|---|---|---|
setState | Low | Local widgets, simple state |
InheritedWidget | Moderate | Sharing state to child widgets |
Provider | Low-moderate | Medium apps, easy to learn |
Riverpod | Moderate | Medium-large apps, type-safe |
Bloc/Cubit | Moderate-high | Enterprise apps, testable |
GetX | Low | Rapid development |
// Provider — one of the most popular
// pubspec.yaml: dependencies: provider: ^6.1.0
class KeranjangModel extends ChangeNotifier {
final List<Produk> _items = [];
List<Produk> get items => List.unmodifiable(_items);
int get jumlah => _items.length;
double get total => _items.fold(0, (sum, p) => sum + p.harga);
void tambah(Produk produk) {
_items.add(produk);
notifyListeners(); // notify all listeners to rebuild
}
void hapus(Produk produk) {
_items.remove(produk);
notifyListeners();
}
}
// Setup in main.dart
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => KeranjangModel(),
child: const MyApp(),
),
);
}
// Consume in any widget
class TombolKeranjang extends StatelessWidget {
const TombolKeranjang({super.key});
@override
Widget build(BuildContext context) {
// watch — rebuild when the model changes
final keranjang = context.watch<KeranjangModel>();
return Badge(
label: Text('${keranjang.jumlah}'),
child: IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () => Navigator.pushNamed(context, '/keranjang'),
),
);
}
}
Hot Reload and Hot Restart #
Features that make Flutter development extremely productive:
Hot Reload (r):
• Injects new code into the running VM
• State is preserved
• Very fast (~300ms)
• Good for UI changes: colors, text, layout
Hot Restart (R):
• Restarts the app from scratch
• State is reset
• Slightly slower (~3-5 seconds)
• Needed when: adding dependencies, changing main(), early logic changes
Full Restart:
• Stops and starts from a cold start
• Needed when: changing native code, changing AndroidManifest/Info.plist
Creating a New Flutter Project #
# Create a new project
flutter create nama_aplikasi
# With a specific template
flutter create --template=app --org=com.perusahaan nama_aplikasi
# Project structure
nama_aplikasi/
├── lib/
│ └── main.dart ← app entry point
├── test/
│ └── widget_test.dart ← UI tests
├── assets/ ← images, fonts, etc.
├── android/ ← Android configuration
├── ios/ ← iOS configuration
├── web/ ← web configuration
├── macos/ ← macOS configuration
├── linux/ ← Linux configuration
├── windows/ ← Windows configuration
└── pubspec.yaml ← dependencies
# Run on an emulator/device
flutter run
# Build for production
flutter build apk --release # Android APK
flutter build appbundle --release # Android App Bundle (Google Play)
flutter build ios --release # iOS (needs Xcode on macOS)
flutter build web # Web
flutter build macos # macOS desktop
Summary #
- Everything is a widget — text, padding, buttons, layout, even the app itself are widgets. Thinking in widget trees is Flutter’s way of thinking.
- StatelessWidget for UI that doesn’t change — its appearance is entirely determined by the parameters it receives. Always use
constwhen no parameters can change.- StatefulWidget for UI that needs to change — pair it with a
Stateclass holding the state. UsesetState()to tell Flutter the UI needs updating.initState()for initialization (load data, set up controllers),dispose()for resource cleanup (close controllers, cancel subscriptions). Always callsuperin both.- Flutter layout is entirely widget-based —
Column/Rowfor linear arrangements,Stackfor layering,Expanded/Flexiblefor space allocation,Containerfor decoration.constwidgets aren’t rebuilt when the parent rebuilds — always addconstto widgets that don’t depend on state or changing parameters for optimal performance.- Hot reload (
r) to see UI changes instantly without losing state — one of Flutter’s most productive development features.- State management: use
setStatefor simple local state,ProviderorRiverpodfor state shared across widgets.- One codebase, all platforms — Flutter supports iOS, Android, Web, macOS, Windows, and Linux from the same Dart project.
- The Flutter SDK includes the Dart SDK — if you use Flutter, you already have Dart. All the Dart concepts learned in this series apply directly in Flutter.