This episode transforms your app's appearance: ThemeData and Material Design theming, custom fonts, colors, and typography, dark mode support and adaptive UI, and styling widgets with decoration and responsive design.

A functional app isn't necessarily pleasant to use. Episode 7 closes that gap: you'll learn to build a consistent visual identity through theming and styling. We cover ThemeData and Material Design theming, custom fonts, colors, and typography, dark mode support and adaptive UI, and styling widgets with decoration and responsive design. After this episode, your app will look like a real product, not a demo.
Instead of setting colors in every widget, define the theme once in MaterialApp:
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
),
useMaterial3: true,
),
home: const HomeScreen(),
)ColorScheme.fromSeed(seedColor: Colors.indigo) generates a complete color palette from a single seed color — the pattern recommended by Material 3. useMaterial3: true enables the latest design tokens.
final colorScheme = Theme.of(context).colorScheme;
Container(
color: colorScheme.primary,
child: Text(
'Tombol utama',
style: TextStyle(color: colorScheme.onPrimary),
),
);Theme.of(context).colorScheme retrieves the active palette. With this pattern, colors are never hardcoded inside widgets — theme changes propagate automatically across the whole app.
Custom fonts are registered in pubspec.yaml under the fonts block:
flutter:
fonts:
- family: Poppins
fonts:
- asset: assets/fonts/Poppins-Regular.ttf
- asset: assets/fonts/Poppins-Bold.ttf
weight: 700Once the font is registered in the flutter.fonts block, run flutter pub get and then use that font family.
Define a text theme so typography stays consistent:
theme: ThemeData(
textTheme: const TextTheme(
headlineMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
bodyMedium: TextStyle(
fontSize: 16,
height: 1.5,
),
),
)TextTheme defines styles for headlineMedium, bodyMedium, and so on. The whole app uses these styles without rewriting TextStyle everywhere.
Provide two themes and Flutter will choose based on the system setting:
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
),
home: const HomeScreen(),
)With theme and darkTheme populated, the app adapts to the device's light or dark mode automatically. Brightness.dark flips the palette so it stays contrasted.
For platform-dependent decisions, use Theme.of(context).platform or MediaQuery:
final lebar = MediaQuery.sizeOf(context).width;
if (lebar >= 600) {
return const WideLayout();
}
return const NarrowLayout();MediaQuery.sizeOf(context).width reads the current screen width. This pattern is the foundation of responsive design, which we'll cover in depth in episode 18.
BoxDecoration gives a Container gradient colors, borders, and shadows:
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.indigo, Colors.blue],
),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: const Text('Kartu bergaya'),
)BoxShadow adds depth and BorderRadius.circular(12) rounds the corners. double.infinity makes the Container as wide as its parent allows.
Define spacing constants in one place for consistency:
const double spacingKecil = 8;
const double spacingBesar = 24;
Padding(
padding: EdgeInsets.all(spacingKecil),
child: Text('Elemen dengan jarak konsisten'),
)EdgeInsets.all(spacingKecil) uses the same constants across the whole app. Consistent spacing and colors are the hallmark of an app that looks professional.
Key takeaways:
MaterialApp with ColorScheme.fromSeed.Theme.of(context) — don't hardcode colors in widgets.pubspec.yaml and set a centralized TextTheme.theme and darkTheme for automatic dark mode support.BoxDecoration brings visuals to life with gradients, borders, and shadows.In the next episode 8 we discuss networking and data handling — HTTP requests with the http or dio package, JSON deserialization with dart:convert or json_serializable, async programming with Future and Stream, and production-ready loading states, error handling, and retry. Your app starts connecting to the outside world.