Advanced Flutter
This section will introduce some advanced Flutter concepts to help you improve your development skills.
Rendering Principles
Flutter uses the Skia (gradually switching to Impeller since 3.0) rendering engine to draw UI directly, without using native components.
Widget Tree and Element Tree
- Widget Tree: Configuration information describing the UI (immutable)
- Element Tree: Instantiated object of Widget (mutable)
- RenderObject Tree: Responsible for actual rendering
Understanding rendering principles helps optimize UI performance and avoid unnecessary rebuilds.
InheritedWidget - Data Passing
InheritedWidget allows data to be passed down the Widget tree, and child widgets can access ancestor data.
Example: Custom InheritedWidget
// Define data container
class MyData extends InheritedWidget {
final int value;
const MyData({
super.key,
required this.value,
required super.child,
});
// Convenience method: get the nearest MyData
static MyData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyData>()!;
}
// Determine whether the child Widget needs to be rebuilt
@override
bool updateShouldNotify(MyData oldWidget) {
return value != oldWidget.value;
}
}
// Usage
class ParentWidget extends StatelessWidget {
const ParentWidget({super.key});
@override
Widget build(BuildContext context) {
return MyData(
value: 42,
child: const ChildWidget(),
);
}
}
class ChildWidget extends StatelessWidget {
const ChildWidget({super.key});
@override
Widget build(BuildContext context) {
// Get ancestor's MyData
final myData = MyData.of(context);
return Text('Value: ${myData.value}');
}
}
class MyData extends InheritedWidget {
final int value;
const MyData({
super.key,
required this.value,
required super.child,
});
// Convenience method: get the nearest MyData
static MyData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyData>()!;
}
// Determine whether the child Widget needs to be rebuilt
@override
bool updateShouldNotify(MyData oldWidget) {
return value != oldWidget.value;
}
}
// Usage
class ParentWidget extends StatelessWidget {
const ParentWidget({super.key});
@override
Widget build(BuildContext context) {
return MyData(
value: 42,
child: const ChildWidget(),
);
}
}
class ChildWidget extends StatelessWidget {
const ChildWidget({super.key});
@override
Widget build(BuildContext context) {
// Get ancestor's MyData
final myData = MyData.of(context);
return Text('Value: ${myData.value}');
}
}
RepaintBoundary - Local Repaint
Using RepaintBoundary can limit the repaint area and improve performance.
Example: Using RepaintBoundary
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: Container(
// Drawing in this area will not affect the outside
color: Colors.red,
child: const Text(Independent Area),
),
);
}
}
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: Container(
// Drawing in this area will not affect the outside
color: Colors.red,
child: const Text(Independent Area),
),
);
}
}
Deep Understanding of Keys
Keys help Flutter identify Widgets and distinguish new Widgets from existing Widgets.
ValueKey vs ObjectKey vs UniqueKey
| Type | Description |
|---|---|
| ValueKey | Use values as keys (such as IDs, numbers, strings) |
| ObjectKey | Using object references as keys |
| UniqueKey | Generate a unique key on each creation, destroying and rebuilding |
CustomPainter - Custom Drawing
Using CustomPainter can achieve fully custom graphics drawing.
Example: Using CustomPainter
class CustomPainterExample extends StatelessWidget {
const CustomPainterExample({super.key});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: MyCirclePainter(),
size: const Size(200, 200),
);
}
}
class MyCirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// Draw a circle
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 10;
canvas.drawCircle(center, radius, paint);
// Draw a border
final borderPaint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawCircle(center, radius, borderPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
const CustomPainterExample({super.key});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: MyCirclePainter(),
size: const Size(200, 200),
);
}
}
class MyCirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// Draw a circle
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 10;
canvas.drawCircle(center, radius, paint);
// Draw a border
final borderPaint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawCircle(center, radius, borderPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
Stream and Future
Example: Stream Usage
// Create a Stream
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i; // Send value
}
}
// Use StreamBuilder
class StreamExample extends StatelessWidget {
const StreamExample({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: countStream(10),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(Count: ${snapshot.data});
} else if (snapshot.hasError) {
return Text(Error: ${snapshot.error});
}
return const CircularProgressIndicator();
},
);
}
}
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i; // Send value
}
}
// Use StreamBuilder
class StreamExample extends StatelessWidget {
const StreamExample({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: countStream(10),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(Count: ${snapshot.data});
} else if (snapshot.hasError) {
return Text(Error: ${snapshot.error});
}
return const CircularProgressIndicator();
},
);
}
}
Summary
This Flutter introductory tutorial covers the following:
- Flutter installation and environment configuration
- Widget Basics (StatelessWidget and StatefulWidget)
- Layout System (Row, Column, Stack)
- User Input Handling
- State Management (setState, Provider)
- Network requests and data storage
- Navigation and Routing
- Testing and Release
For deeper learning, follow:
- BLoC Pattern
- Riverpod state management
- GetX routing and state management
- Flutter performance optimization
- Flutter Web and Desktop Development