Custom Knob

If the built-in knobs do not meet your needs, you can create your own custom Knobs. This allows you to extend the functionality of Widgetbook and tailor it to your specific requirements.

Guide

In this guide, we will be creating a "range knob" that allows you to select a range of values using a RangeSlider.

0. Base structure

Start by creating a new file in your widgetbook project (e.g. range_knob.dart).

widgetbook/lib/knobs/range_knob.dart
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';

class RangeKnob extends Knob<RangeValues> {
  RangeKnob({
    required super.label,
    required super.initialValue,
  });

  @override
  List<Field> get fields {
    // TODO
  }

  @override
  RangeValues valueFromQueryGroup(Map<String, String> group) {
    // TODO
  }
}

Now let's start by implementing each of the class members step by step.

1. fields getter

This is a list of fields that describe how the knob will be represented in both:

  • The Widgetbook UI โ€“ the "Knobs" panel.
  • The URL query parameters.

In this case we need two fields to represent the RangeValues:

  1. DoubleInputField - to pick a double value for the start of the range.
  2. DoubleInputField - to pick a double value for the end of the range.
@override
List<Field> get fields => [
  DoubleInputField(
    name: '$label.min',
    initialValue: initialValue.start,
  ),
  DoubleInputField(
    name: '$label.max',
    initialValue: initialValue.end,
  ),
];

2. valueFromQueryGroup method

This method is responsible for parsing the query parameters back to the knob value (i.e. RangeValues). This can be done by using the valueOf helper method to extract the values from the query group by giving it the field's name.

@override
RangeValues valueFromQueryGroup(Map<String, String> group) {
  final start = valueOf<double>('$label.min', group)!;
  final end = valueOf<double>('$label.max', group)!;

  return RangeValues(start, end);
}

3. Registering the Knob

To use the custom knob, we need to create an extension on KnobsBuilder. This extension includes a range method that adds our RangeKnob to the builder.

extension RangeKnobBuilder on KnobsBuilder {
  RangeValues range({
    required String label,
    RangeValues initialValue = const RangeValues(0, 10),
  }) =>
      onKnobAdded(
        RangeKnob(
          label: label,
          initialValue: initialValue,
        ),
      )!;
}

4. Using the Knob

Now that we have implemented the RangeKnob, we can use it in our use cases. Here's an example of how to use it with a RangeSlider widget:

widgetbook/lib/widgets/range_slider_use_case.dart
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';

import '../knobs/range_knob.dart';

@UseCase(name: 'CustomRangeSlider', type: RangeSlider)
Widget buildRangeSliderUseCase(BuildContext context) {
  return RangeSlider(
    values: context.knobs.range(label: 'Range'),
    max: 10,
    min: 0,
    onChanged: (_) {},
  );
}

Multi-snapshot Support

Custom knobs can be used with Multi Snapshot Reviews. The configuration varies based on the number of fields in your custom knob.

Single-field Knobs

If your custom knob is a single-field knob, you can use KnobConfig to configure it for multi-snapshot support. Here's an example:

widgetbook/lib/widgets/custom_widget.dart
import 'package:widgetbook_annotation/widgetbook_annotation.dart';

@UseCase(
  name: 'Default',
  type: MyWidget,
  cloudKnobsConfigs: { 
    'custom': [KnobConfig('foo', 'value')], 
  }, 
)
Widget buildUseCase(BuildContext context) {
  return MyWidget(
    foo: context.knobs.myKnob(label: 'foo'), 
  );
}

Multi-field Knobs

If your knobs has more than one field, similar to the RangeKnob defined above, you can use MultiFieldKnobConfig to configure it for multi-snapshot support. Here's how you can do it:

widgetbook/lib/widgets/range_slider_use_case.dart
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';

import '../knobs/range_knob.dart';

@widgetbook.UseCase(
  name: 'CustomRangeSlider',
  type: RangeSlider,
  cloudKnobsConfigs: { 
    'same range': [ 
      MultiFieldKnobConfig({ 
        'range.min': 5, 
        'range.max': 5, 
      }), 
    ], 
    'small range': [ 
      MultiFieldKnobConfig({ 
        'range.min': 4, 
        'range.max': 6, 
      }), 
    ], 
  }, 
)
Widget buildRangeSliderUseCase(BuildContext context) {
  return RangeSlider(
    values: context.knobs.range(label: 'range'), 
    max: 10,
    min: 0,
    onChanged: (_) {},
  );
}