6 Non-obvious (open-source) Flutter and Dart Packages

LeanCode
11 min readNov 30, 2022

--

Author: Bartek Pacia/LeanCode

Last article update: October 2023

There are tens of thousands of packages on the main site of Dart language — pub.dev, and many are released and updated daily. Novice developers sometimes feel guilty about using a package rather than coding what it provides, but there’s nothing to worry about.

Almost every production-grade app depends on tens of packages. Using packages lets us quickly implement standard functionalities and focus more on what makes every app unique.

In apps by LeanCode, we like to keep the number of packages we depend on to a sensible minimum because every new dependency may be another possible point of failure. But we also like experimenting with new packages, and when we find one that is reliable and makes our work easier — we use it!

In this article, we look closely at 6 small, useful, but not-so-popular Dart and Flutter packages. Moreover, all of them are made and tested by LeanCode’s developers, so we are sure you can rely on them.

Each package usually started as a class or two in a project, and once they proved helpful, they were copied over to new projects. Eventually, someone with an open-source mindset extracted them to a separate repository, added documentation, and shared them with the world!

Our top Flutter and Dart packages

Package comms

The comms package is a type-safe and more flexible alternative to the event_bus package.

Problem

Very often, we need to communicate between two components of an application. For example, we might have two blocs, with the first one being above the second one.

└── App
└── FirstBloc
└── SecondBloc

Facilitating unidirectional communication from `FirstBloc` to `SecondBloc` is easy to do — just pass `FirstBloc` to `SecondBloc`s constructor. But what if our case is more complex, and we need bidirectional communication between these 2 blocs?

We used the event_bus package to do this for some time. `event_bus` claims to reduce coupling between application components.

In our experience, unfortunately, using `event_bus` doesn’t help to solve this situation. Coupling is still there, but the dependency becomes hidden, which is actually even worse.

We needed something better, and here enters the `comms` package!

Solution

The `comms` package is really simple. It has only 2 structures: a `Sender` mixin and a `Listener` mixin. Once you mix one of them in your class, you’ll give it the ability to send and receive messages.

Let’s assume that you have a `FooWorker` class, which crunches some numbers, and the result of that number crunching is a `Uint8List`, wrapped in some `FooData` class:

class FooData {
FooData(this.data, this.generatedAt);

final Uint8List data;
final DateTime generatedAt;
}

class FooWorker {
Future<FooData> doWork() async {
// do some work
}
}

Now imagine that there’s also some `FooConsumer`, which is interested in getting that data.

class FooConsumer {
// wants to perform some action when FooData from Worker becomes available
}

Of course, there are myriad ways to accomplish this, but I will do this with `comms` since that’s what we’re talking about. You only have to do 2 things!

First, let’s mix in `Sender`on our `FooWorker` and use the `send` method that `Sender` mixin provides to propagate our data:

