Fifty games in one Flutter workspace. This page covers everything from opening the project to uploading a build, and it describes what the code does rather than what a template usually promises.

Prerequisites start here if you are new

You need a working Flutter install and, for store builds, the platform toolchain for whichever store you are shipping to.

WhatVersionNotes
Flutter3.44 or newerBuilt and tested on 3.44.7 stable.
Dart^3.12.2Comes with Flutter; the constraint is in pubspec.yaml.
AndroidStudio + SDK 36compileSdk 36, targetSdk 36, minSdk 24 (Android 7.0).
iOSXcode 15+Deployment target 13.0. macOS only.
Python 33.9+Only for regenerating the app icons.

Check your setup before anything else:

flutter doctor

Every line that matters for this project should be a tick. A warning about Chrome or Linux desktop is harmless — Sweetbox targets iOS and Android.

Getting started

The package unzips to a folder with the Flutter workspace in source/. Everything below is run from there.

cd source
flutter pub get
flutter run

That launches the hub: the fifty game launcher, with search, category filters and a "carry on" row for whatever you last played. No account, no Firebase project, no ad account and no store setup is needed — the app runs on device storage with Google's test ad units.

The one file you edit first. lib/app_config.dart holds every choice this project asks you to make: hub or single game, which game, ads on or off, purchases on or off, ad unit ids, and the economy numbers. Nothing else in the app reads a feature flag.

What you get

  • Fifty complete games, each playable, each saving its own progress.
  • Thirteen screens per game, shared: splash, onboarding, main menu, mode select, board, pause, win, lose, leaderboard, daily reward, shop, settings and the interstitial. A game supplies only its mode select and its board.
  • Seven engine families: grid, match, card, board, arcade, quiz and idle.
  • Monetisation: AdMob banner, interstitial and rewarded, an IAP shop, UMP consent, the iOS ATT prompt and an age gate that runs before all of it.
  • One theme layer: lib/theme/tokens.dart drives colour, radii, type and the gel button look across every screen.
  • 825 tests, including independent solvers that verify the puzzle generators actually produce solvable, single-answer boards.
  • The design kits: the HTML UI designs every game was drawn from.

What is not in v1.0. There is no online multiplayer, no quest system, no seasonal pass and no daily spin. Some of those screens exist as designs in the kits; they are drawings, not features. Board and card games offer pass and play on one device.

Configuration lib/app_config.dart

The file is divided into five numbered sections. These are the values you will actually change.

1 · What to build

ConstantDefaultWhat it does
appModeAppMode.hubHub launcher, or a single game as its own app.
singleGameGameId.game2048Read only when appMode is singleGame.
appTitleOverridenullSplash and store title. Null uses the catalogue title.

2 · Services

ConstantDefaultWhat it does
backendBackend.localDevice storage, or Firebase — see Saves & backend.
adsEnabledtrueMaster switch. False builds a completely ad free app.
iapEnabledtrueMaster switch for in app purchases.
consentEnabledtrueUMP consent and the iOS ATT prompt.

3 · Ads

See Ads & consent. In short: AdConfig.units stays AdUnits.test until your own AdMob ids are in the six fields below it.

4 · Purchases

Product ids, exactly as you enter them in App Store Connect and the Play Console, plus how many coins each coin product grants.

5 · Economy

Hearts, the refill timer, whether losing costs a heart, the seven day reward ladder and what a rewarded ad pays. The full tuning table — booster prices, starting balances, what the booster bundle grants — is in lib/core/economy/balance.dart.

One game as its own app

Every game is self-contained: its engine and its two own screens live in one folder under lib/games/, and no game imports another. A test enforces that rule, which is what makes the following safe.

./tools/build_single.sh sudoku

That one command:

  1. sets appMode to singleGame and singleGame to that game;
  2. rewrites lib/games/games.dart so only that game is compiled in — the other forty nine never reach the binary;
  3. sets the Android applicationId and all six iOS PRODUCT_BUNDLE_IDENTIFIER entries to com.webkoding.sweetbox.sudoku;
  4. installs that game's launcher icon on both platforms;
  5. runs the analyzer to prove the result still compiles.

Run it with no arguments to list the fifty names. To go back:

./tools/build_single.sh hub

Change the bundle id prefix from com.webkoding.sweetbox to your own in tools/build_single.sh before your first release build.

Licensing. Each standalone game you publish is a separate end product and needs its own Envato licence. See the FAQ.

App icons

Every game has its own drawn icon, and so does the hub. One script turns each 1024 master into the four files a build needs, and refuses to run if a game in the catalogue has no artwork.

# all of them
python3 tools/build_icons.py

# just one
python3 tools/build_icons.py sudoku

