This document outlines how to contribute code to Forui. It assumes that you're familiar with Flutter, writing golden tests, MDX, and Trunk-Based Development.
There are many ways to contribute beyond writing code. For example, you can report bugs, provide feedback, and enhance existing documentation.
In general, contributing code involves the adding/updating of:
- Widgets.
- Relevant unit/golden tests.
- Relevant sample under the samples project.
- Relevant documentation under the docs project.
- CHANGELOG.md.
Before starting work on a PR, please check if a similar issue/
PR exists. We recommend that first time contributors start with
existing issues that are labelled with difficulty: easy
and/or duration: tiny
.
If an issue doesn't exist, create one to discuss the proposed changes. After which, please comment on the issue to indicate that you're working on it.
This helps to:
- Avoid duplicate efforts by informing other contributors about ongoing work.
- Ensure that the proposed changes align with the project's goals and direction.
- Provide a platform for maintainers and the community to offer feedback and suggestions.
If you're stuck or unsure about anything, feel free to ask for help in our discord.
-
Avoid double negatives when naming things, i.e. a boolean field should be named
enabled
instead ofdisabled
. -
Avoid past tense when naming callbacks, prefer present tense instead.
✅ Prefer this:
final VoidCallback onPress;
❌ Instead of:
final VoidCallback onPressed;
-
Format all Dart code with 120 characters line limit, i.e.
dart format . --line-length=120
. -
Prefix all publicly exported widgets and styles with
F
, i.e.FScaffold
.
There is a wide variety of competing state management packages. Picking one may discourage users of the other packages
from adopting Forui. Use InheritedWidget
instead.
Additional knobs can always be introduced later if there's sufficient demand. Changing these knobs is time-consuming and constitute a breaking change.
✅ Prefer this:
class Foo extends StatelessWidget {
final int _someKnobWeDontKnowIfUsefulToUsers = 42;
const Foo() {}
@override
void build(BuildContext context) {
return Placeholder();
}
}
❌ Instead of:
class Foo extends StatelessWidget {
final int someKnobWeDontKnowIfUsefulToUsers = 42;
const Foo(this.someKnobWeDontKnowIfUsefulToUsers) {}
@override
void build(BuildContext context) {
return Placeholder();
}
}
Subclasses can interact with Forui in unforeseen ways, and cause potential issues. It is not breaking to initially mark
classes as final
, and subsequently unmark it. The inverse isn't true. Favor composition over inheritance.
Cupertino and Material specific widgets should be avoided when possible.
3rd party packages introduce uncertainty. It is difficult to predict whether a package will be maintained in the future. Furthermore, if a new major version of a 3rd party package is released, applications that depend on both Forui and the 3rd party package may be forced into dependency hell.
In some situations, it is unrealistic to implement things ourselves. In these cases, we should prefer packages that:
- Are authored by Forus Labs.
- Are maintained by a group/community rather than an individual.
- Have a "pulse", i.e. maintainers responding to issues in the past month at the time of introduction.
Lastly, types from 3rd party packages should not be publicly exported by Forui.
class FooStyle with Diagnosticable { // ---- (1)
final Color color;
FooStyle({required this.color}); // ---- (2)
FooStyle.inherit({FFont font, FColorScheme scheme}): color = scheme.primary; // ---- (2)
FooStyle copyWith({Color? color}) => FooStyle( // ---- (3)
color: color ?? this.color,
);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { // ---- (4)
super.debugFillProperties(properties);
properties.add(ColorProperty<BorderRadius>('color', color));
}
@override
bool operator ==(Object other) => identical(this, other) || other is FStyle && color == other.color; // ---- (5)
@override
int get hashCode => color.hashCode; // ---- (5)
}
They should:
- mix-in Diagnosticable.
- provide a primary constructor, and a named constructor,
inherit(...)
, that configures itself based on an ancestorFTheme
. - provide a
copyWith(...)
method. - override debugFillProperties.
- implement
operator ==
andhashCode
.
Lastly, the order of the fields and methods should be as shown above.
Golden images are generated in the test/golden
directory instead of relative to the test file.
Only the Inter
font is loaded by default.
By default, matchesGoldenFile(...)
has a 0.5% threshold. In other words, images that differ by 0.5% or less will be
considered a match. Due to how different platforms render the images differently, this threshold may need to be adjusted.
To adjust the threshold, move the golden test file into a nested folder if it is in test/src/widgets/
and place a
flutter_test_config.dart
file beside the golden test file.
Copy the following code into the flutter_test_config.dart
file:
import 'dart:async';
import '../../flutter_test_config.dart'; // Adjust path based on the location of the `test/flutter_test_config.dart`.
const _kGoldenTestsThreshold = 7 / 100; // Adjust the threshold as necessary, i.e. 7%.
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
await configureGoldenTests(_kGoldenTestsThreshold);
await testMain();
}
Golden tests should follow these guidelines:
- Golden test files should be suffixed with
golden_test
, i.e.button_golden_test.dart
. - Golden tests should be annotated with
@Tags(['golden'])
. - Widgets under test should be tested against all themes specified in
TestScaffold.themes
. - Widgets under test should be wrapped in a
TestScaffold
.
In addition to the API reference, you should also update forui.dev/docs if necessary.
forui.dev is split into two parts:
- The samples website, which is a Flutter webapp that provides the example widgets.
- The documentation website, which provides overviews and examples of widgets from the samples website embedded
using
<Widget/>
components in MDX files.
We will use a secondary-styled button as an example in the following sections.
The button's corresponding sample is:
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:forui/forui.dart';
import 'package:forui/src/widgets/button/button.dart';
import 'package:forui_samples/sample_scaffold.dart';
final variants = {
for (final value in Variant.values) value.name: value,
};
@RoutePage()
class ButtonTextPage extends SampleScaffold { // - (1)
final Variant variant;
final String label;
ButtonTextPage({
@queryParam super.theme, // - (2)
@queryParam String style = 'primary',
@queryParam this.label = 'Button',
}) : variant = variants[style] ?? Variant.primary;
@override
Widget child(BuildContext context) => IntrinsicWidth(
child: FButton(
label: Text(label),
style: variant,
onPress: () {},
),
);
}
- Samples should extend
SampleScaffold
which centers and wraps the widget returned by the overriddenchild(...)
method in aFTheme
. - The current theme, provided as a URL query parameter.
The samples website uses auto_route
to generate a route for each sample. In general, each sample has its own page and
URL. Generate the route by running dart pub run build_runner build --delete-conflicting-outputs
. After which,
register the route with _AppRouter
in main.dart.
A route's path should follow the format /<widget-name>/<variant>
in kebab-case. In this case, the button route's path
is /button/text
. <variant>
should default to default
and never be empty.
Each widget should have its own MDX file in the documentation website's docs folder.
The file should contain the following sections:
- A brief overview and minimal example.
- Usage section that details the various constructors and their parameters.
- Examples.
See FButton
's mdx file.
Each example should be wrapped in a <Tabs/>
component. It contains a <Widget/>
component and a code block. The
<Widget/>
component is used to display a sample widget hosted on the samples website, while the code block displays
the corresponding Dart code.
<Tabs items={['Preview', 'Code']}>
<Tabs.Tab>
<Widget
name='button' <!-- (1) -->
variant='text' <!-- (2) -->
query={{style: 'secondary'}} <!-- (3) -->
height={500} <!-- (4) -->
/>
</Tabs.Tab>
<Tabs.Tab>
```dart {3} <!-- (5) -->
FButton(
label: const Text('Button'),
style: FButtonStyle.secondary,
onPress: () {},
);
```
</Tabs.Tab>
</Tabs>
- The name corresponds to a
<widget-name>
in the samples website's route paths. - The variant corresponds to a
<variant>
in the samples website's route paths. Defaults todefault
if not specified. - The query parameters to pass to the sample widget.
- The height of the
<Widget/>
component. {}
specifies the lines to highlight.