class FooWorker with Sender<FooData> {
Future<FooData> doWork() async {
// do some work

final data = Uint8List.fromList([1, 2, 3];
final now = DateTime.now();
send(FooData(data, now));
}
}

The second and last step is to mix in `Listener`on our `FooConsumer` :

class FooConsumer with Listener<FooData> {
@override
void onMessage(FooData message) {
// do whatever you want with the message once it becomes available
}
}

And that’s all! Whenever someone calls `doWork()` on `FooWorker`, the data will be propagated to every class that is mixed with `Listener<FooData>`.

A nice property of `comms` is that senders and listeners don’t know anything about each other. This makes it easy to find places that can send or receive objects of a specific type and makes the whole app less coupled. And we maintain strong typing — no more `dynamics`. Just awesome!

It’s also worth noting that the `comms` package doesn’t depend on Flutter — you’re free to use it in your command-line tool or a backend service based on, for example, dart_frog.

And if you are indeed using Flutter, then there’s the `flutter_comms` package that extends `comms` with Flutter-specific functionality.

Package dispose_scope

The dispose_scope package helps you keep track of your resources and dispose of them when they’re no longer needed.

Problem

Let’s say that you’re writing a command-line app in Dart and starting some subprocesses with the `dart:io` library.

Future<void> main() async {
final proc = await Process.start('adb', ['forward', 'tcp:8081', 'tcp:8081']);
final file = await File('config.yaml').open();

// doing useful things
}

It’s always a good practice to clean up after ourselves by closing the opened file descriptors. Also, subprocesses don’t die when their parent dies, so we also take care of that.

Future<void> main() async {
final proc = await Process.start('adb', ['forward', 'tcp:8081', 'tcp:8081']);
final file = await File('config.yaml').open();

// doing useful things

proc.kill();
await file.close();
}

The above code is fine, but only because our example is really tiny! Now imagine that it’s a real CLI app that consists of a few dozen Dart files, has a few layers of subcommands, starts subprocesses at different times, and acquires many other resources. Suddenly, disposing of what we acquire becomes annoying and fragile.

Solution

That’s where the `dispose_scope` package comes into the picture. It makes it possible to acquire the resource and register it for disposal in the same place, thus freeing you from remembering about this. With the help of `dispose_scope`, the above example could be rewritten to:

import 'package:dispose_scope/dispose_scope.dart';

Future<void> main() async {
final disposeScope = DisposeScope();

final proc = await Process.start('adb', ['forward', 'tcp:8081', 'tcp:8081']);
disposeScope.addDispose(() async => proc.kill());

final file = await File('config.yaml').open();
disposeScope.addDispose(() async => file.close());

// doing useful things

await disposeScope.dispose();
}

Wait, what has just happened? Let’s go over the new lines step by step.

First, we’re creating our dispose scope object.

final disposeScope = DisposeScope();

Right after starting a subprocess, we’re telling our dispose scope how to clean up the process. We do this by creating an anonymous function that, once called, will kill the subprocess.

disposeScope.addDispose(() async => proc.kill());

Then we open a file descriptor and tell the dispose scope again how to dispose of our resource (the file descriptor) when we tell it to.

final file = await File('config.yaml').open();
disposeScope.addDispose(() async => file.close());So far, we’ve added 2 callbacks to the dispose scope.

When we’re done with the important stuff and want to free the resources, we have to call `dispose()` on the dispose scope. This method calls the callbacks we’ve earlier added with `addDispose`. The resources get disposed of, and that’s it!

await disposeScope.dispose();

This was just a simple example, but I hope that you can see how it can be useful in bigger projects. Dispose scope is a very simple yet flexible solution.

It’s good to know that you can have child dispose scopes, which can get disposed by some parent dispose scope. That’s a useful feature when your project gets bigger and more complex.

If you are using Flutter, then there’s flutter_dispose_scope, which comes with a bunch of Flutter-specific goodies. There’s also bloc_dispose_scope, which seamlessly integrates `DisposeScope` with `Bloc`s and `Cubit`s.

Package bloc_presentation

The bloc_presentation package extends `Bloc`s and `Cubit`s with a separate stream for one-off events.

Problem

We love the `bloc` package. At LeanCode, it’s the standard state management library you’ll find in all our apps. It’s simple by default and powerful when needed.

Unfortunately, we found out that sometimes `Event`s and `State`s are not enough to represent the app’s behavior. We were not alone in this. Sometimes, we’d like to fire a one-off event showing a toast or snackbar. Since the result of such an event is only visible for a few seconds, we don’t want to store it in the bloc’s state — it feels wrong.

Our solution to this is the bloc_presentation package.

Solution

`bloc_presentation` provides the `BlocPresentationMixin`, which you mix in on your `Cubit` or `Bloc` to add a stream to it — the presentation stream. The presentation stream is used to dispatch one-off events we talked about earlier.

Imagine that we’re building out a login form and want to show a snackbar when the user enters the wrong credentials or when they don’t have internet access.

Let’s assume that we already have a simple `SignInCubit`:

class SignInCubit extends Cubit<SignInState> {
SignInCubit() : super(SignInState.initial());


Future<void> signIn() async {
// implementation
}
}

First, we’re defining our presentation events:

sealed class AuthPresentationEvent {}

class InvalidCredentials implements BlocPresentationEvent {}

class NoConnection implements BlocPresentationEvent {}

Then, we have to mix `BlocPresentationMixin` with our cubit:

class SignInCubit extends Cubit<SignInState>
with BlocPresentationMixin<SignInState, AuthPresentationEvent> {
// implementation
}

Now we can use the `emitPresentation()` method in our `SignInCubit`:

Future<void> signIn() async {
try {
// authenticate
} on InvalidCredentialsException catch (err) {
emitPresentation(InvalidCredentials());
} on SocketException catch (err) {
emitPresentation(NoConnection());
}
}

We’re done with the business part. Now onto the fun things: let’s update the UI. Similar to how there’s `BlocListener`, there’s also `BlocPresentationListener`:

// in the sign in screen widget

@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocPresentationListener<SignInCubit, AuthPresentationEvent>(
listener: (context, event) => switch (event) {
InvalidCredentials _ => ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
const SnackBar(content: Text('Invalid credentials')),
),
NoConnection _ => ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
const SnackBar(content: Text('No internet connection')),
),
},
child: LoginForm(),
),
);
}

