Cricer
v1.0.0Local cricket scoring flutter (android & ios)
Installation
Everything needed to build Cricer from source — toolchain, first run, rebranding it as your own, and signed release builds for both stores.
Thank you for purchasing Cricer — offline cricket scoring for Android and iOS. This section takes you from the source archive to a signed build on your own store listing.
- Version: v1.0.0
- Framework: Flutter, with Riverpod for state and Drift/SQLite for storage
- Platforms: Android and iOS
There is no server to install. Cricer keeps everything on the device — no account, no backend, no network calls. Nothing in this section involves hosting, a database server or a domain; the whole job is building the app and putting it on a store.
Toolchain
The SDK versions Cricer builds against, and the vendored SQLite source the first build needs.
Cricer is a standard Flutter app, so a working Flutter install is the only prerequisite. The versions below are what the project is developed and tested against.
| Tool | Version |
|---|---|
| Flutter | 3.44.0+ (stable) — developed on 3.47.5 |
| Dart | 3.12+ — developed on 3.13.4 |
| Java | 17 — for the Android build |
| Android SDK | compiled against SDK 36 |
| iOS | 13.0+ — needs a Mac with Xcode |
| Gradle | 8.14 · AGP 8.13.0 · Kotlin 2.3.20 |
Confirm your machine is ready before building:
flutter doctorThe vendored SQLite source
Cricer does not download SQLite at build time. It is compiled from a source file included in the package at third_party/sqlite3/, so the app builds even behind a firewall or on a slow connection, with nothing extra to install.
Ifthird_party/sqlite3/sqlite3.cis missing, add it before the first build. It is excluded from Git because it is a ~9 MB third-party file. The one-minute step is inthird_party/sqlite3/README.md, which also explains what the file is and how to replace it with your own copy from sqlite.org.
Note on AGP: flutter build warns that AGP 8.x support will be dropped in a future release. Moving to AGP 9 also needs Gradle 9 and Flutter's built-in Kotlin migration, so it is a deliberate step to take when you are ready — not a prerequisite. The versions above build release APKs and bundles today.
First Run
Fetching packages, running the app, and the two commands that must stay clean.
From the project root, fetch the packages and run it on a connected device or emulator:
flutter pub get
flutter runThe app opens on the New Match screen with nothing in it. Before a match can be started you need two teams with players — a match requires each side's active squad to equal the squad size exactly. The Manual covers that from the user's side.
Tests and analysis
Both of these should stay clean as you make changes:
flutter test
flutter analyzeThe suite is 633 tests, and flutter analyze must report 0 issues.
After changing the database
The Drift schema is code-generated. Any edit under lib/database/ needs the generator re-run, or the build fails on stale generated files:
dart run build_runner build --delete-conflicting-outputsThe database is schema version 1 — the shipped schema, created whole on first launch. Once you have published, a schema change means raisingschemaVersioninlib/database/app_database.dartand adding the migration step that carries version 1 forward. Without it your users lose their saved matches on update.
Rebranding
The five changes required before you can publish, and two optional ones.
Seven steps. The first five are required — without them the app still carries the demo's branding, and two of them are refused outright by both stores. The last two are yours to choose.
| # | Step | Required? | If you skip it |
|---|---|---|---|
| 1 | Launcher icon | Must | Your app ships with the demo logo |
| 2 | Package / bundle id | Must | Both stores reject the upload — the id is already taken |
| 3 | App name | Must | The app calls itself "Cricer" everywhere |
| 4 | Backup file name | Must | Your users' backups are named scoring_backup_*.db |
| 5 | Privacy policy link | Must | Both stores reject the listing — a policy URL is mandatory |
| 6 | Accent colour | Optional | Users keep the built-in palette |
| 7 | Version and tagline | Optional | The About row reads the demo's wording |
Nothing else carries the demo branding. Everything the app shows or writes comes fromkAppNameandkFileBaseNameinlib/core/app_config.dart, so once steps 3 and 4 are done there is no stray name left to hunt for.
Step 1 — Launcher icon
The icon is built from two PNGs in assets/icon/, and a new logo means replacing both:
app_icon.png— 1024×1024, the finished square with its background included. No transparency; the App Store rejects an alpha channel. Used by iOS, and by Android 7 and older.app_icon_foreground.png— 1024×1024, the glyph alone on transparency. Used by Android 8+ and the startup screen.
Then set the background the glyph sits on — adaptive_icon_background in pubspec.yaml, currently #436AD1 — and generate every Android and iOS size:
dart run flutter_launcher_iconsReplacing only app_icon.png is the easy mistake. The old glyph stays on every Android 8+ phone and nothing reports an error — the build succeeds and ships the previous logo.Tip: launchers crop a little off the edge, so keep important artwork away from the very edge of the square. Android also caches launcher icons, so uninstall and reinstall to see a change. assets/icon/README.md has the full sizing rules for both files.Step 2 — Package / bundle id
Currently com.shahinlabs.cricer. Change it in three places:
- Android —
android/app/build.gradle.kts:namespaceandapplicationId. - Android —
android/app/src/main/kotlin/…/MainActivity.kt: thepackageline, and move the folders to match. - iOS —
ios/Runner.xcodeproj/project.pbxproj: everyPRODUCT_BUNDLE_IDENTIFIER.
Verify it landed by inspecting the built APK, not the gradle file:
aapt dump badging build/app/outputs/flutter-apk/app-release.apkStep 3 — App name
The name appears in three files — one for everything inside the app, and one per platform for the name under the launcher icon.
lib/core/app_config.dart—kAppName, which covers everything shown inside the app.android/app/src/main/AndroidManifest.xml—android:label, the Android launcher.ios/Runner/Info.plist—CFBundleDisplayNameandCFBundleName, the iOS launcher.
kAppName alone reaches the About row, the Android task switcher, the backup email subject, the "not a backup file" message and the startup failure screen.
The launcher name cannot be driven from Dart. The OS reads it before your code runs, so the two platform files are unavoidable on every Flutter app.
Step 4 — Backup file name
The app writes its database and its backups under one base name, set in lib/core/app_config.dart:
const String kFileBaseName = 'scoring';That single constant covers the app's private database, the backup files your users save into their Downloads, and the temporary rollback file used during a restore. Set it to something that suits your brand — lowercase with no spaces, since it becomes a real filename on both platforms.
Change it before your first release. Renaming it on a live app points the code at a database that does not exist, so every user opens to an empty app. If you must change it later, tell users to export a backup first and import it afterwards.
While testing, uninstall the app before running it again. The old install still holds a database under the previous name and the app now looks for the new one, so it opens with no teams and no matches and nothing reports an error. That is the rename working, not a bug.
Step 5 — Privacy policy link
Both stores require a privacy policy. Paste your URL into lib/core/app_config.dart:
const String kPrivacyPolicyUrl = 'https://yoursite.com/privacy';A Privacy Policy row then appears in Settings → About and opens that page in the device browser.
Tip: while the constant is left empty the row is hidden, so the app never shows a link that goes nowhere. Include the https:// — an address without it is rejected.Step 6 — Accent colour
lib/theme/app_accent.dart holds the palette users pick from in Settings — Green, Blue, Purple and Orange. Edit the entries, or cut the list down to the single colour you want.
Step 7 — Version and tagline
Also in lib/core/app_config.dart, both shown on the Settings → About row:
const String kAppTagline = 'Offline cricket scoring app';
const String kAppVersion = '1.0.0';KeepkAppVersionin step with theversion:line inpubspec.yaml— that second one is what the stores actually read.
Release Builds
Signing and building for the Play Store and the App Store.
Once the rebranding steps are done, the app is ready to sign and build. Android can be built on any machine; iOS requires a Mac.
Step 1 — Create your Android keystore
Done once, and kept forever:
keytool -genkey -v -keystore release-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias releaseBack up the .jks file and its passwords. Lose either and you can never update the app on the same Play Store listing again. There is no recovery.Step 2 — Wire up the signing config
Copy android/key.properties.example to android/key.properties and fill in your values:
storePassword=…
keyPassword=…
keyAlias=release
storeFile=../release-keystore.jksThe file is gitignored — it holds your signing passwords.
Without this file the build falls back to the debug key, which the Play Store will not accept.
Step 3 — Build for Android
flutter build appbundle --release
flutter build apk --release --split-per-abi| Output | Path |
|---|---|
| Bundle | build/app/outputs/bundle/release/app-release.aab |
| APK, per ABI | build/app/outputs/flutter-apk/app-<abi>-release.apk |
The bundle is what you upload to the Play Store; Google splits it per device. The APKs are for direct install, sideloading, or selling outside the store.
Use--split-per-abifor every APK build. Without it Flutter packs all three ABIs into one file — around 65 MB, where thearm64-v8abuild any modern phone actually installs is roughly 24 MB. Hand testers thearm64-v8aAPK unless you know the device is older.
Step 4 — Build for iOS
Requires macOS with Xcode; it cannot be built on Windows.
flutter build ipa --releaseOpen ios/Runner.xcworkspace in Xcode first and set your team and signing certificate. The bundle id must match the one in App Store Connect.
Modifying the Source
How the project is laid out, and the six constraints that are not obvious from reading the code.
For buyers who want to edit the source. The project is feature-first: each screen owns its pages, widgets, providers and repository.
lib/
core/ errors · PDF export · backup/restore · theme + match defaults
database/ Drift schema, tables and migrations
models/ plain data types shared across screens
router/ go_router configuration
screens/ one folder per feature
theme/ colours, typography, device scaling
utils/ cricket arithmetic and dismissal wording
widgets/ shared dialogs, fields and list tilesEach feature folder under screens/ holds pages/ for the routes, widgets/ for the pieces those pages are built from, providers/ for Riverpod state and repository/ for the code that talks to the database.
Before you change anything
These six constraints are not obvious from reading the code, and breaking one produces a bug that is hard to trace back.
The scoring page must never scroll
A scorer needs the score and the run pad visible at once. Every size on that page comes from ScoringMetrics.forHeight, derived from the height the page actually got. Never add a scroll view. test/scoring/scoring_layout_test.dart holds it to this from a 320px phone up to a 600px tablet, at 1.0× and 1.3× system text.
Sizes go through context.s
Literal sizes are authored against a 390×844 reference device and scaled from there, so one design fits every screen. Use s.w(), s.h(), s.sp() and s.sq() — never raw numbers.
Portrait only
Locked in three places, all of which must agree: lib/main.dart via SystemChrome.setPreferredOrientations, AndroidManifest.xml via android:screenOrientation, and ios/Runner/Info.plist via UISupportedInterfaceOrientations — iPhone and iPad both. On iOS the plist is the hard limit; SystemChrome alone does not hold it, especially on iPad. Landscape breaks every height-derived layout.
The ball feed is the source of truth
A match's state is replayed from its ball_events rows, not held in memory. That is what makes undo and resume work. Anything that changes the state of a match — including a retirement and a batter resuming — is written there as an event.
Errors never reach the user raw
Repositories wrap database work in guard(), the UI wraps calls in runGuarded() to show a toast instead of crashing, and mapError() turns anything thrown into a readable sentence. Add new wording in mapError, not at the call site. A user should never see SqliteException or a stack trace.
Every database stream is autoDispose
Restoring a backup closes the live SQLite connection, replaces the file and opens a new one. Any watch() stream still alive across that swap holds the outgoing connection, so its screen keeps serving the database that was just replaced. Leaving one alive is not a leak you would notice; it is a screen showing the old data after a restore.
SQLite holds the file open. Overwriting it under a live handle makes the next open fail with disk I/O error (code 522), so the connection is fully closed before the file is touched — never merely detached.Packages
All permissively licensed (BSD / MIT / Apache-2.0), safe for commercial use. SQLite itself is vendored as source and is public domain, with no licence conditions at all.
flutter_riverpod— state managementdriftandsqlite3_flutter_libs— local databasego_router— navigationpdfandprinting— scorecard exportshare_plusandfile_picker— backup and restoreurl_launcher— opening the privacy policy linkshared_preferences— settingspath_providerandpath— file locations
Manual
How to use Cricer day to day — every screen in the app, in the order you will meet them.
This section explains how to use Cricer once it is installed. The app has four tabs along the bottom — Home, Teams, History and Settings — and the pages below follow the order you will actually meet them in: build a squad, set up a match, score it, then read it back.
Everything stays on the device. There is no account, no sign-in and no network. Your teams and matches live in a database on the phone itself, which is why the backup in Settings matters: it is the only copy that survives losing the device.
The four tabs
- Home — set up and start a new match.
- Teams — your teams and their players.
- History — every match, finished or in progress.
- Settings — theme, match defaults, backup and restore.
Switching tabs returns that tab to its first screen. A match setup you were part-way through is kept, and a match in progress is never affected — it is stored as you score it, not held on screen.
Teams & Players
Creating teams, adding players with roles, and the active switch that decides who is picked.
The Teams tab is where everything starts. A match needs two teams with players, so this is the first screen to visit on a fresh install.
Creating a team
Tap the + button and give the team a name. The list shows each team with its player count. Tap a team to open its squad.
Adding players
Inside a team, tap the add player button and enter a name and a role:
- Batsman — badged
BAT - Bowler — badged
BWL - All-rounder — badged
AR, and the default for a new player
The role is a label for your own reference. It does not restrict who can bat or bowl — any player can do either.
The active switch
Every player row carries a switch. Active players are the ones eligible for a match; inactive players stay in the squad but are left out of new match rosters and shown dimmed, marked Inactive.
This is how a roster can be larger than the XI. Keep your whole club in the team, and switch off whoever is not playing today.
This switch is what the squad check reads. When you set up a match you enter a squad size, and each side must have exactly that many active players — not more, not fewer. If a team has too many, set the extras inactive rather than deleting them.
Editing and removing
The menu on each row offers Edit and Delete for both teams and players.
Deleting never destroys a scorecard. Teams and players that appear in a match are kept for the records and merely hidden from the active lists; only those with no history at all are permanently deleted. A team with a match still in progress cannot be deleted until that match is finished.
Starting a Match
Picking the two sides, setting overs and squad size, and recording the toss.
The Home tab is the New Match screen. It is filled in from the top down, and the button at the bottom only appears once the toss is recorded.
Host and Visitor
Pick the two sides from the dropdowns. Only teams with at least 2 players appear in the list.
Picking the team the other side already holds swaps them. Choosing your Visitor as the Host moves the displaced team across rather than refusing the choice — it is the quick way to switch who is at home.
Match Settings
Two numbers, and both are required:
- Overs — how many overs each innings lasts. Minimum 1.
- Players — the squad size per side, which is also the number of wickets available. Minimum 2.
These can be filled in before you pick the teams. If you set defaults in Settings → Match Defaults, both fields arrive pre-filled.
The squad check
Once both teams and a squad size are set, each side is checked against its active player count. The error appears under that team's own dropdown, and names what is wrong:
- Too few — add players to that team, or set more of them active.
- Too many — set the extras inactive in the Teams tab.
Errors stay hidden until you press the button. The form does not scold you while you are still filling it in: press Choose Opening Players and anything missing is revealed, each message on the field it belongs to. A number below its minimum is flagged straight away.
Toss
The toss section appears once both teams are chosen. Pick which side won it, then whether they elected to Bat or Field. The app works out who bats first from the two answers.
Changing a team clears the toss. A toss belongs to a specific pair of sides, so swapping one out resets it and the opening players along with it. Set the teams first, then the toss.
Opening Players
Choosing the two openers and the first bowler, and what gets frozen when the match starts.
The last step before scoring. Three choices, each from the right side: two batters from the team batting first, and the opening bowler from the side in the field.
- Strike Batsman — takes the first ball.
- Non-Strike Batsman — at the other end. The striker is removed from this list, so the same player cannot be picked twice.
- Opening Bowler — from the fielding side.
Only active players appear. Start Scoring stays disabled until all three are chosen.
What happens when you start
Pressing Start Scoring does three things in one go: it creates the match, freezes both squads into the match, and records the openers so the match is resumable from this moment rather than from its first delivery.
The squads are a snapshot. From here on the match uses its own frozen copy of both rosters, so renaming a player, setting someone inactive or deleting a team later cannot change a match already under way — or rewrite a scorecard already finished.
Scoring a Match
The scoring screen — the run pad, extras, wickets, retirement, undo, and how an over and an innings end.
This is the screen you spend the match on. It never scrolls: the score, the batters, the bowler and the run pad are all visible at once, on any size of phone.
What is on screen
- Score header — the batting side's runs and wickets, the overs bowled, and the current run rate (
CRR). In the second innings it also shows the target, the required rate (RRR) and how many runs are needed from how many balls. - Batters — both players at the crease with their runs and balls; the striker is marked.
- Bowler — the current bowler's figures.
- This over — a strip of the balls bowled so far this over.
- Extras and actions — two rows of buttons.
- Run pad — the numbers.
Scoring runs
Tap the number of runs the batter scored. That is the whole of the common case. A dot ball is 0, and strike changes over automatically on odd runs and at the end of an over.
Extras
Four buttons, each opening a small dialog:
- Wide — enter the extra runs. Not a legal ball, so the over does not advance.
- No Ball — enter the extra runs and, separately, any runs off the bat. Runs hit off a no-ball score to the batter while the no-ball penalty itself goes to the side, so the two are recorded apart. The extra cannot go below 1. Not a legal ball.
- Bye — runs taken with no contact. A legal ball, so it advances the over.
- Leg Bye — runs off the body. A legal ball.
Penalty runs are added from the run pad, defaulting to 5 and allowing 1 to 25. Like a wide or a no-ball, a penalty is not a legal delivery.
This is why wides and no-balls cannot end an over or an innings on balls, while byes and leg-byes can. The app follows that rule for you — the over strip and the ball count only move on legal deliveries.
Wickets
The Wicket button asks how the batter fell. Six kinds of dismissal are offered:
- Bowled, Caught, LBW, Stumped, Hit Wicket — all credited to the bowler.
- Run Out — credited to no bowler, and the only dismissal that can take the non-striker.
Caught, stumped and run out also ask which fielder took it, from the fielding side's roster. Caught-and-bowled is handled as its own case.
After the wicket is recorded the app sends in the next batter at whichever end fell vacant — a run out can take the non-striker, in which case the striker keeps strike.
Retirement
The Retire button follows the Laws properly, because the choice changes the score and not just the wording:
- Retired Not Out — injury, illness or another unavoidable cause. No wicket is lost, and the batter may return at any later fall of wicket.
- Retired Out — any other reason. This counts as a wicket, credited to no bowler. The batter may only resume if the opposing captain consents, which the dialog asks you to tick.
You choose who is retiring and who comes in to replace them. A retirement is recorded as an event like any other, so it reaches the scorecard marked with its kind and can be undone.
The other two buttons
- Swap — exchange the striker and non-striker, for when they cross and the app has it the other way round.
- Undo — reverses the last ball. There is no limit and no special case: runs, extras, wickets and retirements all come back off.
End of an over
When the sixth legal ball is bowled, the app asks for the next over's bowler. The bowler who just finished is left out of the list, so nobody can bowl two in a row by accident.
End of an innings
An innings ends when the overs run out, the side is all out, or the chase passes the target. After the first innings the app asks for the second innings' openers and shows the target; after the second it goes to the result screen.
Leaving needs two presses. Back once shows a warning, and only a second press within a few seconds leaves the match. The match is not lost either way — it stays in History and can be resumed exactly where you left it.
Players During a Match
Viewing both frozen rosters mid-match, and substituting a player who cannot continue.
The Players icon on the scoring screen opens both sides' rosters as they were frozen at the start of the match.
Making a substitution
Tap a player to replace them with an available active member of their squad. The replacement starts with fresh stats; the player being replaced keeps everything they already scored.
The three players currently on the field cannot be replaced — the two batters at the crease and the bowler of the current over. A batter who cannot continue retires, and a bowler who cannot finish an over is relieved by a bowling change at the end of it.
Opened from a finished match's scoreboard, this screen is read-only: it shows who played, with no way to change it.
Scoreboard
Full batting and bowling figures for both innings, and the over-by-over view.
The Scoreboard icon on the scoring screen opens the full card. It is available mid-match as well as after it, so you can check figures without interrupting the scoring.
Tabs
Each innings contributes two tabs — Bat and Bowl — so a completed match shows four. The second innings' tabs appear only once it has begun.
- Batting — each batter's runs, balls and how they were dismissed, with the extras itemised.
- Bowling — each bowler's overs, runs conceded and wickets.
Over-by-over
The Overs icon opens a per-over breakdown: every delivery of every over, in order, with a tab per innings. A dot ball is shown as •, and extras carry their own short marks — Wd, Nb, B, Lb and P.
The Players and PDF icons are here too, so the scoreboard is a complete view of the match on its own.
Match Result
The result screen shown when the second innings ends.
When the second innings ends the app works out the result and saves it automatically — there is no button to press and nothing to confirm.
The screen shows the winning line, then a card per innings with the runs, wickets and overs, and the target the chasing side was set.
From here you can open the full Scoreboard, export a PDF, or return Home to start the next match.
Nothing is lost by leaving this screen. The match and its result are already saved. It reappears in History with the result on its row, and its scoreboard and PDF stay available permanently.
Match History
Every match kept on the device, resuming an unfinished one, and deleting.
The History tab lists every match, newest first — finished and in progress alike. Each row names the two sides and what happened.
Resuming a match
A match still in progress carries a Resume action. It picks the match up exactly where you stopped: the right innings, the right over, the right batters, and which of them is on strike.
Resume survives the app being killed. The match is rebuilt from the balls you scored, not from anything held in memory, so closing the app — or the phone dying — loses nothing.
A match with no scored balls cannot be resumed, because nothing records who was playing. The app says so and asks you to start a new match instead.
Scoreboard and PDF
A finished match opens its full Scoreboard from the row. The PDF action is offered on every match whatever its state — a card of the score so far is worth exporting mid-innings too.
Deleting
Each row can be deleted, with a confirmation first.
Deleting a match cannot be undone. The scorecard, its balls and its result all go. Export a PDF first if you want to keep a copy.
Exporting a Scorecard
Producing a PDF of any match, and the two places it can go.
The PDF button appears on four screens: the match result, the scoreboard, each row in History, and the scoring screen's scoreboard. Wherever you find it, it does the same thing.
Tapping it asks where the scorecard should go:
- Share — hand it to another app: email, a messaging app, anything on the share sheet.
- Save to device — choose a folder and keep the file.
The card is inked in the accent colour chosen in Settings, so an export matches the app it came from.
A PDF is a snapshot. It is built from the match as it stands the moment you export it, so exporting mid-innings gives you the score so far — and exporting again later gives you an updated card.
Settings
Theme and accent, match defaults, and the backup that is your only copy.
The Settings tab holds four groups.
Appearance
Choose System default, Light or Dark, and an accent colour from the palette. The accent carries through the app and into exported PDFs.
Match Defaults
Pre-fills the overs and squad size on the New Match screen, so a club that always plays the same format sets it once. Leave a field blank to enter it by hand each time.
Data
Three actions, and the reason this screen matters:
- Export via Gmail — sends a backup to yourself by email.
- Export to device — choose where to save a backup file.
- Import database — restore from a local or emailed backup file.
The backup is your only copy. Cricer stores everything on the device and nothing anywhere else, so a lost or wiped phone takes your teams and matches with it. Export a backup after any match you would mind losing.
Importing replaces everything. The current teams and matches are swapped for whatever is in the backup file, and it cannot be undone. The app asks you to confirm first. Once it finishes, the restored data appears immediately — no restart needed.
About
Shows the app name, tagline and version. A Privacy Policy row appears here when one has been configured, and opens it in the device browser.