devsnack
Documentation

Screwly documentation

Run it, point it at your own accounts, ship it under your own brand. Written against version 1.0.0 of the download you get on CodeCanyon.

$11 on CodeCanyon
On this page
01

Interface tour

Screenshots taken from the running app on an iPhone simulator, unedited. The captions explain what each screen does. The dashed “Banner ad” strip marks where AdMob’s banner loads; in your build a real (test) banner appears there.

Home screen
HomePlay opens the furthest level reached. The coin balance sits top right, and the banner slot sits at the bottom.
Level map
Level mapA winding path through every level, opened at the current one. Every 10th level carries a Hard badge.
Tutorial overlay
TutorialLevels 1 to 3 explain the four rules by pointing at the real board. It never repeats once finished or skipped.
Early gameplay
Early levelTap an exposed screw: it goes to the box of its colour, or waits in the buffer slots below the boxes.
Late-level gameplay
Late levelUp to 72 screws on five layers of plates. A screw can be tapped only when no plate above covers it.
Hint pulsing a screw
HintThe solver pulses a winning next move. It answers at once while you are on the level’s stored solution.
Win sheet
WinCoins earned, twice as many on a hard level, an optional rewarded ad to double them, and Next.
Lost sheet
Out of spaceOne continue per attempt, for a rewarded ad or 100 coins. Restart is always free.
Shop
ShopBoosters for coins, free coins for a rewarded ad, and the Remove Ads purchase with Restore Purchases.
Settings
SettingsSound, music, vibration and colour-blind mode, plus the privacy and about links.
Colour-blind mode
Colour-blind modeEvery colour also carries a symbol, on the screw heads and on the boxes.
Dark theme gameplay
Dark themeFollows the device setting. Every colour comes from one config file.
02

Overview

Screwly is a complete screw-sort puzzle game for Flutter. Plates are stacked in layers and held down by coloured screws. The player taps exposed screws to send each one to the box of its colour, parks screws in a few spare slots when no box matches, and clears plates to uncover the ones beneath.

The game ships with 1000 levels generated at build time. Each one is proven winnable with zero boosters by a solver that lives in the project, and the same solver powers the Hint button. A player can never be handed a level that needs a paid booster or an ad to finish.

It is a single Flutter app for Android and iOS, with a web build for demos. There is no backend: progress, coins and settings are stored on the device, so there are no server costs.

  • Monetization ready: AdMob banner, interstitial and rewarded ads, Google’s consent flow (UMP), and a Remove Ads in-app purchase. It ships with Google’s test ad IDs.
  • Conservative ad defaults: no interstitial before level 10, at most one every 3 completed levels, never within 90 seconds of the last, never after a loss, and never a banner over the board.
  • No lives, energy or timers: restarting is always free.
  • One file to reskin: name, colours, fonts, economy, ad IDs and the IAP product ID live in lib/config/app_config.dart.
03

What’s included

  • Full Flutter source for Android, iOS and web.
  • packages/screw_engine: the pure-Dart rules, solver and level generator, with its command-line level generator.
  • assets/levels/levels.json.gz: the 1000 generated levels, each with its stored winning solution.
  • Light and dark Material 3 themes, bundled open-licence fonts (Fredoka and Nunito), and three plate materials: wood, brushed metal and frosted acrylic.
  • App icon and splash art, drawn in code from the theme colours, with the generator configuration to rebuild them.
  • English strings in lib/l10n/app_en.arb, ready for translation.
  • Unit, widget and architecture tests for the app, plus the engine’s own tests.
  • This documentation, a quick-start guide and LICENSES.md.
  • 6 months of item support, as standard on CodeCanyon.
Note
Not included: hosting, a backend, cloud save or leaderboards, a level editor, sound effect or music files, and any third-party account. See Third-party services and costs and Known limitations.
04

Features

Gameplay

  • Layered plates in eight shapes: rectangle, rounded rectangle, circle, capsule, L, T, bar and ring. A screw is blocked while any plate above covers it.
  • Two active boxes of 3 screws each, a queue of boxes behind them with a next-box preview, and 5 buffer slots.
  • Chain reactions: a full box leaves, the next one slides in and pulls any matching screws out of the buffer, and an empty plate drops away.
  • A loss happens only when the buffer is full and no exposed screw fits a box.
  • Boosters:
    • +Slot adds one buffer slot for the rest of the level.
    • Hint shows a winning next move, or says honestly when the position cannot be won.
    • Undo takes back up to 20 moves.
  • A tutorial on levels 1 to 3, a free restart, and progress saved after every move, so the game resumes exactly where it was even after the app is killed.

Levels

  • 1000 levels over five difficulty bands. Colours, screw counts and layers grow, and the spare room shrinks.
  • Every 10th level is a Hard level: it is tighter and pays double coins. The level after it is a relief level.
  • Every level is proven winnable with no boosters. The engine’s tests replay each level’s stored solution to a win.

Screens

  • Splash, home, level map, game, Win sheet with confetti, Lost sheet with continue, shop and settings.
  • Colour-blind mode: a symbol on every screw head and box.
  • Light and dark themes that follow the device. Sound, music and vibration toggles.