Output goes to assets/icons/<slug>/:

FileUsed for
1024.pngThe iOS icon, and the source flutter_launcher_icons scales from.
512.pngThe Google Play listing icon.
foreground.pngThe Android adaptive foreground: the same drawing inset into the safe zone, so no launcher mask clips it.
background.txtThe colour behind that foreground, sampled from the artwork rather than guessed.

To use your own artwork, replace assets/icons/<slug>/1024.png with a 1024×1024 PNG and re-run the script, then ./tools/build_single.sh <game> to install it. The script needs Python's Pillow (pip3 install pillow); it does not need a browser or any design software.

Ads & consent

The project ships with Google's official test ad unit ids. They fill every time and earn nothing, which is exactly what you want while building.

Going live

  1. Create the app and the three ad units in your AdMob account.
  2. Paste the six ids into AdConfig in lib/app_config.dart.
  3. Set AdConfig.units to AdUnits.production.
  4. Put your AdMob app id into android/app/src/main/AndroidManifest.xml and ios/Runner/Info.plist.

Testing with production ids on your own device is a policy violation and gets accounts suspended. Leave AdUnits.test set until release.

How often ads show

SettingDefaultEffect
interstitialEveryNLevels3An interstitial after every third level exit.
interstitialCooldownSeconds45Never two interstitials closer than this.
bannerFreeSessions2No board banner for the first two sessions.

Raise these for a gentler first session. Both stores penalise apps that open with an interstitial.

Consent, ATT and the age gate

The age gate runs before anything else — before consent, before the ad SDK, before the first screen. A player who answers under the age of consent gets non-personalised ads and no tracking prompt. On iOS, ATT is requested after UMP, in the order Apple requires. Set consentEnabled to false only if you are shipping without ads entirely.

Purchases

Four products ship configured:

Product idTypeGrants
remove_adsNon-consumableRemoves every ad, permanently. Restorable.
coins_smallConsumable500 coins
coins_mediumConsumable1 500 coins
coins_largeConsumable5 000 coins
booster_bundleConsumable20 hints, 20 undos, 10 shuffles, 10 extra times, 5 skips

Create these ids in App Store Connect and the Play Console exactly as spelled above, or change the spelling in IapConfig to match what you created. The shop screen builds its rows from what the store actually returned, so a product you have not set up simply does not appear — and the coin-priced booster half of the shop keeps working regardless.

Restore is always visible, as the App Store requires.

Economy

One wallet, shared by all fifty games: coins, hearts and boosters.

  • Hearts — five at most, one regenerating every twenty minutes. Losing costs one. Set EconomyConfig.heartsCostOnLoss to false for a relaxed build; puzzle audiences often prefer it.
  • Coins — earned per round (each game decides what a good round is worth from its own score), from the daily reward, and from rewarded ads. Spent on hearts and boosters.
  • Boosters — hint, undo, shuffle, extra time and skip level. Priced in Balance.boosterCost. A new player starts with three hints and three undos so the first one is free.
  • Daily reward — a seven day ladder, 50 coins climbing to 500. The day is stored in UTC, so the reward cannot be claimed twice by changing the phone clock or crossing a time zone, and missing a day restarts the streak.

To make the game more generous, raise EconomyConfig.dailyRewardCoins and rewardedAdCoins; to make it tighter, raise the booster prices in Balance. Hot restart and the whole suite follows.

Saves & backend

By default everything is on the device, in shared_preferences: progress, high scores, the resume snapshot for each game and the wallet. The leaderboard fills itself with generated opponents around the player's own score, so the screen is never empty and no server is needed.

Games never touch storage themselves. They hand a snapshot to GameSession, and the shell writes it. That is why swapping the backend is a two line change in main() rather than a rewrite.

Firebase

For accounts, cloud save and a shared score table, documentation/firebase.md in the package carries the complete adapter source, the Firestore security rules and the main() wiring. The adapters are shipped as source rather than compiled in because the Firebase Android plugin refuses to build without your own google-services.json — a file we cannot ship — so a package with Firebase pre-wired would fail the first build anyone ran.

If you set backend to Backend.firebase and forget to pass the adapters, boot stops with a message telling you so, rather than quietly saving to the device.

Project structure

source/
├── lib/
│   ├── app_config.dart        the only file you have to edit
│   ├── main.dart              boot, then hub or single game
│   ├── core/                  services: ads, iap, consent, economy, storage
│   ├── theme/                 tokens, the theme, the shared widgets
│   ├── shell/                 the thirteen screens every game shares
│   ├── hub/                   the fifty game launcher
│   └── games/
│       ├── _shared/           widgets more than one game uses
│       └── <game>/            one folder per game: engine/ + screens/ + game.dart
├── packages/
│   ├── sweetbox_domain/       models and repository interfaces — no Flutter import
│   └── sweetbox_engines/      the reusable engine families
├── assets/icons/              the generated icon masters
├── test/                      825 tests
└── tools/
    ├── build_single.sh        turn the workspace into one game's app
    └── build_icons.py         redraw the icon masters

