Custom Flutter Renderers: Building Complex Custom Widgets

Abdulbosit Komilov
4 min readAug 6, 2023

Custom Flutter Renderers open up a whole new world of possibilities for creating visually rich and highly interactive custom widgets that go beyond the capabilities of standard Flutter widgets. By building a custom renderer, you can directly manipulate the rendering process at a lower level, giving you full control over the appearance and behavior of your widgets.

Overview of Custom Flutter Renderers:

In Flutter, the rendering process works through a layered architecture. The widget tree is built using the declarative approach, where each widget describes its appearance based on the current state and properties. The rendering engine then takes this information and converts it into the corresponding platform-specific views or canvas drawing commands.

With a custom renderer, you can create a custom widget by overriding the rendering process for specific cases, allowing you to implement unique animations, complex interactions, or entirely new visual effects. It’s important to note that building custom renderers requires a deep understanding of Flutter’s rendering pipeline, and it’s generally reserved for advanced use cases.

Example: Creating a Custom Circular Progress Indicator Widget:

Let’s create a custom circular progress indicator widget using a custom renderer. This example demonstrates how to build a circular progress indicator that draws a custom gradient and animates it in a circular motion.

  1. Creating the Custom Widget Class:
import 'package:flutter/material.dart';

class CustomCircularProgressIndicator extends LeafRenderObjectWidget {
final double progress;

CustomCircularProgressIndicator({required this.progress});

// Step 1: Create the RenderObject
@override
RenderObject createRenderObject(BuildContext context) {
// Step 1.1: Instantiate the custom RenderObject with the provided progress
return _CustomCircularProgressIndicatorRenderObject(progress: progress);
}

// Step 2: Update the RenderObject when the widget's properties change
@override
void updateRenderObject(
BuildContext context, _CustomCircularProgressIndicatorRenderObject renderObject) {
// Step 2.1: Update the progress property of the custom RenderObject
renderObject..progress = progress;
}
}

Step 1: createRenderObject: This method is responsible for creating the custom RenderObject that will handle the rendering of the CustomCircularProgressIndicator. It is called when the widget is first created.

Step 1.1: return _CustomCircularProgressIndicatorRenderObject(progress: progress);: In this step, we instantiate the custom RenderObject, _CustomCircularProgressIndicatorRenderObject, with the progress property provided by the CustomCircularProgressIndicator widget.

Step 2: updateRenderObject: This method is called whenever the properties of the CustomCircularProgressIndicator widget change, and it is used to update the existing RenderObject to reflect these changes.

Step 2.1: renderObject..progress = progress;: In this step, we update the progress property of the existing custom RenderObject with the new progress value from the updated widget. By doing this, we ensure that the visual representation of the CustomCircularProgressIndicator reflects the latest changes.

2. Creating the Custom Render Object:

class _CustomCircularProgressIndicatorRenderObject extends RenderBox {
double progress;

_CustomCircularProgressIndicatorRenderObject({required this.progress});

// Step 1: Indicate that this RenderBox uses parent's constraints for sizing
@override
bool get sizedByParent => true;

// Step 2: Perform layout to set the size of the RenderBox
@override
void performLayout() {
// Step 2.1: Set the size of the RenderBox to be the biggest possible within its constraints
size = constraints.biggest;
}

// Step 3: Paint the visual representation of the custom circular progress indicator
@override
void paint(PaintingContext context, Offset offset) {
// Step 3.1: Create a Paint object with a radial gradient shader
final Paint paint = Paint()
..shader = RadialGradient(
colors: [Colors.blue, Colors.green],
stops: [0.0, 1.0],
center: Alignment.center,
).createShader(Rect.fromCircle(center: size.center(offset), radius: size.shortestSide / 2));

// Step 3.2: Create a rectangle that covers the entire RenderBox area
final rect = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);

// Step 3.3: Draw an arc using the custom progress value
// starting from the top (-pi / 2) and spanning the percentage of the circle (progress * 2 * pi)
context.canvas.drawArc(rect, -pi / 2, progress * 2 * pi, true, paint);
}
}

Step 1: sizedByParent: This property indicates that the size of this RenderBox is determined by its parent widget's constraints. In this case, we want the parent widget to control the size of the circular progress indicator.

Step 2: performLayout: This method is responsible for calculating and setting the size of the RenderBox based on its constraints. Since we want the circular progress indicator to take the maximum available space, we set its size to constraints.biggest.

Step 2.1: size = constraints.biggest;: This line sets the size of the RenderBox to the maximum size allowed by its parent's constraints.

Step 3: paint: This method is called when the RenderBox needs to paint its visual representation on the screen.

Step 3.1: final Paint paint = Paint()..shader = ...;: In this step, we create a Paint object and set its shader to a radial gradient. The radial gradient is created using the RadialGradient class with two colors (blue and green) and their corresponding stops, creating a smooth transition between the colors.

Step 3.2: final rect = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);: This line creates a rectangle that covers the entire area of the RenderBox.

Step 3.3: context.canvas.drawArc(rect, -pi / 2, progress * 2 * pi, true, paint);: Finally, we use the drawArc method of the canvas to draw an arc using the custom progress value. The arc starts from the top (-pi / 2) and spans the percentage of the circle determined by the progress value, filling it with the gradient paint.

3. Using the Custom Widget:

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Custom Progress Indicator')),
body: Center(
child: CustomCircularProgressIndicator(progress: 0.5),
),
),
);
}
}

In this example, we created a CustomCircularProgressIndicator widget that extends LeafRenderObjectWidget. The custom render object _CustomCircularProgressIndicatorRenderObject is a subclass of RenderBox, which handles the actual rendering logic using the paint method. The performLayout method is overridden to set the size of the custom widget, and we use the RadialGradient to create a custom gradient for the circular progress.

This is just a basic example to showcase the concept of custom renderers. In a real-world scenario, you can create much more sophisticated custom widgets with advanced animations, gesture handling, and interactions.

--

--