Skip to content

Build1 publisher3 min readPublished

One game, two codebases: where parity belongs when you ship native on iOS and Android

A developer shipped Mathastic as separate SwiftUI and Compose apps, keeping nine domain types in common and letting the implementations diverge. The discipline sits in the vocabulary, not the code.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying One game, two codebases: where parity belongs when you ship native on iOS and Android
Generated illustration

What happened

  • Tapadyuti Chatterjee released Mathastic, a fast-paced math puzzle game, for iOS and Android as native applications, described in a post on DEV (dev.to).
  • The iOS version of Mathastic uses SwiftUI, observable state, and UserDefaults.
  • The Android version uses Jetpack Compose, a ViewModel with StateFlow, Hilt for dependency injection, and SharedPreferences with Gson.
  • Both apps share the same product rules and domain concepts, but each follows the conventions of its platform instead of forcing an identical implementation.
  • Both versions have equivalents of GameConfiguration, MathQuestion, Score, GameResult, GameMission, MissionProgress, PlayerProfile, OperationStat and DailyChallenge.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

Tapadyuti Chatterjee released Mathastic, an arithmetic puzzle game, as two separate native applications: SwiftUI with observable state and UserDefaults on iOS, Jetpack Compose with a ViewModel over StateFlow, Hilt for dependency injection, and SharedPreferences plus Gson on Android [1][2][3]. The useful part of his write-up on DEV is not the launch, it is the line he drew: both apps share the same product rules and domain concepts, and neither is forced into an identical implementation [4].

The parity is carried by names. Both codebases have equivalents of GameConfiguration, MathQuestion, Score, GameResult, GameMission, MissionProgress, PlayerProfile, OperationStat and DailyChallenge [5], which is nine shared domain types [20], with enumerations covering difficulty, operation, game mode, theme and mission type [6]. Chatterjee calls this one of the most useful architectural decisions in the project, on the grounds that a stable product vocabulary makes platform-specific code easier to reason about [7]. His formulation: "Timed Sprint" should mean the same thing whether its state lives in a Swift property wrapper or a Kotlin StateFlow [8]. And: "The UI implementations can differ. The game rules should not." [9]

That is a cheaper contract to keep than a shared runtime. The rules surface here is not small. Four modes, Timed Sprint, Practice, Endless and Daily Challenge, each behave differently under load, with Daily Challenge generated deterministically from the current day [15]. Difficulty is not just operand size; it also moves the timer, scoring, penalties, streak bonuses and XP [16]. The question factory picks an operation, chooses operands by difficulty, computes the correct result and produces three plausible wrong answers, with mixed mode resolving to a specific operation per question [17]. All of that is duplicated logic in two languages, and all of it is the part a player would notice diverging.

The layering is conventional and does the duplication a favour: UI and navigation on top, then game session state, then question, mission and challenge generation, then local scores, progress and preferences [10]. The state layer owns scoring, streaks, timing, progression and mission updates [11]; small factories produce questions, daily challenges and missions [12]; the score store persists completed runs and derives XP, levels, achievements, best scores and operation accuracy [13]. Because the design is local-first, with no account or server round trip needed to start a game [14], there is no server acting as the referee for any of those derived values. Each platform computes its own.

Chatterjee's stated reason for skipping a backend is that immediate startup and offline play matter more than introducing one before it is necessary [14]. That is a defensible trade for a single-player arithmetic game, and it is where the duplication bill eventually arrives.

What to watch: whether the Daily Challenge really produces the same challenge on both platforms, since the generator is written twice and seeded from the date [15][17]; whether the derived progression values stay comparable across two different persistence stacks, UserDefaults on one side and SharedPreferences with Gson on the other [2][3][13]; and whether the nine-name vocabulary holds when a feature ships on one platform first [5]. Shared nouns are only a contract while someone keeps checking them. Chatterjee notes he used AI to format and polish the article's wording, and that the architecture and decisions are his own [19].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories