Skip to content

Functional Themes

Accessing Functional Themes

Every AFib app starts with a default functional theme, called XXXDefaultTheme, and stored in lib/ui/themes/xxx_default_theme.dart. All themes are derived from AFFunctionalTheme. That superclass provides a lot of functionality. It's API docs are probably worth skimming through.

Every SPI has a theme member variable, also accessible via spi.t or spi.theme. Initially, all your UI components will use your default theme.

The Purpose of Functional Themes

Functional themes are useful for accessing standardized UI attributes using typeahead, for sharing simple re-usable bits of UI generation across multiple screens, and for making third party UI libraries more flexible and extensible.

Accessing UI attributes with typeahead

When you first start using Flutter, it is very easy to end up with code like this:

    return Container(
        padding: EdgeInsets.all(8.0),
        margin: EdgeInsets.all(8.0),
        ...
    )

Your default theme has many typeahead attributes which match the attributes commonly required by widgets. For example, you could use this instead:

    final t = spi.theme;

    return Container(
        // the standard amount of padding, which you can easily configure globally.
        padding: t.padding.standard,

        // your biggest vertical amount of margin, again, configurable later.
        margin: t.margin.v.biggest,
        ...
    )

Note that if a widget takes a parameter like padding, you can find what the theme has to offer by typing t.pad and looking for the typeahead.

For example, if I discovered that the BoxDecoration has color and borderRadius parameters, I could easily discover that AFib provides some configurable defaults for those attributes:

    return Container(
        decoration: BoxDecoration(
            // you'd fine this by typing t.col... and watching the typeahead, because 
            // the parameter itself is 'color:'
            color: t.colorPrimary,

            // just a border radius at the top.
            borderRadius: t.borderRadius.t.standard,


        )
    );
In the example above, you might also choose to introduce your own decorationPrimaryHeader method, which would capture the decoration of some conceptual UI element type, and use it like this, referring to t.colorPrimary and t.borderRadius from within it:

    return Container(
        decoration: t.decorationPrimaryHeader(),
        ...
    );

Or, I might discover styles the same way when configuring a Text widget:

    final t = spi.theme;
    return Text("Hello!",
        style: t.styleOnCard.bodyMedium,
    );

Adding Your Own Simple UI conventions

Although you can, in theory, make a widget class for every small piece of UI code you wish to standardize, I often find that tedious, and it does not play very well with typeahead.

For example, if I wish to use a single add icon across my entire app, I could place this in a separate file:

class XXXAddIcon extends Icon {
    XXXAddIcon({
        Color? color,
        double? size,
    }): super(Icons.add_box_sharp, color: color, size: size);
}

I would prefer to add a function to XXXDefaultTheme which achieves the same result:

    Icon iconAdd({
        Color? color,
        double? size,
    }) {
        return Icon(Icons.add_box_sharp,
            color: color,
            size: size
        );
    }

Because this icon will be easily discoverable on the theme using the same principle I used to find t.margin or t.colorPrimary.

    final t = spi.theme;
    return IconButton(
        // as I type t.icon..., I will see typeahead for all the standardized icons in my app.
        icon: t.iconAdd(),
        ...
    );

Note that the vast majority of Flutter widgets take a child parameter which is a widget, or a children parameter which is a list of widgets. Consequently, you will find utilities for creating simple widgets, or lists of widgets, under t.child... and t.children... respectively.

Making Third Party UI Libraries More Flexible

As described in the section on reusing AFib-aware UI libraries below, you can override the themes of third party libraries with your own code. Open-source UI libraries are encouraged to add bespoke child... methods to their themes, making it easy for you to override the functions and change the appearance of their UIs in unexpected ways.

For example, in a simple case you might override a bespoke icon:

    @override
    Widget iconRaisedHand() {
        return MySpecialIconFromAPaidIconLibraryOrWhatever();
    }

However, because everything is a widget in Flutter, you can also more significantly alter the UI. For example, if a third-party signin page has a sign-in button, and you wish to add a 'View Terms' text button below it, you might do something like:

    @override 
    Widget childButtonSignin({
        AFWidgetID? wid
        required Widget child,
        ...
    }) {
        // note: we are inside the theme here, this is like t.column() in UI code.
        final rows = column();

        // add in the original button.
        colExtra.add(super.childButtonPrimary(wid: wid, child: child)); 

        // this is a simple example, but in-general, creating an AFib-aware child widget would work
        // better, as it would have its own state view and SPI, and be easier to test.
        colExtra.add(TextButton(
            child: Text("View Terms"),
            onPressed: () {
                // note that the theme itself has a context member variable
                context.navigatePush(TermsScreen.navigatePush());
            )
        );

        return Column(children: colExtra);
    }
As you can see, this code adds a new button below the original signin button created by the superclass.

Configuring Theme Attributes

Functional themes were originally called 'functional' because they are not allowed to have configurable data, they are only allowed to introduce functions. Each AFib application has a single AFFundamentalTheme which contains configurable theme data. Functional themes can access this data, but they cannot have data of their own.

Configuring Your Fundamental Theme

The defineFundamentalTheme function in your lib/initialization/xxx_define_core.dart file configures your fundamental theme. If you look at that function, you will see that it is based on Flutter's native material themes, but also allows you to add custom data to your fundamental theme.

Populating Custom Theme Attributes

If you wish to populate your fundamental theme with custom data, you can use the AFAppFundamentalThemeAreaBuilder.setValue in xxx_define_core.dart:

void defineFundamentalTheme(AFFundamentalDeviceTheme device, AFComponentStates appState, AFAppFundamentalThemeAreaBuilder primary) {
  primary.setValue(XXXThemeID.achievementColor, Colors.yellow);
}

You can then access it from within your functional theme using fundamentals.findValue.

Creating Additional Themes

Note that the generate ui subcommand supports creating additional custom themes. For example, you might use:

dart bin/xxx_afib.dart generate ui XXXSettingsTheme
To create a separate theme for the settings area of your app. If you feel that your default theme has become excessively large, you can use this technique to give your themes more focus.

Accessing Third Party Themes

Because functional themes do not have their own data, AFib can always allow you to cast from on theme to another. For example, if you wanted to create buttons in your UI in the same style as the AFib Signin library's buttons, you could use:

Widget _buildBody(ExtraRegistrationScreenSPI spi) {
    final xxxTheme = spi.t;
    final afsiTheme = t.accessTheme<AFSIDefaultTheme>(AFSIThemeID.defaultTheme);
    final button = afsiTheme.childButtonPrimary(...);
    ...
}