Monetization

  • Banner ads on home and the level map only.
  • Interstitials between levels, under the rules in Ads, consent and Remove Ads.
  • Rewarded ads, always the player’s choice: continue after a loss, double the win coins, free coins in the shop.
  • Remove Ads, a one-time purchase: it removes banners and interstitials and keeps rewarded ads available. Restore Purchases is included.
  • Google’s User Messaging Platform (UMP) for consent, with an ad-choices entry in Settings where the region requires it.
05

Architecture

screwly/
screwly/
├── lib/
│   ├── main.dart              opens storage, prepares the wallet, registers font licences
│   ├── app.dart               MaterialApp.router with light and dark themes and localisation
│   ├── config/app_config.dart every value you change to reskin the game
│   ├── core/
│   │   ├── logic/             pure-Dart rules for ads, the level pack format and more
│   │   ├── models/            save data (freezed + json_serializable)
│   │   ├── providers/         Riverpod providers
│   │   ├── router/            go_router routes
│   │   ├── services/          storage, saves, wallet, progress, settings, audio, haptics
│   │   │   ├── ads/           AdMob and consent, behind an interface with a web no-op
│   │   │   └── iap/           the Remove Ads purchase, behind an interface with a web no-op
│   │   └── widgets/           shared widgets: coin pill, banner slot, pegboard, coin icon
│   ├── features/              splash, home, level_map, game, results, tutorial, shop, settings
│   └── l10n/                  app_en.arb and the generated localisations
├── packages/screw_engine/
│   ├── lib/src/               rules, solver, generator, difficulty bands, level pack, PRNG
│   ├── tool/generate_levels.dart  the level generator's command-line script
│   └── test/
├── assets/
│   ├── levels/levels.json.gz  the 1000 levels
│   ├── fonts/                 Fredoka and Nunito, with their OFL licences
│   └── branding/              source images for the icon and splash
├── tool/brand_art_test.dart   draws the icon and splash art from the theme colours
├── test/                      app tests, including test/architecture_test.dart
├── android/  ios/  web/
├── LICENSES.md
└── pubspec.yaml

Tech stack

Framework
Flutter 3.44 or later (Dart 3.12 or later); tested on Flutter 3.47.2
State
flutter_riverpod 3.3 with riverpod_generator
Routing
go_router 17
Models
freezed 3.2.5 and json_serializable (generated files are committed)
Storage
hive_ce, storing JSON maps on the device
Ads and consent
google_mobile_ads 9.0.0, including Google’s User Messaging Platform
Purchases
in_app_purchase (StoreKit and Google Play Billing)
Audio
audioplayers with a preloaded effect pool
Level pack
archive, a pure-Dart gzip decoder that also works on web
Rendering
CustomPainter only. No game engine and no physics.
Branding
flutter_launcher_icons and flutter_native_splash

Design rules the tests enforce

test/architecture_test.dart fails the build if any of these break:

  1. 01
    The engine in packages/screw_engine/lib is pure Dart: no Flutter, dart:io, dart:ui or dart:isolate. The game, the Hint and the level generator all use this one package, so they can never disagree about the rules.
  2. 02
    Colours and AdMob IDs appear only under lib/config/.
  3. 03
    The game screen never builds a banner. Home and the level map each place a banner slot.
  4. 04
    There is no lives, energy or timer system.
  5. 05
    The web build never compiles the ads or store plugins.
  6. 06
    lib/core/logic/ is pure Dart.
06

Setup and store builds

Requirements

  • Flutter 3.44 or later, installed from the official Flutter site.
  • Android: Android Studio with the Android SDK Platform 37 and JDK 17. The app runs on Android 7.0 (API 24) and later.
  • iOS: a Mac with Xcode and CocoaPods. The app runs on iOS 17 and later.
  • A device, emulator or simulator for running the app.

Run the app

terminal
flutter pub get
flutter devices                # pick a device
flutter run -d <device id>     # Android, iOS or Chrome

No account or key is needed to run it. Ads load Google’s test ads on Android and iOS; on web, ads and purchases are switched off.

Code generation

The generated *.g.dart and *.freezed.dart files are committed, so the project builds without this step. Run it only after you change a freezed model or a Riverpod provider:

terminal
dart run build_runner build

Checks

terminal
flutter analyze                          # should report no issues
flutter test                             # app, widget and architecture tests
(cd packages/screw_engine && dart test)  # rules, solver, generator and every shipped level

Android release build

  1. 01
    Copy android/key.properties.example to android/key.properties and fill it in.
  2. 02

    Create an upload keystore from the android/ folder, if you don’t already have one:

    terminal
    keytool -genkey -v -keystore upload-keystore.jks \
      -keyalg RSA -keysize 2048 -validity 10000 -alias upload
  3. 03

    Build the bundle for Google Play:

    terminal
    flutter build appbundle --release
Warning

Back up the keystore. Without it you can never update the app under the same Play listing. key.properties, *.jks and *.keystore are gitignored.

Until key.properties exists, release builds fall back to the debug key, so flutter run --release works, but a debug-signed bundle cannot be uploaded to Play.

Release builds shrink the code with R8. android/app/proguard-rules.pro keeps what AdMob, the consent SDK, Play Billing and WorkManager need; without those rules the release app can crash on launch. Always install a release build on a real device before uploading.

The SDK levels are set as numbers in android/app/build.gradle.kts: minSdk = 24, targetSdk = 37, compileSdk = 37.

