Flutter: Widget Fundamentals
Widgets are Flutter's building blocks — understand Widgets and you've grasped Flutter's DNA.
📋 Prerequisites: You need to have completed the following first
- Lesson 2: Dart Language Crash Course
1. What You Will Learn
- Widget-Element-RenderObject three-tree architecture and its performance implications
- StatelessWidget vs StatefulWidget lifecycle comparison
- Common basic widgets: Text, Image, Icon, Container, Scaffold
- Key's purpose and use cases: ValueKey / ObjectKey / GlobalKey
- ShopApp product card StatelessWidget implementation
2. A Real Story from a Frontend Developer Transitioning to Flutter
(1) The Pain: The DOM Mindset Trap
Bob transitioned from React to Flutter and instinctively tried to understand Widgets through a "component renders DOM" mental model. He found that product cards were frequently rebuilt during list scrolling, causing a 1000-item product page to lag at 30fps. Even more confusing, adding key to the list made performance worse — because he was using UniqueKey.
(2) The Three-Tree Architecture Solution
Flutter is not a simple "component → DOM" mapping, but a "Widget → Element → RenderObject" three-layer architecture. Widgets are configuration descriptions (lightweight), Elements manage instances (diff core), and RenderObjects handle layout and painting (heavyweight). Understanding the three trees, Bob realized he should optimize at the Element layer via diffing, rather than rebuilding at the Widget layer.
import 'package:flutter/material.dart';
// Lightweight widget: just configuration
const ProductCard({required this.product, super.key});
// Flutter internally creates Element and RenderObject
// Widget rebuilds are cheap - Element diffing avoids RenderObject rebuild
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
(3) The Result: Smooth 60fps Scrolling
After switching to ValueKey(product.id), Element diff precisely matched items, and the 1000-product list maintained a stable 60fps.
3. Widget-Element-RenderObject Three Trees
graph LR
W[Widget Tree] --> E[Element Tree]
E --> R[RenderObject Tree]
W -.->|canUpdate| E
E -.->|adoptChild| R
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
(1) Three-Tree Responsibilities
| Layer | Created By | Responsibility | Weight |
|---|---|---|---|
| Widget | Developer's build() |
Immutable configuration description | Very light |
| Element | Framework automatically | Manage tree structure, diff updates | Medium |
| RenderObject | Framework automatically | Measure, layout, paint | Heavy |
(2) canUpdate Rule
Element determines whether to reuse via Widget.canUpdate(oldWidget, newWidget):
import 'package:flutter/material.dart';
// canUpdate logic
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
| Scenario | runtimeType | key | Result |
|---|---|---|---|
| Same type, same key | Same | Same | Reuse Element |
| Same type, different key | Same | Different | Rebuild Element |
| Different type | Different | - | Rebuild Element |
4. Common Basic Widgets
(1) Text and Styles
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Text with various styles
import 'package:flutter/material.dart';
// Basic text
const Text('ShopApp')
// Styled text
Text(
'Flash Sale!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.red,
letterSpacing: 1.5,
),
)
// Rich text with multiple styles
Text.rich(
TextSpan(
text: 'Price: ',
style: const TextStyle(fontSize: 16, color: Colors.grey),
children: [
TextSpan(
text: '\$99.99',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
TextSpan(
text: ' \$149.99',
style: TextStyle(
fontSize: 14,
decoration: TextDecoration.lineThrough,
color: Colors.grey[400],
),
),
],
),
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
(2) Container: The Universal Container
| Container Property | Purpose | Type |
|---|---|---|
padding |
Inner padding | EdgeInsets |
margin |
Outer margin | EdgeInsets |
decoration |
Background decoration | BoxDecoration |
constraints |
Constraints | BoxConstraints |
alignment |
Child widget alignment | Alignment |
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Container product card
import 'package:flutter/material.dart';
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
'https://cdn.shopapp.com/products/laptop.jpg',
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Pro Laptop', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text('\$1,299.99', style: const TextStyle(fontSize: 18, color: Colors.green)),
],
),
),
],
),
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
(3) Icon and IconButton
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Icons in an e-commerce context
import 'package:flutter/material.dart';
// Icon with color and size
const Icon(Icons.shopping_cart, color: Colors.blue, size: 32)
// IconButton for actions
IconButton(
icon: const Icon(Icons.favorite_border),
onPressed: () {
// Add to wishlist
},
tooltip: 'Add to Wishlist',
)
// Badge icon for cart count
Stack(
children: [
const Icon(Icons.shopping_cart, size: 28),
Positioned(
right: 0,
top: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
child: const Text('3', style: TextStyle(fontSize: 10, color: Colors.white),
textAlign: TextAlign.center),
),
),
],
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
5. Key Mechanism
Keys control Element reuse strategy, which is especially critical during list updates.
(1) Key Types
| Key Type | Comparison Basis | Use Case |
|---|---|---|
ValueKey(value) |
Value equality | List items with unique identifiers (e.g., product.id) |
ObjectKey(object) |
Object reference | Unique object instances |
UniqueKey() |
Different every time | Force no reuse (rarely used) |
GlobalKey |
Globally unique | Cross-widget State access |
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Key's role in lists
import 'package:flutter/material.dart';
// Simplified class definition
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
// BAD: No key, Flutter matches by position
ListView(
children: [
ProductTile(product: products[0]), // index 0
ProductTile(product: products[1]), // index 1
],
)
// When products reorder, Element mismatches State
// Solution: Use ValueKey with unique id
// GOOD: ValueKey enables correct Element-Widget matching
ListView(
children: [
ProductTile(key: ValueKey(products[0].id), product: products[0]),
ProductTile(key: ValueKey(products[1].id), product: products[1]),
],
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
6. Complete Example: ShopApp Product Card Widget
import 'package:flutter/material.dart';
class Product {
final int id;
final String name;
final double price;
final String imageUrl;
final double rating;
final int reviewCount;
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
this.rating = 0.0,
this.reviewCount = 0,
});
}
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback? onAddToCart;
final VoidCallback? onTap;
const ProductCard({
super.key,
required this.product,
this.onAddToCart,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Card(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: Stack(
children: [
Image.network(product.imageUrl, fit: BoxFit.cover,
width: double.infinity),
if (product.price < 50)
Positioned(top: 8, left: 8,
child: _buildBadge('SALE', Colors.red)),
],
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product.name, maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text('\$${product.price.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.green, fontSize: 16)),
const Spacer(),
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: const Icon(Icons.add_shopping_cart, size: 20),
onPressed: onAddToCart,
),
),
],
),
),
),
],
),
),
);
}
Widget _buildBadge(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
child: Text(text, style: const TextStyle(color: Colors.white, fontSize: 10)),
);
}
}
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
❓ FAQ
setState triggers a parent rebuild. A StatelessWidget itself cannot trigger a rebuild.errorBuilder parameter to show a placeholder image, combined with the cached_network_image package for caching and loading indicators.📖 Summary
- Widget (configuration) → Element (management) → RenderObject (painting) three-layer architecture
- Widgets are immutable and lightweight, can be rebuilt frequently; Elements perform diff to decide whether to update RenderObjects
- Keys control Element reuse strategy: ValueKey matches by value, UniqueKey forces no reuse
- Container is the universal container; Scaffold is the page scaffold
- Use ValueKey(product.id) in lists to ensure correct Element matching
📝 Exercises
- Basic (difficulty ⭐): Create a
UserAvatarStatelessWidget that accepts name and imageUrl, displaying a circular avatar and username. - Intermediate (difficulty ⭐⭐): Implement a
PriceTagwidget supporting original/discount price display, with the discount price in large green text and the original price with a strikethrough. - Challenge (difficulty ⭐⭐⭐): Build a sortable product list where drag-reordering uses ValueKey to ensure State follows correctly, and after sorting, display the new order via SnackBar.