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')),
  ),
)

// 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:

SolutionComplexityBest for
setStateLowLocal widgets, simple state
InheritedWidgetModerateSharing state to child widgets
ProviderLow-moderateMedium apps, easy to learn
RiverpodModerateMedium-large apps, type-safe
Bloc/CubitModerate-highEnterprise apps, testable
GetXLowRapid 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 const when no parameters can change.
  • StatefulWidget for UI that needs to change — pair it with a State class holding the state. Use setState() to tell Flutter the UI needs updating.
  • initState() for initialization (load data, set up controllers), dispose() for resource cleanup (close controllers, cancel subscriptions). Always call super in both.
  • Flutter layout is entirely widget-basedColumn/Row for linear arrangements, Stack for layering, Expanded/Flexible for space allocation, Container for decoration.
  • const widgets aren’t rebuilt when the parent rebuilds — always add const to 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 setState for simple local state, Provider or Riverpod for 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.

← Previous: Memcached   Next: Conduit →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact