Creating a Flutter widget from scratch
Tags: Dart, Programming, Flutter Widget, Flutter

A guide to building your own custom RenderObject

The custom render object widget you’ll create

The normal way to create widgets in Flutter is through composition, which means combining several basic widgets into a more complex one. That’s not what this article is about, but if your not familiar with the concept, you can read more about it in Creating Reusable Custom Widgets in Flutter.

When the existing Flutter widgets can’t be combined in a way to match what you need, you can use a CustomPaint widget to draw exactly what you want. Again, that’s not what this article is about, but you can see some good examples of it here and here.

When you browse the Flutter source code you’ll discover that the vast majority of widgets use neither composition nor CustomPaint . Instead they use RenderObject or or one of its subclasses, especially RenderBox . Creating a custom widget in this way will give you the most flexibility in painting and sizing your widget — but also the most complexity, which is where this article comes in.

Are you sure you’re ready?

This article assumes you’re already familiar with render objects. If you’re not, you can start by reading Flutter Text Rendering, which includes an exploration of widgets, elements, and render objects.

Also, if this is your first time creating a custom widget, you should probably try the composition or custom painting methods mentioned above first. But if you dare to go low-level, then welcome along. You’ll learn the most by following each step as you read the directions.

The code in this article up to date for Flutter 1.22 and Dart 2.10.

The big picture

Creating a custom render object involves the following aspects:

Widget : The widget is your interface with the outside world. This is how you get the properties you want. You’ll use these properties to either create or update the render object.

: The widget is your interface with the outside world. This is how you get the properties you want. You’ll use these properties to either create or update the render object. Children : A widget, and in turn a render object, can have zero, one, or many children. The number of children affect the size and layout of the render object. In this article you’ll make a render object with no children. This will simplify a number of steps.

: A widget, and in turn a render object, can have zero, one, or many children. The number of children affect the size and layout of the render object. In this article you’ll make a render object with no children. This will simplify a number of steps. Layout : In Flutter, constraints go down, so you’re given a max and min width and length from the parent and then expected to report back how big you want to be within those constraints. Will you wrap your content tightly or will you try to expand as large as your parent allows? Part of creating a render object is making information about your intrinsic size available. Since the custom widget that you’ll be making in this article won’t have any children, you don’t need to worry about asking them how big they want to be or about positioning them. However, you will need to report back how much space you need to paint your content.

: In Flutter, constraints go down, so you’re given a max and min width and length from the parent and then expected to report back how big you want to be within those constraints. Will you wrap your content tightly or will you try to expand as large as your parent allows? Part of creating a render object is making information about your available. Since the custom widget that you’ll be making in this article won’t have any children, you don’t need to worry about asking them how big they want to be or about positioning them. However, you will need to report back how much space you need to paint your content. Painting : This step is very similar to what you would do in a CustomPaint widget. You’ll receive a canvas that you can draw on.

: This step is very similar to what you would do in a widget. You’ll receive a canvas that you can draw on. Hit testing : This tells Flutter whether you want to handle touch events or let them pass through to the widget below. In this article you’ll also add a gesture detector to help you handle the touch events.

: This tells Flutter whether you want to handle touch events or let them pass through to the widget below. In this article you’ll also add a gesture detector to help you handle the touch events. Semantics: This is related to providing additional text information about your widget. You won’t see this text, but Flutter can use to help visually impaired users. If you don’t know anyone who’s blind, then it’s tempting to skip this step. But don’t.

So are you sure you want to make a custom render object? There’s still time to back out.

All right. Here we go then.

Preview

The custom widget you’ll make will look like this:

It’s a simplified version of the Slider widget.

Note: Why not just use a Slider widget then, you say, and skip all the trouble of making a custom render object? The reason I chose this is because I’d like to make an audio player progress bar that shows the download buffer in addition to the current play location. The standard Slider doesn’t do that, though it might be possible to hack a RangeSlider into something close. One could probably also get CustomPaint to work, but it doesn’t give much flexibility with layout. Anyway, given the basic render object above, it will be a fairly simple matter to pass in a few more properties and update the paint method to create the audio player progress bar. Besides, this is an opportunity for both you and me to finally learn about making our own render objects.

We’ll follow the general outline I described in The Big Picture section above. If you get lost along the way, scroll down to the bottom of this article where you’ll find the full code.

Widget

The first step is to make a widget that will allow you to pass properties in to your render object. There are a million customizations that you could allow, but let’s start with these three:

progress bar color

thumb color

thumb size

The colors will allow you to learn about repainting, and the size will allow you to learn about updating the size of the widget.

Note: I don’t really know why the thumb is called a thumb, but that’s what the Flutter Slider calls it, so I’m using the same name. Think of it more like a handle or a knob. Another name for the progress bar is track bar or seek bar.

Understanding the main parts of the widget

Before creating the widget, have a look at a skeletal outline of its contents:

class ProgressBar extends LeafRenderObjectWidget {

@override

RenderProgressBar createRenderObject(...) {} @override

void updateRenderObject(...) {} @override

void debugFillProperties(...) {}

}

Notes:

Your widget will extend LeafRenderObjectWidget because it won’t have any children. If you were making a render object with one child you would use SingleChildRenderObjectWidget and for multiple children you’d use MultiChildRenderObjectWidget .

because it won’t have any children. If you were making a render object with one child you would use and for multiple children you’d use . The Flutter framework (that is, the element) will call createRenderObject when it wants to create the render object associated with this widget. Since we named the widget ProgressBar , it’s customary to prefix this with “Render” when naming the render object. That’s why you have the return value of RenderProgressBar . Note that you haven’t created this class yet. It’s the render object class that you’ll be working on later.

when it wants to create the render object associated with this widget. Since we named the widget , it’s customary to prefix this with “Render” when naming the render object. That’s why you have the return value of . Note that you haven’t created this class yet. It’s the render object class that you’ll be working on later. Widgets are inexpensive to create, but it would be expensive to recreate render objects every time there was an update. So when a widget property changes, the system will call updateRenderObject , where you will simply update the public properties of your render object without recreating the whole object.

, where you will simply update the public properties of your render object without recreating the whole object. The debugFillProperties method provides information about the class properties during debugging, but it’s not very interesting for the purposes of this article.

Filling in the details

Create a new file called progress_bar.dart. Add the following code to it. This is just a fuller version of what you saw above.

Ignore the errors about the RenderProgressBar for now. You haven’t created it yet.

In this filled out version of ProgressBar , you can see the barColor , thumbColor , and thumbSize properties are used in the following ways:

initializing the constructor

creating a new instance of RenderProgressBar

updating an existing instance of RenderProgressBar

providing debug information

Now that you’ve created the ProgressBar widget, it’s time to create the RenderProgressBar class.

Render object

In this section you’ll create the render object class and add the properties you need.

Creating the class

Create a new class named RenderProgressBar as shown below. You can keep it in the same file as the ProgressBar widget.

class RenderProgressBar extends RenderBox { }

RenderBox is a subclass of RenderObject and has a two-dimensional coordinate system. That is, it has a width and a height.

Adding the constructor

Add the following constructor to RenderProgressBar :

RenderProgressBar ({

Color barColor,

Color thumbColor,

double thumbSize,

}) : _barColor = barColor,

_thumbColor = thumbColor,

_thumbSize = thumbSize;

This defines the public properties that you want, but you’ll see some errors since you haven’t created the private fields for them yet. You’ll do that next.

Adding the properties

Add the code for barColor :

Color get barColor => _barColor;

Color _barColor;

set barColor(Color value) {

if (_barColor == value)

return;

_barColor = value;

markNeedsPaint();

}

Since the setter is updating the color, you’ll need to repaint the bar with a new color. Calling markNeedsPaint at the end of the method tells the framework to call the paint method at some point in the near future. Since painting can be potentially expensive, you should only call markNeedsPaint when necessary. That’s the reason for the early return at the beginning of the setter.

