From 10f8cd8c69adc0070ac8bf3d6bb1a1fab5ca8615 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 16 Sep 2024 14:22:31 -0500 Subject: [PATCH] chore: standardize tabs usages --- src/content/docs/architecture/index.mdx | 141 +-- .../{code_style.md => code_style.mdx} | 45 +- .../docs/error_handling/error_handling.md | 50 +- src/content/docs/navigation/navigation.mdx | 185 ++-- ...ile_testing.md => golden_file_testing.mdx} | 49 +- src/content/docs/testing/testing.mdx | 587 +++++------ .../docs/theming/{theming.md => theming.mdx} | 100 +- .../docs/very_good_engineering/philosophy.mdx | 41 +- src/content/docs/widgets/layouts.mdx | 985 ++++++++---------- 9 files changed, 1051 insertions(+), 1132 deletions(-) rename src/content/docs/code_style/{code_style.md => code_style.mdx} (71%) rename src/content/docs/testing/{golden_file_testing.md => golden_file_testing.mdx} (75%) rename src/content/docs/theming/{theming.md => theming.mdx} (86%) diff --git a/src/content/docs/architecture/index.mdx b/src/content/docs/architecture/index.mdx index c742bd9..d922dea 100644 --- a/src/content/docs/architecture/index.mdx +++ b/src/content/docs/architecture/index.mdx @@ -111,88 +111,89 @@ Each layer abstracts the underlying layers' implementation details. Avoid indire When using layered architecture, data should only flow from the bottom up, and a layer can only access the layer directly beneath it. For example, the `LoginPage` should never directly access the `ApiClient`, or the `ApiClient` should not be dependent on the `UserRepository`. With this approach, each layer has a specific responsibility and can be tested in isolation. -Good ✅ - -```dart -class LoginPage extends StatelessWidget { - ... - LoginButton( - onPressed: => context.read().add(const LoginSubmitted()); - ) - ... -} - -class LoginBloc extends Bloc { - ... - Future _onLoginSubmitted( - LoginSubmitted event, - Emitter emit, - ) async { - try { - await _userRepository.logIn(state.email, state.password); - emit(const LoginSuccess()); - } catch (error, stackTrace) { - addError(error, stackTrace); - emit(const LoginFailure()); + + + ```dart + class LoginPage extends StatelessWidget { + ... + LoginButton( + onPressed: => context.read().add(const LoginSubmitted()); + ) + ... + } + + class LoginBloc extends Bloc { + ... + Future _onLoginSubmitted( + LoginSubmitted event, + Emitter emit, + ) async { + try { + await _userRepository.logIn(state.email, state.password); + emit(const LoginSuccess()); + } catch (error, stackTrace) { + addError(error, stackTrace); + emit(const LoginFailure()); + } } } -} -class UserRepository { - const UserRepository(this.apiClient); + class UserRepository { + const UserRepository(this.apiClient); - final ApiClient apiClient; + final ApiClient apiClient; - final String loginUrl = '/login'; + final String loginUrl = '/login'; - Future logIn(String email, String password) { - await apiClient.makeRequest( - url: loginUrl, - data: { - 'email': email, - 'password': password, - }, - ); - } -} -``` - -Bad ❗️ - -```dart -class LoginPage extends StatelessWidget { - ... - LoginButton( - onPressed: => context.read().add(const LoginSubmitted()); - ) - ... -} - -class LoginBloc extends Bloc { - ... - - final String loginUrl = '/login'; - - Future _onLoginSubmitted( - LoginSubmitted event, - Emitter emit, - ) async { - try { + Future logIn(String email, String password) { await apiClient.makeRequest( url: loginUrl, data: { - 'email': state.email, - 'password': state.password, + 'email': email, + 'password': password, }, - ); + ); + } + } + ``` + + + ```dart + class LoginPage extends StatelessWidget { + ... + LoginButton( + onPressed: => context.read().add(const LoginSubmitted()); + ) + ... + } + + class LoginBloc extends Bloc { + ... + + final String loginUrl = '/login'; - emit(const LoginSuccess()); - } catch (error, stackTrace) { - addError(error, stackTrace); - emit(const LoginFailure()); + Future _onLoginSubmitted( + LoginSubmitted event, + Emitter emit, + ) async { + try { + await apiClient.makeRequest( + url: loginUrl, + data: { + 'email': state.email, + 'password': state.password, + }, + ); + + emit(const LoginSuccess()); + } catch (error, stackTrace) { + addError(error, stackTrace); + emit(const LoginFailure()); + } } } -} -``` + ``` + + In this example, the API implementation details are now leaked and made known to the bloc. The API's login url and request information should only be known to the `UserRepository`. Also, the `ApiClient` instance will have to be provided directly to the bloc. If the `ApiClient` ever changes, every bloc that relies on the `ApiClient` will need to be updated and retested. diff --git a/src/content/docs/code_style/code_style.md b/src/content/docs/code_style/code_style.mdx similarity index 71% rename from src/content/docs/code_style/code_style.md rename to src/content/docs/code_style/code_style.mdx index 6935c7f..ea7791b 100644 --- a/src/content/docs/code_style/code_style.md +++ b/src/content/docs/code_style/code_style.mdx @@ -3,41 +3,48 @@ title: Code Style description: Best practices for general code styling that goes beyond linter rules. --- +import { TabItem, Tabs } from "@astrojs/starlight/components"; + In general, the best guides for code style are the [Effective Dart](https://dart.dev/effective-dart) guidelines and the linter rules set up in [very_good_analysis](https://pub.dev/packages/very_good_analysis). However, there are certain practices we've learned outside of these two places that will make code more maintainable. ## Record Types Among other things, the release of Dart 3.0 introduced [record types](https://dart.dev/language/records), a way to store two different but related pieces of data without creating a separate data class. When using record types, be sure to choose expressive names for positional values. -Bad ❗️ + + + ```dart + Future<(String, String)> getUserNameAndEmail() async => return _someApiFetchMethod(); -```dart -Future<(String, String)> getUserNameAndEmail() async => return _someApiFetchMethod(); + final userData = await getUserNameAndEmail(); -final userData = await getUserNameAndEmail(); + // a bunch of other code... -// a bunch of other code... + if (userData.$1.isValid) { + // do stuff + } + ``` -if (userData.$1.isValid) { - // do stuff -} -``` + + The above example will compile, but it is not immediately obvious what value `userData.$1` refers to here. The name of the function gives the reader the impression that the second value in the record is the email, but it is not clear. Particularly in a large codebase, where there could be more processing in between the call to `getUserNameAndEmail()` and the check on `userData.$1`, reviewers will not be able to tell immediately what is going on here. -Good ✅ - -```dart -Future<(String, String)> getUserNameAndEmail() async => return _someApiFetchMethod(); + + + ```dart + Future<(String, String)> getUserNameAndEmail() async => return _someApiFetchMethod(); -final (username, email) = await getUserNameAndEmail(); + final (username, email) = await getUserNameAndEmail(); -// a bunch of other code... + // a bunch of other code... -if (email.isValid) { - // do stuff -} -``` + if (email.isValid) { + // do stuff + } + ``` + + Now, we are expressly naming the values that we are getting from our record type. Any reviewer or future maintainer of this code will know what value is being validated. diff --git a/src/content/docs/error_handling/error_handling.md b/src/content/docs/error_handling/error_handling.md index cb7dd85..89ef44e 100644 --- a/src/content/docs/error_handling/error_handling.md +++ b/src/content/docs/error_handling/error_handling.md @@ -11,35 +11,31 @@ Properly documenting possible exceptions allows developers to handle exceptions, - -```dart -/// Permanently deletes an account with the given [name]. -/// -/// Throws: -/// -/// * [UnauthorizedException] if the active role is not [Role.admin], since only -/// admins are authorized to delete accounts. -void deleteAccount(String name) { - if (activeRole != Role.admin) { - throw UnauthorizedException('Only admin can delete account'); - } - // ... -} -``` - + ```dart + /// Permanently deletes an account with the given [name]. + /// + /// Throws: + /// + /// * [UnauthorizedException] if the active role is not [Role.admin], since only + /// admins are authorized to delete accounts. + void deleteAccount(String name) { + if (activeRole != Role.admin) { + throw UnauthorizedException('Only admin can delete account'); + } + // ... + } + ``` - -```dart -/// Permanently deletes an account with the given [name]. -void deleteAccount(String name) { - if (activeRole != Role.admin) { - throw UnauthorizedException('Only admin can delete account'); - } - // ... -} -``` - + ```dart + /// Permanently deletes an account with the given [name]. + void deleteAccount(String name) { + if (activeRole != Role.admin) { + throw UnauthorizedException('Only admin can delete account'); + } + // ... + } + ``` diff --git a/src/content/docs/navigation/navigation.mdx b/src/content/docs/navigation/navigation.mdx index d1eec9f..1e1c3ca 100644 --- a/src/content/docs/navigation/navigation.mdx +++ b/src/content/docs/navigation/navigation.mdx @@ -19,30 +19,26 @@ Structure your routes in a way that makes logical sense. Avoid placing all of yo - -```txt -/ -/flutter -/flutter/news -/flutter/chat -/android -/android/news -/android/chat -``` - + ```txt + / + /flutter + /flutter/news + /flutter/chat + /android + /android/news + /android/chat + ``` - -```txt -/ -/flutter -/flutter-news -/flutter-chat -/android -/android-news -/android-chat -``` - + ```txt + / + /flutter + /flutter-news + /flutter-chat + /android + /android-news + /android-chat + ``` @@ -110,22 +106,19 @@ For reasons listed in the [Prefer navigating by name over path](#prefer-navigati - -```dart -context.goNamed('categories', queryParameters: {'size': 'small', 'color': 'blue'}) -``` - + ```dart + context.goNamed('categories', queryParameters: {'size': 'small', 'color': 'blue'}) + ``` + Navigating by path: -Navigating by path: - -```dart -context.go('/categories?size=small&color=blue'); -``` - + ```dart + context.go('/categories?size=small&color=blue'); + ``` + ::: ### Prefer `go` over `push` methods @@ -160,19 +153,15 @@ Mobile app users will likely never see your route's path, but web app users can - -```txt -/user/update-address -``` - + ```txt + /user/update-address + ``` - -```txt -/user/update_address -/user/updateAddress -``` - + ```txt + /user/update_address + /user/updateAddress + ``` @@ -237,18 +226,14 @@ GoRouter provides extension methods on `BuildContext` to simplify navigation. Fo - -```dart -context.goNamed('flutterNews'); -``` - + ```dart + context.goNamed('flutterNews'); + ``` - -```dart -GoRouter.of(context).goNamed('flutterNews'); -``` - + ```dart + GoRouter.of(context).goNamed('flutterNews'); + ``` @@ -349,59 +334,63 @@ GoRouter has the ability to pass objects from one page to another. Most of the t The `extra` option used during navigation does not work on the web and cannot be used for deep linking, so we do not recommend using it. ::: -Bad ❗️ - -```dart -@TypedGoRoute( - name: 'flutterArticle', - path: 'article', -) -@immutable -class FlutterArticlePageRoute extends GoRouteData { - const FlutterArticlePageRoute({ - required this.article, - }); + + + ```dart + @TypedGoRoute( + name: 'flutterArticle', + path: 'article', + ) + @immutable + class FlutterArticlePageRoute extends GoRouteData { + const FlutterArticlePageRoute({ + required this.article, + }); - final Article article; + final Article article; - @override - Widget build(context, state) { - return FlutterArticlePage(article: article); - } -} -``` + @override + Widget build(context, state) { + return FlutterArticlePage(article: article); + } + } + ``` -```dart -FlutterArticlePageRoute(article: article).go(context); -``` + ```dart + FlutterArticlePageRoute(article: article).go(context); + ``` + + In this example, we are passing the `article` object to the article details page. If your app is designed to only work on mobile and there are no plans of deep linking to the articles details page, then this is fine. But, if the requirements change and now you want to support the web or deep link users directly to the details of a particular article, changes will need to be made. Instead, pass the identifier of the article as a path parameter and fetch the article information from inside of your article details page. -Good ✅ - -```dart -FlutterArticlePageRoute(id: state.article.id).go(context); -``` + + + ```dart + FlutterArticlePageRoute(id: state.article.id).go(context); + ``` -```dart -@TypedGoRoute( - name: 'flutterArticle', - path: 'article/:id', -) -@immutable -class FlutterArticlePageRoute extends GoRouteData { - const CategoriesPageRoute({ - required this.id, - }); + ```dart + @TypedGoRoute( + name: 'flutterArticle', + path: 'article/:id', + ) + @immutable + class FlutterArticlePageRoute extends GoRouteData { + const CategoriesPageRoute({ + required this.id, + }); - final String id; + final String id; - @override - Widget build(context, state) { - return FlutterArticlePage(id: id); - } -} -``` + @override + Widget build(context, state) { + return FlutterArticlePage(id: id); + } + } + ``` + + :::note This does not necessarily mean that you have to make another network request to fetch the article information if you already have it. You may need to refactor your repository layer to retrieve the article information from the cache if the data has already been fetched, otherwise make the request to fetch the article information. diff --git a/src/content/docs/testing/golden_file_testing.md b/src/content/docs/testing/golden_file_testing.mdx similarity index 75% rename from src/content/docs/testing/golden_file_testing.md rename to src/content/docs/testing/golden_file_testing.mdx index 9f8b562..f62dc9c 100644 --- a/src/content/docs/testing/golden_file_testing.md +++ b/src/content/docs/testing/golden_file_testing.mdx @@ -3,6 +3,8 @@ title: Golden File Testing description: Golden testing best practices. --- +import { TabItem, Tabs } from "@astrojs/starlight/components"; + The term golden file refers to a master image that is considered the true rendering of a given widget, state, application, or other visual representation you have chosen to capture. :::note @@ -13,35 +15,36 @@ To learn more about Golden file testing refer to [Testing Fundamentals video abo Golden tests should be tagged to make it easier to run them separately from other tests. -Bad ❗️ - -```dart -testWidgets('render matches golden file', (WidgetTester tester) async { - await tester.pumpWidget(MyWidget()); - - await expectLater( - find.byType(MyWidget), - matchesGoldenFile('my_widget.png'), - ); -}); -``` - -Good ✅ - -```dart - testWidgets( - 'render matches golden file', - tags: TestTag.golden, - (WidgetTester tester) async { + + + ```dart + testWidgets( + 'render matches golden file', + tags: TestTag.golden, + (WidgetTester tester) async { + await tester.pumpWidget(MyWidget()); + + await expectLater( + find.byType(MyWidget), + matchesGoldenFile('my_widget.png'), + ); + }, + ); + ``` + + + ```dart + testWidgets('render matches golden file', (WidgetTester tester) async { await tester.pumpWidget(MyWidget()); await expectLater( find.byType(MyWidget), matchesGoldenFile('my_widget.png'), ); - }, - ); -``` + }); + ``` + + :::tip [You should avoid using magic strings to tag tests](../testing/#avoid-using-magic-strings-to-tag-test). Instead, use constants to tag tests. This helps to avoid typos and makes it easier to refactor. diff --git a/src/content/docs/testing/testing.mdx b/src/content/docs/testing/testing.mdx index 283516f..9623ea4 100644 --- a/src/content/docs/testing/testing.mdx +++ b/src/content/docs/testing/testing.mdx @@ -75,161 +75,170 @@ You can find more information about package layouts in the [Dart Package layout All tests should have one or more statements at the end of the test asserting the test result using either an [expect](https://api.flutter.dev/flutter/flutter_test/expect.html) or [verify](https://pub.dev/documentation/mocktail/latest/). -Bad ❗️ + + + ```dart + testWidgets('calls [onTap] on tapping widget', (tester) async { + var isTapped = false; + await tester.pumpWidget(SomeTappableWidget({ + onTap: isTapped = true, + } + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(SomeTappableWidget()); + await tester.pumpAndSettle(); + + expect(isTapped, isTrue); + }); + ``` + + + ```dart + testWidgets('can tap widget', (tester) async { + await tester.pumpWidget(SomeTappableWidget()); + await tester.pumpAndSettle(); -```dart -testWidgets('can tap widget', (tester) async { - await tester.pumpWidget(SomeTappableWidget()); - await tester.pumpAndSettle(); + await tester.tap(SomeTappableWidget()); + await tester.pumpAndSettle(); + }); + ``` + + - await tester.tap(SomeTappableWidget()); - await tester.pumpAndSettle(); -}); -``` The above test would pass coverage on `SomeTappableWidget`, and pass as long as no exception is thrown, but it doesn't really tell any valuable information about what the widget should do. -Good ✅ - -```dart -testWidgets('calls [onTap] on tapping widget', (tester) async { - var isTapped = false; - await tester.pumpWidget(SomeTappableWidget({ - onTap: isTapped = true, - } - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(SomeTappableWidget()); - await tester.pumpAndSettle(); - - expect(isTapped, isTrue); -}); -``` - Now, we are explicitly testing that we have accessed the `onTap` property of `SomeTappableWidget`, which makes this test more valuable, because its behavior is also tested. ## Use matchers and expectations [Matchers](https://api.flutter.dev/flutter/package-matcher_matcher/package-matcher_matcher-library.html) provides better messages in tests and should always be used in [expectations](https://api.flutter.dev/flutter/flutter_test/expect.html). -Bad ❗️ - -```dart -expect(name, 'Hank'); -expect(people.length, 3); -expect(valid, true); -``` - -Good ✅ - -```dart -expect(name, equals('Hank')); -expect(people, hasLength(3)); -expect(valid, isTrue); -``` + + + ```dart + expect(name, equals('Hank')); + expect(people, hasLength(3)); + expect(valid, isTrue); + ``` + + + ```dart + expect(name, 'Hank'); + expect(people.length, 3); + expect(valid, true); + ``` + + ## Use string expression with types If you're referencing a type within a test description, use a [string expression](https://dart.dev/language/built-in-types#string) to ease renaming the type: -Bad ❗️ - -```dart -testWidgets('renders YourView', (tester) async {}); -``` - -Good ✅ - -```dart -testWidgets('renders $YourView', (tester) async {}); -``` + + + ```dart + testWidgets('renders $YourView', (tester) async {}); + ``` + + + ```dart + testWidgets('renders YourView', (tester) async {}); + ``` + + If your [test](https://pub.dev/documentation/test/latest/test/test.html) or [group](https://pub.dev/documentation/test/latest/test/group.html) description only contains a type, consider omitting the string expression: -Bad ❗️ - -```dart -group('$YourView', () {}); -``` - -Good ✅ + + + ```dart + group(YourView, () {}); + ``` + + + ```dart + group('$YourView', () {}); + ``` + + -```dart -group(YourView, () {}); -``` ## Descriptive test Don't be afraid of being verbose in your tests. Make sure everything is readable, which can make it easier to maintain over time. -Bad ❗️ - -```dart -testWidgets('renders', (tester) async {}); -test('works', () async {}); -blocTest('emits',); -``` - -Good ✅ - -```dart -testWidgets('renders $YourView', (tester) async {}); -testWidgets('renders $YourView for $YourState', (tester) async {}); -test('given an [input] is returning the [output] expected', () async {}); -blocTest('emits $StateA if ...',); -``` + + + ```dart + testWidgets('renders $YourView', (tester) async {}); + testWidgets('renders $YourView for $YourState', (tester) async {}); + test('given an [input] is returning the [output] expected', () async {}); + blocTest('emits $StateA if ...',); + ``` + + + ```dart + testWidgets('renders', (tester) async {}); + test('works', () async {}); + blocTest('emits',); + ``` + + ## Test with a single purpose Aim to test one scenario per test. You might end up with more tests in the codebase, but this is preferred over creating one single test to cover several cases. This helps with readability and debugging failing tests. -Bad ❗️ - -```dart -testWidgets('renders $WidgetA and $WidgetB', (tester) async {}); -``` - -Good ✅ - -```dart -testWidgets('renders $WidgetA', (tester) async {}); -testWidgets('renders $WidgetB', (tester) async {}); -``` + + + ```dart + testWidgets('renders $WidgetA', (tester) async {}); + testWidgets('renders $WidgetB', (tester) async {}); + ``` + + + ```dart + testWidgets('renders $WidgetA and $WidgetB', (tester) async {}); + ``` + + ## Use keys carefully Although keys can be an easy way to look for a widget while testing, they tend to be harder to maintain, especially if we use hard-coded keys. Instead, we recommend finding a widget by its type. -Bad ❗️ - -```dart - expect(find.byKey(Key('homePageKey')), findsOneWidget); -``` - -Good ✅ - -```dart - expect(find.byType(HomePage), findsOneWidget); -``` + + + ```dart + expect(find.byType(HomePage), findsOneWidget); + ``` + + + ```dart + expect(find.byKey(Key('homePageKey')), findsOneWidget); + ``` + + ## Use private mocks Developers may reuse mocks across different test files. This could lead to undesired behaviors in tests. For example, if you change the default values of a mock in one class, it could effect your test results in another. In order to avoid this, it is better to create private mocks for each test file. -Bad ❗️ - -```dart -class MockYourClass extends Mock implements YourClass {} - -``` - -Good ✅ - -```dart -class _MockYourClass extends Mock implements YourClass {} -``` + + + ```dart + class _MockYourClass extends Mock implements YourClass {} + ``` + + + ```dart + class MockYourClass extends Mock implements YourClass {} + ``` + + :::tip The analyzer will warn you about unused private mocks (but not if they're public!) if the [`unused_element` diagnostic message](https://dart.dev/tools/diagnostic-messages?utm_source=dartdev&utm_medium=redir&utm_id=diagcode&utm_content=unused_element#unused_element) is not suppressed. @@ -258,141 +267,141 @@ If test setup methods are outside of a group, those setups may cause side effect In order to avoid such issues, refrain from adding `setUp` and `setUpAll` (as well as `tearDown` and `tearDownAll`) methods outside a group: -Bad ❗️ - -```dart -void main() { - late ApiClient apiClient; - - setUp(() { - apiClient = _MockApiClient(); - // mock api client methods... - }); - - group(UserRepository, () { - // Tests... - }); -} -``` - -Good ✅ - -```dart -void main() { - group(UserRepository, () { - late ApiClient apiClient; - - setUp(() { - apiClient = _MockApiClient(); - // mock api client methods... - }); - - // Tests... - }); -} -``` + + + ```dart + void main() { + group(UserRepository, () { + late ApiClient apiClient; + + setUp(() { + apiClient = _MockApiClient(); + // mock api client methods... + }); + + // Tests... + }); + } + ``` + + + ```dart + void main() { + late ApiClient apiClient; + + setUp(() { + apiClient = _MockApiClient(); + // mock api client methods... + }); + + group(UserRepository, () { + // Tests... + }); + } + ``` + + ## Shared mutable objects should be initialized per test We should ensure that shared mutable objects are initialized per test. This avoids the possibility of tests affecting each other, which can lead to flaky tests due to unexpected failures during test parallelization or random ordering. -Bad ❗️ - -```dart -class _MySubjectDependency { - var value = 0; -} - -class _MySubject { - // Although the constructor is constant, it is mutable. - const _MySubject(this._dependency); - - final _MySubjectDependency _dependency; - - get value => _dependency.value; - - void increase() => _dependency.value++; -} - -void main() { - group(_MySubject, () { - final _MySubjectDependency myDependency = _MySubjectDependency(); - - test('value starts at 0', () { - // This test assumes the order tests are run. - final subject = _MySubject(myDependency); - expect(subject.value, equals(0)); - }); - - test('value can be increased', () { - final subject = _MySubject(myDependency); + + + ```dart + void main() { + group(_MySubject, () { + late _MySubjectDependency myDependency; + + setUp(() { + myDependency = _MySubjectDependency(); + }); + + test('value starts at 0', () { + // This test no longer assumes the order tests are run. + final subject = _MySubject(myDependency); + expect(subject.value, equals(0)); + }); + + test('value can be increased', () { + final subject = _MySubject(myDependency); + + subject.increase(); + + expect(subject.value, equals(1)); + }); + }); + } + ``` + + + ```dart + class _MySubjectDependency { + var value = 0; + } - subject.increase(); + class _MySubject { + // Although the constructor is constant, it is mutable. + const _MySubject(this._dependency); - expect(subject.value, equals(1)); - }); - }); -} + final _MySubjectDependency _dependency; -``` + get value => _dependency.value; -Good ✅ + void increase() => _dependency.value++; + } -```dart + void main() { + group(_MySubject, () { + final _MySubjectDependency myDependency = _MySubjectDependency(); -void main() { - group(_MySubject, () { - late _MySubjectDependency myDependency; + test('value starts at 0', () { + // This test assumes the order tests are run. + final subject = _MySubject(myDependency); + expect(subject.value, equals(0)); + }); - setUp(() { - myDependency = _MySubjectDependency(); - }); + test('value can be increased', () { + final subject = _MySubject(myDependency); - test('value starts at 0', () { - // This test no longer assumes the order tests are run. - final subject = _MySubject(myDependency); - expect(subject.value, equals(0)); - }); + subject.increase(); - test('value can be increased', () { - final subject = _MySubject(myDependency); - - subject.increase(); - - expect(subject.value, equals(1)); - }); - }); -} - -``` + expect(subject.value, equals(1)); + }); + }); + } + ``` + + ## Avoid using magic strings to tag tests When [tagging tests](https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags), avoid using magic strings. Instead, use constants to tag tests. This helps to avoid typos and makes it easier to refactor. -Bad ❗️ - -```dart -testWidgets( - 'render matches golden file', - tags: 'golden', - (WidgetTester tester) async { - // ... - }, -); -``` - -Good ✅ - -```dart -testWidgets( - 'render matches golden file', - tags: TestTag.golden, - (WidgetTester tester) async { - // ... - }, -); -``` + + + ```dart + testWidgets( + 'render matches golden file', + tags: TestTag.golden, + (WidgetTester tester) async { + // ... + }, + ); + ``` + + + ```dart + testWidgets( + 'render matches golden file', + tags: 'golden', + (WidgetTester tester) async { + // ... + }, + ); + ``` + + :::caution @@ -427,61 +436,57 @@ When tests share state (such as relying on static members), the order that tests - -```dart -class _Counter { - int value = 0; - void increment() => value++; - void decrement() => value--; -} - -void main() { - group(_Counter, () { - late _Counter counter; - - setUp(() => counter = _Counter()); - - test('increment', () { - counter.increment(); - expect(counter.value, 1); - }); - - test('decrement', () { - counter.decrement(); - expect(counter.value, -1); - }); - }); -} -``` - + ```dart + class _Counter { + int value = 0; + void increment() => value++; + void decrement() => value--; + } + + void main() { + group(_Counter, () { + late _Counter counter; + + setUp(() => counter = _Counter()); + + test('increment', () { + counter.increment(); + expect(counter.value, 1); + }); + + test('decrement', () { + counter.decrement(); + expect(counter.value, -1); + }); + }); + } + ``` - -```dart -class _Counter { - int value = 0; - void increment() => value++; - void decrement() => value--; -} - -void main() { - group(_Counter, () { - final _Counter counter = _Counter(); - - test('increment', () { - counter.increment(); - expect(counter.value, 1); - }); - - test('decrement', () { - counter.decrement(); - // The expectation only succeeds when the previous test executes first. - expect(counter.value, 0); - }); - }); -} -``` - + ```dart + class _Counter { + int value = 0; + void increment() => value++; + void decrement() => value--; + } + + void main() { + group(_Counter, () { + final _Counter counter = _Counter(); + + test('increment', () { + counter.increment(); + expect(counter.value, 1); + }); + + test('decrement', () { + counter.decrement(); + // The expectation only succeeds when the previous test executes first. + expect(counter.value, 0); + }); + }); + } + ``` @@ -497,13 +502,11 @@ This practice ensures that tests do not share state or rely on the side effects - -```sh -# Randomize test ordering using the --test-randomize-ordering-seed option -flutter test --test-randomize-ordering-seed random -dart test --test-randomize-ordering-seed random -very_good test --test-randomize-ordering-seed random -``` - + ```sh + # Randomize test ordering using the --test-randomize-ordering-seed option + flutter test --test-randomize-ordering-seed random + dart test --test-randomize-ordering-seed random + very_good test --test-randomize-ordering-seed random + ``` diff --git a/src/content/docs/theming/theming.md b/src/content/docs/theming/theming.mdx similarity index 86% rename from src/content/docs/theming/theming.md rename to src/content/docs/theming/theming.mdx index 4a8a88d..8f3b2f5 100644 --- a/src/content/docs/theming/theming.md +++ b/src/content/docs/theming/theming.mdx @@ -3,6 +3,8 @@ title: Theming description: Theming best practices. --- +import { TabItem, Tabs } from "@astrojs/starlight/components"; + The theme plays a crucial role in defining the visual properties of an app, such as colors, typography, and other styling attributes. Inconsistencies within the theme can result in poor user experiences and potentially distort the intended design. Fortunately, Flutter offers a great design system that enables us to develop reusable and structured code that ensures a consistent theme. :::note @@ -12,8 +14,8 @@ Flutter uses [Material Design](https://docs.flutter.dev/ui/design/material) with :::tip[Did you know?] Not everyone in the community is happy about Material and Cupertino being baked into the framework. Check out these discussions: -- -- +- https://github.com/flutter/flutter/issues/101479 +- https://github.com/flutter/flutter/issues/110195 ::: @@ -21,56 +23,60 @@ Not everyone in the community is happy about Material and Cupertino being baked By using `ThemeData`, widgets will inherit their styles automatically which is especially important for managing light/dark themes as it allows referencing the same token in widgets and removes the need for conditional logic. -Bad ❗️ - -```dart -class BadWidget extends StatelessWidget { - const BadWidget({super.key}); + + + ```dart + class BadWidget extends StatelessWidget { + const BadWidget({super.key}); - @override - Widget build(BuildContext context) { - return ColoredBox( - color: Theme.of(context).brightness == Brightness.light - ? Colors.white - : Colors.black, - child: Text( - 'Bad', - style: TextStyle( - fontSize: 16, + @override + Widget build(BuildContext context) { + return ColoredBox( color: Theme.of(context).brightness == Brightness.light - ? Colors.black - : Colors.white, - ), - ), - ); - } -} -``` + ? Colors.white + : Colors.black, + child: Text( + 'Bad', + style: TextStyle( + fontSize: 16, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + ), + ); + } + } + ``` + + The above widget might match the design and visually look fine, but if you continue this structure, any design updates could result in you changing a bunch of files instead of just one. -Good ✅ - -```dart -class GoodWidget extends StatelessWidget { - const GoodWidget({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - - return ColoredBox( - color: colorScheme.surface, - child: Text( - 'Good', - style: textTheme.bodyLarge, - ), - ); - } -} -``` + + + ```dart + class GoodWidget extends StatelessWidget { + const GoodWidget({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + + return ColoredBox( + color: colorScheme.surface, + child: Text( + 'Good', + style: textTheme.bodyLarge, + ), + ); + } + } + ``` + + Now, we are using `ThemeData` to get the `ColorScheme` and `TextTheme` so that any design update will automatically reference the correct value. diff --git a/src/content/docs/very_good_engineering/philosophy.mdx b/src/content/docs/very_good_engineering/philosophy.mdx index aaa33b1..370141c 100644 --- a/src/content/docs/very_good_engineering/philosophy.mdx +++ b/src/content/docs/very_good_engineering/philosophy.mdx @@ -66,33 +66,28 @@ Declarative programming is [difficult to define][declarative] and depends on you - -```dart -// declarative coding -// (saying what it should be) - -return Visualizer( - children: [ - VisualElement(), - ], -); -``` - + ```dart + // declarative coding + // (saying what it should be) + + return Visualizer( + children: [ + VisualElement(), + ], + ); + ``` - - ```dart - // imperative coding - // (saying what should happen) + ```dart + // imperative coding + // (saying what should happen) -final visualizer = Visualizer(); -final text = VisualElement(); -visualizer.add(text); - -return visualizer; - -```` + final visualizer = Visualizer(); + final text = VisualElement(); + visualizer.add(text); + return visualizer; + ``` diff --git a/src/content/docs/widgets/layouts.mdx b/src/content/docs/widgets/layouts.mdx index ce71326..a6e6930 100644 --- a/src/content/docs/widgets/layouts.mdx +++ b/src/content/docs/widgets/layouts.mdx @@ -73,45 +73,41 @@ In an ideal world, every phone would have the same physical size and resolution. - -
-```dart -Column( - mainAxisSize: MainAxisSize.min, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` -
- An example of using MainAxisSize.min -
+ +
+ ```dart + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using MainAxisSize.min +
- - -
- -```dart -Column( - mainAxisSize: MainAxisSize.max, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using MainAxisSize.max -
+ +
+ ```dart + Column( + mainAxisSize: MainAxisSize.max, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using MainAxisSize.max +
-
### `MainAxisAlignment` @@ -120,135 +116,113 @@ Column( - -
- -```dart -Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using MainAxisAlignment.start -
+ +
+ ```dart + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using MainAxisAlignment.start +
- - -
- -```dart -Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using MainAxisAlignment.end -
+ +
+ ```dart + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using MainAxisAlignment.end +
- - - - -
- -```dart -Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using MainAxisAlignment.center -
+ + +
+ ```dart + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using MainAxisAlignment.center +
- - - - -
- -```dart -Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using MainAxisAlignment.spaceAround -
+ + +
+ ```dart + Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using MainAxisAlignment.spaceAround +
- - - - -
- -```dart -Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using MainAxisAlignment.spaceBetween -
+ + +
+ ```dart + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using MainAxisAlignment.spaceBetween +
- - - - -
- -```dart -Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using MainAxisAlignment.spaceEvenly -
+ + +
+ ```dart + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using MainAxisAlignment.spaceEvenly +
-
### `CrossAxisAlignment` @@ -261,93 +235,78 @@ Column( - - -
- -```dart -Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using CrossAxisAlignment.start -
+ +
+ ```dart + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using CrossAxisAlignment.start +
- - -
- -```dart -Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using CrossAxisAlignment.end -
+ +
+ ```dart + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using CrossAxisAlignment.end +
- - - - -
- -```dart -Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using CrossAxisAlignment.center -
+ + +
+ ```dart + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using CrossAxisAlignment.center +
- - - - -
- -```dart -Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using CrossAxisAlignment.stretch -
+ + +
+ ```dart + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using CrossAxisAlignment.stretch +
-
## `Expanded`, `Flexible`, and `Spacer` @@ -364,68 +323,62 @@ Within a row or column, you may want different widgets to take up differing amou The `Expanded` widget will cause its child widget to expand to fill all the available space across the main axis of its parent widget. - +
- -```dart -Column( - children: [ - Expanded(child: Box()), - Box(), - Box(), - Box(), - ], -), -``` - + ```dart + Column( + children: [ + Expanded(child: Box()), + Box(), + Box(), + Box(), + ], + ), + ```
An example of using Expanded -
+
### `Spacer` The `Spacer` widget creates an empty space that fills all the available space across the main axis of its parent widget. - +
- -```dart -Column( - children: [ - Box(), - Box(), - Spacer(), - Box(), - Box(), - ], -), -``` - + ```dart + Column( + children: [ + Box(), + Box(), + Spacer(), + Box(), + Box(), + ], + ), + ```
An example of using Spacer -
+
### `Flexible` The `Flexible` widget is a more flexible (pun intended) expanded widget that lets you choose wether to fill the expandable space (or not). - +
- -```dart -Column( - children: [ - Flexible(fit: FlexFit.loose, child: Box()), - Flexible(fit: FlexFit.tight, child: Box()), - Box(), - Box(), - ], -), -``` - + ```dart + Column( + children: [ + Flexible(fit: FlexFit.loose, child: Box()), + Flexible(fit: FlexFit.tight, child: Box()), + Box(), + Box(), + ], + ), + ```
An example of using Flexible -
+
### Flex Factor @@ -433,49 +386,43 @@ Column( - -
- -```dart -Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded(child: Box()), - Box(), - Spacer(), - Box(), - Box(), - ], -), -``` - -
- An example of not using a flex factor -
+ +
+ ```dart + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: Box()), + Box(), + Spacer(), + Box(), + Box(), + ], + ), + ``` +
+ An example of not using a flex factor +
- - -
- -```dart -Column( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded(flex: 4, child: Box()), - Box(), - Spacer(flex: 1), - Box(), - Box(), - ], -), -``` - -
- An example of using a flex factor -
+ +
+ ```dart + Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded(flex: 4, child: Box()), + Box(), + Spacer(flex: 1), + Box(), + Box(), + ], + ), + ``` +
+ An example of using a flex factor +
-
## Rules for Parents and Children @@ -490,55 +437,49 @@ Constraints that are set by the parent are enforced on the child widgets. If the - -
- -```dart -Container( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Box(), - Box(), - Box(), - Box(), - ], - ), -), -``` - -
- An example of not using constraints -
+ +
+ ```dart + Container( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ), + ``` +
+ An example of not using constraints +
- - -
- -```dart -Container( - width: 300, - height: 500, - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Box(), - Box(), - Box(), - Box(), - ], - ), -), -``` - -
- An example of using constraints -
+ +
+ ```dart + Container( + width: 300, + height: 500, + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ), + ``` +
+ An example of using constraints +
-
### Sizes Go up @@ -547,81 +488,71 @@ Children set their sizes within parents, but they cannot override any constraint - -
- -```dart -Container( - child: Column( - children: [ - Box(), - Box(), - Box(), - Box(), - ], - ), -), -``` - -
- An example of not using sizes -
+ +
+ ```dart + Container( + child: Column( + children: [ + Box(), + Box(), + Box(), + Box(), + ], + ), + ), + ``` +
+ An example of not using sizes +
- - -
- -```dart -Column( - children: [ - Container( - height: 100, - width: 300, - color: Colors.redAccent, - ), - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using children's sizes -
+ +
+ ```dart + Column( + children: [ + Container( + height: 100, + width: 300, + color: Colors.redAccent, + ), + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using children's sizes +
- - - - -
- -```dart -Container( - width: 200, - child: Column( - children: [ - Container( - height: 100, - width: 300, - color: Colors.redAccent, - ), - Box(), - Box(), - Box(), - Box(), - ], - ), -), -``` - -
- An example of using parents sizes -
+ + +
+ ```dart + Container( + width: 200, + child: Column( + children: [ + Container( + height: 100, + width: 300, + color: Colors.redAccent, + ), + Box(), + Box(), + Box(), + Box(), + ], + ), + ), + ``` +
+ An example of using parents sizes +
-
### Parent Sets Position @@ -659,105 +590,93 @@ A `SingleChildScrollView` will wrap a widget and make it scrollable. It is ideal - -
- -```dart -Column( - children: [ - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of overflowing widgets -
+ +
+ ```dart + Column( + children: [ + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of overflowing widgets +
- - -
- -```dart -Wrap( - direction: Axis.vertical, - children: [ - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using wrap -
+ +
+ ```dart + Wrap( + direction: Axis.vertical, + children: [ + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using wrap +
- - -
- -```dart -ListView( - scrollDirection: Axis.vertical, - children: [ - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - ], -), -``` - -
- An example of using a listview -
+ +
+ ```dart + ListView( + scrollDirection: Axis.vertical, + children: [ + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + ], + ), + ``` +
+ An example of using a listview +
- - - - -
- -```dart -SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - children: [ - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - Box(), - ], - ), -) -``` - -
- An example of using a SingleChildScrollView -
+ + +
+ ```dart + SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + children: [ + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + Box(), + ], + ), + ) + ``` +
+ An example of using a SingleChildScrollView +