iOS release build

  1. 01
    Open ios/Runner.xcworkspace in Xcode. Under Signing & Capabilities, choose your team and set your bundle identifier.
  2. 02

    Build the archive:

    terminal
    flutter build ipa
  3. 03
    Upload it with Xcode’s Organizer or Apple’s Transporter app.

The Podfile targets iOS 17. If pod install reports that CocoaPods’ specs are out of date, run pod repo update in ios/.

Web build (for a demo)

terminal
flutter build web --release
# served from a sub-folder, e.g. example.com/screwly/:
flutter build web --release --base-href /screwly/

Upload the contents of build/web/ to any static host. Ads and purchases are off on web because neither plugin supports it.

07

Save data and levels

Save data

Screwly has no database server. Everything is stored on the device with Hive (lib/core/services/storage_service.dart):

  • the level in progress, with its undo history, saved after every move;
  • progress: the current and highest unlocked level, and whether the tutorial has been seen;
  • the wallet (coins and boosters), the settings and the Remove Ads entitlement.

If you change the shape of stored data, bump AppConfig.schemaVersion and add a migration in storage_service.dart. A save that can’t be read is dropped rather than crashing the app.

Levels

The levels in assets/levels/levels.json.gz are produced by the generator in packages/screw_engine. Level n is always generated from the same seed, so regenerating gives the same levels on any machine. Each level is solved during generation, is rejected if it can’t be won without boosters, and is stored with its winning solution.

BandLevelsColoursScrewsPlate layersTarget spare slots
Tutorial
1–3
2
9–12
1–2
4–5
Easy
4–50
3–4
12–24
2–3
3–4
Medium
51–200
4–5
21–36
3–4
2–3
Hard
201–600
5–7
30–54
3–5
1–2
Expert
601–1000
6–8
42–72
4–5
0–1

“Spare slots” is how many buffer slots, out of 5, a level leaves unused on its best line. Every 10th level is one slot tighter, and the level after it one looser.

Regenerate the levels

terminal
dart run packages/screw_engine/tool/generate_levels.dart              # 1000 levels
dart run packages/screw_engine/tool/generate_levels.dart --count 1500 # a bigger pack
dart run packages/screw_engine/tool/generate_levels.dart --help       # every option

Run it from the project root. It writes assets/levels/levels.json.gz and prints a report for every 50 levels, and it keeps a cache so an interrupted run carries on where it stopped (--no-cache starts fresh). Then run the engine tests, which replay every stored solution to a win.

Warning
Never edit levels.json.gz by hand. Always regenerate it.
08

Third-party services and costs

Warning

This item is source code, not a hosted service. The purchase includes no hosting, no developer account, no ad network account and no credit with any provider.

The services below are run by third parties. Each one is billed to you by that third party, under pricing DevSnack does not set or control, and that pricing can change. Check each provider’s current pricing yourself before you rely on it. Any figure below is indicative and was correct when this was written.

Required to publish

ServiceWhat it’s forWho bills youIndicative cost
Google Play Console
Publishing the Android app
Google
A one-time registration fee (about US$25)
Apple Developer Program
Publishing the iOS app
Apple
An annual membership (about US$99 a year)
A Mac with Xcode
Building the iOS app
Your own hardware
Varies

No server, database or hosting is needed to run the game: it is fully offline.

Optional

ServiceWhat it’s forWho bills youCost notes
Google AdMob
Banner, interstitial and rewarded ads
Google
Free to join. Google pays you ad revenue under its own terms and payment thresholds.
In-app purchases (Remove Ads)
Selling ad removal
Apple and Google
Charged per transaction: each store keeps a commission on every sale, commonly 15% or 30% depending on its programme and your eligibility.
Static web hosting
A web demo
Your host
Many hosts have free tiers; paid plans vary.
Sound effects and music
Game audio, not bundled
The seller, if you license any
Free if you make your own or use CC0 sounds.

What happens when an optional service isn’t set up is covered in Integrations.

09

Integrations

IntegrationUnlocksWhere it’s configuredWithout it
AdMob ads
Banners, interstitials and rewarded ads
Unit IDs in AdUnits; app IDs in android/app/src/main/AndroidManifest.xml and ios/Runner/Info.plist
Google’s test IDs ship, so test ads show out of the box. Set AdsConfig.enableAds to false to remove every ad.
Consent (UMP)
GDPR consent and the iOS tracking prompt
Messages published in AdMob under Privacy & messaging
With no message published, no consent form is shown and ads are requested as the SDK allows.
Remove Ads (IAP)
A one-time purchase that removes banners and interstitials
IapConfig.removeAdsProductId, plus a product with that ID in each store
The shop shows the price as unavailable, and Restore finds nothing to restore. The rest of the game is unaffected.
Sounds and music
Game audio
Files in assets/audio/
The game is silent. A missing file is skipped without an error.
Web
A browser demo
None
Ads and purchases are off; the game plays fully.
10

Ads, consent and Remove Ads

Ad placements and rules

PlacementDefault ruleSetting
Banner
Home and level map only. Never over the board.
AdsConfig.bannerHeight
Interstitial
Only after a win on level 10 or later, at most once every 3 completed levels, at least 90 seconds apart, and never after a loss
interstitialFirstLevel, interstitialEveryLevels, interstitialMinGap, interstitialAfterLoss
Rewarded
Always the player’s choice: continue after a loss, double the win coins, or 40 free coins in the shop
EconomyConfig.rewardedAdCoins
Note
Why the defaults are conservative. The most repeated complaint in screw-puzzle store reviews is about levels that seem designed to force ads. Aggressive interstitials and unwinnable levels bring one-star reviews, and those reviews cost more installs than the extra impressions earn. Raise the frequency if you like, but weigh that trade-off first.