Now add the code for thumbColor :

Color get thumbColor => _ thumbColor ;

Color _ thumbColor ;

set thumbColor (Color value) {

if (_ thumbColor == value)

return;

_ thumbColor = value;

markNeedsPaint();

}

This works the same as barColor did.

Finally, add the code for thumbSize :

double get thumbSize => _ thumbSize ;

double _ thumbSize ;

set thumbSize (double value) {

if (_ thumbSize == value)

return;

_ thumbSize = value;

markNeedsLayout();

}

The one difference here is that instead of calling markNeedsPaint , now you are calling markNeedsLayout . That’s because changing the size of the handle will also affect the size of the whole render object. Calling markNeedsLayout tells the system to call the layout method in the near future. Another layout call will automatically result in a repaint, so there is no need to add an additional markNeedsPaint .

Layout and size

If you tried to use your widget now like this:

Scaffold(

body: Center(

child: Container(

color: Colors.white,

child: ProgressBar(

barColor: Colors.blue,

thumbColor: Colors.red,

thumbSize: 20.0,

),

),

),

),

the IDE wouldn’t complain at you (until you try to run the app). Even if you did run the app, though, there would be nothing to see. One reason is because your widget doesn’t have any intrinsic size, and the other reason is because you haven’t painted any content. First let’s handle the size issue.

Take another look at a the progress bar that we want to make:

On a typical screen you’d probably want to width to expand to whatever the parent width is. But for the height, you’d want it to hug height of the handle.

Given that information you can set the size.

Setting the desired size

The performLayout method is the key method for setting the size of your render object.

Update: There is a scheduled change to replace performLayout with computeDryLayout . If that change has already entered the Flutter stable version by the time you read this, you should use it. See the directions here. Also ping me to update this article.

Add the following code to RenderProgressBar :

@override

void performLayout() {

final desiredWidth = constraints.maxWidth;

final desiredHeight = thumbSize;

final desiredSize = Size(desiredWidth, desiredHeight);

size = constraints.constrain(desiredSize);

}

Notes:

This method is where you would lay out the children if the widget had any. To do so you would call layout on each of them, passing in some min and max size constraints. This would result in the system calling performLayout on each child, where that child sets its own desired size. That would give you (the parent render object) the information you need to place them and determine your own size. (Remember the quote, “Constraints go down. Sizes go up. Parent sets position.”) Since RenderProgressBar doesn’t have any children, though, (you made a LeafRenderObjectWidget if you recall), all you need to do here is set your own size.

on each of them, passing in some min and max size constraints. This would result in the system calling on each child, where that child sets its own desired size. That would give you (the parent render object) the information you need to place them and determine your own size. (Remember the quote, “Constraints go down. Sizes go up. Parent sets position.”) Since doesn’t have any children, though, (you made a if you recall), all you need to do here is set your own size. The constraints variable is of type BoxContraints and is a property of RenderBox . These BoxContraints are passed in from the parent and tell you the max and min width and length that you’re allowed to be. You can choose any size for yourself within those bounds. By choosing maxWidth you’re saying that you want to expand to be as big as the parent allows. For the desired height you’re hugging your content by using the thumbSize property.

variable is of type and is a property of . These are passed in from the parent and tell you the max and min width and length that you’re allowed to be. You can choose any size for yourself within those bounds. By choosing you’re saying that you want to expand to be as big as the parent allows. For the desired height you’re hugging your content by using the property. Passing your desired size into constraints.constrain makes sure that you are still within the allowed constraints. For example, if thumbSize were large, it could exceed the constraints.maxHeight from the parent, which isn’t allowed.

makes sure that you are still within the allowed constraints. For example, if were large, it could exceed the from the parent, which isn’t allowed. The size variable is also a property of RenderBox . You should only set it from within the performLayout method. Everywhere else you should call markNeedsLayout .

