Skip to content

Latest commit

 

History

History
371 lines (268 loc) · 13.6 KB

CONTRIBUTING.md

File metadata and controls

371 lines (268 loc) · 13.6 KB

Contributing to Forui

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 You Contribute

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.

Conventions

  • Avoid double negatives when naming things, i.e. a boolean field should be named enabled instead of disabled.

  • 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.

Design Guidelines

Avoid translucent colors

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.

Be agnostic about state management

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.

Be conservative when exposing configuration knobs

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();
  }
}

Extend FChangeNotifier & FValueNotifier<T> instead of ChangeNotifier & ValueNotifier<T>

These subclasses have additional life-cycle tracking capabilities baked-in.

Mark widgets as final when sensible

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.

Minimize dependency on Cupertino/Material

Cupertino and Material specific widgets should be avoided when possible.

Minimize dependency on 3rd party packages

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:

Lastly, types from 3rd party packages should not be publicly exported by Forui.

Widget Styles

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:

  1. mix-in Diagnosticable.
  2. provide a primary constructor, and a named constructor, inherit(...) , that configures itself based on an ancestor FTheme.
  3. provide a copyWith(...) method.
  4. override debugFillProperties.
  5. implement operator == and hashCode.

Lastly, the order of the fields and methods should be as shown above.

Writing Golden Tests

Golden images are generated in the test/golden directory instead of relative to the test file.

Only the Inter font is loaded by default.

Blue Screen Tests

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));
});

Configuring Golden Test Threshold

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.

See button_golden_test.dart.

Writing Documentation

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.

Creating a Sample

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: () {},
        ),
      );
}
  1. Samples should extend Sample/StatefulSample which centers and wraps the widget returned by the overridden sample(...) method in a FTheme.
  2. 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.

Creating a Documentation Page

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>
  1. The name corresponds to a <widget-name> in the samples website's route paths.
  2. The variant corresponds to a <variant> in the samples website's route paths. Defaults to default if not specified.
  3. The query parameters to pass to the sample widget.
  4. The height of the <Widget/> component.
  5. {} specifies the lines to highlight.

Updating Localizations

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-l10n

Inside 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();
}