Going live with AdMob

  1. 01
    Create an app for Android and one for iOS in your AdMob account, then create banner, interstitial and rewarded ad units for each.
  2. 02
    Put the unit IDs into the _prod* strings in AdUnits in lib/config/app_config.dart, and set AdsConfig.useTestAds to false. A blank production ID falls back to Google’s test unit.
  3. 03
    Replace the value of com.google.android.gms.ads.APPLICATION_ID in android/app/src/main/AndroidManifest.xml, and of GADApplicationIdentifier in ios/Runner/Info.plist, with your AdMob app IDs. They can’t be set from Dart, because the ads SDK reads them before Flutter starts.
  4. 04
    In AdMob, under Privacy & messaging, publish a GDPR message, and an IDFA explainer message for the iOS tracking prompt. The wording of that prompt is NSUserTrackingUsageDescription in Info.plist.
  5. 05
    Before each iOS release, compare the SKAdNetworkItems list in Info.plist with the current list on Google’s AdMob iOS quick-start page.
Warning
google_mobile_ads is pinned to exactly 9.0.0. Version 9.1.0 imports a private GoogleMobileAds header that Xcode refuses in a framework build (“Include of non-modular header inside framework module”). Move up only when a later release fixes that.

Remove Ads (in-app purchase)

  1. 01
    In Google Play Console and App Store Connect, create a non-consumable product with the ID in IapConfig.removeAdsProductId (dev.devsnack.screwly.remove_ads by default; change it to match your bundle ID). Where to click in each console is in the console table under Production checklist.
  2. 02
    Test with a Play licence tester account and an App Store sandbox tester. Buy it, reinstall, and use Restore Purchases.

If the shop shows the price as “Unavailable”, see Troubleshooting.

Ownership is stored on the device. Remove Ads hides banners and interstitials; rewarded ads stay available, because they are how a player without coins gets unstuck.

Privacy and store forms

  • ios/Runner/PrivacyInfo.xcprivacy declares the app’s own use of required-reason APIs: file timestamps for the save files, and system boot time for timers. The ads SDK and plugins ship their own privacy manifests.
  • Fill in the App Store privacy label and the Google Play Data safety form. Declare the data AdMob collects; the game itself collects nothing.
  • Set your privacy policy and terms URLs in AppConfig. Settings links to them.
11

Customization

The config file

lib/config/app_config.dart holds every value a reskin needs, one class per area:

AppConfig
App name, support email, website, privacy and terms URLs, store URLs (Rate appears once they are set), version, schema version, splash duration, tutorial levels
EconomyConfig
Coins and booster prices (table below)
AdsConfig, AdUnits
Ad switches, interstitial rules, banner height, test and production unit IDs
IapConfig
The Remove Ads product ID
ThemeConfig
Light and dark colours, fonts, theme mode (system, light or dark), plate material
PaletteConfig
The eight screw colours and their colour-blind symbols
BoardConfig
Plate materials, the workbench colours in each theme, and animation timings

Economy defaults

Starting coins / boosters of each kind
50 / 2
Coins per win / per hard-level win
10 / 20
+Slot / Hint / Undo price
100 / 50 / 30 coins
Continue after a loss
100 coins, or a rewarded ad
Rewarded ad in the shop / booster refill
40 coins / 1 booster

Rename the app

  1. 01
    AppConfig.appName for the name shown inside the app.
  2. 02
    Android: namespace and applicationId in android/app/build.gradle.kts; move MainActivity.kt under android/app/src/main/kotlin/ to the matching package; set android:label in android/app/src/main/AndroidManifest.xml.
  3. 03
    iOS: the bundle identifier in Xcode (Runner target), and CFBundleDisplayName and CFBundleName in ios/Runner/Info.plist.
  4. 04
    Web: the <title> in web/index.html, and the names in web/manifest.json.
  5. 05
    Update IapConfig.removeAdsProductId to your own bundle ID.

Colours, fonts and dark mode

Edit the colours in ThemeConfig. Both the light and dark themes are built from them, and test/theme_test.dart checks text contrast against WCAG AA. To use other fonts, replace the files in assets/fonts/, update fonts: in pubspec.yaml and ThemeConfig.displayFont / bodyFont, and update LICENSES.md. Keep to open-licence fonts bundled with the app; don’t use a package that downloads fonts at runtime.

App icon and splash

The icon and splash art in assets/branding/ are drawn from the theme colours. After changing the colours, redraw and regenerate:

terminal
flutter test tool/brand_art_test.dart   # redraws assets/branding/*.png
dart run flutter_launcher_icons         # Android, iOS and web icons
dart run flutter_native_splash:create   # splash screens

To use your own artwork instead, replace the four PNGs in assets/branding/ and skip the first command. The generator settings are at the bottom of pubspec.yaml; keep their colour values in step with ThemeConfig.

Warning
After running flutter_launcher_icons, check ios/Runner.xcodeproj/project.pbxproj. The tool can change ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES to = AppIcon; set it back to YES.