Two rules are enforced by tests rather than by convention: sweetbox_domain may not import Flutter, and no game may import another game.

Adding your own game

A game is a folder and a class. The thirteen shared screens come free.

  1. Add a value to GameId and an entry to GameCatalog in packages/sweetbox_domain/ — slug, title, tagline, glyph, accent and engine kind.
  2. Create lib/games/<id>/ with game.dart, an engine/ for the rules and a screens/ for the mode select and the board.
  3. Extend SweetboxGame: return your GameId, build the selection screen and build the board.
  4. Use GameSession for saving, pausing, finishing and rewarded ads. Never navigate, never touch the ad SDK, never write to disk directly.
  5. Run ./tools/build_single.sh hub to register it, and python3 tools/build_icons.py <slug> to draw its icon.

Keep the engine free of Flutter imports and it stays testable without a widget test — which is how every generator in this package is verified.

Tests

cd source
flutter test                                   # 825 tests
cd packages/sweetbox_domain && dart test       # the catalogue and its claims

The suite is not decoration. It includes independently written checkers that verify each generator's output: that a Sudoku has exactly one solution, that a Nonogram is line-solvable, that a Sokoban is reachable, that a slide puzzle has the right parity, that a tube sort can actually be finished, and a chess perft that matches the published move counts. It also checks the listing's own claims — that no tagline sells a feature that does not exist, and that every game in the catalogue is registered in the build.

Release checklist

  1. Set appMode and, for a single game, run ./tools/build_single.sh <game>.
  2. Change the bundle id prefix from com.webkoding.sweetbox to your own.
  3. Paste your AdMob ids into AdConfig and set units to AdUnits.production.
  4. Put your AdMob app id in AndroidManifest.xml and Info.plist.
  5. Create the product ids in both stores.
  6. Replace the app name in AndroidManifest.xml and Info.plist.
  7. Set your own signing config — the Android build ships with debug signing so it runs out of the box, and you must replace it before uploading.
  8. Bump version: in pubspec.yaml.
  9. Write your privacy policy URL into the store listing; the app requests consent and ATT, and both stores require a policy for that.
flutter build appbundle --release      # Google Play
flutter build ipa --release            # App Store

Troubleshooting

The Android build fails in a Kotlin task

Kotlin's incremental compiler and Gradle 9.1 disagree about a cache directory. android/gradle.properties sets kotlin.incremental=false for this reason. If you remove that line and the build starts failing on a clean tree, put it back.

Ads never appear

Check three things in order: adsEnabled is true, the player has not bought remove ads, and consent was granted. The ad SDK is only initialised after consent allows it — that is deliberate, not a bug.

The shop is empty

The rows come from what the store returned. On a debug build with no store configuration there are no products, and the screen says so. The coin-priced boosters at the top still work.

Icon generation fails

tools/build_icons.py drives Google Chrome headlessly and expects it at /Applications/Google Chrome.app. Change the CHROME constant at the top of the script for another location or platform.

A single game build still shows the hub

Run ./tools/build_single.sh <game> rather than editing app_config.dart by hand — the mode and the registered game list have to agree, and the script sets both.

FAQ

Can I publish all fifty games separately?

Technically yes, and the tooling is built for it. Commercially, each published app is a separate end product under the Envato licence and needs its own licence. Fifty apps means fifty licences.

Do I need a Firebase project?

No. Every game runs fully offline on device storage. Firebase is optional and documented.

Can I change the look without touching every screen?

Yes. lib/theme/tokens.dart holds the colours, radii, spacing and type. Change a token and all fifty games follow.

Is there online multiplayer?

No. Board and card games offer pass and play on one device. Some kit designs draw online screens; those are designs, not features.

What language is the app in?

English. All user-facing strings live in the widgets rather than in an ARB file, so adding flutter_localizations is a normal Flutter task rather than an untangling job.

Where did the content come from?

Every word list, question bank and puzzle set was written for this product. The Piano Tiles melodies are public domain and each records its own source. Flags are Unicode emoji, so no flag artwork is bundled. The full register is in documentation/licences.md.

Support

Six months of support is included with your purchase and can be extended to twelve. Updates are free for life. Write to support@webkoding.com — include your Envato purchase code, your Flutter version and the exact error, and you will normally have a reply within one business day.

Sweetbox — Flutter 50-in-1 Game Suite · documentation for version 1.0.0