Steven's Knowledge

Advanced Usage

Advanced Flutter techniques — RenderObject, Slivers, CustomPainter, isolates, platform channels, FFI

Advanced Usage

Techniques for breaking past the limits of built-in Widgets. These APIs are not "must-know", but they are the only answer when you hit performance bottlenecks, unusual interactions, or deep native integration.

Three Trees: Widget / Element / RenderObject

Understanding the three trees is a prerequisite for advanced Flutter work.

Widget Tree              Element Tree            RenderObject Tree
(config / immutable)     (lifecycle / state)     (layout / paint / hit-test)
─────────────            ────────────            ─────────────────
Container        ←→     ComponentElement
  Padding        ←→     ComponentElement   →    RenderPadding
    Text         ←→     ComponentElement   →    RenderParagraph
TreeResponsibilityWhen It Changes
WidgetImmutable description of UIsetState / parent rebuild
ElementHolds Widget + State + parent-child relationshipsFollows Widget changes; reuse minimizes creation
RenderObjectPerforms layout / paint / hitTestOnly when properties change (markNeedsLayout / Paint)

The essence of perf optimization: fewer Widget rebuilds → Element reuse → RenderObject unchanged → skip layout/paint.

CustomPainter

For drawing graphics built-in Widgets cannot express: charts, signatures, waveforms, custom progress indicators.

class WavePainter extends CustomPainter {
  final double progress;
  WavePainter({required this.progress});

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    final path = Path()..moveTo(0, size.height);
    for (double x = 0; x <= size.width; x++) {
      final y = size.height / 2 +
          20 * sin((x / size.width * 2 * pi) + progress * 2 * pi);
      path.lineTo(x, y);
    }
    path.lineTo(size.width, size.height);
    path.close();
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(WavePainter old) => old.progress != progress;
}

// Usage
CustomPaint(painter: WavePainter(progress: animation.value), size: Size(300, 100))

Key Points

  • Implement shouldRepaint correctly — returning true unnecessarily wastes work; returning false incorrectly stops repaints
  • Isolate with RepaintBoundary — frequently repainted canvases deserve their own layer to avoid contaminating siblings
  • Cache complex paths via PictureRecorder — pre-record the static portion and only redraw the dynamic part each frame
  • Avoid allocating in paint — create paths and paints once and reuse them

Custom RenderObject

When you need a completely custom layout algorithm (CustomPaint is not enough — it only paints, not lays out), drop down to RenderObject.

Typical scenarios:

  • Masonry / waterfall layout
  • Custom typography (text wrapping around shapes, specialized rich text)
  • Adaptive grids
  • Physics-driven layout
class FlowLayout extends MultiChildRenderObjectWidget {
  FlowLayout({super.key, required super.children});

  @override
  RenderObject createRenderObject(BuildContext context) => _RenderFlowLayout();
}

class _RenderFlowLayout extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, FlexParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, FlexParentData> {
  @override
  void setupParentData(RenderBox child) {
    child.parentData = FlexParentData();
  }

  @override
  void performLayout() {
    double xCursor = 0, yCursor = 0, rowHeight = 0;
    var child = firstChild;
    while (child != null) {
      child.layout(constraints.loosen(), parentUsesSize: true);
      final pd = child.parentData as FlexParentData;
      if (xCursor + child.size.width > constraints.maxWidth) {
        xCursor = 0;
        yCursor += rowHeight;
        rowHeight = 0;
      }
      pd.offset = Offset(xCursor, yCursor);
      xCursor += child.size.width;
      rowHeight = max(rowHeight, child.size.height);
      child = pd.nextSibling;
    }
    size = Size(constraints.maxWidth, yCursor + rowHeight);
  }

  @override
  void paint(PaintingContext context, Offset offset) =>
      defaultPaint(context, offset);

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) =>
      defaultHitTestChildren(result, position: position);
}

Custom RenderObject has a steep learning curve. Read RenderFlex and RenderWrap source first to understand the conventions.

Sliver