Now you’ve officially set the size of your render object, and the parent render object will also have that information.

Note: If you are simply expanding to fill the parent or wrapping a single child, then you don’t need to override performLayout . See the documentation for more on this.

Setting the intrinsic size

Given only a height constraint, how wide would your widget naturally want to be? Or given only a width constrain, how tall would your widget naturally want to be. That’s what intrinsic size is all about.

Add the following four methods to RenderProgressBar :

static const _minDesiredWidth = 100.0; @override

double computeMinIntrinsicWidth(double height) => _minDesiredWidth; @override

double computeMaxIntrinsicWidth(double height) => _minDesiredWidth; @override

double computeMinIntrinsicHeight(double width) => thumbSize; @override

double computeMaxIntrinsicHeight(double width) => thumbSize;

Notes:

The min intrinsic width is the narrowest that the widget would ever want to be. That’s not guaranteeing it’ll never have a smaller width, but this is saying that the widget isn’t designed to be any narrower than that. The Flutter Slider widget uses a hard coded value of 144.0 (or three times a 48-pixel touch target). I figured we could go a little narrower with 100.0 .

(or three times a 48-pixel touch target). I figured we could go a little narrower with . In setting the max width, you could have used double.infinity , but is infinity intrinsically the size you want to be, though? If not, then using the min width is reasonable. This is similar to what the Slider widget does.

, but is infinity intrinsically the size you want to be, though? If not, then using the min width is reasonable. This is similar to what the Slider widget does. I used the same values for the max and min widths and also the same values for the max and min heights. That’s because a progress bar’s height isn’t affected by the width constraints. In the same way, the width isn’t affected by the height constraints. If you think about the Text widget though, that would make a difference. As you can see in the animation below, making the width narrower causes the intrinsic height of the text to want to be taller.

Text height being affected by width constraint

To understand intrinsic sizes, it’s also helpful to see the difference between width , minIntrinsicWidth , and maxIntrinsicWidth for a Text widget.

This image shows the width. This is based on what the parent told the widget to be and is independent of the content.

The following image shows minIntrinsicWidth. It is the narrowest that this widget would ever want to be. Notice that it is the size of the word “Another”. If you forced the widget to be any narrower than this width, it would make “Another” have to break across lines unnaturally.

Finally, the last image shows maxIntrinsicWidth. It’s the widest that this widget would ever want to be. The first two lines end with a

newline character. However, because of the width constraint imposed by the parent, the third line soft-wrapped so that “wraps around.” is on the fourth line. If you took the full width of “A line of text that wraps around.” without making it wrap, that would be the value of maxIntrinsicWidth.

See my Stack Overflow answer here for the code and more details.

Testing it out

Our widget doesn’t paint itself yet, so it’s effectively invisible, but it does have a size now. That means we can wrap it with a colored Container as a trick to “see” it.

Replace your main.dart file with the following simple layout:

Run that and you’ll see a cyan colored bar:

Since the Container has the same size as our ProgressBar , we know that the size is working. That’s good.

On to painting some content in there!

Painting

All the drawing action of a render object happens in the paint method.

Add the following code to the RenderProgressBar class:

double _currentThumbValue = 0.5; @override

void paint(PaintingContext context, Offset offset) {

final canvas = context.canvas;

canvas.save();

canvas.translate(offset.dx, offset.dy); // paint bar

final barPaint = Paint()

..color = barColor

..strokeWidth = 5;

final point1 = Offset(0, size.height / 2);

final point2 = Offset(size.width, size.height / 2);

canvas.drawLine(point1, point2, barPaint); // paint thumb

final thumbPaint = Paint()..color = thumbColor;

final thumbDx = _currentThumbValue * size.width;

final center = Offset(thumbDx, size.height / 2);

canvas.drawCircle(center, thumbSize / 2, thumbPaint); canvas.restore();

}

Notes:

We’ll define _currentThumbValue to be a number from 0 to 1 that will represent the thumb position on the progress bar, where 0 means far left and 1 means far right. Using 0.5 will place it in middle of the bar for now.

