Dev diary

Building Big Bands solo with Flutter and Flame — a dev diary

By Nikola Dimovski · September 29, 2026 · 7 min read

Big Bands is my third shipped product but my first game. I've built SwapGenie (a grocery barcode scanner) and ExactCups (an ingredient conversion reference), and both taught me a lot about shipping alone. Games are different. Here is what I've learned so far, honestly, from the middle of a build.

Why Flutter + Flame

I evaluated four options seriously: Unity, Godot, native Kotlin, and Flutter with Flame. Here's the short version of why each got ruled in or out for a puzzle game built solo.

Unity is the industry default and it earned that position. If I were building a 3D game or needed rich physics, this is where I'd be. For a 2D puzzle game with basic sprite work and grid math, Unity is a substantial installation, a substantial learning curve, and a substantial monthly cost once your revenue crosses their threshold. I couldn't justify any of the three for what Big Bands actually needs.

Godot is a genuine indie darling and I recommend it constantly. GDScript is elegant, the engine is lean, the tooling is improving fast. What kept me away was the Play Console integration story — third-party plugins for AdMob and Google Play Games Services on Godot 4 are still catching up, and I didn't want to be debugging plugin compatibility when I could be building levels.

Native Kotlin gives you the most control and the best runtime performance. It also gives you zero iOS path and forces you to hand-roll every animation. For a game where visual feedback is the entire product, that is a lot of code to write.

Flutter with Flame won. Flutter gives me native Android performance, a straightforward path to iOS later, a huge package ecosystem (AdMob and Play Games are both first-class), and hot-reload for tuning feel. Flame layers a proper game loop, sprite management, and camera control on top. The combined stack is close to Unity in developer experience for 2D and better in iteration speed.

The one thing Flutter + Flame is not good at is heavy 3D or physics. For a merge puzzle on an 8×8 grid, that's not a limitation I'll ever hit.

The stack that runs the whole thing

Big Bands' entire production stack is:

Total infrastructure cost right now: zero. Firebase and Netlify both have generous free tiers, AdMob is revenue-share not upfront, and the Play Console has a one-time $25 fee. Everything above scales to real user counts before I owe anyone a dollar.

The data-driven-everything principle

The single most important architectural decision I made — and the one I want to yell about to any other solo dev — is putting every tunable value in JSON, not in Dart code.

Level definitions, difficulty parameters, currency prices, ad frequencies, drop rates, quote pools, visual themes: all JSON. Dart is the engine that reads the JSON and renders the game. Dart is the "how"; JSON is the "what".

The reason this matters: I ship a Play Store build maybe once a week. But I want to tune ad frequency daily, based on retention data. If ad frequency were a Dart constant, every change would need a full build cycle: rebuild, sign, upload to Play Console, wait 2-3 hours for review, publish, wait for player devices to download. Twelve hours minimum, and I burn a version number each time.

With JSON in Firebase Remote Config, I edit a value in the Firebase console and player devices pick it up on their next launch. Ten seconds. No build, no review, no version bump. If a change hurts retention, I revert in another ten seconds.

This one architectural decision has probably saved me a hundred hours already, and the game hasn't even launched.

What worked better than expected

Hot reload for feel work

Flutter's r (hot reload) and R (hot restart) commands are game-changers for tuning. Cascade animation feels too slow? Change one constant, press r, the game keeps its state and reflects the change in about 200ms. This alone is worth more than any other technical decision I've made.

The 8×8 grid decision

I spent a genuinely uncomfortable amount of time debating grid size. 8×8 vs 9×9 vs 10×10 vs variable. Every hour of that debate ended up being useful, because the 8×8 lock is everywhere in the design now — it drives piece shape counts, tile pixel sizes for finger ergonomics, difficulty curves, and the visual density of a full board. Locking it early let me build hundreds of downstream decisions on top of a stable foundation.

Lesson: decisions that feel minor when you make them (grid size, tile color palette, currency name) usually reach into everything. Spend the time on them upfront, not later.

Named tunable constants

Every value that answers a feel question — how far above the finger the drag preview floats, how long a cascade delay lasts, how bouncy the merge pop is — lives as a named top-level constant in main.dart:

const double kDragLift = 4.0; // cells above finger
const int kCascadeDelayMs = 350;
const double kMergePopScale = 1.42;

When my wife tests the game and says "the drag feels off," I change one line, hot-reload, and she tries again. Total iteration time: 15 seconds. Without the named constants, I'd be hunting through logic to find where I hardcoded 4.0.

What was harder than expected

Coordinate systems

Flutter's coordinate spaces (screen coords, widget coords, Stack-local coords, RenderBox coords) are all correct in isolation but easy to mix up. My first drag-and-drop implementation had the preview ghost showing one row above the finger because PointerEvent.position is global but my Stack was inside a SafeArea, offsetting its local coord space by the status bar height. The fix was moving the Stack outside SafeArea. The debug time was three hours.

Lesson for other Flutter devs: when a coord bug appears, draw the coordinate math on paper before touching code. Which frame of reference does each variable live in?

Audio

The audioplayers package is fine for background music but slow for rapid sound effects. My first attempt at cascade sounds ("ka-ching" per merged tile) blocked the drag animation for 30-40ms per sound, making the whole game feel laggy. The fix is a pool of pre-instantiated players used round-robin with unawaited() to prevent the future-await from blocking the frame. I'm still tuning this — audio in casual games is deceptively hard because you have to handle audio focus, backgrounding, and interruptions in addition to just "playing a sound."

Google Play Games Services v2

The upgrade from Play Games v1 to v2 broke a lot of tutorials and Stack Overflow answers. Expect a couple of days of pain wiring up authentication and cloud save. The good news: once it's wired, it's rock-solid.

The one-repo discipline

Everything for Big Bands lives in a single repository:

I learned the one-repo lesson the hard way on SwapGenie, where I initially had separate repos for app, website, and content and burned real time reconciling changes across them. Big Bands lives in dimovski-studio/big-bands and every piece of the product is one cd away from any other.

What comes next

The foundation is solid. The current build has drag-and-drop that feels right, cascade physics that pop, a mode picker, and a six-second intro splash that plants the brand from second one. Next up is the money layer: a Bands currency HUD, cascade earnings, and the first spend mechanic (Coin Cash Merge — 500 Bands to trigger one slot-spin merge on the board). After that, Firebase wiring, AdMob integration, and content — 50 launch levels growing to 500+ across the first year.

If you're a fellow solo dev building casual games, the honest summary is this: Flutter + Flame gets you to a shippable game with less friction than any other stack I've tried, but the last 10% of feel work (audio, coordinate math, animation timing) is where you'll spend real time. Budget for it.

Follow the build at github.com/dimovski-studio, or read the comparison of block-merge puzzle games if you want to see where Big Bands is trying to fit in the market.


Written by Nikola Dimovski, solo developer at Dimovski Studio. Also builds SwapGenie and ExactCups. Follow on GitHub: @nikoladimovski71-svg.