Sliver is Flutter's "scrollable region" protocol — it lets you mix different scroll behaviors inside a single scroll container:

CustomScrollView(
  slivers: [
    SliverAppBar(                     // collapsing AppBar
      expandedHeight: 200,
      flexibleSpace: FlexibleSpaceBar(...),
    ),
    SliverPersistentHeader(           // sticky header
      pinned: true,
      delegate: _StickyHeaderDelegate(),
    ),
    SliverGrid.count(                 // grid
      crossAxisCount: 2,
      children: List.generate(20, (i) => Card(...)),
    ),
    SliverList(                       // list
      delegate: SliverChildBuilderDelegate(
        (_, i) => ListTile(title: Text('Item $i')),
        childCount: 100,
      ),
    ),
    SliverFillRemaining(child: Center(child: Text('end'))),
  ],
)

Key Slivers

SliverUse
SliverAppBarCollapsing / hiding top bar
SliverList / SliverGridLazy list / grid
SliverFixedExtentListFixed-height children — best perf
SliverPersistentHeaderSticky to top / bottom
SliverPaddingPadding for a Sliver
SliverToBoxAdapterEmbed a regular Widget into Sliver-land
SliverFillRemainingFill the leftover space
NestedScrollViewNested scrolling (outer + inner TabBarView)

InheritedWidget / InheritedModel

InheritedWidget is the foundation under every state management library. Understanding it lets you see through Provider/Riverpod.

class AppConfig extends InheritedWidget {
  final String apiBase;
  const AppConfig({required this.apiBase, required super.child});

  static AppConfig of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<AppConfig>()!;

  @override
  bool updateShouldNotify(AppConfig old) => apiBase != old.apiBase;
}

// Usage
final api = AppConfig.of(context).apiBase;

InheritedModel: Granular Subscription

InheritedWidget's pain point: any field change notifies every subscriber. InheritedModel allows subscribing per aspect:

class ThemeModel extends InheritedModel<String> {
  final Color primary;
  final Color background;
  // ...
  @override
  bool updateShouldNotifyDependent(ThemeModel old, Set<String> aspects) {
    if (aspects.contains('primary') && primary != old.primary) return true;
    if (aspects.contains('background') && background != old.background) return true;
    return false;
  }
}

// Subscribe only to primary
final theme = InheritedModel.inheritFrom<ThemeModel>(context, aspect: 'primary');

Isolates and compute

Dart is single-threaded; to actually use multiple cores you must use Isolates ("processes" that do not share memory).

Simple Case: compute

// Run heavyParse in another Isolate without blocking the UI
final result = await compute(heavyParse, jsonString);

List<Item> heavyParse(String json) {
  return jsonDecode(json).map<Item>((e) => Item.fromJson(e)).toList();
}

Use for: one-shot computation, JSON parsing, image processing.

Long-running: Isolate.spawn

final receivePort = ReceivePort();
await Isolate.spawn(_worker, receivePort.sendPort);

receivePort.listen((msg) {
  if (msg is SendPort) {
    msg.send('hello');  // main → worker
  } else {
    print('from worker: $msg');
  }
});

void _worker(SendPort mainSendPort) {
  final port = ReceivePort();
  mainSendPort.send(port.sendPort);
  port.listen((msg) {
    mainSendPort.send('echo: $msg');
  });
}

Notes:

  • Isolates can only exchange serializable data (no Functions, no native handles)
  • Spawning an Isolate has a cost (~1 ms plus memory) — do not spawn in a loop
  • Use Isolate.run() (Dart 2.19+) for short tasks

Platform Channels

The standard way to talk to native code (Kotlin / Swift).

MethodChannel (request/response)

// Dart
const channel = MethodChannel('com.app/battery');
final level = await channel.invokeMethod<int>('getBatteryLevel');
// Android
class MainActivity : FlutterActivity() {
  override fun configureFlutterEngine(engine: FlutterEngine) {
    super.configureFlutterEngine(engine)
    MethodChannel(engine.dartExecutor.binaryMessenger, "com.app/battery")
      .setMethodCallHandler { call, result ->
        if (call.method == "getBatteryLevel") {
          result.success(getBatteryLevel())
        } else result.notImplemented()
      }
  }
}

