Last updated:
0 purchases
micha core
Extensions, widgets and other utilities that are missing in Flutter's SDK.
Features #
This package includes some extensions and widgets to help avoid imperative boilerplate by leaning on Flutter's declarative UI approach.
Other than that, the features in this package are mostly unrelated to each other.
It is just a collection of useful things that otherwise require boilerplate, like accessing BuildContext, and are typically needed in many projects.
Usage #
Separate items of collections #
Need to add some items between existing items of a List or Iterable? Use collection.separated(separator):
[1, 2, 3].separated(0); // [1, 0, 2, 0, 3]
copied to clipboard
Are these separators dependent on preceding and succeeding items? Use collection.separatedBy((before, after) => separator):
[1, 2, 3].separatedBy((before, after) => before + after); // [1, 3, 2, 5, 3]
copied to clipboard
Split items of collections #
In dart, it is trivial to split a string by any pattern, like this:
"abc".split("b"); // ["a", "b"]
copied to clipboard
This functionality didn't exist for arbitrary lists and Iterables.
It has been added as an extension and is used like this:
[1, 2, 3, 4].split((item) => item == 2); // [[1], [3, 4]]
[1, 2, 3, 4].splitIndexed((index, item) => index == 2); // [[1, 2], [4]]
copied to clipboard
Wrapper for Optional parameters #
Ever needed to differentiate the value of a nullable parameter that was purposefully passed as null from a value that was simply omitted?
For example copyWith should replace exactly those parameters that were passed. But how can it handle nullable parameters?
// status-quo, bad example
Foo copyWith({
double? foo,
}) {
return Foo(
// The caller cannot raplce a non-null value of foo by null.
foo: foo ?? this.foo,
);
}
copied to clipboard
Flutter doesn't actually handle this case properly in their own classes, but we can do better by using the Wrapper type from this package:
Foo copyWith({
Wrapper<double?>? foo,
}) {
return Foo(
// Applies any wrapped value of foo (including null).
// Omit foo entirely and the old value is kept.
foo: foo == null ? this.foo : foo.value,
);
}
copied to clipboard
Any value can be wrapped by calling the wrapped getter on it:
fooInstance.copyWith(foo: 1.5.wrapped);
copied to clipboard
Map or "transform" individual values #
We sometimes need to use ternary operators to deal with nullable types, which is needlessly imperative and can lead to undesired method calls:
children: [
foo() == null ? null : Text(foo()),
]
copied to clipboard
Note that foo may be called twice in this example.
We can instead use a more functional approach: transform can be called on any value and behaves similarly to the map function on iterables:
children: [
foo()?.transform((value) => Text(value)),
]
copied to clipboard
Convert empty Iterable/Map to null #
There is an extension on any Iterable or Map to convert them to null when they're empty:
final List<int> itMaybeEmpty = [];
final List<int>? itMaybeNull = itMaybeEmpty.nullWhenEmpty;
assert(itMaybeNull == null);
final Map<String, int> mapMaybeEmpty = {};
final Map<String, int>? mapMaybeNull = mapMaybeEmpty.nullWhenEmpty;
assert(mapMaybeNull == null);
final String strMaybeEmpty = "";
final String? strMaybeNull = strMaybeEmpty.nullWhenEmpty;
assert(strMaybeNull == null);
copied to clipboard
Calling nullWhenEmpty is equivalent to the following transform:
final maybeNull = maybeEmpty.transform((it) => it.isEmpty ? null : it);
copied to clipboard
Get a non-null value from a Map with "getOrPut" #
Getting a value from a Map returns a nullable value:
final fooMap = {
'foo': 1,
};
final foo = fooMap['foo']; // `foo` has type `int?` instead of `int`
assert(foo == 1);
final fee = fooMap['fee'];
assert(fee == null);
copied to clipboard
There are times where you may instead want to get a default value that is also inserted into the Map immediately. Use getOrPut for this:
final fooMap = {
'foo': 1,
};
final foo = fooMap.getOrPut('foo', () => 0); // `foo` has type `int`
assert(foo == 1);
final fee = fooMap.getOrPut('fee', () => 0);
assert(fee == 0);
copied to clipboard
Find enum values "byNameOrNull" #
In dart, you can find enum values by their names, like:
enum TestEnum { one, two, three }
TestEnum.values.byName('two');
copied to clipboard
But what if that value does not exist? Dart will throw an ArgumentError.
Use byNameOrNull to receive null instead:
TestEnum.values.byNameOrNull('four');
copied to clipboard
waiting for a condition to be fulfilled #
waitFor repeatedly calls and waits until a given condition returns true.
It needs to be called with await in a function marked as async:
bool condition() {
// ...
}
void foo() async {
await waitFor(
condition,
timeout: const Duration(seconds: 10),
interval: const Duration(milliseconds: 100),
);
}
copied to clipboard
The caller can pass a timeout to set a maximum time to wait, after which a TimeoutException will be thrown. There is no timeout by default.
The caller can also override the default interval of 200 milliseconds, which is the idle duration between checking condition.
retrying operations #
The retried function retries a given operation until it succeeds and forwards the return value.
A call is attempted at least once and at most maxAttemptCount times.
The operation is retried after throwing an exception of type TException or after any exception, if no type is specified.
Use shouldRetry to further restrict exceptions to retry for.
Uses exponential backoff by default, but a different strategy can be used.
A RetryException Can be thrown with a custom delay to override the strategy.
final response = await retried<int, RetryException>(
() async {
final response = makeHttpCall();
if (response.status == 429) {
throw RetryException(
'Too Many Requests',
delay: Duration(seconds: int.parse(response.headers['Retry-After'])),
);
}
return response;
},
);
copied to clipboard
rate limiting #
The RateLimiter class enforces a minimum time interval between consecutive executions of any given operation.
This is useful in scenarios where you want to throttle the execution of tasks to avoid overloading a system or API.
final rateLimiter = RateLimiter<int>(Duration(seconds: 1)); // sets the interval at which operations can be called
await rateLimiter.execute(() async => 42);
await rateLimiter.execute(() async => 42); // waits for the duration of the interval
copied to clipboard
Logging #
Quickly setup colored log printing for your terminal by calling initLogging(). This will also make the log level of your project independent of the log level of its dependencies.
void main() {
initLogging({
// these are the default levels
projectLogLevel: Level.ALL,
dependenciesLogLevel: Level.WARNING,
});
runApp(const App());
}
copied to clipboard
Then create and use specific loggers for any type:
class MyClass {
static final logger = createLogger(MyClass);
...
void logSomething {
logger.info('something');
}
}
copied to clipboard
Log messages will be formatted like INFO 2023-11-26T20:53:56.712849 MyClass: something.
ANSI String formatting #
You can print colored or otherwise formatted Strings to consoles by using getter extension methods for ANSI control sequences:
logger.info('foo'.red.bold.italic.underline);
copied to clipboard
Use resetAll to reset any prior styles.
Use hidden to make a String invisible.
Use bold, dim, italic, underlined, overlined, struckThrough for the respective formatting.
Use black, red, green, yellow, blue, magenta, cyan and white to apply the respective foreground color.
Use bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan and bgWhite to apply the respective background color.
Use inverted to swap foreground and background colors.
Bright colors are not included, because they are not supported by all terminals.
Gap #
We often require some small space between widgets. Flutter's widget catalog has limited options:
Flutter's Spacer takes up all available space, which is often more than we want.
Flutter's Padding unfortunately needs to be wrapped around widgets, adding boilerplate, while also being hard to read inside of lists.
The best that Flutter offers is a SizedBox with a specified width or height.
However, this still has some problems: We need to set either width or height, depending on whether the SizedBox is placed inside a Row or Column.
We also need to repeat the same constant pixel size all throughout the application.
Instead, consider using a Gap:
Column(
children: [
Text('first'),
Gap(),
Text('second'),
],
);
copied to clipboard
Gap takes up a fixed space in both directions.
It takes 16 pixels by default, but can be configured using the GapThemeData theme extension or its constructor parameters.
When you need a little more or less space relative to the configured theme, create a Gap with a scale factor, like Gap(scale: 2) or Gap(scale: 0.5) respectivly.
You can also add to, subtract from, multiply or devide a Gap to scale it, e.g. Gap() * 2.
There is also a package named "Gap", which works in a similar way, but has a few more features, which I personally don't need.
ThemedText #
Avoid accessing ThemeData manually to set a themed textStyle. Use a ThemedText widget instead:
ThemedText.headlineMedium('Some headline');
copied to clipboard
AsyncBuilder #
Using Flutter's FutureBuilder requires a lot of imperative boilerplate.
You need to explicitly catch loading, error and no-data states while also keeping the Future in state.
If you do this: FutureBuilder(future: load(), builder: ...);, then FutureBuilder will call load() whenever your widget rebuilds.
As an alternative, AsyncBuilder offers a declarative API:
AsyncBuilder(
createFuture: (context) => Future.delayed(
const Duration(seconds: 1),
() => 'some data',
),
builder: (context, data) => Text(data),
);
copied to clipboard
AsyncBuilder only reloads when its key changes.
You can also customize initialData and change the look of loading, no-data and error states, which each have sensible defaults.
Use AsyncBuilder.asset to avoid explicitly getting the DefaultAssetBundle from BuildContext:
AsyncBuilder.asset(
(assetBundle) => assetBundle.loadString('assets/file.txt'),
builder: (context, data) => Text(data),
);
copied to clipboard
Spinner #
Spinner is essentially a slightly improved CircularProgressIndicator.
It is always centered, can be given a fixed size, its strokeWidth is a bit more narrow and it all can be themed using SpinnerThemeData.
It is also the default loading indicator used by AsyncBuilder.
Link #
Flutter comes with some clickable and tappable widgets, but none that look like a regular HTML <a> tag.
The Link widget is just that. It performs an action when tapped and makes its child: Text look like an HTML link with an underline and that classic blue color. This style can be customized through constructor parameters or by using the LinkThemeData theme extension.
Link(
onTap: () {
// do something
},
child: const Text('Click me'),
),
copied to clipboard
Pagination #
Need improved performance when displaying many elements on screen at once? Try Pagination:
Pagination(
maxPageSize: 20,
// getPage returns a `Paginated` instance with `totalItemCount` and generic `items`
getPage: (int pageIndex) => ...
builder: (context, items) => ListView(
children: [
for (final item in items)
ListTile(
title: Text(item),
),
],
),
),
copied to clipboard
Pagination calls getPage, handles the Future and displays numbered controls below the return value of builder, which can be customized with PaginationThemeData. If needed, disable controls with showControls: false and take external control using a PaginationController.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.