Package dashed_line

The dashed_line package lets you easily draw dashed lines.

Problem

No surprise here: in one of the projects, we had to draw a dashed line, and we haven’t found any good packages for doing that. Then we wrote our own!

Solution

The `dashed_line` package provides the `DashedLine` widget. You give it a path, and `DashedLine` draws it but dashed. You can also customize a few aspects of the dashed line, such as the segment’s length, the gap between segments, or the cap style.

Let’s see some code. Here’s how you’d draw a very simple dashed line:

// in some widget

@override
Widget build(BuildContext context) {
final path = Path()..cubicTo(-40, 53, 14, 86, 61, 102);

return Scaffold(
appBar: AppBar(
title: const Text('dashed_line package'),
),
body: DashedLine(
path: path,
color: Colors.black,
),
);
}

Here’s how it’d look like:

@override
Widget build(BuildContext context) {
final path = Path()..cubicTo(-40, 53, 14, 86, 61, 102);

return Scaffold(
appBar: AppBar(
title: const Text('dashed_line package'),
),
body: DashedLine(
path: path,
width: 20,
dashSpace: 20,
dashCap: StrokeCap.round,
color: Colors.blue,
),
);
}

And that’s it! “Do one thing and do it well” in its purest form — peak Unix philosophy.

By the way, if you’re wondering where we got the path values from: it’s just some pretty nice math. If you’d like to learn more about that math, do check out this great video.

Package sorted

The sorted package makes advanced sorting simple.

Problem

Sorting is a very common operation. Dart, being the convenient language it is, provides us with the `sort()` method that works for simple cases, like a list of strings:

void main() {
final names = ['Jane', 'Ann', 'John', 'Bart', 'John', 'Zoe'];
names.sort();
print(names); // [Ann, Bart, Jane, John, John, Zoe]
}

What if our list of names was a list of people, and we wanted to sort the people by their last names? We should use the `sortBy()` method provided by the collection package.

class Person {
const Person(this.firstName, this.lastName);

final String firstName;
final String lastName;

@override
String toString() => '$firstName $lastName';
}

void main() {
final people = [
Person('Jane', 'Appleseed'),
Person('Ann', 'Doe'),
Person('John', 'Appleseed'),
Person('Bart', 'Appleseed'),
Person('John', 'Doe'),
Person('Zoe', 'Dashbird'),
];

people.sortBy((person) => person.lastName);
print(people); // [Jane Appleseed, John Appleseed, Bart Appleseed, Zoe Dashbird, Ann Doe, John Doe]
}

