Skip to content

Repository files navigation

PackCal

Website: https://ebeeries.github.io/PackCal/ · Download APK: latest release

PackCal answers one question quickly: how many calories are in this whole package?

Nutrition labels print energy per 100 g or per serving, but rarely the total for the pack you are holding. Type the calories per 100 g and the package weight, and PackCal shows the whole-package total instantly, plus portions, a "how much can I eat?" reverse calculator, multipack maths, side-by-side comparison, saved products and history.

totalCalories = caloriesPer100 × packageWeight / 100

527 kcal/100g × 45 g   → 237.15 kcal
 85 kcal/100g × 400 g  → 340 kcal
610 kcal/100g × 30 g   → 183 kcal

The formula works for every package size. Nothing in the app assumes a package is at least 100 g.

Screenshots

Coming soon.

Calculate Compare Saved Settings
docs/screenshots/calculate.png docs/screenshots/compare.png docs/screenshots/saved.png docs/screenshots/settings.png

Features

  • Live calculation while typing, no Calculate button. Accepts 123.5 and 123,5.
  • g / ml basis switch and kcal / kJ input toggle (1 kcal = 4.184 kJ, conversion shown inline).
  • Derived portions: 1 g, 10 g, 25 g, 50 g (only when smaller than the pack), ¼, ½ and whole package.
  • Portion slider from 0 to the full package, with an exact-amount field.
  • How much can I eat? Enter a calorie limit and get the matching amount and percentage, capped at the package with a clear "you can eat the whole package" message.
  • Multipack mode for "6 × 25 g" style products, including "eating N items".
  • Package size presets grouped by product type, plus your own favourite sizes.
  • Compare two products side by side with neutral difference wording.
  • Saved products with categories and history of recent calculations, both stored locally.
  • Optional daily calorie context ("12 % of your 1,900 kcal target"). PackCal is a calculator, not a tracker.
  • Label scanner (secondary): on-device OCR with ML Kit reads "kcal per 100 g" and net weight. Values are always shown for confirmation before use.
  • English and Greek, light and dark themes, TalkBack labels, large text support, edge-to-edge layout, launcher shortcut "Quick Calculate".

Technology

Area Choice
Language Kotlin 2.1
UI Jetpack Compose, Material 3
Architecture MVVM, unidirectional data flow, StateFlow
Navigation Navigation Compose (type-safe routes)
Persistence Room (products, history, favourite sizes), DataStore Preferences (settings)
Async Kotlin coroutines / Flow
OCR ML Kit Text Recognition (bundled, on-device) + CameraX
Build Gradle Kotlin DSL, version catalog, AGP 8.12
Min / target SDK 26 / 36

No backend, no accounts, no analytics, no ads. Camera permission is requested only when the user opens Scan label.

Project layout

app/src/main/java/com/packcal/app
├── PackCalApplication.kt      Application; owns AppContainer
├── AppContainer.kt            Manual dependency container (no DI framework)
├── MainActivity.kt            Single activity, splash, edge-to-edge, shortcut handling
├── domain/
│   ├── model/                 ProductCalculation, SavedProduct, CalculationHistory,
│   │                          PackagePreset(s), UserPreferences, units, results
│   ├── calc/                  CalorieCalculator (every formula), NumberParser, InputLimits
│   ├── format/                NumberFormatter (locale-aware display rounding)
│   └── scan/                  LabelParser (pure-Kotlin OCR text heuristics)
├── data/
│   ├── db/                    Room entities, DAOs, PackCalDatabase
│   ├── prefs/                 UserPreferencesRepository (DataStore)
│   ├── repository/            SavedProduct / History / FavoriteSize repositories
│   └── session/               CalculatorSession: in-memory hand-off between screens
└── ui/
    ├── theme/                 Colours, typography, shapes, PackCalTheme
    ├── components/            Reusable pieces: NumericInputField, SegmentedToggle,
    │                          AnimatedNumberText, cards, dialogs, haptics, formatting
    ├── navigation/            PackCalApp: bottom bar + NavHost
    ├── calculate/             CalculateScreen, sections, CalculateViewModel, UI state
    ├── compare/               CompareScreen, CompareViewModel
    ├── saved/                 SavedScreen (Products + History tabs), SavedViewModel
    ├── settings/              SettingsScreen, SettingsViewModel
    └── scan/                  ScanScreen (CameraX), ScanViewModel, TextRecognitionEngine

