├── .gitignore ├── README.md ├── images ├── benchmark_async.png ├── collection_of_one.png ├── contains_no_diacritics.png ├── deep_checkbox.gif ├── deep_checkbox.png ├── fallback_asset_bundle.png ├── go_router_uri.png ├── is_version_greater_than.png ├── json_object_model.png ├── json_try_decode.png ├── optional.png ├── price_format.png ├── pump_app_tester.png ├── safe_complete.png ├── safe_id.png ├── separated_with.png ├── sliver_separated_child_builder_delegate.png ├── stacked_gradients.png ├── stream_converter.png ├── test_with.png ├── translated_locale_name.png └── unwrap.png └── source ├── benchmark_async.dart ├── collection_of_one.dart ├── contains_no_diacritics.dart ├── deep_checkbox.dart ├── go_router_uri.dart ├── is_version_greater_than.dart ├── json_object_model.dart ├── json_try_decode.dart ├── optional.dart ├── override_asset_bundle.dart ├── price_format.dart ├── pump_app_tester.dart ├── responsive.dart ├── safe_complete.dart ├── safe_id.dart ├── separated_with.dart ├── sliver_separated_child_builder_delegate.dart ├── stacked_gradients.dart ├── stream_converter.dart ├── test_with.dart ├── translated_locale_name.dart └── unwrap.dart /.gitignore: -------------------------------------------------------------------------------- 1 | carbon_config.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Tips 2 | 3 | [![Twitter Follow](https://img.shields.io/twitter/follow/TesteurManiak?style=social)](https://twitter.com/TesteurManiak) 4 | 5 | Some tips and tricks in Flutter & Dart. 6 | 7 | # Table of Contents 8 | 9 | * [Optional](#optional) 10 | * [CollectionOfOne](#collectionofone) 11 | * [Safe ID](#safe-id) 12 | * [Unwrap](#unwrap) 13 | * [Separated with](#separated-with) 14 | * [SliverSeparatedChildBuilderDelegate](#sliverseparatedchildbuilderdelegate) 15 | * [Test with](#test-with) 16 | * [Override Asset Bundle](#override-asset-bundle) 17 | * [Price Format](#price-format) 18 | * [Deep Checkbox](#deep-checkbox) 19 | * [Translated Locale Name](#translated-locale-name) 20 | * [Go Router Uri](#go-router-uri) 21 | * [Json Object Model](#json-object-model) 22 | * [Pump App Tester](#pump-app-tester) 23 | * [Safe Complete](#safe-complete) 24 | * [Stream Converter](#stream-converter) 25 | * [String contains diacritics insensitive](#string-contains-diacritics-insensitive) 26 | * [Stacked Gradients](#stacked-gradients) 27 | * [Compare two versions](#compare-two-versions) 28 | * [Benchmark async method](#benchmark-async-method) 29 | * [JSON decoding](#json-decoding) 30 | 31 | # Optional 32 | 33 | [Source Code](source/optional.dart) 34 | 35 | ![](images/optional.png) 36 | 37 | # CollectionOfOne 38 | 39 | [Source Code](source/collection_of_one.dart) 40 | 41 | ![](images/collection_of_one.png) 42 | 43 | # Safe ID 44 | 45 | [Source Code](source/safe_id.dart) 46 | 47 | ![](images/safe_id.png) 48 | 49 | # Unwrap 50 | 51 | [Source Code](source/unwrap.dart) 52 | 53 | ![](images/unwrap.png) 54 | 55 | # Separated with 56 | 57 | [Source Code](source/separated_with.dart) 58 | 59 | ![](images/separated_with.png) 60 | 61 | # SliverSeparatedChildBuilderDelegate 62 | 63 | [Source Code](source/sliver_separated_child_builder_delegate.dart) 64 | 65 | [Try a demo](https://dartpad.dev/?id=e478318e74333e0c981f9b94188508ab) 66 | 67 | ![](images/sliver_separated_child_builder_delegate.png) 68 | 69 | # Test with 70 | 71 | [Source Code](source/test_with.dart) 72 | 73 | ![](images/test_with.png) 74 | 75 | # Override Asset Bundle 76 | 77 | [Source Code](source/override_asset_bundle.dart) 78 | 79 | ![](images/fallback_asset_bundle.png) 80 | 81 | # Price Format 82 | 83 | [Source code](source/price_format.dart) 84 | 85 | [Try a demo](https://dartpad.dev/?id=db6fd485e310437f816bb0d32635803c) 86 | 87 | ![](images/price_format.png) 88 | 89 | # Deep Checkbox 90 | 91 | [Source code](source/deep_checkbox.dart) 92 | 93 | [Try a demo](https://dartpad.dev/?id=ae8fcb79313fecd4117c83aea43dae5c) 94 | 95 | ![](images/deep_checkbox.gif) 96 | 97 | # Translated Locale Name 98 | 99 | [Source code](source/translated_locale_name.dart) 100 | 101 | ![](images/translated_locale_name.png) 102 | 103 | # Go Router Uri 104 | 105 | [Source code](source/go_router_uri.dart) 106 | 107 | ![](images/go_router_uri.png) 108 | 109 | # Json Object Model 110 | 111 | [Source Code](source/json_object_model.dart) 112 | 113 | ![](images/json_object_model.png) 114 | 115 | # Pump App Tester 116 | 117 | [Source Code](source/pump_app_tester.dart) 118 | 119 | ![](images/pump_app_tester.png) 120 | 121 | # Safe Complete 122 | 123 | [Source Code](source/safe_complete.dart) 124 | 125 | ![](images/safe_complete.png) 126 | 127 | # Stream Converter 128 | 129 | [Source Code](source/stream_converter.dart) 130 | 131 | ![](images/stream_converter.png) 132 | 133 | # String contains diacritics insensitive 134 | 135 | [Source Code + Tests](source/contains_no_diacritics.dart) 136 | 137 | ![](images/contains_no_diacritics.png) 138 | 139 | # Stacked gradients 140 | 141 | [Source Code](source/stacked_gradients.dart) 142 | 143 | [Try a demo](https://dartpad.dev/?id=0164591f591b08e52c6785307e52fd4f) 144 | 145 | ![](images/stacked_gradients.png) 146 | 147 | # Compare two versions 148 | 149 | [Source Code + Tests](source/is_version_greater_than.dart) 150 | 151 | ![](images/is_version_greater_than.png) 152 | 153 | # Benchmark async method 154 | 155 | [Source Code](source/benchmark_async.dart) 156 | 157 | ![](images/benchmark_async.png) 158 | 159 | # JSON decoding 160 | 161 | [Source Code + Tests](source/json_try_decode.dart) 162 | 163 | ![](images/json_try_decode.png) 164 | 165 | # Credits 166 | 167 | * **Inspiration**: [vandadnp's](https://github.com/vandadnp) repository [flutter-tips-and-tricks](https://github.com/vandadnp/flutter-tips-and-tricks) 168 | * **Code snippets**: [CodeSnap](https://marketplace.visualstudio.com/items?itemName=adpyke.codesnap) 169 | -------------------------------------------------------------------------------- /images/benchmark_async.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/benchmark_async.png -------------------------------------------------------------------------------- /images/collection_of_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/collection_of_one.png -------------------------------------------------------------------------------- /images/contains_no_diacritics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/contains_no_diacritics.png -------------------------------------------------------------------------------- /images/deep_checkbox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/deep_checkbox.gif -------------------------------------------------------------------------------- /images/deep_checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/deep_checkbox.png -------------------------------------------------------------------------------- /images/fallback_asset_bundle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/fallback_asset_bundle.png -------------------------------------------------------------------------------- /images/go_router_uri.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/go_router_uri.png -------------------------------------------------------------------------------- /images/is_version_greater_than.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/is_version_greater_than.png -------------------------------------------------------------------------------- /images/json_object_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/json_object_model.png -------------------------------------------------------------------------------- /images/json_try_decode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/json_try_decode.png -------------------------------------------------------------------------------- /images/optional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/optional.png -------------------------------------------------------------------------------- /images/price_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/price_format.png -------------------------------------------------------------------------------- /images/pump_app_tester.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/pump_app_tester.png -------------------------------------------------------------------------------- /images/safe_complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/safe_complete.png -------------------------------------------------------------------------------- /images/safe_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/safe_id.png -------------------------------------------------------------------------------- /images/separated_with.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/separated_with.png -------------------------------------------------------------------------------- /images/sliver_separated_child_builder_delegate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/sliver_separated_child_builder_delegate.png -------------------------------------------------------------------------------- /images/stacked_gradients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/stacked_gradients.png -------------------------------------------------------------------------------- /images/stream_converter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/stream_converter.png -------------------------------------------------------------------------------- /images/test_with.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/test_with.png -------------------------------------------------------------------------------- /images/translated_locale_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/translated_locale_name.png -------------------------------------------------------------------------------- /images/unwrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TesteurManiak/flutter-tips/HEAD/images/unwrap.png -------------------------------------------------------------------------------- /source/benchmark_async.dart: -------------------------------------------------------------------------------- 1 | Future benchmarkAsync(Future asyncOperation) async { 2 | final stopwatch = Stopwatch()..start(); 3 | final result = await asyncOperation; 4 | stopwatch.stop(); 5 | print('Operation executed in ${stopwatch.elapsed}'); 6 | return result; 7 | } 8 | -------------------------------------------------------------------------------- /source/collection_of_one.dart: -------------------------------------------------------------------------------- 1 | extension type CollectionOfOne._(Iterable _value) implements Iterable { 2 | CollectionOfOne(T value) : this._value = List.filled(1, value); 3 | } 4 | -------------------------------------------------------------------------------- /source/contains_no_diacritics.dart: -------------------------------------------------------------------------------- 1 | extension ContainsNoDiacritics on String { 2 | static const diacritics = 3 | "ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž"; 4 | static const letters = 5 | "AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCcDIIIIiiiiUUUUuuuuNnSsYyyZz"; 6 | 7 | String get withoutDiacritics => splitMapJoin( 8 | "", 9 | onNonMatch: (char) => char.isNotEmpty && diacritics.contains(char) 10 | ? letters[diacritics.indexOf(char)] 11 | : char, 12 | ); 13 | 14 | bool containsNoDiacritics(Pattern other, [int startIndex = 0]) => 15 | withoutDiacritics.contains(other, startIndex); 16 | } 17 | 18 | // Test it 19 | void main() { 20 | group("withoutDiacritics", () { 21 | test("should replace diacritics letter with their counterparts", () { 22 | expect( 23 | ContainsNoDiacritics.diacritics.withoutDiacritics, 24 | ContainsNoDiacritics.letters, 25 | ); 26 | }); 27 | }); 28 | 29 | group("containsNoDiacritics", () { 30 | test( 31 | "should return true if the word contains é while the pattern include e", 32 | () { 33 | expect("é".containsNoDiacritics("e"), true); 34 | }, 35 | ); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /source/deep_checkbox.dart: -------------------------------------------------------------------------------- 1 | /// A checkbox updating itself according to the state of its [child] if it 2 | /// contains another [DeepCheckbox]. 3 | /// 4 | /// Example: 5 | /// ```dart 6 | /// class CheckboxesPage extends StatelessWidget { 7 | /// const CheckboxesPage({Key? key}) : super(key: key); 8 | /// 9 | /// @override 10 | /// Widget build(BuildContext context) { 11 | /// return Scaffold( 12 | /// body: DeepCheckbox( 13 | /// label: 'Level 1', 14 | /// child: Padding( 15 | /// padding: const EdgeInsets.only(left: 20), 16 | /// child: Column( 17 | /// crossAxisAlignment: CrossAxisAlignment.start, 18 | /// mainAxisSize: MainAxisSize.min, 19 | /// children: const [ 20 | /// DeepCheckbox(label: 'Level 2-1'), 21 | /// DeepCheckbox(label: 'Level 2-2'), 22 | /// ], 23 | /// ), 24 | /// ), 25 | /// ), 26 | /// ); 27 | /// } 28 | /// } 29 | /// ``` 30 | class DeepCheckbox extends StatefulWidget { 31 | final String label; 32 | final bool initialValue; 33 | final ValueChanged? onChanged; 34 | final Widget? child; 35 | 36 | const DeepCheckbox({ 37 | Key? key, 38 | required this.label, 39 | this.child, 40 | this.initialValue = false, 41 | this.onChanged, 42 | }) : super(key: key); 43 | 44 | static DeepCheckboxState? of(BuildContext context) { 45 | final scope = context.dependOnInheritedWidgetOfExactType<_CheckboxScope>(); 46 | return scope?._deepCheckboxState; 47 | } 48 | 49 | @override 50 | State createState() => DeepCheckboxState(); 51 | } 52 | 53 | class DeepCheckboxState extends State { 54 | final _checkboxes = {}; 55 | 56 | int _generation = 0; 57 | bool? _selected; 58 | 59 | @override 60 | void initState() { 61 | super.initState(); 62 | _selected = widget.initialValue; 63 | } 64 | 65 | @override 66 | void deactivate() { 67 | DeepCheckbox.of(context)?._unregister(this); 68 | super.deactivate(); 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | DeepCheckbox.of(context)?._register(this); 74 | final child = widget.child; 75 | return _CheckboxScope( 76 | deepCheckboxState: this, 77 | generation: _generation, 78 | child: Column( 79 | crossAxisAlignment: CrossAxisAlignment.start, 80 | mainAxisSize: MainAxisSize.min, 81 | children: [ 82 | Row( 83 | mainAxisSize: MainAxisSize.min, 84 | children: [ 85 | Checkbox( 86 | tristate: true, 87 | value: _selected, 88 | onChanged: _onChanged, 89 | ), 90 | Text(widget.label), 91 | ], 92 | ), 93 | if (child != null) child, 94 | ], 95 | ), 96 | ); 97 | } 98 | 99 | void _onChanged(bool? value) { 100 | setState(() => _selected = value ?? false); 101 | 102 | // Notify the parent if it exists 103 | DeepCheckbox.of(context)?._fieldDidChange(_selected); 104 | 105 | // Update all registered childrens 106 | for (final checkboxState in _checkboxes) { 107 | checkboxState._onChanged(value); 108 | } 109 | } 110 | 111 | void _fieldDidChange(bool? value) { 112 | widget.onChanged?.call(value); 113 | 114 | final hasSelectedBox = 115 | _checkboxes.any((box) => (box._selected ?? false) == true); 116 | final hasNullBox = _checkboxes.any((box) => box._selected == null); 117 | final hasUnselectedBox = 118 | _checkboxes.any((box) => (box._selected ?? false) == false); 119 | 120 | final bool? selected; 121 | if (hasSelectedBox && !hasNullBox && !hasUnselectedBox) { 122 | selected = true; 123 | } else if (!hasSelectedBox && !hasNullBox && hasUnselectedBox) { 124 | selected = false; 125 | } else { 126 | selected = null; 127 | } 128 | 129 | setState(() { 130 | _selected = selected; 131 | ++_generation; 132 | }); 133 | 134 | // Notify the parent if it exists. 135 | DeepCheckbox.of(context)?._fieldDidChange(selected); 136 | } 137 | 138 | void _register(DeepCheckboxState checkbox) { 139 | _checkboxes.add(checkbox); 140 | } 141 | 142 | void _unregister(DeepCheckboxState checkbox) { 143 | _checkboxes.remove(checkbox); 144 | } 145 | } 146 | 147 | class _CheckboxScope extends InheritedWidget { 148 | final DeepCheckboxState _deepCheckboxState; 149 | 150 | /// Incremented every time a checkbox has changed. This lets us know when 151 | /// to rebuild. 152 | final int _generation; 153 | 154 | const _CheckboxScope({ 155 | Key? key, 156 | required Widget child, 157 | required DeepCheckboxState deepCheckboxState, 158 | required int generation, 159 | }) : _deepCheckboxState = deepCheckboxState, 160 | _generation = generation, 161 | super( 162 | key: key, 163 | child: child, 164 | ); 165 | 166 | @override 167 | bool updateShouldNotify(_CheckboxScope old) => _generation != old._generation; 168 | } 169 | -------------------------------------------------------------------------------- /source/go_router_uri.dart: -------------------------------------------------------------------------------- 1 | /// Allows you to navigate using `context.go` & `context.push` by passing a 2 | /// [Uri]. 3 | /// 4 | /// ```dart 5 | /// context.goUri(Uri(path: '/home', queryParameters: {'index': '1'})); 6 | /// ``` 7 | extension GoRouterUriExtension on BuildContext { 8 | void goUri(Uri uri, {Object? extra}) => go(uri.toString(), extra: extra); 9 | void pushUri(Uri uri, {Object? extra}) => push(uri.toString(), extra: extra); 10 | } 11 | 12 | /// Allows you to navigate to a specific route from a [Uri] object. 13 | /// 14 | /// ```dart 15 | /// Uri(path: '/home', queryParameters: {'index': '1'}).go(context); 16 | /// ``` 17 | extension GoRouterNavigationExtension on Uri { 18 | void go(BuildContext context, {Object? extra}) => context.goUri( 19 | this, 20 | extra: extra, 21 | ); 22 | 23 | void push(BuildContext context, {Object? extra}) => context.pushUri( 24 | this, 25 | extra: extra, 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /source/is_version_greater_than.dart: -------------------------------------------------------------------------------- 1 | /// Parse and compare [newVersion] with [currentVersion]. 2 | /// 3 | /// If [newVersion] is greater than [currentVersion], return `true`, otherwise 4 | /// return `false`. 5 | /// 6 | /// The expected format is `x.y.z` where `x`, `y` and `z` are integers. 7 | /// 8 | /// Example: 9 | /// 10 | /// ```dart 11 | /// String a = "1.39.2"; 12 | /// String b = "1.38.14"; 13 | /// 14 | /// isVersionGreaterThan(newVersion: a, currentVersion: b); // true 15 | /// ``` 16 | bool isVersionGreaterThan({ 17 | required String newVersion, 18 | required String currentVersion, 19 | }) { 20 | final _currentVersion = currentVersion.split('.'); 21 | final _newVersion = newVersion.split('.'); 22 | 23 | bool isGreater = false; 24 | for (int i = 0; i < 3; i++) { 25 | final newVersionI = int.parse(_newVersion[i]); 26 | final currentVersionI = int.parse(_currentVersion[i]); 27 | isGreater = newVersionI > currentVersionI; 28 | if (newVersionI != currentVersionI) break; 29 | } 30 | return isGreater; 31 | } 32 | 33 | // Test it 34 | void main() { 35 | test("should return false if version is inferior", () { 36 | expect( 37 | isVersionGreaterThan(newVersion: "1.0.0", currentVersion: "1.0.1"), 38 | false, 39 | ); 40 | }); 41 | 42 | test("should return false if version is equal", () { 43 | expect( 44 | isVersionGreaterThan(newVersion: "1.0.0", currentVersion: "1.0.0"), 45 | false, 46 | ); 47 | }); 48 | 49 | test("should return true if version is greater", () { 50 | expect( 51 | isVersionGreaterThan(newVersion: "1.0.1", currentVersion: "1.0.0"), 52 | true, 53 | ); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /source/json_object_model.dart: -------------------------------------------------------------------------------- 1 | import "dart:convert"; 2 | 3 | import "package:equatable/equatable.dart"; 4 | 5 | /// Extending this class will ensure that your class can be converted to a JSON 6 | /// object. 7 | abstract class JsonObjectModel extends Equatable { 8 | const JsonObjectModel(); 9 | 10 | /// Returns a [Map] representation of the object. 11 | Map toJson(); 12 | 13 | /// Returns a representation of [toJson] encoded as a [String] with 14 | /// [jsonEncode]. 15 | String toRawJson() => jsonEncode(toJson()); 16 | 17 | @override 18 | String toString() => toRawJson(); 19 | } 20 | 21 | /// This mixin provide a default implementation of [toJson], based on the 22 | /// [jsonKeys] property. 23 | mixin AutoJsonMixin on JsonObjectModel { 24 | /// Declare the keys used for the JSON. You must ensure that the length is the 25 | /// same as the [props]. 26 | /// 27 | /// The keys will be associated to the [props] value at the same index. 28 | Set get jsonKeys; 29 | 30 | @override 31 | Map toJson() { 32 | assert(jsonKeys.length == props.length); 33 | final data = {}; 34 | for (int i = 0; i < jsonKeys.length; i++) { 35 | final key = jsonKeys.elementAt(i); 36 | final value = props.elementAt(i); 37 | data[key] = value; 38 | } 39 | return data; 40 | } 41 | } 42 | 43 | /// Example 44 | 45 | const companyEntry = 'company'; 46 | const usernameEntry = 'username'; 47 | const passwordEntry = 'password'; 48 | const roleEntry = 'role'; 49 | const phoneEntry = 'phone'; 50 | const cellEntry = 'cell'; 51 | 52 | class MyUser extends JsonObjectModel { 53 | final String company; 54 | final String username; 55 | final String password; 56 | final String role; 57 | final String phone; 58 | final String cell; 59 | 60 | const MyUser({ 61 | required this.company, 62 | required this.username, 63 | required this.password, 64 | required this.role, 65 | required this.phone, 66 | required this.cell, 67 | }); 68 | 69 | factory MyUser.fromJson(Map json) { 70 | return MyUser( 71 | company: json[companyEntry] as String, 72 | username: json[usernameEntry] as String, 73 | password: json[passwordEntry] as String, 74 | role: json[roleEntry] as String, 75 | phone: json[phoneEntry] as String, 76 | cell: json[cellEntry] as String, 77 | ); 78 | } 79 | 80 | @override 81 | List get props => [ 82 | company, 83 | username, 84 | password, 85 | role, 86 | phone, 87 | cell, 88 | ]; 89 | 90 | @override 91 | Map toJson() { 92 | return { 93 | companyEntry: company, 94 | usernameEntry: username, 95 | passwordEntry: password, 96 | roleEntry: role, 97 | phoneEntry: phone, 98 | cellEntry: cell, 99 | }; 100 | } 101 | } 102 | 103 | class MyUser2 extends JsonObjectModel with AutoJsonMixin { 104 | final String company; 105 | final String username; 106 | final String password; 107 | final String role; 108 | final String phone; 109 | final String cell; 110 | 111 | const MyUser2({ 112 | required this.company, 113 | required this.username, 114 | required this.password, 115 | required this.role, 116 | required this.phone, 117 | required this.cell, 118 | }); 119 | 120 | @override 121 | Set get jsonKeys => { 122 | companyEntry, 123 | usernameEntry, 124 | passwordEntry, 125 | roleEntry, 126 | phoneEntry, 127 | cellEntry, 128 | }; 129 | 130 | @override 131 | List get props => [ 132 | company, 133 | username, 134 | password, 135 | role, 136 | phone, 137 | cell, 138 | ]; 139 | } 140 | -------------------------------------------------------------------------------- /source/json_try_decode.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | typedef JsonDecodeReviver = Object? Function(Object? key, Object? value); 4 | 5 | Object? jsonTryDecode(String source, {JsonDecodeReviver? reviver}) { 6 | try { 7 | return jsonDecode(source, reviver: reviver); 8 | } catch (e) { 9 | return null; 10 | } 11 | } 12 | 13 | // Test it 14 | void main() { 15 | test("should return null if invalid source", () { 16 | expect(jsonTryDecode(""), isNull); 17 | }); 18 | 19 | test("should return a valid JSON if source is correct", () { 20 | expect(jsonTryDecode('{"a":1}'), {"a": 1}); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /source/optional.dart: -------------------------------------------------------------------------------- 1 | /// Dart implementation of Swift's `Optional` type. 2 | /// 3 | /// {@template optional} 4 | /// Creates an object that holds a nullable value which can be unwrapped 5 | /// safely. 6 | /// {@endtemplate} 7 | /// 8 | /// Swift specs: https://developer.apple.com/documentation/swift/optional 9 | final class Optional { 10 | /// {@macro optional} 11 | const Optional(this.value); 12 | 13 | /// Creates an [Optional] object that holds a non-null value. 14 | const Optional.some(Wrapped this.value); 15 | 16 | /// Creates an [Optional] object that holds a null value. 17 | const Optional.none() : value = null; 18 | 19 | final Wrapped? value; 20 | 21 | /// {@template optional_map} 22 | /// Evaluates the given closure when this [Optional] instance is not null, 23 | /// passing the unwrapped [value] as a parameter. 24 | /// {@endtemplate} 25 | U? map(U Function(Wrapped value) cb) { 26 | return switch (value) { 27 | final value? => cb(value), 28 | null => null, 29 | }; 30 | } 31 | 32 | /// {@macro optional_map} 33 | Optional flatMap( 34 | Optional Function(Wrapped value) cb, 35 | ) { 36 | return switch (value) { 37 | final value? => cb(value), 38 | null => const Optional.none(), 39 | }; 40 | } 41 | 42 | @override 43 | bool operator ==(Object other) => 44 | other is Optional && other.value == value; 45 | 46 | @override 47 | int get hashCode => Object.hash(runtimeType, value); 48 | } 49 | -------------------------------------------------------------------------------- /source/override_asset_bundle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | class MyApp extends StatelessWidget { 8 | const MyApp({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return DefaultAssetBundle( 13 | bundle: FallbackAssetBundle(fallbackKey: 'assets/error.png'), 14 | child: TestWidget(), 15 | ); 16 | } 17 | } 18 | 19 | /// Used to load a fallback asset when the main asset is not found. 20 | class FallbackAssetBundle extends CachingAssetBundle { 21 | final String? fallbackKey; 22 | 23 | FallbackAssetBundle({required this.fallbackKey}); 24 | 25 | Future _loadAsset(String key) async { 26 | final Uint8List encoded = 27 | utf8.encoder.convert(Uri(path: Uri.encodeFull(key)).path); 28 | final ByteData? asset = await ServicesBinding 29 | .instance.defaultBinaryMessenger 30 | .send('flutter/assets', encoded.buffer.asByteData()); 31 | return asset; 32 | } 33 | 34 | @override 35 | Future load(String key) async { 36 | final asset = await _loadAsset(key); 37 | if (asset != null && asset.lengthInBytes > 0) return asset; 38 | 39 | if (fallbackKey != null) { 40 | final fallbackAsset = await _loadAsset(fallbackKey!); 41 | if (fallbackAsset != null) return fallbackAsset; 42 | } 43 | 44 | throw FlutterError('FallbackAssetBundle - Unable to load asset: $key'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/price_format.dart: -------------------------------------------------------------------------------- 1 | extension PriceFormatExt on double { 2 | /// Return the value formatted as would be a price with spaces. 3 | /// 4 | /// If the [currency] parameter is used, it will be used as the currency 5 | /// symbol. 6 | /// 7 | /// ## Example 8 | /// 9 | /// ```dart 10 | /// 3700440.0.toPriceValue(); // 3 700 440,00 11 | /// 3700440.0.toPriceValue(currency: '€'); // 3 700 440,00 € 12 | /// ``` 13 | String toPriceValue({String? currency}) { 14 | final base = toStringAsFixed(2); 15 | final parts = base.split('.'); 16 | final intPart = parts[0]; 17 | final decimalPart = parts[1]; 18 | final buffer = StringBuffer(); 19 | final intPartContent = []; 20 | final reversedIntPart = intPart.split('').reversed.toList(); 21 | for (int i = 0; i < reversedIntPart.length; i++) { 22 | final char = reversedIntPart[i]; 23 | if (i % 3 == 0) { 24 | intPartContent.add(' '); 25 | } 26 | intPartContent.add(char); 27 | } 28 | buffer 29 | ..write(intPartContent.reversed.join().trim()) 30 | ..write(',$decimalPart'); 31 | 32 | if (currency != null) { 33 | buffer.write(' $currency'); 34 | } 35 | 36 | return buffer.toString(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/pump_app_tester.dart: -------------------------------------------------------------------------------- 1 | extension PumpApp on WidgetTester { 2 | /// Call [pumpWidget] with the given [child] wrapped inside a [MaterialApp]. 3 | Future pumpApp(Widget child) { 4 | return pumpWidget(MaterialApp(home: child)); 5 | } 6 | 7 | /// Call [pumpWidget] with the given [child] wrapped inside a [Localizations] 8 | /// widget using the given [locale]. 9 | Future pumpLocalized(Widget child, Locale locale) { 10 | return pumpWidget( 11 | Localizations( 12 | locale: locale, 13 | delegates: const [ 14 | GlobalMaterialLocalizations.delegate, 15 | GlobalWidgetsLocalizations.delegate, 16 | GlobalCupertinoLocalizations.delegate, 17 | ], 18 | child: child, 19 | ), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /source/responsive.dart: -------------------------------------------------------------------------------- 1 | extension Responsive on BuildContext { 2 | T responsive( 3 | T defaultValue, { 4 | T? sm, 5 | T? md, 6 | T? lg, 7 | T? xl, 8 | }) { 9 | final w = MediaQuery.of(this).size.width; 10 | return w >= 1200 11 | ? (xl ?? lg ?? md ?? sm ?? defaultValue) 12 | : w >= 1024 13 | ? (lg ?? md ?? sm ?? defaultValue) 14 | : w >= 768 15 | ? (md ?? sm ?? defaultValue) 16 | : w >= 640 17 | ? (sm ?? defaultValue) 18 | : defaultValue; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /source/safe_complete.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | extension CompleterExtension on Completer { 4 | /// Ensure that the completer is not already completed before calling 5 | /// [complete] with the given [value]. 6 | void safeComplete(FutureOr value) { 7 | if (isCompleted) return; 8 | complete(value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /source/safe_id.dart: -------------------------------------------------------------------------------- 1 | extension type ID._(String _value) implements String { 2 | const ID(String value) : this._value = value; 3 | } 4 | -------------------------------------------------------------------------------- /source/separated_with.dart: -------------------------------------------------------------------------------- 1 | extension SeparatedWith on Iterable { 2 | /// Return a new iterable with the elements of this iterable separated by the 3 | /// given [separator]. 4 | /// 5 | /// Example: 6 | /// ```dart 7 | /// final list = [1, 2, 3]; 8 | /// final result = list.separatedWith(0); 9 | /// print(result.toList()); // [1, 0, 2, 0, 3] 10 | /// ``` 11 | /// 12 | /// Especially useful to create separated columns or rows in Flutter: 13 | /// ```dart 14 | /// final list = ['Hello', 'World']; 15 | /// Column( 16 | /// children: [ 17 | /// ...list.separatedWith(const SizedBox(height: 10)), 18 | /// ], 19 | /// ); 20 | /// ``` 21 | Iterable separatedWith(T separator) sync* { 22 | for (int i = 0; i < length; i++) { 23 | final elem = elementAt(i); 24 | yield elem; 25 | 26 | if (i < length - 1) { 27 | yield separator; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /source/sliver_separated_child_builder_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class SliverSeparatedChildBuilderDelegate extends SliverChildBuilderDelegate { 6 | SliverSeparatedChildBuilderDelegate({ 7 | required NullableIndexedWidgetBuilder builder, 8 | required IndexedWidgetBuilder separatorBuilder, 9 | required int childCount, 10 | }) : super( 11 | (context, index) => _separatedBuilder( 12 | context, 13 | index, 14 | builder, 15 | separatorBuilder, 16 | ), 17 | childCount: math.max(0, childCount * 2 - 1), 18 | semanticIndexCallback: _separatedSemanticIndexCallback, 19 | ); 20 | 21 | static int? _separatedSemanticIndexCallback(Widget _, int localIndex) { 22 | if (localIndex.isEven) { 23 | return localIndex ~/ 2; 24 | } 25 | return null; 26 | } 27 | 28 | static Widget? _separatedBuilder( 29 | BuildContext context, 30 | int index, 31 | NullableIndexedWidgetBuilder builder, 32 | IndexedWidgetBuilder separatorBuilder, 33 | ) { 34 | final int itemIndex = index ~/ 2; 35 | if (index.isEven) { 36 | return builder(context, itemIndex); 37 | } 38 | return separatorBuilder(context, itemIndex); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /source/stacked_gradients.dart: -------------------------------------------------------------------------------- 1 | /// Returns a [Stack] widget with multiple gradients overlapped in the 2 | /// background. 3 | class StackedGradients extends StatelessWidget { 4 | final Widget? child; 5 | final List gradients; 6 | 7 | const StackedGradients({ 8 | Key? key, 9 | this.child, 10 | required this.gradients, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Stack( 16 | children: [ 17 | for (final e in gradients) 18 | Positioned.fill( 19 | child: DecoratedBox(decoration: BoxDecoration(gradient: e)), 20 | ), 21 | if (child != null) child!, 22 | ], 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /source/stream_converter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | typedef StreamConverterCallback = S Function(T value); 4 | 5 | extension StreamConverterExtension on Stream { 6 | /// Convert the stream by applying the given [handler] to each value 7 | /// received and convert it to a new type. 8 | Stream convertTo(StreamConverterCallback handler) { 9 | final transformer = StreamTransformer.fromHandlers( 10 | handleData: (T data, Sink sink) { 11 | final value = handler(data); 12 | sink.add(value); 13 | }, 14 | ); 15 | return transform(transformer); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /source/test_with.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | typedef TestWithCallback = FutureOr Function(T arg); 7 | 8 | @isTest 9 | void testWith(String name, TestWithCallback testFn, Iterable args) { 10 | for (final arg in args) { 11 | test('_ with $arg', () => testFn(arg)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/translated_locale_name.dart: -------------------------------------------------------------------------------- 1 | extension TranslateLocaleExtension on Locale { 2 | /// Returns the locale name in its corresponding language. 3 | String translatedLocaleName() { 4 | switch (toLanguageTag()) { 5 | case 'de': 6 | case 'de-DE': 7 | return 'Deutsh'; 8 | case 'en': 9 | case 'en-US': 10 | return 'English'; 11 | case 'es': 12 | case 'es-ES': 13 | return 'Español'; 14 | case 'fr': 15 | case 'fr-FR': 16 | return 'Français'; 17 | case 'it': 18 | case 'it-IT': 19 | return 'Italiano'; 20 | case 'ja': 21 | case 'ja-JP': 22 | return '日本語'; 23 | case 'ko': 24 | case 'ko-KR': 25 | return '한국어'; 26 | case 'pt': 27 | case 'pt-BR': 28 | return 'Português'; 29 | case 'ru': 30 | case 'ru-RU': 31 | return 'Русский'; 32 | case 'zh': 33 | case 'zh-CN': 34 | return '简体中文'; 35 | case 'zh-TW': 36 | return '繁體中文'; 37 | default: 38 | debugPrint('Locale $this not found.'); 39 | return 'N/A'; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/unwrap.dart: -------------------------------------------------------------------------------- 1 | typedef UnwrapSuccess = ResultType Function( 2 | T value, 3 | ); 4 | typedef UnwrapFailed = ResultType Function(); 5 | 6 | extension Unwrap on T? { 7 | /// Unwrap the value if it is not null. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// final String? value = 'Hello'; 12 | /// value.unwrap( 13 | /// onNotNull: (value) => print(value), 14 | /// orElse: () => print('Value is null'), 15 | /// ); // Will print 'Hello' 16 | /// ``` 17 | ResultType? unwrap({ 18 | required UnwrapSuccess onNotNull, 19 | UnwrapFailed? orElse, 20 | }) { 21 | if (this case final nonNull?) { 22 | return onNotNull(nonNull); 23 | } 24 | return orElse?.call(); 25 | } 26 | } 27 | --------------------------------------------------------------------------------