Sounds

No audio files are bundled. The game looks for these files in assets/audio/:

click.wav
A button is pressed, or a tap is turned down
lift.wav
A screw is unscrewed and lifts off its plate
box_complete.wav
A box fills up
plate_drop.wav
A plate loses its last screw and drops away
win.wav / lose.wav
A level is won / lost
music_loop.mp3
Background music, looped

Accepted formats. Audio plays through the audioplayers package, which uses each platform’s own decoder, so a file must be in a format that Android, iOS and browsers all play:

FormatAndroidiOSWebUse it for
WAV (16-bit PCM)
Yes
Yes
Yes
Short effects: no decoding delay
MP3
Yes
Yes
Yes
Music, or effects when size matters
M4A (AAC)
Yes
Yes
Yes
Music, as an alternative to MP3
OGG (Vorbis) or Opus
Yes
No
Not in Safari
Don’t use: convert to WAV or MP3
  • Free sound libraries often offer OGG or FLAC downloads. Convert them to WAV or MP3 first; the free editor Audacity does this under File › Export Audio.
  • Keep effects short (under a second is typical) at 44.1 or 48 kHz. Mono is fine for effects.
  • The file names, extension included, are fixed in the Sfx list in lib/core/services/audio_service.dart and in AudioService.musicFile. To use click.mp3 instead of click.wav, either convert the file or change the name there.
  • A file that is missing or can’t be played is skipped silently, so check each sound on a real Android and iOS device.

Add the files, declare - assets/audio/ under assets: in pubspec.yaml, and list each file’s source and licence in LICENSES.md. Use sounds you made or that are licensed for commercial use, such as CC0.

Levels and difficulty

See Save data and levels to regenerate or enlarge the pack. The difficulty bands are defined in packages/screw_engine/lib/src/difficulty.dart.

Languages

Every string is in lib/l10n/app_en.arb. To add a language, copy it to app_<code>.arb (for example app_fr.arb), translate the values, and run flutter gen-l10n. The language picker in Settings appears once there is more than one language.

12

Production checklist

Steps that happen in a web console name the menu path. The paths for each console are gathered in the tables after the list.

  1. 01
    Rename the app and set your IDs: AppConfig, android/app/build.gradle.kts, the Xcode Runner target and ios/Runner/Info.plist (Customization).
  2. 02
    Set the support email, website, privacy policy, terms and store URLs in AppConfig.
  3. 03
    Create the store listings: in Play Console, All apps › Create app; in App Store Connect, Apps › + › New App. For iOS, first register the bundle ID in the Apple Developer site under Certificates, Identifiers & Profiles › Identifiers › +.
  4. 04
    Create your AdMob apps and ad units (Apps › Add app, then Ad units › Add ad unit). Replace the AdMob app IDs in AndroidManifest.xml and Info.plist, fill AdUnits, and set AdsConfig.useTestAds = false.
  5. 05
    Publish the consent messages in AdMob under Privacy & messaging: the European regulations (GDPR) message and, for iOS, the IDFA explainer. Refresh SKAdNetworkItems in Info.plist.
  6. 06
    Upload a first build to a test track. Google Play won’t let you create an in-app product until a build of the app is uploaded (Test and release › Testing › Internal testing › Create new release). For iOS, upload with Xcode’s Organizer or Transporter; the build then appears under TestFlight.
  7. 07

    Create the Remove Ads product in both stores with your IapConfig.removeAdsProductId, as a non-consumable (a one-time purchase that is never used up), and test buying and restoring:

    • Google Play: Monetize with Play › Products › One-time products › Create one-time product, then activate it. Older consoles call this page In-app products. Selling anything needs a payments profile first: Settings › Payments profile.
    • App Store: Apps › your app › Monetization › In-App Purchases › +, type Non-Consumable. Add a display name, a price and a review screenshot (the shop screen) until its status reads Ready to Submit. Your first in-app purchase is submitted together with an app version: add it in the version page’s In-App Purchases section. The Paid Apps agreement must be active (in the Business section, formerly Agreements, Tax, and Banking).
  8. 08
    Regenerate the icon and splash, or replace assets/branding/. Check the Xcode asset-symbol setting afterwards.
  9. 09
    Add your sounds to assets/audio/ in a supported format (Customization), declare the folder in pubspec.yaml, and update LICENSES.md.
  10. 10
    Set the version in pubspec.yaml and AppConfig.version; a test checks they match.
  11. 11
    Run flutter analyze, flutter test and the engine tests.
  12. 12
    Android: create android/key.properties, build the app bundle, and install a release build on a real device.
  13. 13
    iOS: set your signing team, build with flutter build ipa, and test on a device.
  14. 14
    Fill in the store forms: the Google Play Data safety form, ads declaration and content rating (Policy and programs › App content), and the App Store privacy label (Apps › your app › App Privacy).
  15. 15
    Once the app is live, link each AdMob app to its store listing (Apps › your app › App settings) and publish an app-ads.txt file on the website named in your store listing. AdMob limits ad serving to apps it can’t verify.

Where to find it in each console

Menu names are as they were at the time of writing. Google and Apple rename and move pages from time to time; if a path below doesn’t match, type the page name into the console’s search box.

