{"id":24482671,"url":"https://github.com/turskyi/portion_control","last_synced_at":"2026-04-02T00:04:18.303Z","repository":{"id":271170388,"uuid":"912221114","full_name":"Turskyi/portion_control","owner":"Turskyi","description":"PortionControl is a minimalist weight management app that helps users track food portions in grams and adjust intake based on weight trends. With simple logging and dynamic recommendations, it offers a stress-free alternative to calorie counting, focusing on sustainable, healthy eating habits.","archived":false,"fork":false,"pushed_at":"2026-03-06T16:41:17.000Z","size":18621,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-03-06T19:59:48.258Z","etag":null,"topics":["bloc","drift","flutter"],"latest_commit_sha":null,"homepage":"https://portioncontrol.ca","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Turskyi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-01-05T00:15:05.000Z","updated_at":"2026-03-06T03:57:37.000Z","dependencies_parsed_at":"2026-01-06T19:11:23.684Z","dependency_job_id":null,"html_url":"https://github.com/Turskyi/portion_control","commit_stats":null,"previous_names":["turskyi/portion_control"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/Turskyi/portion_control","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fportion_control","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fportion_control/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fportion_control/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fportion_control/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Turskyi","download_url":"https://codeload.github.com/Turskyi/portion_control/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fportion_control/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30452952,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-12T21:31:01.033Z","status":"ssl_error","status_checked_at":"2026-03-12T21:30:43.161Z","response_time":114,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bloc","drift","flutter"],"created_at":"2025-01-21T12:14:52.443Z","updated_at":"2026-04-02T00:04:18.269Z","avatar_url":"https://github.com/Turskyi.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg)](https://stand-with-ukraine.pp.ua)\n[![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints)\n[![codecov](https://codecov.io/gh/Turskyi/portion_control/graph/badge.svg?token=66LWUIL7WJ)](https://codecov.io/gh/Turskyi/portion_control)\n[![Code Quality and Tests](https://github.com/Turskyi/portion_control/actions/workflows/code_quality_tests.yml/badge.svg)](https://github.com/Turskyi/portion_control/actions/workflows/code_quality_tests.yml)\n[![Upload Android Build to App Tester](https://github.com/Turskyi/portion_control/actions/workflows/flutter_android_ci.yml/badge.svg)](https://github.com/Turskyi/portion_control/actions/workflows/flutter_android_ci.yml)\n[![Codemagic build status](https://api.codemagic.io/apps/67b7c966163430e15999e56f/67b7c966163430e15999e56e/status_badge.svg)](https://codemagic.io/app/67b7c966163430e15999e56f/67b7c966163430e15999e56e/latest_build)\n![GitHub release (latest by date)](https://img.shields.io/github/v/release/Turskyi/portion_control)\n[![Deploy to Firebase Hosting on merge](https://github.com/Turskyi/portion_control/actions/workflows/firebase-hosting-merge.yml/badge.svg)](https://github.com/Turskyi/portion_control/actions/workflows/firebase-hosting-merge.yml)\n[![Deploy to Firebase Hosting on PR](https://github.com/Turskyi/portion_control/actions/workflows/firebase-hosting-pull-request.yml/badge.svg)](https://github.com/Turskyi/portion_control/actions/workflows/firebase-hosting-pull-request.yml)\n[![wakatime](https://wakatime.com/badge/user/f9df5074-b4ea-4c17-b001-fff428ab82aa/project/f2a257e8-361a-4986-8bdf-2cb2a69c765d.svg)](https://wakatime.com/badge/user/f9df5074-b4ea-4c17-b001-fff428ab82aa/project/f2a257e8-361a-4986-8bdf-2cb2a69c765d)\n\u003cimg alt=\"GitHub commit activity\" src=\"https://img.shields.io/github/commit-activity/m/Turskyi/portion_control\"\u003e\n\n# Portion Control\n\n**Portion Control** is a simple and intuitive Flutter app designed to help users\ntrack their food intake and weight straightforwardly. The app allows users to\nrecord their body weight and food portions, making it easier to monitor the\nrelationship between food consumption and weight changes.\n\n**Official Website:** [portioncontrol.ca](https://portioncontrol.ca)\n\n## Table of Contents\n\n- [Features](#features)\n- [Getting Started](#getting-started)\n- [Project Structure](#project-structure)\n- [Code Explanation](#code-explanation)\n- [Testing](#testing)\n- [Contributing](#contributing)\n- [Contact](#contact)\n- [Screenshot](#screenshot)\n- [Download](#download)\n\n## Features\n\n- **Track Body Weight**: Enter your current body weight (in kilograms).\n- **Track Food Portions**: Input food portion weight (in grams) before every\n  meal.\n- **Simple Interface**: The app focuses on simplicity, with no complex meal\n  logging or calorie counting.\n\n## Getting Started\n\nTo get started with the project, follow the steps below:\n\n### Prerequisites\n\nBefore you begin, make sure you have the following installed:\n\n1. **Flutter SDK**: You can follow the installation instructions on the\n   [Flutter website](https://docs.flutter.dev/get-started/install).\n2. **Android Studio** and **Visual Studio Code**: IDE for Flutter\n   development.\n3. **Dart SDK**: It comes bundled with Flutter, but ensure you\u0026#39;re on the\n   latest\n   stable version.\n\n### Installation\n\nClone this repository to your local machine:\n\n```bash\ngit clone https://github.com/Turskyi/portion_control.git\n```\n\nNavigate to the project directory:\n\n```bash\ncd portion_control\n```\n\nCreate a `.env` file and add the following content:\n\n```env\nRESEND_API_KEY=\"re_abcdefg12345\"\n```\n\nYou can get a free API key from [https://resend.com](https://resend.com).\n\nInstall dependencies:\n\n```bash\nflutter pub get\n```\n\n### Create generated files:\n\n```bash\ndart run build_runner clean\ndart run build_runner build --delete-conflicting-outputs\n```\n\n#### Post-clone required files (important)\n\nAfter cloning the repository, some files are not versioned but are required for\nthe app to run. Add the following files before building or running the app:\n\n1. `android/key.properties` with the following content (fill values as needed):\n\n```properties\n# dev debug environment variables\ndev.SIGNING_KEY_DEBUG_PATH=../keystore/portion_control_debug.keystore\ndev.SIGNING_KEY_DEBUG_PASSWORD=\ndev.SIGNING_KEY_DEBUG_KEY=portion_control_debug\ndev.SIGNING_KEY_DEBUG_KEY_PASSWORD=\n# production release environment variables\nproduction.SIGNING_KEY_RELEASE_PATH=../keystore/portion_control_release.keystore\nproduction.SIGNING_KEY_RELEASE_PASSWORD=\nproduction.SIGNING_KEY_RELEASE_KEY=portion_control_release\nproduction.SIGNING_KEY_RELEASE_KEY_PASSWORD=\nFIREBASE_ANDROID_APP_ID=\nFIREBASE_TOKEN=\nCODECOV_TOKEN=\nGOOGLE_SERVICES=\nRELEASE_KEYSTORE=\nDEBUG_KEYSTORE=\nKEY_PROPERTIES=\n```\n\n2. `android/keystore/portion_control_debug.keystore` (create or obtain the debug\n   keystore)\n3. `android/keystore/portion_control_release.keystore` (create or obtain the\n   release keystore)\n4. `android/app/google-services.json` (Firebase Android configuration file)\n\n**Notes:**\n\n- Keep these files out of version control.\n- Fill in passwords and IDs (e.g., Firebase App ID, tokens) in\n  `android/key.properties` as appropriate.\n\n### Running the App\n\nTo run the app, use the following command in your terminal:\n\n```bash\nflutter run\n```\n\nThis will launch the app on your connected device or emulator.\n\n## Project Structure\n\nHere\u0026apos;s a breakdown of the main components in this Flutter app:\n\n```\nlib/\n├── application_services/\n├── di/\n├── domain/\n├── env/\n├── extensions/\n├── infrastructure/\n├── localization/\n├── res/\n├── router/\n├── services/\n├── ui/\n├── app.dart\n└── main.dart\n```\n\n## Code Explanation\n\n`main.dart`\n\nThis file contains the main entry point for the app.\n\n`home_page.dart`\n\nContains the layout for the home page, where users input their body weight and\nfood portion weight.\n\n`Theme`\n\nThe app uses `Material3` components and a color scheme to create a clean and\nvibrant user interface.\n\n## Why are `repositories` located in the `infrastructure` component?\n\n**Repositories** are placed in the `infrastructure` component because they\ndirectly interact with the `database`. If we were to place `repositories` in\nthe application services layer, they would not have access to the outer layer,\nand hence to the `database`.\n\n## Testing\n\nJoin our testing program and provide valuable feedback:\n\n- [Android App Distribution Tester Invite](https://appdistribution.firebase.dev/i/3a90590762e477b7)\n- [Apple TestFlight Invite](https://testflight.apple.com/join/aJkP43FB)\n\n### Widget Testing\n\nTests for this project ensure that the app\u0026#39;s UI components render correctly.\nYou can run the tests using the following command:\n\n```bash\nflutter test\n```\n\n## Roadmap\n\nThis project is under active development.\n\n## Contributing\n\nWe welcome contributions from the community!\nTo contribute:\n\n1. Fork this repository.\n2. Create a `.env` file and add the following content:\n\n   ```env\n   RESEND_API_KEY=\"re_abcdefg12345\"\n   ```\n\n   You can get a free API key from [https://resend.com](https://resend.com).\n\n   **Also:** after cloning the repository add the unversioned files required to\n   run the app (see **\"Post-clone required files\"** in the **Getting Started**\n   section for details).\n\n3. Create a new branch (`git checkout -b feature-name`).\n4. Commit your changes (`git commit -am 'Add feature'`).\n5. Push to the branch (`git push origin feature-name`).\n6. Create a new Pull Request.\n\nPlease ensure your code follows the existing style guidelines and includes\ntests where applicable.\n\n\u003cdetails style=\"border: 1px solid #aaa; border-radius: 4px; padding: 0.5em 0.5em 0;\"\u003e\n  \u003csummary style=\"font-weight: bold; margin: -0.5em -0.5em 0; padding: 0.5em; border-bottom: 1px solid #aaa;\"\u003eStyle guides:\n\n[Style guide for Flutter](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo),\n[Dart style guide](https://dart.dev/effective-dart).\n\n  \u003c/summary\u003e\n\n- [DO use trailing commas for all function calls and declarations unless the function call or definition, from the start of the function name up to the closing parenthesis, fits in a single line.](https://dart-lang.github.io/linter/lints/require_trailing_commas.html)\n\n- [DON'T cast a nullable value to a non-nullable type. This hides a null check and most of the time it is not what is expected.](https://dart-lang.github.io/linter/lints/avoid_as.html)\n\n- [PREFER using\n  `const` for instantiating constant constructors](https://dart-lang.github.io/linter/lints/prefer_const_constructors.html)\n\nIf a constructor can be invoked as const to produce a canonicalized instance,\nit's preferable to do so.\n\n- [DO sort constructor declarations before other members](https://dart-lang.github.io/linter/lints/sort_constructors_first.html)\n\n- ### Avoid Mental Mapping\n\nA single-letter name is a poor choice; it\u0026#39;s just a placeholder that the\nreader must mentally map to the actual concept. There can be no worse reason\nfor using the name `c` than because `a` and `b` were already taken.\n\n- ### Method names\n\nMethods should have verb or verb phrase names like `postPayment`, `deletePage`,\nor `save`. Accessors, mutators, and predicates should be named for their value\nand prefixed with `get`…, `set`…, and `is`… respectively.\n\n- ### Use Intention-Revealing Names\n\nIf a name requires a comment, then the name does not reveal its intent.\n\n- ### Use Pronounceable Names\n\nIf you can\u0026#39;t pronounce it, you can\u0026#39;t discuss it without sounding silly.\n\n- ### Class Names\n\nClasses and objects should have noun or noun phrase names and not include\nindistinct noise words:\n\n```dart\nGOOD: Customer, WikiPage, Account, AddressParser.\nBAD: Manager, Processor, Data, Info\n```\n\n- ### Functions should be small\n\nFunctions should hardly ever be 20 lines long.\nBlocks within `if` statements, `else` statements, `while` statements, and so on\nshould be **_one_** line long. Probably that line should be a function call.\n\n- ### Functions should do one thing\n\nTo know that a function is doing more than “one thing” is if you can extract\nanother function from it with a name that is not merely a restatement of its\nimplementation.\n\n- ### One Level of Abstraction per Function\n\nWe want the code to read like a top-down narrative. We want every function to\nbe followed by those at the next level of abstraction so that we can read the\nprogram, descending one level of abstraction at a time as we read down the list\nof functions.\n\n- ### Dependent Functions\n\nIf one function calls another, they should be vertically close, and the caller\nshould be **_above_** the callee, if possible.\n\n- ### Use Descriptive Names\n\nDon\u0026#39;t be afraid to make a name long. A long descriptive name is better than\na short enigmatic name. A long descriptive name is better than a long\ndescriptive comment.\n\n- ### Function Arguments\n\nThe ideal number of arguments for a function is zero (niladic). Next comes one\n(monadic), followed closely by two (dyadic). Three arguments (triadic) should\nbe avoided where possible.\n\nGOOD:`includeSetupPage()`\n\nBAD: `includeSetupPageInto(newPageContent)`\n\n- ### Flag Arguments\n\nFlag arguments are ugly. Passing a boolean into a function is a truly terrible\npractice. It immediately complicates the signature of the method, loudly\nproclaiming that this function does more than one thing. It does one thing if\nthe flag is true and another if the flag is false!\n\nGOOD: `renderForSuite()`\n`renderForSingleTest()`\n\nBAD: `render(bool isSuite)`\n\n- ### Explain Yourself in Code\n\nOnly the code can truly tell you what it does. Comments are, at best, a\nnecessary evil. Rather than spend your time writing the comments that explain\nthe mess you\u0026#39;ve made, spend it cleaning that mess. Inaccurate comments are\nfar worse than no comments at all.\n\nBAD:\n`// Check to see if the employee is eligible for full benefits`\n`if((employee.flags \u0026 hourlyFlag) \u0026\u0026 (employee.age \u003e 65))`\n\nGOOD:\n`if (employee.isEligibleForFullBenefits())`\n\n- ### TODO Comments\n\nNowadays, good IDEs provide special gestures and features to locate all the\n`//TODO` comments, so it\u0026#39;s not likely that they will get lost.\n\n- ### Public APIs\n\nThere is nothing quite so helpful and satisfying as a well-described public API.\nIt would be challenging, at best, to write programs without them.\n\n```dart\n/// dart doc comment\n```\n\n- ### Commented-Out Code\n\nWe\u0026#39;ve had good source code control systems for a very long time now. Those\nsystems will remember the code for us. We don\u0026#39;t have to comment it out\nanymore.\n\n- ### Position Markers\n\nIn general, they are the clutter that should be eliminated—especially the noisy\ntrain of slashes at the end. If you overuse banners, they\u0026#39;ll fall into the\nbackground noise and be ignored.\n\n```dart\n// Actions //////////////////////////////////\n```\n\n- ### Don\u0026#39;t Return Null\n\nWhen we return `null`, we are essentially creating work for ourselves and\nfoisting problems upon our callers. All it takes is one missing `null` check to\nsend an app spinning out of control.\n\n- ### Don\u0026#39;t Pass Null\n\nIn most programming languages, there is no **GOOD** way to deal with a `null`\nthat is passed by a caller accidentally. Because this is the case, the rational\napproach is to forbid passing null by default. When you do, you can code with\nthe knowledge that a `null` in an argument list is an indication of a problem,\nand end up with far fewer careless mistakes.\n\n- ### Classes Should Be Small!\n\nWith functions, we measured size by counting physical lines. With classes, we\nuse a different measure. **We count responsibilities.** The Single\nResponsibility Principle (SRP) states that a class or module should have one,\nand only one, reason to change. The name of a class should describe what\nresponsibilities it fulfills. The more ambiguous the class name, the more\nlikely it has too many responsibilities. The problem is that too many of us\nthink that we are done once the program works. We move on to the next problem\nrather than going back and breaking the overstuffed classes into decoupled\nunits with single responsibilities.\n\n- ### Artificial Coupling\n\nIn general, an artificial coupling is a coupling between two modules that\nserves no direct purpose. It is a result of putting a variable, constant, or\nfunction in a temporarily convenient, though inappropriate, location. For\nexample, general `enum`s should not be contained within more specific classes\nbecause this forces the app to know about these more specific classes. The same\ngoes for general purpose `static` functions being declared in specific classes.\n\n- ### Prefer Polymorphism to If/Else or Switch/Case\n\nThere may be no more than one switch statement for a given type of selection.\nThe cases in that switch statement must create polymorphic objects that take\nthe place of other such switch statements in the rest of the system.\n\n- ### Replace Magic Numbers with Named Constants\n\nIn general, it is a bad idea to have raw numbers in your code. You should hide\nthem behind well-named constants. The term “Magic Number” does not apply only\nto numbers. It applies to any token that has a value that is not\nself-describing.\n\n- ## Encapsulate Conditionals\n\nBoolean logic is hard enough to understand without having to see it in the\ncontext of an `if` or `while` statement. Extract functions that explain the\nintent of the conditional.\n\nGOOD: `if(shouldBeDeleted(timer))`\n\nBAD: `if (timer.hasExpired() \u0026\u0026 !timer.isRecurrent())`\n\n- ### Avoid Negative Conditionals\n\nNegatives are just a bit harder to understand than positives. So, when\npossible, conditionals should be expressed as positives.\n\nGOOD: `if (buffer.shouldCompact())`\n\nBAD: `if (!buffer.shouldNotCompact())`\n\n- ### Encapsulate Boundary Conditions\n\nBoundary conditions are hard to keep track of. Put the processing for them in\none place.\n\n```\nBAD:\nif(level + 1 \u003c tags.length) {\n  parts = Parse(body, tags, level + 1, offset + endTag);\n  body = null;\n}\n\nGOOD:\nint nextLevel = level + 1;\nif (nextLevel \u003c tags.length) {\n  parts = Parse(body, tags, nextLevel, offset + endTag);\n  body = null;\n}\n```\n\n- ### Constants versus Enums\n\nDon\u0026#39;t keep using the old trick of public `static` `final` `int`s. `enum`s\ncan have methods and fields. This makes them very powerful tools that allow much\nmore expression and flexibility.\n\n\u003c/details\u003e\n\n## Contact\n\nFor any inquiries, please visit [portioncontrol.ca](https://portioncontrol.ca)\nor contact [support@portioncontrol.ca](mailto:support@portioncontrol.ca).\n\n## Screenshot\n\n\u003c!--suppress CheckImageSize --\u003e\n\u003cimg src=\"screenshots/Screenshot_20250208_180426.png\" width=\"400\"  alt=\"screenshot\"\u003e\n\n## Download\n\n\u003c!--suppress HtmlDeprecatedAttribute --\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://play.google.com/store/apps/details?id=com.turskyi.portion_control\" target=\"_blank\"\u003e\n    \u003cimg src=\"https://play.google.com/intl/en_gb/badges/static/images/badges/en_badge_web_generic.png\" width=\"280\" style=\"vertical-align: middle;\" alt=\"google play badge\"/\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://apps.apple.com/ca/app/portion-control/id6743641654\" target=\"_blank\"\u003e\n    \u003cimg src=\"https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg\" height=\"80\" style=\"vertical-align: middle;\" alt=\"app store badge\"/\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fturskyi%2Fportion_control","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fturskyi%2Fportion_control","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fturskyi%2Fportion_control/lists"}