A while ago I wrote about the spatial grid particle system in my Flutter Web portfolio. The trick was keeping the connecting-line effect close to O(n) instead of the O(n²) you get from checking every pair. After that post, a few people asked if they could use the widget in their own projects.
Short answer: they couldn't. The widget was wired into my app. That bothered me, so I extracted it into a package: constellation_particles.
This post is not about the algorithm again — the first post covers that. It is about the extraction itself: what it takes to turn a widget from your own app into something a stranger can install.
The Problem: It Looked Reusable, But It Wasn't
On paper the widget was self-contained. One file, a CustomPainter, a particle list. But the constructor told a different story. It reached into a GetX controller for the current scene, read an accent color and a speed from it, and listened with ever():
final director = Get.find<SceneDirector>();
_config = director.blendedConfig.value;
_configWorker = ever(director.blendedConfig, (cfg) {
_config = cfg;
});
Drop this into an app without a SceneDirector and it throws immediately. So the widget forced two things on every user: GetX, and my scene abstraction. Neither one has anything to do with drawing particles.
What It Actually Needed
I checked what the widget really reads from that config. Two values: a color and a speed multiplier. That's it. A full state-management dependency existed to deliver one Color and one double.
So they became constructor parameters, together with everything else you may want to tune:
const ConstellationParticles({
this.particleCount = 100,
this.color = const Color(0xFF64FFDA),
this.speed = 1.0,
this.connectionDistance = 120.0,
this.repulsionRadius = 200.0,
this.repulsionForce = 50.0,
this.seed = 42,
});
The ever() listener became didUpdateWidget. If the color or the count changes, the widget reacts — the normal Flutter way. No plugin, no controller, nothing to register at app start. The dependency list is just Flutter.
I think this part is underrated when people publish packages: a package that brings a state-management library with it is not a small package, no matter how few lines it has. The goal was that using it costs you nothing extra.
What I Kept
The extraction was mostly deleting coupling. But a few parts were worth keeping, and they are easy to lose in a rewrite:
- Lifecycle-aware — the ticker pauses when the app is hidden or backgrounded. No frames burned in a tab nobody is looking at.
- Accessibility — particle count drops to half when the platform asks for high contrast.
-
Repaint gating —
shouldRepaintchecks a generation counter, so the painter repaints only when the simulation actually moved. Paints and the glow gradient are cached, not rebuilt every frame.
Nothing here is clever. But it is the difference between a demo and something you can leave running behind real content.
Using It
Stack(
children: [
const Positioned.fill(child: ConstellationParticles()),
yourContent,
],
)
- GitHub: https://github.com/Yusufihsangorgel/constellation_particles
- pub.dev: https://pub.dev/packages/constellation_particles
If you build something with it, I would like to see it. If you find a bug, the issue tracker is open. Would love feedback, especially on the API surface — is this parameter set enough, or already too much?
Top comments (0)