TaskGoogle Play Console (play.google.com/console)App Store Connect (appstoreconnect.apple.com)
Create the app
All apps › Create app
Apps › + › New App (register the bundle ID first on developer.apple.com)
Upload a test build
Test and release › Testing › Internal testing › Create new release, then upload the .aab
Upload from Xcode’s Organizer or Transporter; the build appears under TestFlight
Create the Remove Ads product
Monetize with Play › Products › One-time products (older consoles: In-app products)
Apps › your app › Monetization › In-App Purchases › +, type Non-Consumable
Enable paid products
Settings › Payments profile
Business (formerly Agreements, Tax, and Banking): sign the Paid Apps agreement and add bank and tax details
Add test buyers
Settings › License testing (from the All apps page), and add the same accounts to your internal testing track’s Testers tab
Users and Access › Sandbox › Test Accounts
Privacy and data forms
Policy and programs › App content › Data safety
Apps › your app › App Privacy
Declare ads
Policy and programs › App content › Ads
Covered by the App Privacy answers
Age / content rating
Policy and programs › App content › Content rating
Apps › your app › App Information › Age Rating
Create an app (one per platform)
Apps › Add app
Find the app ID (has a ~)
Apps › your app › App settings
Create ad units (IDs have a /)
Apps › your app › Ad units › Add ad unit: one banner, one interstitial and one rewarded unit per platform
Consent messages
Privacy & messaging: the European regulations (GDPR) message, and the IDFA explainer for iOS
Register a test device
Settings › Test devices › Add test device
Warning
New Google Play developer accounts. At the time of writing, a personal developer account created after November 2023 must run a closed test with at least 12 testers for 14 days before it can publish to production. Check the current requirement on your Play Console dashboard, and plan the launch date around it.
13

Troubleshooting

Most problems come from store or AdMob setup rather than from the code. The app writes a short line to the log whenever an ad, the consent form or the store fails, so start there.

Reading the log

  • While developing: the terminal running flutter run shows every line.
  • Android release build: connect the device and run adb logcat. The app’s lines are tagged flutter; a crash appears under AndroidRuntime as FATAL EXCEPTION.
  • iOS: run from Xcode (ios/Runner.xcworkspace) and read its console, or open Window › Devices and Simulators and choose Open Console or View Device Logs.
Ads did not start:
The ads SDK failed to start this session; it retries the next time ads are needed.
Banner did not load: / Interstitial did not load: / Rewarded ad did not load:
AdMob returned no ad. The message after the colon is AdMob’s reason, such as “No fill”.
Consent status: / Consent form:
The consent SDK couldn’t reach Google or couldn’t show its form.
Products not found:
The store has no product with the ID in IapConfig.removeAdsProductId.
Product query: / The store did not start:
The device’s store couldn’t be reached.

Ads don’t appear

Work down the list; each item is a common cause.

  1. 01

    The placement rules are working.

    There is never a banner on the game screen, and no interstitial before level 10, more than once every 3 levels, within 90 seconds of the last one, or after a loss (Ads, consent and Remove Ads). Testing an interstitial on level 2 shows nothing, by design.
  2. 02

    Ads are switched off.

    AdsConfig.enableAds is false, or you’re running the web build, which never shows ads.
  3. 03

    Remove Ads is owned on this device.

    Banners and interstitials stay hidden after a test purchase or restore; rewarded ads still show. Clear the app’s data or reinstall to test again.
  4. 04

    Consent was refused.

    In the EEA, the UK and Switzerland, no ad is requested until the player answers the consent form, and a refusal can leave no ads to serve. Change the answer from Settings, or clear the app’s data.
  5. 05

    Test ads show, real ads don’t.

    Then the code is working and the cause is on the AdMob side:

    • New ad units can take an hour or more to start serving, and a new AdMob account or app serves few or no ads until Google has reviewed it. The log shows “No fill” meanwhile.
    • The app ID in AndroidManifest.xml or Info.plist must belong to the same AdMob app as the unit IDs in AdUnits, for that platform. An app ID contains ~; a unit ID contains /.
    • Link the AdMob app to its published store listing and publish app-ads.txt (Production checklist).
    • Never tap your own real ads. Register your phone under Settings › Test devices in AdMob so it gets test ads safely.
  6. 06

    Real IDs were entered but test ads still show.

    Set AdsConfig.useTestAds to false. A blank _prod* ID also falls back to Google’s test unit.
  7. 07

    No network on the emulator or simulator.

    Check that a browser on it can load a page.

The consent form never shows

  1. 01

    You’re outside the regions that require it.

    Google only shows the GDPR form to players in the EEA, the UK and Switzerland. Elsewhere, nothing appearing is correct.
  2. 02

    No message is published.

    In AdMob, open Privacy & messaging, create the European regulations (GDPR) message, select your apps, and publish it. Without a published message there is nothing to show.
  3. 03

    It was already answered.

    The answer is remembered, so the form appears once. Clear the app’s data (Android: Settings › Apps › your app › Storage › Clear storage; iOS: delete and reinstall) to see it again. Where the region requires it, players can reopen it from the privacy entry in the game’s Settings.
  4. 04

    The iOS tracking prompt doesn’t appear.

    It needs the IDFA explainer message published in AdMob, and iOS never shows it when Settings › Privacy & Security › Tracking › Allow Apps to Request to Track is off on the device.

Testing the form from outside Europe. Tell the consent SDK to treat your device as if it were in the EEA. In lib/core/services/ads/admob/admob_ad_network.dart, in _requestConsent, replace ConsentRequestParameters() with:

