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.
After cloning the repository, and before starting work on a PR, run the following commands in the forui project directory:
dart run build_runner build --delete-conflicting-outputsThis command generates the necessary files for the project to build successfully.
-
Avoid double negatives when naming things, i.e. a boolean field should be named
enabledinstead 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.
Translucent colors may not render as expected on different backgrounds. They are usually used as disabled and hovered
states. Instead, use the FColorScheme.disable and FColorScheme.hover functions to generate colors for disabled and
hovered states respectively.
Alternatively, use alpha-blending to generate an equivalent solid color.
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();
}
}These subclasses have additional life-cycle tracking capabilities baked-in.
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.
Prefer AlignmentGeometry/BorderRadiusGeometry/EdgeInsetsGeometry over Alignment/BorderRadius/EdgeInsets
Prefer the Geometry variants when possible because they are more flexible.
part 'foo.style.dart'; // --- (1)
class FooStyle with Diagnosticable, _$FooStyleFunctions { // ---- (2) (3)
final Color color;
FooStyle({required this.color});
FooStyle.inherit({FFont font, FColorScheme scheme}): color = scheme.primary; // ---- (4)
FooStyle copyWith({Color? color}) => FooStyle( // ---- (5)
color: color ?? this.color,
);
}They should:
- include a generated part file which includes
_$FooStyleFunctions. To generate the file, rundart run build_runner build --delete-conflicting-outputsin the forui/forui directory. - mix-in Diagnosticable.
- mix-in
_$FooStyleFunctions, which contains several utility functions. - provide a primary constructor, and a named constructor,
inherit(...), that configures itself based on an ancestorFTheme.
Lastly, the order of the fields and methods should be as shown above.
Prefer hiding internal members (blacklisting) rather than showing public members (whitelisting) in barrel files.
✅ Prefer this:
library forui.widgets.calendar;
export '../src/widgets/calendar/calendar.dart';
export '../src/widgets/calendar/day/day_picker.dart' hide DayPicker;
export '../src/widgets/calendar/shared/entry.dart' hide Entry;
export '../src/widgets/calendar/shared/header.dart' hide Header, Navigation;
export '../src/widgets/calendar/calendar_controller.dart';❌ Instead of:
export '../src/widgets/calendar/calendar.dart';
export '../src/widgets/calendar/day/day_picker.dart' show FCalendarDayPickerStyle;
export '../src/widgets/calendar/shared/entry.dart' show FCalendarDayData, FCalendarEntryStyle;
export '../src/widgets/calendar/shared/header.dart' show FCalendarHeaderStyle, FCalendarPickerType;
export '../src/widgets/calendar/calendar_controller.dart';This prevents accidental omission of generated public members.
To detect memory leaks, leak_tracker_flutter_testing
is enabled by default in all tests.
Leak tracking results currently are not shown when running tests via an IntelliJ run configuration. As a workaround, run the tests via the terminal.
It is recommended to wrap disposable objects created in tests with autoDispose(...), i.e. final focus = autoDispose(FocusNode()).
Golden images are generated in the test/golden directory instead of relative to the test file.
Golden tests should follow these guidelines:
- Golden test files should be suffixed with
golden_test, i.e.button_golden_test.dart. - Widgets under test should be tested against all themes specified in
TestScaffold.themes. - Widgets under test should be wrapped in a
TestScaffold.
The CI pipeline will automatically generate golden images for all golden tests on Windows & macOS. Contributors on Linux should not* commit locally generated golden images.
Blue screen tests are a special type of golden tests. All widgets should have a blue screen test. It uses a special theme that is all blue. This allows us to verify that custom/inherited themes are being applied correctly. The resultant image should be completely blue if applied correctly, hence the name.
Example
testWidgets('blue screen', (tester) async {
await tester.pumpWidget(
TestScaffold.blue( // (1) Always use the TestScaffold.blue(...) constructor.
child: FSelectGroup(
style: TestScaffold.blueScreen.selectGroupStyle, // (2) Always use the TestScaffold.blueScreen theme.
children: [
FSelectGroupItem.checkbox(value: 1),
],
),
),
);
// (3) Always use expectBlueScreen.
await expectBlueScreen(find.byType(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.dart';
final variants = {
for (final value in Variant.values) value.name: value,
};
@RoutePage()
class ButtonTextPage extends Sample { // - (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 sample(BuildContext context) => IntrinsicWidth(
child: FButton(
label: Text(label),
style: variant,
onPress: () {},
),
);
}- Samples should extend
Sample/StatefulSamplewhich centers and wraps the widget returned by the overriddensample(...)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 todefaultif not specified. - The query parameters to pass to the sample widget.
- The height of the
<Widget/>component. {}specifies the lines to highlight.
In most cases, you will not need to update localizations. However, if you do, please read Internationalizing Flutter apps. before continuing.
Each ARB file in the lib/l10n represents a localization for a specific language. We try to maintain parity with the
languages Flutter natively supports. To add a missing language, run the fetch_arb script in the tool directory.
After adding the necessary localization messages, run the following command in the forui project directory which will
generate the localization files in lib/src/localizations:
flutter gen-l10nInside the generated localizations.dart file, change:
static FLocalizations of(BuildContext context) {
return Localizations.of<FLocalizations>(context, FLocalizations);
}To:
static FLocalizations of(BuildContext context) {
return Localizations.of<FLocalizations>(context, FLocalizations) ?? DefaultLocalizations();
}