Now, let’s complicate things even more — we want to sort the list by the person’s last name and then sort by the first name. This isn’t easily achievable, even with the `collection` package.

Solution

`sorted` is a small library that provides a single extension method — `sorted()`. It accepts your iterable and an arbitrary number of `SortedRule`s, which makes it very easy to express complex sorting rules.

That’s how we could sort the `people` list by the person’s last name and then by the person’s last name with the `sorted` package:

final people = [
Person('Jane', 'Appleseed'),
Person('Ann', 'Doe'),
Person('John', 'Appleseed'),
Person('Bart', 'Appleseed'),
Person('John', 'Doe'),
Person('Zoe', 'Dashbird'),
];

final sortedPeople = people.sorted(
[
SortedComparable<Person, String>((person) => person.lastName),
SortedComparable<Person, String>((person) => person.firstName),
],
);

print(sortedPeople);

The `sorted` package can handle much more than we’ve shown with this simple example, so do check it out!

Package patrol_finders

The patrol_finders package makes it easy to write clean and robust widget tests.

You’ve probably heard about Patrol — an open-source UI testing framework made by LeanCode.

Patrol shines when doing integration testing, but in smaller projects, there’s often no time to write them and, let’s face it, no money to execute them regularly on CI. An interesting alternative in such a case can be writing widget tests — to test only a critical part of the app’s UI, with external dependencies mocked.

If you’re only writing widget tests, you can still use the default patrol plugin, but did you know that there’s a leaner, more lightweight alternative? Welcome to the patrol_finders package!

Problem

We all know and love the default `flutter_test` package, but its API is a bit verbose. Let’s see a sample login screen test:

testWidgets('signs up', (WidgetTester tester) async {
await tester.pumpWidget(ExampleApp());
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(Key('emailTextField')),
'charlie@root.me',
);
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(Key('nameTextField')),
'Charlie',
);
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(Key('passwordTextField')),
'ny4ncat',
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(Key('termsCheckbox')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(Key('signUpButton')));
await tester.pumpAndSettle();
expect(find.text('Welcome, Charlie!'), findsOneWidget);
});

This is a really simple test, but it’s not that short. It’s also annoying to have to write all those `pumpAndSettle()`s manually.

Solution

Patrol’s custom finders enable you to quickly write shorter tests that read almost like an English sentence:

import 'package:patrol_finders/patrol_finders.dart';

patrolWidgetTest('signs up', (PatrolTester $) async {
await $.pumpWidgetAndSettle(ExampleApp());
await $(#emailTextField).enterText('charlie@root.me');
await $(#nameTextField).enterText('Charlie');
await $(#passwordTextField).enterText('ny4ncat');
await $(#termsCheckbox).tap();
await $(#signUpButton).tap();
await $('Welcome, Charlie!').waitUntilVisible();
});

Custom finders were an integral part of the `patrol` plugin in the past, but since its release, we’ve received a lot of feedback from our users. One of the most requested changes came from the users who weren’t using Patrol’s native automation feature — because they weren’t writing integration tests. They were asking us to separate custom finders into a separate package so they could use just them without pulling the native dependencies of the full `patrol` plugin — and that’s how `patrol_finders` was born.

Summing up

So that’s it! We hope you liked our 6 non-obvious Dart and Flutter packages. We wanted to share them with you because we think it might improve your work on mobile app development. We encourage you to give them a try in your projects as well. All of the packages we mentioned are open-source and actively maintained since we use them in LeanCode’s Flutter projects. We’d love to hear from you if you have any tips or questions!

If you are looking for a package for testing Flutter apps, you should try our open-source package — Patrol UI testing framework.

--

--

LeanCode
LeanCode

Written by LeanCode

We‘re a group of technology enthusiasts working together for our clients to create better solutions for their digital consumers. See more at https://leancode.co

Responses (2)