admob_ad_network.dart
ConsentRequestParameters(
  consentDebugSettings: ConsentDebugSettings(
    debugGeography: DebugGeography.debugGeographyEea,
    testIdentifiers: ['YOUR-DEVICE-ID'],
  ),
)

Run the app once without it first: the consent SDK prints your device’s ID in the log, in a line that mentions addTestDeviceHashedId (Android) or testDeviceIdentifiers (iOS). Remove this change before a release build.

In-app purchases show “Unavailable”

The shop shows the Remove Ads price as “Unavailable” when the store answered but has no matching product, and says “The store is not available on this device” when it can’t reach the store at all. Check the log for Products not found:.

Both stores

  • The product ID in the console must match IapConfig.removeAdsProductId exactly, including case.
  • The app’s package name (Android) or bundle ID (iOS) must match the app the product was created under.
  • A newly created product can take a few hours to reach devices.
  • The web build has no store, by design.

Google Play

  • The product must be Active, and a build of the app must be uploaded to a testing track.
  • The device must have the Play Store app and be signed in. Android emulators need a system image marked “Google Play”; others have no store.
  • Your test account must be listed under Settings › License testing, added to the testing track, and have accepted the track’s opt-in link.
  • “This version of the application is not configured for billing”: install the build from the testing track, or build with the same version code and signing key as the uploaded one.

App Store

  • The Paid Apps agreement must be Active in the Business section of App Store Connect. Until it is, the store returns no products, even to sandbox testers.
  • The product must have its display name, price and review screenshot filled in; a product showing Missing Metadata isn’t served.
  • Test on a real iPhone or iPad with a sandbox account from Users and Access › Sandbox.
  • “Restore finds nothing” means the account signed in on the device never bought it. Sandbox purchases belong to the sandbox account.

The release build crashes on launch

A debug build that runs and a release build that closes at once almost always means one of these. Read the crash in the log first (above).

  1. 01

    The AdMob app ID is missing or wrong.

    The ads SDK reads it before Flutter starts and stops the app if it’s absent or malformed. The log shows Missing application ID or initialized incorrectly. Check com.google.android.gms.ads.APPLICATION_ID in AndroidManifest.xml and GADApplicationIdentifier in Info.plist: each must be an app ID (with ~), not a unit ID (with /). Keep Google’s test app IDs in place even with ads switched off, because the SDK is still in the build.
  2. 02

    Android: code stripped by R8.

    Release builds shrink the code, and anything the SDKs reach by reflection must be kept by android/app/proguard-rules.pro. The log shows ClassNotFoundException, NoSuchMethodException or a crash inside androidx.room or WorkManager. Don’t remove the proguardFiles(…) line from android/app/build.gradle.kts, and if you added a plugin, add its keep rules to the file.
  3. 03

    iOS: the tracking description was removed.

    NSUserTrackingUsageDescription must stay in Info.plist, because the consent flow asks iOS for tracking permission.
  4. 04

    The splash says the levels couldn’t be loaded.

    That isn’t a crash: assets/levels/levels.json.gz is missing, or - assets/levels/ was removed from pubspec.yaml.

To reproduce a release-only problem with the log attached, run flutter run --release on a connected device.

Build problems

Xcode: “Include of non-modular header inside framework module ‘google_mobile_ads…’”
Keep google_mobile_ads: 9.0.0 exactly in pubspec.yaml; 9.1.0 causes this. Then run pod update Google-Mobile-Ads-SDK in ios/.
CocoaPods: “specs repository is too out-of-date”
Run pod repo update in ios/.
“Target of URI hasn’t been generated” after editing a model
Run dart run build_runner build.
The iOS build fails after regenerating icons
In ios/Runner.xcodeproj/project.pbxproj, set ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS back to YES.
No sound at all
No audio files ship. Add them in a supported format (Customization) and declare assets/audio/. Also check the toggles in Settings.
Play Console rejects the bundle as debug-signed
Create android/key.properties (Setup and store builds). Without it, release builds fall back to the debug key.
14

FAQ

Do I need AdMob to publish the game?
No. Set AdsConfig.enableAds to false in lib/config/app_config.dart and the game shows no ads at all: no banners, no interstitials, and no rewarded offers. The Remove Ads purchase disappears from the shop, because there is nothing to remove. Players can still continue after a loss for coins. Leave Google’s test app IDs in AndroidManifest.xml and Info.plist as they are, because the ads SDK is still part of the build.
Do I need a Mac?
Only for the iOS version. The Android version builds and publishes from Windows, macOS or Linux with Flutter and Android Studio. Building for iOS needs Xcode, which runs only on macOS; if you don’t own a Mac, a cloud build service that provides one is an alternative.
What happens to a player’s progress if they uninstall and reinstall?

Progress, coins, boosters and settings are stored only on the device; there are no accounts and no cloud save. Uninstalling deletes them, and a fresh install starts at level 1.

  • Android may bring the data back on a reinstall, through the phone’s own Google backup, if the player has backup switched on. Android decides when this happens, so don’t promise it to players.
  • iOS deletes the data with the app. It comes back only if the player restores the whole device from an iCloud or computer backup.
  • Remove Ads is never lost. It belongs to the player’s store account: Shop › Restore purchases brings it back on any device signed in to the same account.
  • Updating the app through the store keeps everything.