EventChannel (streaming)

For continuous data such as sensors, location, or battery:

const channel = EventChannel('com.app/sensor');
channel.receiveBroadcastStream().listen((data) => print(data));

pigeon generates strongly-typed code on both sides from a schema, eliminating typos in method-name strings:

// pigeons/api.dart
@HostApi()
abstract class BatteryApi {
  int getLevel();
}
// Running pigeon generates Dart / Kotlin / Swift code automatically

FFI (Foreign Function Interface)

Direct calls into C/C++ libraries with no serialization overhead — the fastest option. Suited to image processing, cryptography, audio/video codecs.

import 'dart:ffi';
import 'package:ffi/ffi.dart';

final lib = DynamicLibrary.open('libnative.so');
final int Function(int, int) add = lib
    .lookup<NativeFunction<Int32 Function(Int32, Int32)>>('add')
    .asFunction();

print(add(2, 3));  // 5

MethodChannel vs FFI:

DimensionMethodChannelFFI
OverheadSerialization + cross-threadDirect function call
Suited forOccasional calls, calling SDKsHigh-frequency calls, pure compute
ComplexityLowHigh (manual memory management)
AsyncNaturally asyncRoll your own (use an Isolate)

Flutter Engine and PlatformView

Embedding Native Views

AndroidView / UiKitView embed native Views into the Flutter tree. Common uses: maps, WebView, camera preview.

AndroidView(
  viewType: 'native-map',
  creationParams: {'lat': 39.9, 'lng': 116.4},
  creationParamsCodec: StandardMessageCodec(),
)

Performance note: early PlatformView composition (SurfaceTexture / HybridComposition) was costly. On Android, prefer Hybrid Composition Plus mode; iOS is generally fine.

Texture Widget

For camera or video frames, Texture is faster than PlatformView: native code writes frames into an OpenGL Texture and Flutter composites them directly.

Animations Beyond the Basics

TweenAnimationBuilder: implicit animation without a Controller

TweenAnimationBuilder<double>(
  tween: Tween(begin: 0, end: progress),
  duration: Duration(milliseconds: 300),
  builder: (_, value, __) => LinearProgressIndicator(value: value),
)

AnimatedBuilder: efficient subscription

Restrict an animation's rebuilds to the necessary subtree only:

AnimatedBuilder(
  animation: _controller,
  child: const ExpensiveChild(),  // not rebuilt
  builder: (_, child) => Transform.rotate(
    angle: _controller.value * 2 * pi,
    child: child,
  ),
)

Hero animations

Shared element transitions across pages:

// Page A
Hero(tag: 'avatar-$id', child: CircleAvatar(...))

// Page B
Hero(tag: 'avatar-$id', child: CircleAvatar(radius: 80, ...))

The tag must be globally unique.

Custom ScrollPhysics

Tune scroll feel — damping, bounce, inertia:

class HeavyPhysics extends ScrollPhysics {
  const HeavyPhysics({super.parent});

  @override
  HeavyPhysics applyTo(ScrollPhysics? ancestor) =>
      HeavyPhysics(parent: buildParent(ancestor));

  @override
  double get minFlingVelocity => 1000;  // raise the minimum fling velocity

  @override
  SpringDescription get spring => const SpringDescription(
    mass: 100, stiffness: 100, damping: 1,
  );
}

ListView(physics: const HeavyPhysics(), children: [...])

Custom Render Layers: RepaintBoundary and OffsetLayer

Manually manage layers to isolate repaints:

RepaintBoundary(
  child: AnimatedWidget(...),  // animation lives in its own layer
)

Inspect layer composition in DevTools' Performance Layers view.

When to reach for these techniques

Do not use "advanced" techniques for the sake of looking advanced. Build with the built-in Widgets first, profile to find the bottleneck, then drop down. Most business pages are perfectly served by StatelessWidget + ListView.builder + Provider.

On this page