to be a number from to that will represent the thumb position on the progress bar, where means far left and means far right. Using will place it in middle of the bar for now. The offset parameter of the paint method tells you where the top left corner of your layout is on the canvas. Saving the canvas and then translating to that position means you don’t need to worry about this offset for any of the other drawing that your about to do. However, at the end of the paint method you call restore to undo your saved translate so that other widgets that may paint later don’t get messed up.

parameter of the method tells you where the top left corner of your layout is on the canvas. Saving the canvas and then translating to that position means you don’t need to worry about this offset for any of the other drawing that your about to do. However, at the end of the method you call to undo your saved so that other widgets that may paint later don’t get messed up. For the actual painting, first draw the bar that the thumb moves along. The color is taken from the widget parameter barColor .

. Then paint the thumb. It’s vertically centered and the horizontal position is based on _currentThumbValue . When that value changes and markNeedsPaint is called, the new position gets repainted here.

In main.dart, comment out the color on the parent Container widget:

child: Container(

// color: Colors.cyan, <-- comment this out

child: ProgressBar(

...

Now run the app again:

Nice! You can see it now!

It still isn’t interactive yet, though, so let’s handle that.

Hit testing

Hit testing just tells Flutter whether or not you want your widget to handle touch events. Since we want to be able to move the thumb on our progress bar, we definitely do want the render object to handle touch events.

Add the following imports to progress_bar.dart:

import 'package:flutter/gestures.dart';

import 'package:flutter/rendering.dart';

And then add the code below to RenderProgressBar :

HorizontalDragGestureRecognizer _drag; @override

bool hitTestSelf(Offset position) => true; @override

void handleEvent(PointerEvent event, BoxHitTestEntry entry) {

assert(debugHandleEvent(event, entry));

if (event is PointerDownEvent) {

_drag.addPointer(event);

}

}

Notes:

Since you want to be able to move the thumb horizontally along the bar, HorizontalDragGestureRecognizer allows you to get notifications about these kind of touch events.

allows you to get notifications about these kind of touch events. Returning true in hitTestSelf tells Flutter that touch events get handled by this widget. They won’t be passed on to any widgets below this one. hitTestSelf also provides a position parameter, so you could theoretically sometimes return true and sometimes return false based on the position of the touch event. This would be useful if you had a donut-shaped widget where you wanted to let touch events in the hole and on the outside pass through.

in tells Flutter that touch events get handled by this widget. They won’t be passed on to any widgets below this one. also provides a parameter, so you could theoretically sometimes return and sometimes return based on the of the touch event. This would be useful if you had a donut-shaped widget where you wanted to let touch events in the hole and on the outside pass through. The docs say to use debugHandleEvent here. So I did. Apparently it does something useful.

here. So I did. Apparently it does something useful. handleEvent adds a PointerDownEvent to the drag gesture recognizer, but you still to initialize it and handle other events, which you’ll do next.

Dealing with the gesture recognizer

Replace the RenderProgressBar constructor with the following code:

RenderProgressBar({

Color barColor,

Color thumbColor,

double thumbSize,

}) : _barColor = barColor,

_thumbColor = thumbColor,

_thumbSize = thumbSize {



// initialize the gesture recognizer

_drag = HorizontalDragGestureRecognizer()

..onStart = (DragStartDetails details) {

_updateThumbPosition(details.localPosition);

}

..onUpdate = (DragUpdateDetails details) {

_updateThumbPosition(details.localPosition);

};

}

And then add the _updateThumbPosition method:

void _updateThumbPosition(Offset localPosition) {

var dx = localPosition.dx.clamp(0, size.width);

_currentThumbValue = dx / size.width;

markNeedsPaint();

markNeedsSemanticsUpdate();

}

Notes:

The localPosition is the touch location of the drag event in relation to the top left corner of our widget. This can go out of bounds so you clamp it between zero and the width of the widget.

is the touch location of the drag event in relation to the top left corner of our widget. This can go out of bounds so you it between zero and the width of the widget. _currentThumbValue needs to be a value between 0 and 1 , so you divide the touch position on the x axis by the total width.

needs to be a value between and , so you divide the touch position on the x axis by the total width. Call markNeedsPaint to repaint the new position of the thumb.

to repaint the new position of the thumb. If the thumb position changes, this represents a new state value of our widget. Calling markNeedsPaint tells Flutter to update the visual representation of this new state and calling markNeedsSemanticsUpdate tells Flutter to update the textual description of this new state. You haven’t implemented that yet, but you’ll do that in the next step.

Run the app again and try dragging the thumb:

Great! It works!

Do you need a repaint boundary?

To achieve the visual effects you got above, you had to repaint the widget every time there was a touch event update. An interesting thing about Flutter is that when a render object repaints itself, generally the parent render objects repaint themselves, too.

You can observe what parts of your app are getting repainted by turning on the debug repaint rainbow. Let’s do that now. In main.dart replace this line:

void main() => runApp(MyApp());

with the following:

void main() {

debugRepaintRainbowEnabled = true;

runApp(MyApp());

}

The debugRepaintRainbowEnabled flag turns the repaint rainbow on. An alternate way to do it is to use the Dart DevTools as I described here. The following images are made using the DevTool version. (Sometime the flag version wasn’t adding a rainbow border for me.)

Now restart the app and move the thumb.

The regions that are being repainted have a rainbow border that changes colors on each repaint. As you can see, the entire window is getting repainted every time you update the thumb position.

Now, for widgets that don’t repaint themselves very often, it doesn’t really matter if the whole parent tree repaints. This is the default behavior in Flutter and even the standard Slider widget is the same. However, an audio progress bar is going to be doing a lot of repainting, not just for when users move the thumb but also whenever the music is playing. For that reason it seems to me that it would be good to limit the repainting to just our widget and not make all of the parent widgets repaint themselves, too.

To limit repainting to just our widget, add this single getter to the RenderProgressBar class:

@override

bool get isRepaintBoundary => true;

The default was false , but now you’re setting it to true .

Run the app again and see the difference:

Now only the progress bar widget is getting repainted. The parent widget tree isn’t.

That’s great isn’t it? Why wouldn’t you always do this? Why isn’t that the default? Well, when you put a repaint boundary around your widget, Flutter makes a new painting layer that is separate from the rest of the tree. Doing so takes more memory resources. If the widget repaints a lot then that’s probably a good use of resources. But if it doesn’t then you’re wasting memory. You need to make that call for your own widget, but in my opinion it makes sense for this one.

Even for widgets that don’t have a repaint boundary, developers can always add one by putting RepaintBoundary in their widget tree. Read the documentation on that and also watch the excellent video Improve your Flutter Apps performance with a RepaintBoundary to learn more.

You can remove the debugging repaint rainbow flag in main.dart now:

void main() {

// debugRepaintRainbowEnabled = true; // <-- delete this

runApp(MyApp());

}

There’s one more step before we’re done.

Semantics

Semantics is about adding the necessary information for Flutter to tell a screen reader what to say when users are interacting with your widget.

Using the semantics debugger

First of all, let’s see what visually impaired users “see” when they use your app. Go to main.dart and wrap MaterialApp with a SemanticsDebugger widget.

class MyApp extends StatelessWidget {

@override

Widget build(BuildContext context) {

return SemanticsDebugger( // <-- Add this

child: MaterialApp(

home: Scaffold(...

Then run your app again:

SemanticsDebugger view

It’s blank. So it looks like our app is completely useless to visually impaired people. Not good. Let’s fix that.

Adding semantics configuration

In progress_bar.dart, add the following methods to RenderProgressBar :

Here’s what that code does:

describeSemanticsConfiguration is where you set the textual description for your widget by setting properties on the config object. This method will be called whenever you call markNeedsSemanticsUpdate from elsewhere within the render object.

is where you set the textual description for your widget by setting properties on the object. This method will be called whenever you call from elsewhere within the render object. The label and value are what a screen reader would read when describing this widget. Since we don’t really want the screen reader to say “0.3478595746 Progress bar”, we converted the thumb value to a nicer number like 35%. This is what the Slider widget does as well.

and are what a screen reader would read when describing this widget. Since we don’t really want the screen reader to say “0.3478595746 Progress bar”, we converted the thumb value to a nicer number like 35%. This is what the widget does as well. Users with a screen reader are able to use custom gestures to perform actions. By adding callbacks for onIncrease and onDecrease you are supporting those custom gestures. This provides an alternate way to move the thumb since these users can’t see its visual location. The _semanticActionUnit of 0.05 just means that whenever the onIncrease or onDecrease action is triggered, the thumb will increase or decrease by 5%.

Run your app again:

SemanticsDebugger view “Progress bar (adjustable)”

It’s hard to see because the font is small, but it says “Progress bar (adjustable)”. You’re widget is now “visible” and interactable to screen readers. Good job.

Remove the SemanticsDebugger widget that you added earlier from your widget layout in main.dart. Run the app again and everything should be back to normal.

A few more notes on semantics

First of all, thank you to creativecreatorormaybenot for help with semantics in the video A definitive guide to RenderObjects in Flutter. Watch that video for a much more in depth guide to making render objects.

I still don’t have much experience using a screen reader so I haven’t actually tested the semantics on a real device. This means there’s a good chance I’m still missing something. Please let me know if you find a bug and I’ll update the article.

Accessibility issues are important to think about. Just recently one of my friends who is visually impaired asked me if I would consider working on a screen reader for traditional Mongolian. That reminded me I need to make sure all of the custom widgets I create are accessible — and semantics is the key to that.

By the way, have you ever tested your app with the system screen reader turned on? Let’s all do that before we publish our next update.

Here’s an introduction to TalkBack, the Android screen reader. It even includes a short discussion about sliders:

Conclusion

Although there is certainly more work that needs to be done on this custom widget, this article took you though all the main steps that you need to think about when building a custom render object. Check out the links below to learn more.

Further study

Full code

main.dart

progress_bar.dart

Update

Are you still here? Well, since you are, would you like to hear about the progress I made after finishing this article?

After making additional improvements to the widget I finally published it on Pub as audio_video_progress_bar . Here is what it looks like:

These are some of the improvements:

Added a buffered duration to show the progress of the download buffer for streamed content.

duration to show the progress of the download buffer for streamed content. Added text labels for the current playing progress and total time. At first I added these as Text widgets by wrapping the ProgressBar render object widget with a StatelessWidget and formed the layout using composition with rows and columns. I wanted to add a repaint boundary, but constantly rebuilding the widget with new values for the Text widgets was triggering a new layout and causing all of the parent widgets in the whole UI to get rebuilt (and repainted). You can read about that problem here. I ended up discarding the StatelessWidget and solved it by painting the labels directly with a TextPainter . That way I could avoid calling markNeedsLayout every time the widget rebuilds. This, in combination with setting isRepaintBoundary to true , solved the problem and the widget no longer causes the whole widget tree to repaint when it rebuilds.

Added more parameters for changing the colors and sizes. I also made the colors default to the app theme’s primary color and the labels’ style default to the text theme’s bodyText1 attribute. This makes the widget still look good when the users switch themes, even to a dark theme.

Added an enum parameter to allow showing the text labels on the sides or not at all. That made the painting and touch event position handling especially tricky. But in the end it seems to be working all right.

Added some widget tests. For a package that lots of people will be using, it’s especially important to add tests. I need to learn more about widget testing, though, because there was some behavior I didn’t know how to test (like dragging the thumb and checking the new duration).

Added documentation comments for the public methods, properties and classes. This is also very important for a widget that other developers will be using. I also added some comments to remind the future me about what is happening in some of the tricky parts of the code.