Do I need a server or hosting?
No. The game runs fully offline and has no backend. Hosting only matters if you want to put the web build online as a demo.
Can I publish on only one store?
Yes. Build and publish only the platform you want; nothing in the code depends on the other.
Do I need to know how to code?
A reskin (name, colours, prices, ad IDs) is editing values in one file, lib/config/app_config.dart, then following the Production checklist. Publishing needs Flutter installed and store accounts set up. Changing how the game plays needs Dart and Flutter knowledge.
Can I add more levels?
Yes. Run the generator with a larger --count (Save data and levels). Every new level is checked for a zero-booster win before it’s written, just like the first 1000.
Why are the ads test ads?
The project ships with Google’s official test IDs, so ads work out of the box and can be tapped safely during development. Replace them with your own before release (Ads, consent and Remove Ads).
15

Upgrading to a new version

When a new version of Screwly is released, you download a complete new copy of the project from your CodeCanyon downloads. Your own changes are not in it, so the job is to move them across. The changelog lists what changed in each version.

Before you customise: keep an untouched copy

The easiest upgrades use git, even if you’ve never used it for anything else. Once, before your first change:

terminal
git init
git add -A
git commit -m "Screwly 1.0.0 as downloaded"
git branch upstream          # an untouched copy of each release lives here
# now make your changes and commit them on the main branch as usual

When a new version arrives:

terminal
git switch upstream
git rm -r -q .           # removes the project's files; your keystore and key.properties stay
# now unzip the new version and copy everything inside its project folder into this one
git add -A
git commit -m "Screwly 1.1.0 as downloaded"
git switch -             # back to your branch
git merge upstream       # brings in the new version and keeps your changes
Warning
Don’t clear the folder by hand with a file manager. android/key.properties and your keystore are deliberately kept out of git, so deleting “everything” deletes them too, and without the keystore you can’t update your app on Google Play.

git stops only where the new version and you changed the same lines, which is usually lib/config/app_config.dart. In each conflict, keep your values and keep any new settings the new version added.

Without git: what to carry across

Start from the new version and copy your changes into it, file by file. Don’t copy an old file over a new one wholesale: a new version can add settings, and the old file would drop them.

lib/config/app_config.dart
Name, URLs, colours, fonts, economy, ad rules, ad unit IDs, product ID. Copy value by value.
android/app/build.gradle.kts, android/app/src/main/kotlin/
applicationId, namespace and the MainActivity.kt package
android/app/src/main/AndroidManifest.xml
App label and AdMob app ID
android/key.properties and your keystore
Never part of the download. Copy them across and keep your backup.
ios/Runner/Info.plist and the Xcode Runner target
Display name, AdMob app ID, tracking wording, SKAdNetworkItems, bundle ID and signing team
assets/branding/, assets/audio/, assets/fonts/
Your art, sounds and fonts, then rerun the icon and splash commands (Customization)
pubspec.yaml
The assets/audio/ entry, fonts, icon and splash settings, and any packages you added. Keep the new version’s package versions.
lib/l10n/app_*.arb
Your translations; add any new strings from the new app_en.arb
LICENSES.md
Entries for your own sounds, fonts and art
web/index.html, web/manifest.json
Web title and names

If you regenerated the levels

  • If you ran the generator with your own options, such as --count 1500, run the same command again after upgrading, then the engine tests. Don’t copy your old levels.json.gz into the new version: if the generator changed, the new tests check the pack against the new rules.
  • If the changelog says the generator changed, regenerated levels are different from the ones your players have. Their progress (the level they’ve reached, coins and boosters) carries over. A level they were partway through may no longer match its save; to restart any unfinished level cleanly, bump AppConfig.schemaVersion and add a migration that clears the saved game:
    storage_service.dart
    // lib/core/services/storage_service.dart, in _migrations
    2: (storage) async => storage.saves.clear(),

After upgrading

terminal
flutter pub get
flutter analyze
flutter test
(cd packages/screw_engine && dart test)

Then install the build over the previous release on a test device, and check that progress, coins and Remove Ads survive the update.

Note
Players’ saved data carries across app updates on its own. If you change what the app stores, bump AppConfig.schemaVersion and add a migration (Save data and levels). Never rename the storage box names in storage_service.dart: that orphans every player’s save.
16

Known limitations

  • No sound files are bundled. The audio system and its toggles are in place; add your own files (Customization).
  • English only. Every string is ready for translation.
  • No backend: no cloud save, accounts or leaderboards. Progress stays on the device.
  • No level editor. Levels come from the generator.
  • Web: ads and in-app purchases are off, because neither plugin supports web.
  • Portrait only on phones and tablets.
17

Changelog

Every version published so far. Updates are free for the life of the item.

v1.0.0 · Sep 2026
Initial release
18

Support and licensing

Item support is included for 6 months, as standard on CodeCanyon. It covers questions about the item and help with setup. It doesn’t include hosting, store or ad accounts, or custom development.

Licensing follows Envato’s Regular and Extended Licenses.

What support covers

  • Six months of support from purchase, extendable at checkout
  • Support covers bugs in the template and questions about how it is put together
  • It does not cover custom feature work, third-party API changes or store review outcomes
  • The Regular licence covers one free end product. Charging users for the app itself needs the Extended licence