Architecture overview

  • Domain layer is plain Kotlin with no Android imports. CalorieCalculator is the only place that multiplies calories; composables never contain formulas. Everything here is covered by JVM unit tests.
  • Data layer wraps Room and DataStore. Reads are Flows that fall back to empty data on I/O errors; writes return Result so the UI can show a snackbar instead of crashing.
  • ViewModels hold raw input text in SavedStateHandle (survives rotation and process death) and derive an immutable UiState with combine. History is written automatically after the inputs settle for ~2 s.
  • Screens are stateless Compose functions driven by UiState and callbacks. Navigation uses one Scaffold with a Material 3 bottom bar; the scanner is a full-screen route.
  • CalculatorSession is a tiny in-memory state holder used to load a saved product, history entry or scanned label into the calculator, and to focus the calorie field when the app is opened via the Quick Calculate shortcut.

Building

Requirements

  • Android Studio Ladybug or newer (any version bundling JDK 17+ works), or a JDK 17+ and the Android SDK with platform 36 and build-tools 36.
  • Internet access on the first build to download Gradle 8.14 and dependencies.

Open in Android Studio

  1. File ▸ Open… and pick the PackCal folder.
  2. Let Gradle sync. Android Studio uses its bundled JDK automatically.
  3. Run the app configuration on a device or emulator (API 26+).

Command line

Create local.properties with your SDK path if Android Studio has not already:

sdk.dir=C\:\\Users\\<you>\\AppData\\Local\\Android\\Sdk

Then:

# Windows
gradlew.bat :app:assembleDebug
# macOS / Linux
./gradlew :app:assembleDebug

If your default java is older than 17, point Gradle at a JDK 17+:

JAVA_HOME=/path/to/jdk-17 ./gradlew :app:assembleDebug

Run the unit tests

./gradlew :app:testDebugUnitTest

Tests cover the whole-package formula (including the examples above), reverse calculation, multipack, kJ conversion, zero/negative/malformed input handling, display formatting in English and Greek locales, and the OCR label parser.

Release APK / AAB

Add a signing config to app/build.gradle.kts (or sign afterwards with apksigner), then:

./gradlew :app:assembleRelease   # APK
./gradlew :app:bundleRelease     # AAB for Play

Release builds have R8 minification and resource shrinking enabled.

Where the output goes

Artifact Path
Debug APK app/build/outputs/apk/debug/app-debug.apk
Release APK app/build/outputs/apk/release/app-release.apk (unsigned if no signing config)
Release AAB app/build/outputs/bundle/release/app-release.aab
Unit test report app/build/reports/tests/testDebugUnitTest/index.html

Icon and logo

The launcher icon and in-app logo are PNGs generated by tools/generate_icons.py (requires Python 3 with Pillow: pip install Pillow). It renders the PackCal artwork (white snack wrapper with "kcal", green calculator badge, green sparks) at every density into app/src/main/res/mipmap-*/ and the in-app logo into drawable-nodpi/packcal_logo.png. art/icon_preview.png is the full 1024 px icon with wordmark for store listings.

To use the original illustration instead of the procedural rendering, save a square, transparent PNG of the wrapper illustration as art/logo_source.png and run python tools/generate_icons.py again; every asset is regenerated from it.

OCR notes

  • The scanner uses ML Kit Text Recognition (Latin) bundled in the APK, so it works offline and needs no API key. CameraX provides the preview and capture; a gallery picker is offered as an alternative that needs no permission at all.
  • LabelParser is a heuristic: it looks for kcal / kJ numbers near "per 100 g/ml" markers and for net-weight style amounts, ignores nutrient lines (fat, sugars, protein…) and treats the "100 g" basis as never being the package size. kJ-only labels are converted.
  • OCR is never trusted blindly. The review sheet shows what was detected, lets the user edit both values and choose the unit, and only then loads them into the calculator.
  • The scanner is isolated in ui/scan. Removing the CameraX and ML Kit dependencies and the ScanRoute leaves the rest of the app fully functional.

Privacy

Everything stays on the device: saved products, history, favourite sizes and settings live in a local Room database and DataStore file. There is no network code, no account, no analytics or advertising SDK. The only runtime permission is the camera, requested only when Scan label is opened, and photos are processed locally and never stored.

Licence

Copyright © PackCal contributors. All rights reserved unless a licence file is added.

About

Calories in the whole package. Free, offline Android calculator (Kotlin, Jetpack Compose).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages