<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammad Uzair</title>
    <description>The latest articles on DEV Community by Muhammad Uzair (@uzairxdev).</description>
    <link>https://dev.to/uzairxdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4163455%2F0300d474-bb67-4077-8a05-f7936953db8e.jpg</url>
      <title>DEV Community: Muhammad Uzair</title>
      <link>https://dev.to/uzairxdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/uzairxdev"/>
    <language>en</language>
    <item>
      <title>The Future of Mobile DevOps: AI Testing &amp; Auto-Healing</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Sun, 15 Nov 2026 03:01:16 +0000</pubDate>
      <link>https://dev.to/uzairxdev/the-future-of-mobile-devops-ai-testing-auto-healing-4830</link>
      <guid>https://dev.to/uzairxdev/the-future-of-mobile-devops-ai-testing-auto-healing-4830</guid>
      <description>&lt;h1&gt;
  
  
  The Future of Mobile DevOps: AI-Driven Testing and Auto-Healing Pipelines
&lt;/h1&gt;

&lt;p&gt;Mobile DevOps has historically been the most painful area of mobile engineering. Managing provisioning profiles, dealing with flaky UI tests, and handling infinite device fragmentation makes &lt;a href="https://dev.to/blog/automating-flutter-workflows-cicd-2026"&gt;continuous deployment&lt;/a&gt; feel like a pipe dream.&lt;/p&gt;

&lt;p&gt;But in 2026, the landscape has shifted. AI is no longer just a buzzword; it is an active participant in our deployment pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Death of Brittle UI Tests
&lt;/h2&gt;

&lt;p&gt;Traditionally, QA engineers wrote UI tests using explicit XPath or accessibility ID locators. If a designer moved a button or changed a class name, the test broke. This resulted in "flaky" test suites that developers eventually ignored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enter Auto-Healing Tests:&lt;/strong&gt;&lt;br&gt;
Modern mobile DevOps platforms utilize machine learning to understand the &lt;em&gt;intent&lt;/em&gt; of a test. If an element's ID changes, the AI analyzes the DOM tree, visual characteristics, and context to dynamically identify the new element and update the test locator automatically. This reduces test maintenance overhead by up to 80%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Predictive CI/CD Routing
&lt;/h2&gt;

&lt;p&gt;Instead of running an exhaustive 2-hour test suite on every single commit, AI-driven pipelines analyze the code diff to predict which tests are most likely to fail.&lt;br&gt;
If a developer only changes the "SettingsScreen.tsx" file, the CI intelligently subsets the test suite to only run settings-related E2E tests, dropping &lt;a href="https://dev.to/blog/mastering-code-reviews-empathy"&gt;the PR feedback loop&lt;/a&gt; from hours to minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Rollbacks
&lt;/h2&gt;

&lt;p&gt;If a release hits production and &lt;a href="https://dev.to/blog/flutter-firebase-crashlytics-setup"&gt;crash reporting tools&lt;/a&gt; detect a 5% spike in ANRs (Application Not Responding), the modern DevOps pipeline doesn't wait for a human to wake up. It automatically halts &lt;a href="https://developer.apple.com/help/app-store-connect/update-your-app/release-a-version-update-in-phases" rel="noopener noreferrer"&gt;the staged rollout&lt;/a&gt; on Google Play and Apple App Store and rolls back to the previous stable version, sending an alert to the engineering team.&lt;/p&gt;

&lt;p&gt;Mobile DevOps is finally catching up to backend deployment standards, allowing teams to ship faster, safer, and smarter.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/future-of-mobile-devops" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Build an Arrow Puzzle Game in Flutter</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Sun, 15 Nov 2026 03:00:08 +0000</pubDate>
      <link>https://dev.to/uzairxdev/build-an-arrow-puzzle-game-in-flutter-2fki</link>
      <guid>https://dev.to/uzairxdev/build-an-arrow-puzzle-game-in-flutter-2fki</guid>
      <description>&lt;p&gt;When I built KnotGlide, a calm arrow puzzle game, the whole game rested on one simple move: tap an arrow whose path is clear and it glides out of the maze along its own route. A blocked arrow nudges forward and shows what is in its way, so every tap teaches the next move. The game ships with 100 curated levels in 8 chapters, a daily puzzle, time challenges and an endless mode — all fully offline, with no account and no purchases. You can see the finished game in the &lt;a href="https://www.uzairxdev.online/projects/knotglide" rel="noopener noreferrer"&gt;KnotGlide case study&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This guide shows how to build that kind of game in Flutter: model the board as plain Dart data, write the clear-path check, animate the glide, handle ambiguous taps fairly, design levels that are always solvable, and add a daily puzzle with streaks — all on-device, with no network at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an arrow-escape puzzle game?
&lt;/h2&gt;

&lt;p&gt;An arrow-escape puzzle game is a grid puzzle where every piece is an arrow pointing in one of four directions, and the only move is tapping an arrow to slide it out of the board along the direction it points. An arrow escapes only when every cell between it and the edge of the board is empty; if another arrow blocks the way, the tapped arrow nudges forward and shows what is in its way instead of moving. The level is complete when every arrow has escaped, and the puzzle is in the order: freeing one arrow can open the path for another, so the player has to find the right sequence. That is the whole ruleset — no dragging, no rotating, no matching — which is why the game stays calm and readable even on a crowded board. Boards in KnotGlide come in many shapes, including circles, hexagons, rings, diamonds, leaves and hourglasses, but under the hood each one is still a grid of cells, so the same code drives every shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the board as plain Dart data
&lt;/h2&gt;

&lt;p&gt;Model the board as plain Dart data before any widget touches it, because every other part of the game — the escape check, the animation, the save system — reads from the same model. A board is a grid of cells, and each cell either holds an arrow or is empty. An arrow knows its row, its column and the direction it points, and a direction knows how to step one cell: up subtracts a row, down adds a row, left subtracts a column and right adds a column. Keep the model free of widgets, colours and animation state so you can test it in plain Dart unit tests, generate levels from it, and serialize it for saving mid-level progress.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
enum Direction { up, down, left, right }&lt;/p&gt;

&lt;p&gt;extension DirectionStep on Direction {&lt;br&gt;
  (int, int) get step =&amp;gt; switch (this) {&lt;br&gt;
        Direction.up =&amp;gt; (-1, 0),&lt;br&gt;
        Direction.down =&amp;gt; (1, 0),&lt;br&gt;
        Direction.left =&amp;gt; (0, -1),&lt;br&gt;
        Direction.right =&amp;gt; (0, 1),&lt;br&gt;
      };&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;class Arrow {&lt;br&gt;
  Arrow({required this.row, required this.col, required this.direction});&lt;/p&gt;

&lt;p&gt;int row;&lt;br&gt;
  int col;&lt;br&gt;
  final Direction direction;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;class Board {&lt;br&gt;
  Board({required this.rows, required this.cols, required this.arrows});&lt;/p&gt;

&lt;p&gt;final int rows;&lt;br&gt;
  final int cols;&lt;br&gt;
  final List arrows;&lt;/p&gt;

&lt;p&gt;bool isInside(int row, int col) =&amp;gt;&lt;br&gt;
      row &amp;gt;= 0 &amp;amp;&amp;amp; row &amp;lt; rows &amp;amp;&amp;amp; col &amp;gt;= 0 &amp;amp;&amp;amp; col &amp;lt; cols;&lt;/p&gt;

&lt;p&gt;Arrow? arrowAt(int row, int col) {&lt;br&gt;
    for (final arrow in arrows) {&lt;br&gt;
      if (arrow.row == row &amp;amp;&amp;amp; arrow.col == col) return arrow;&lt;br&gt;
    }&lt;br&gt;
    return null;&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Non-rectangular boards need no special case: mark some cells as walls and isInside returns false for them, keeping one code path for every shape in the game.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you check whether an arrow can escape?
&lt;/h2&gt;

&lt;p&gt;An arrow can escape when every cell between it and the edge of the board, in the direction it points, is empty. Walk from the arrow one cell at a time in its direction: if you step outside the board, the path is clear; if you meet another arrow first, the path is blocked. The check is a short loop and the single source of truth for the whole game — the tap handler, the hint system and the level solver all call the same function. When the path is blocked, do not just ignore the tap: nudge the arrow a few pixels toward the blocker and back, so the player sees exactly what is in the way.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
bool canEscape(Board board, Arrow arrow) {&lt;br&gt;
  final (dRow, dCol) = arrow.direction.step;&lt;br&gt;
  var row = arrow.row + dRow;&lt;br&gt;
  var col = arrow.col + dCol;&lt;/p&gt;

&lt;p&gt;while (board.isInside(row, col)) {&lt;br&gt;
    if (board.arrowAt(row, col) != null) return false;&lt;br&gt;
    row += dRow;&lt;br&gt;
    col += dCol;&lt;br&gt;
  }&lt;br&gt;
  return true;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;void tryEscape(Board board, Arrow arrow) {&lt;br&gt;
  if (canEscape(board, arrow)) {&lt;br&gt;
    board.arrows.remove(arrow);&lt;br&gt;
  } else {&lt;br&gt;
    // Play the nudge animation: the arrow shows its blocker.&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The hint system is the same function run over every arrow: scan the board, collect the arrows where canEscape returns true, and light one of them up. In KnotGlide the hint lights up an arrow that can escape right now, so it never suggests a move the game itself would refuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you animate the glide?
&lt;/h2&gt;

&lt;p&gt;Animate the glide with an AnimationController and a Tween between the arrow's current cell and the board edge, so the arrow travels along its own route instead of teleporting out. Scale the duration to the distance — about 90 milliseconds per cell, capped at roughly half a second — so a short hop feels snappy and a long glide across the board still feels fast. Draw the board with a CustomPainter and render each arrow as a positioned widget on top of it, so the animation moves the widget while the painter stays still; the Flutter documentation for &lt;a href="https://docs.flutter.dev/cookbook/effects/custom-painter" rel="noopener noreferrer"&gt;CustomPainter&lt;/a&gt; shows the pattern. When the animation completes, remove the arrow from the board model and rebuild. Respect the player's reduce-motion setting: when MediaQuery disables animations, skip the tween and remove the arrow immediately, because a calm game should never force motion on someone who asked for less of it. In KnotGlide the reduce-motion, haptics, sound and music switches are separate, so a player can silence the game without losing the feedback they do want. If you are new to Flutter animation, the &lt;a href="https://www.uzairxdev.online/blog/flutter-breathing-pacer-animation" rel="noopener noreferrer"&gt;breathing pacer guide&lt;/a&gt; walks through AnimationController and curved tweens in a smaller example.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
Future&amp;lt;void&amp;gt; animateEscape({&lt;br&gt;
  required AnimationController controller,&lt;br&gt;
  required int distance,&lt;br&gt;
  required VoidCallback onDone,&lt;br&gt;
}) async {&lt;br&gt;
  final duration = Duration(milliseconds: (distance * 90).clamp(120, 500));&lt;br&gt;
  controller.duration = duration;&lt;br&gt;
  await controller.forward(from: 0);&lt;br&gt;
  onDone();&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you make taps feel fair?
&lt;/h2&gt;

&lt;p&gt;Make taps feel fair by never guessing when a tap is ambiguous. On a crowded board a fingertip can sit between two arrows, and picking the wrong one punishes the player for the game's imprecision. When a tap lands near more than one arrow, light both of them up and let the player choose — the tap becomes a selection, not a mistake. KnotGlide uses exactly this rule: a tap between two arrows never guesses, both glow and the player chooses. Add an optional Tap Twice mode on top of it: the first tap previews the escape path, highlighting every cell the arrow would travel through, and the second tap sends it out. The preview costs nothing to implement — it reuses canEscape and draws the path — and it turns the most frustrating moment in the game, tapping an arrow that cannot move, into a deliberate choice. Three hearts per level give the player room to learn: a blocked tap costs a heart, and the level ends when the hearts run out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you design levels that are always solvable?
&lt;/h2&gt;

&lt;p&gt;Design levels that are always solvable by building them backwards from the solved state. Start with an empty board, place arrows in reverse escape order — the last arrow to escape goes in first, then the arrow that blocks it, and so on — and every level you build this way has at least one solution by construction. Author each level as a small grid of characters, one character per cell, where a letter marks an arrow and its direction and a dot marks an empty cell; the level file stays readable and editable by hand. Then verify every level with a solver in your tests: the solver tries each escapable arrow in turn, recurses, and reports any level with no solution, so a broken level never reaches the player. Difficulty comes from the number of arrows and how many of them block each other, not from new rules: early chapters use three or four arrows with obvious first moves, later chapters pack the board so the player has to plan several moves ahead. KnotGlide ships 100 curated levels across 8 chapters — Loose Threads, First Knots, Twists, Tangles, Grand Knots, Snarls, Labyrinths and Gordian Knots — with an endless mode after level 100. A Perfect Glide star for clearing a level with no blocked taps gives skilled players a reason to replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you add a daily puzzle and streaks?
&lt;/h2&gt;

&lt;p&gt;Add a daily puzzle by deriving the puzzle from the date instead of storing a list of puzzles. Use the current date as a seed — for example, the number of days since a fixed start date — and generate or pick the board from that seed, so every player in the world gets the same puzzle on the same day and the puzzle changes at midnight without any server. Store the streak on-device: the last date the player completed a daily puzzle and the current streak count. When the player finishes today's puzzle, compare the stored date with yesterday: if it matches, add one to the streak; if it is older, reset the streak to one; if it is today already, do nothing, because replaying a finished puzzle should never inflate the streak. Monthly trophies are the same idea at a larger scale — count completed days in the current month and award a trophy at fixed thresholds. Keep past days playable, as KnotGlide does, so a missed day never locks a player out.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
Future recordDailyPuzzle({&lt;br&gt;
  required DateTime today,&lt;br&gt;
  required DateTime? lastPlayed,&lt;br&gt;
  required int currentStreak,&lt;br&gt;
}) async {&lt;br&gt;
  final yesterday = today.subtract(const Duration(days: 1));&lt;br&gt;
  final isSameDay = lastPlayed != null &amp;amp;&amp;amp;&lt;br&gt;
      lastPlayed.year == today.year &amp;amp;&amp;amp;&lt;br&gt;
      lastPlayed.month == today.month &amp;amp;&amp;amp;&lt;br&gt;
      lastPlayed.day == today.day;&lt;/p&gt;

&lt;p&gt;if (isSameDay) return;&lt;/p&gt;

&lt;p&gt;final isYesterday = lastPlayed != null &amp;amp;&amp;amp;&lt;br&gt;
      lastPlayed.year == yesterday.year &amp;amp;&amp;amp;&lt;br&gt;
      lastPlayed.month == yesterday.month &amp;amp;&amp;amp;&lt;br&gt;
      lastPlayed.day == yesterday.day;&lt;/p&gt;

&lt;p&gt;final newStreak = isYesterday ? currentStreak + 1 : 1;&lt;br&gt;
  // Save newStreak and today with shared_preferences.&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep progress safe and the game calm?
&lt;/h2&gt;

&lt;p&gt;Keep progress safe by saving the board state after every move, so a player who closes the app mid-level resumes exactly where they left off. Serialize the model you already built — the list of arrows with their positions and directions, the hearts left, and the level id — into a small JSON map and write it to on-device storage. Loading is the reverse: read the map, rebuild the board, and the player never notices the app restarted. Keep the game calm by removing pressure from the main levels: no timer, no ads between levels and no banners, with rewarded ads only when the player chooses — for a hint, an extra heart or more seconds in a time challenge. Time challenges are a separate mode with their own rules, so the 12 challenges with up to 36 stars in KnotGlide never leak a countdown into the relaxed levels, and a blocked tap there costs 5 seconds instead of a heart. Light and dark themes, separate sound, music and haptics switches, and pinch-to-zoom on big boards round out the experience. The same on-device, offline-first thinking works for any calm app — the &lt;a href="https://www.uzairxdev.online/blog/flutter-color-blind-friendly-game" rel="noopener noreferrer"&gt;color-blind friendly game guide&lt;/a&gt; shows how accessibility choices like symbols alongside colours shape a game from the start.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.uzairxdev.online/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; and let's talk about your project.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/flutter-arrow-puzzle-game" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>puzzlegame</category>
      <category>gamedev</category>
      <category>animations</category>
    </item>
    <item>
      <title>Designing Inclusive Mobile Apps: Accessibility Done Right</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Sat, 14 Nov 2026 03:07:00 +0000</pubDate>
      <link>https://dev.to/uzairxdev/designing-inclusive-mobile-apps-accessibility-done-right-3j74</link>
      <guid>https://dev.to/uzairxdev/designing-inclusive-mobile-apps-accessibility-done-right-3j74</guid>
      <description>&lt;h1&gt;
  
  
  Designing Inclusive Mobile Apps: Accessibility Beyond Just Contrast Ratios
&lt;/h1&gt;

&lt;p&gt;When developers hear "accessibility," they often think of automated Lighthouse scores or ensuring text has a 4.5:1 contrast ratio. While these technical specifications are important, they barely scratch the surface of &lt;a href="https://dev.to/blog/developer-empathy-ux"&gt;true inclusive design&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/blog/accessibility-in-flutter-guide"&gt;Building an accessible mobile app&lt;/a&gt; means recognizing that your users are navigating the digital world with varying levels of vision, hearing, motor skills, and cognitive processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Dynamic Type and Layout Fluidity
&lt;/h2&gt;

&lt;p&gt;Many developers hardcode font sizes, assuming users will view the app exactly as it appears in Figma. This completely breaks when a visually impaired user increases their system font size by 200%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Practice:&lt;/strong&gt;&lt;br&gt;
Never use fixed heights for text containers. Allow your UI to wrap and flow dynamically. If a user sets their system text to maximum, your elegant single-line button might need to become a multi-line, stacked layout. Embrace the fluidity.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Nuance of Screen Readers
&lt;/h2&gt;

&lt;p&gt;Testing with VoiceOver (iOS) or TalkBack (Android) should be a mandatory part of your QA process. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid using vague labels like "Click Here".&lt;/li&gt;
&lt;li&gt;Use "aria-hidden" or &lt;a href="https://api.flutter.dev/flutter/widgets/Image/excludeFromSemantics.html" rel="noopener noreferrer"&gt;"excludeFromSemantics"&lt;/a&gt; on purely decorative images so the screen reader doesn't waste the user's time reading out useless file names.&lt;/li&gt;
&lt;li&gt;Group related elements together. If you have a custom card with a user's name, avatar, and bio, the screen reader should read it as a single cohesive unit, not three disconnected pieces of data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Designing for Motor Impairments
&lt;/h2&gt;

&lt;p&gt;Not everyone can perform precise gestures or reach the top corners of massive modern smartphones.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Touch Targets:&lt;/strong&gt; Ensure every tappable element is &lt;a href="https://developer.apple.com/design/human-interface-guidelines/accessibility" rel="noopener noreferrer"&gt;at least 44x44 points&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gesture Alternatives:&lt;/strong&gt; If your app relies heavily on complex swipe gestures, always provide a simple button alternative for users with limited dexterity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inclusivity isn't a post-launch "nice-to-have." It is a &lt;a href="https://dev.to/blog/clean-architecture-mobile"&gt;fundamental architectural decision&lt;/a&gt; that expands your user base and improves the UX for absolutely everyone.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/designing-inclusive-mobile-apps" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>mobile</category>
      <category>ux</category>
    </item>
    <item>
      <title>Flutter Role-Based Access Control Guide</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Sat, 14 Nov 2026 03:05:33 +0000</pubDate>
      <link>https://dev.to/uzairxdev/flutter-role-based-access-control-guide-16nj</link>
      <guid>https://dev.to/uzairxdev/flutter-role-based-access-control-guide-16nj</guid>
      <description>&lt;p&gt;When I built the Muntazim Parent App, a school management app for parents, one requirement shaped the whole architecture: a parent must see only their own child's attendance, grades and fee status, never another family's data. Teachers see their classes, school admins see everything, and parents see exactly one slice. That boundary is role-based access control, and getting it wrong means showing one family another family's private information.&lt;/p&gt;

&lt;p&gt;Role-based access control (RBAC) is the pattern of grouping permissions into roles and checking those permissions at every layer of the app: the UI hides what a role cannot use, the router blocks routes a role cannot open, and the server rejects requests a role cannot make. This guide shows how to build that in Flutter, using the same layering I used in Muntazim, where I implemented secure authentication and role-based access for parents. You can see the finished app in the &lt;a href="https://www.uzairxdev.online/projects/muntazim" rel="noopener noreferrer"&gt;Muntazim case study&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is role-based access control in Flutter?
&lt;/h2&gt;

&lt;p&gt;Role-based access control in Flutter is a way of deciding what each user can see and do by assigning them a role, such as parent, teacher or admin, and attaching a fixed set of permissions to each role. A permission is a single capability, like viewing a child's grades, recording attendance, sending an announcement or managing fee payments, and a role is a named bundle of those permissions. The app checks permissions, not roles, wherever a decision is made: a button asks whether the user can edit grades, not whether the user is a teacher. That distinction matters because roles change, a teacher can also be a parent, while permissions stay stable. Checking roles directly scatters hard-coded role names through the codebase and breaks the moment someone holds two roles. Checking permissions keeps every decision in one place: the role-to-permission map. The rest of this guide builds that map and enforces it in the UI, the router and the server.&lt;/p&gt;

&lt;p&gt;In the school app used as the running example here, the roles break down like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Can do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parent&lt;/td&gt;
&lt;td&gt;View own child's attendance, grades and fees; receive announcements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teacher&lt;/td&gt;
&lt;td&gt;View own classes; record attendance; enter grades; send class announcements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin&lt;/td&gt;
&lt;td&gt;Manage users and fees; send school-wide announcements; view all records&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Model roles and permissions as data
&lt;/h2&gt;

&lt;p&gt;Start by modelling roles and permissions as plain Dart data, before any widget or route checks them. Two enums carry the whole system: AppRole for the roles and Permission for the capabilities. A single map connects them, and it is the only place in the app that knows which role can do what. The user model holds a list of roles, not a single role, and exposes a can() method so every check reads the same way:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
enum AppRole { parent, teacher, admin }&lt;/p&gt;

&lt;p&gt;enum Permission {&lt;br&gt;
  viewOwnChild,&lt;br&gt;
  viewClass,&lt;br&gt;
  recordAttendance,&lt;br&gt;
  editGrades,&lt;br&gt;
  sendAnnouncement,&lt;br&gt;
  manageFees,&lt;br&gt;
  manageUsers,&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;const Map&amp;gt; rolePermissions = {&lt;br&gt;
  AppRole.parent: {&lt;br&gt;
    Permission.viewOwnChild,&lt;br&gt;
  },&lt;br&gt;
  AppRole.teacher: {&lt;br&gt;
    Permission.viewClass,&lt;br&gt;
    Permission.recordAttendance,&lt;br&gt;
    Permission.editGrades,&lt;br&gt;
    Permission.sendAnnouncement,&lt;br&gt;
  },&lt;br&gt;
  AppRole.admin: {&lt;br&gt;
    Permission.viewClass,&lt;br&gt;
    Permission.recordAttendance,&lt;br&gt;
    Permission.editGrades,&lt;br&gt;
    Permission.sendAnnouncement,&lt;br&gt;
    Permission.manageFees,&lt;br&gt;
    Permission.manageUsers,&lt;br&gt;
  },&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;class AppUser {&lt;br&gt;
  const AppUser({required this.id, required this.roles});&lt;/p&gt;

&lt;p&gt;final String id;&lt;br&gt;
  final List roles;&lt;/p&gt;

&lt;p&gt;Set get permissions =&amp;gt;&lt;br&gt;
      roles.expand((role) =&amp;gt; rolePermissions[role] ?? {}).toSet();&lt;/p&gt;

&lt;p&gt;bool can(Permission permission) =&amp;gt; permissions.contains(permission);&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A parent who is also a teacher gets the union of both roles' permissions, with no special-casing anywhere else. When the school adds a role, you add one enum value and one map entry, and every screen, route and server rule picks it up. If you are setting up sign-in first, the &lt;a href="https://www.uzairxdev.online/blog/flutter-firebase-authentication-email-google-phone" rel="noopener noreferrer"&gt;Flutter Firebase authentication guide&lt;/a&gt; covers email, Google and phone auth end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate the UI by role, but never trust the UI alone
&lt;/h2&gt;

&lt;p&gt;The UI is where role checks are most visible, and it is also the layer where they matter least for security. Hiding a button stops an honest user from tapping it; it does nothing against a modified client or a direct API call. Gate the UI anyway, because a parent who never sees the fee-management screen cannot tap it by accident, and the interface stays clean for every role. Just treat UI gating as usability, not security, and enforce the same permission again on the server.&lt;/p&gt;

&lt;p&gt;Keep the current user in a small session object, provided through Provider, with one can() method that the whole widget tree shares:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
class UserSession extends ChangeNotifier {&lt;br&gt;
  AppUser? _user;&lt;/p&gt;

&lt;p&gt;AppUser? get user =&amp;gt; _user;&lt;br&gt;
  bool get isSignedIn =&amp;gt; _user != null;&lt;br&gt;
  bool can(Permission permission) =&amp;gt; _user?.can(permission) ?? false;&lt;/p&gt;

&lt;p&gt;void signIn(AppUser user) {&lt;br&gt;
    _user = user;&lt;br&gt;
    notifyListeners();&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;void updateUser(AppUser user) {&lt;br&gt;
    _user = user;&lt;br&gt;
    notifyListeners();&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;void signOut() {&lt;br&gt;
    _user = null;&lt;br&gt;
    notifyListeners();&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then wrap role-restricted UI in a tiny RoleGate widget instead of scattering if statements through every screen:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
class RoleGate extends StatelessWidget {&lt;br&gt;
  const RoleGate({&lt;br&gt;
    super.key,&lt;br&gt;
    required this.permission,&lt;br&gt;
    required this.child,&lt;br&gt;
    this.fallback = const SizedBox.shrink(),&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;final Permission permission;&lt;br&gt;
  final Widget child;&lt;br&gt;
  final Widget fallback;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/override"&gt;@override&lt;/a&gt;&lt;br&gt;
  Widget build(BuildContext context) {&lt;br&gt;
    final session = context.watch();&lt;br&gt;
    return session.can(permission) ? child : fallback;&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Usage reads plainly at the call site:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
RoleGate(&lt;br&gt;
  permission: Permission.manageFees,&lt;br&gt;
  child: ElevatedButton(&lt;br&gt;
    onPressed: _openFeeManagement,&lt;br&gt;
    child: const Text('Manage fees'),&lt;br&gt;
  ),&lt;br&gt;
)&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;One judgement call remains: hide or disable. Hide actions a role will never have, and disable with a short explanation the actions a role might gain later, so users understand the boundary instead of wondering where the button went.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect routes with go_router redirects
&lt;/h2&gt;

&lt;p&gt;UI gating hides buttons, but a user can still reach a screen through a deep link, a bookmarked route or a typed URL on Flutter web. Route protection closes that gap: the router checks the permission before the page builds, and sends an unauthorized user somewhere safe. In go_router that check lives in the redirect callback, which runs on every navigation. Pass the session as refreshListenable so the redirect re-runs whenever the session changes, not just when the user navigates:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
final session = UserSession();&lt;/p&gt;

&lt;p&gt;final router = GoRouter(&lt;br&gt;
  refreshListenable: session,&lt;br&gt;
  redirect: (context, state) {&lt;br&gt;
    final location = state.matchedLocation;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (!session.isSignedIn &amp;amp;&amp;amp; location != '/sign-in') {
  return '/sign-in';
}
if (location.startsWith('/admin') &amp;amp;&amp;amp;
    !session.can(Permission.manageUsers)) {
  return '/home';
}
if (location.startsWith('/fees') &amp;amp;&amp;amp;
    !session.can(Permission.manageFees)) {
  return '/home';
}
return null;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;},&lt;br&gt;
  routes: [&lt;br&gt;
    GoRoute(&lt;br&gt;
      path: '/home',&lt;br&gt;
      builder: (context, state) =&amp;gt; const HomeScreen(),&lt;br&gt;
    ),&lt;br&gt;
    GoRoute(&lt;br&gt;
      path: '/fees',&lt;br&gt;
      builder: (context, state) =&amp;gt; const FeesScreen(),&lt;br&gt;
    ),&lt;br&gt;
    GoRoute(&lt;br&gt;
      path: '/admin',&lt;br&gt;
      builder: (context, state) =&amp;gt; const AdminScreen(),&lt;br&gt;
    ),&lt;br&gt;
  ],&lt;br&gt;
);&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Returning null lets the navigation proceed; returning a path reroutes the user. Keep the redirect purely about access, not loading data or showing dialogs, because it runs often and must stay fast. Provide the same session instance to the widget tree with ChangeNotifierProvider.value, so the router and the UI always agree about who the user is. For the full navigation setup, see the &lt;a href="https://www.uzairxdev.online/blog/flutter-go-router-navigation-guide" rel="noopener noreferrer"&gt;go_router guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce every rule on the server
&lt;/h2&gt;

&lt;p&gt;Everything above runs on the user's device, which means a determined user can bypass it. The server is the only layer you control, so it is the only layer that can say no. Every request that reads or writes protected data must be checked against the caller's roles on the server, using the same role-to-permission map. The client checks exist for a good experience; the server checks exist for security. With Firestore, that means security rules that mirror the client map, including a parent rule that only matches their own child's document:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`&lt;br&gt;
rules_version = '2';&lt;br&gt;
service cloud.firestore {&lt;br&gt;
  match /databases/{database}/documents {&lt;br&gt;
    function roles() {&lt;br&gt;
      return get(/databases/$(database)/documents/users/$(request.auth.uid))&lt;br&gt;
          .data.roles;&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function hasRole(role) {
  return request.auth != null &amp;amp;&amp;amp; role in roles();
}

match /students/{studentId} {
  allow read: if hasRole('admin')
    || hasRole('teacher')
    || request.auth.uid in resource.data.parentIds;
  allow write: if hasRole('admin') || hasRole('teacher');
}

match /users/{userId} {
  allow read: if request.auth.uid == userId || hasRole('admin');
  allow update: if hasRole('admin');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The users rule matters as much as the students rule: roles live in the user document, so if users can edit their own document they can grant themselves admin and walk through every other rule. Only admins may write the roles field. The same principle applies to a REST backend like the one behind Muntazim: verify the auth token on the server, load the caller's roles from the database, and return 403 before touching any data. The client sends the token and the server decides:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
final token = await FirebaseAuth.instance.currentUser?.getIdToken();&lt;br&gt;
final response = await http.get(&lt;br&gt;
  Uri.parse('https://api.example.com/children/abc123/grades'),&lt;br&gt;
  headers: {'Authorization': 'Bearer ' + (token ?? '')},&lt;br&gt;
);&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If the token is missing or the role check fails, the server answers 401 or 403 and the client shows a clear message. Test the server rules with the Firebase emulator or a staging backend before shipping; a wrong rule in production either leaks data or locks out every parent at the school gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle role changes while the app is running
&lt;/h2&gt;

&lt;p&gt;Roles are not permanent. A school promotes a teacher to admin, or removes a parent's access when a child leaves. If the app caches the roles from sign-in and never refreshes them, the old permissions stay live until the next login, which can be days. Stream the user document instead, and update the session on every snapshot, so a role change takes effect while the app is open:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
Stream&amp;lt;AppUser&amp;gt; watchUser(String userId) {&lt;br&gt;
  return FirebaseFirestore.instance&lt;br&gt;
      .collection('users')&lt;br&gt;
      .doc(userId)&lt;br&gt;
      .snapshots()&lt;br&gt;
      .map((doc) {&lt;br&gt;
        final data = doc.data() ?? {};&lt;br&gt;
        final roles = (data['roles'] as List? ?? [])&lt;br&gt;
            .whereType&amp;lt;String&amp;gt;()&lt;br&gt;
            .where((name) =&amp;gt;&lt;br&gt;
                AppRole.values.any((role) =&amp;gt; role.name == name))&lt;br&gt;
            .map(AppRole.values.byName)&lt;br&gt;
            .toList();&lt;br&gt;
        return AppUser(id: doc.id, roles: roles);&lt;br&gt;
      });&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Filter out unknown role strings before calling byName, because byName throws on a name it does not recognise, and one bad value should not crash the app. Subscribe in the session, calling updateUser on each snapshot, and the router's refreshListenable does the rest: a demoted admin is redirected out of /admin the moment the snapshot arrives. Real-time updates flow through the same listeners in Muntazim, whether the payload is a role change or a school announcement; the &lt;a href="https://www.uzairxdev.online/blog/flutter-firebase-push-notifications-guide" rel="noopener noreferrer"&gt;Firebase push notifications guide&lt;/a&gt; covers the notification side in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that break role-based access
&lt;/h2&gt;

&lt;p&gt;Most role-based access failures come from the same few shortcuts, and each one has a boring fix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Checking roles instead of permissions&lt;/td&gt;
&lt;td&gt;Users with two roles break, and role names spread through the code&lt;/td&gt;
&lt;td&gt;Check Permission values from one role map&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gating only the UI&lt;/td&gt;
&lt;td&gt;Hidden screens still open through deep links or direct API calls&lt;/td&gt;
&lt;td&gt;Protect routes and enforce the same rule on the server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Letting users edit their own roles&lt;/td&gt;
&lt;td&gt;Anyone grants themselves admin&lt;/td&gt;
&lt;td&gt;Role fields are writable by admins only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching roles at sign-in forever&lt;/td&gt;
&lt;td&gt;Revoked access stays live until the next login&lt;/td&gt;
&lt;td&gt;Stream the user document and refresh the session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One role per user&lt;/td&gt;
&lt;td&gt;A teacher who is also a parent needs two accounts&lt;/td&gt;
&lt;td&gt;Store a list of roles and union the permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Putting it together in Muntazim
&lt;/h2&gt;

&lt;p&gt;In Muntazim, these layers stack the same way. Parents sign in with secure authentication, the app loads their roles, the interface shows only their own child's attendance, grades and fees, routes outside their role redirect away, notifications keep them posted on announcements, and the backend re-checks every request. No single layer carries the security; the layers agree with each other, so a failure in one is caught by the next. Role-based access control is not a widget or a rule, it is the same permission check repeated at every boundary a user can cross.&lt;/p&gt;

&lt;p&gt;If your app has more than one kind of user, add roles before the second user type ships. Retrofitting permissions across screens, routes and a backend costs far more than modelling them on day one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.uzairxdev.online/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; and let's talk about your project.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/flutter-role-based-access-control" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>rolebasedaccess</category>
      <category>authentication</category>
      <category>authorization</category>
    </item>
    <item>
      <title>Working with Humanizor AI: Crafting the Perfect Message</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Fri, 13 Nov 2026 03:02:54 +0000</pubDate>
      <link>https://dev.to/uzairxdev/working-with-humanizor-ai-crafting-the-perfect-message-32cm</link>
      <guid>https://dev.to/uzairxdev/working-with-humanizor-ai-crafting-the-perfect-message-32cm</guid>
      <description>&lt;h1&gt;
  
  
  Crafting the Perfect Message: Working Alongside Humanizor AI Assistants
&lt;/h1&gt;

&lt;p&gt;The future of writing isn't about AI replacing humans; it's about humans and AI collaborating seamlessly. Content creators are increasingly turning to &lt;a href="https://dev.to/blog/evolution-of-ai-text-humanizors"&gt;Humanizor AI assistants&lt;/a&gt; not to write for them, but to refine and elevate their own ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  A New Workflow
&lt;/h2&gt;

&lt;p&gt;The modern writing workflow often looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Brainstorming:&lt;/strong&gt; The human writer outlines the core concepts and emotional beats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drafting:&lt;/strong&gt; &lt;a href="https://firebase.google.com/docs/ai-logic" rel="noopener noreferrer"&gt;An LLM generates&lt;/a&gt; a rough, structured draft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Humanization:&lt;/strong&gt; The Humanizor AI steps in to reshape the text, adjusting the flow, tone, and vocabulary to sound natural and engaging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final Polish:&lt;/strong&gt; The human writer reviews and adds their final unique touch.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;This collaborative approach allows creators to produce high-quality content at scale while maintaining authenticity. The Humanizor AI acts as a sophisticated editor, ensuring the final product &lt;a href="https://dev.to/blog/passing-the-turing-test-ai-synthesis"&gt;resonates with human readers&lt;/a&gt; on a deeper level. The most successful creators of tomorrow will be those who master this symbiotic relationship with AI.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/crafting-content-with-humanizor-ai" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>contentcreation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Collaborative Travel Planner in Flutter</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Fri, 13 Nov 2026 03:01:48 +0000</pubDate>
      <link>https://dev.to/uzairxdev/collaborative-travel-planner-in-flutter-1f5k</link>
      <guid>https://dev.to/uzairxdev/collaborative-travel-planner-in-flutter-1f5k</guid>
      <description>&lt;p&gt;Planning a group trip usually means a spreadsheet for the itinerary, a group chat for decisions, and a notes app for the packing list — and none of them agree with each other. Someone books a flight the spreadsheet does not know about, the checklist lives on one person's phone, and the split expenses get settled weeks later from memory. That is the problem Turnie was built to solve: one collaborative app that centralizes itineraries, split expenses and packing checklists, so the whole group works from the same plan.&lt;/p&gt;

&lt;p&gt;I built Turnie in Flutter with Firebase as the backend, and implemented real-time collaboration using Firestore. More than 5,000 trips have been planned with it. This guide walks through the patterns that make a shared trip planner work in Flutter: a data model built for multiple editors, real-time listeners instead of manual refreshes, conflict-safe checklists and expense splits, offline support, and security rules that keep each trip private to its members.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the trip as shared documents
&lt;/h2&gt;

&lt;p&gt;A collaborative app starts with a data model that expects more than one writer. The natural shape in Firestore is one document per trip, with subcollections for the things that grow independently: itinerary days, checklist items and expenses. The trip document itself holds the fields every screen needs at a glance — the destination, the dates and the list of member user IDs:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
class Trip {&lt;br&gt;
  const Trip({&lt;br&gt;
    required this.id,&lt;br&gt;
    required this.name,&lt;br&gt;
    required this.destination,&lt;br&gt;
    required this.startDate,&lt;br&gt;
    required this.endDate,&lt;br&gt;
    this.memberIds = const [],&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;final String id;&lt;br&gt;
  final String name;&lt;br&gt;
  final String destination;&lt;br&gt;
  final DateTime startDate;&lt;br&gt;
  final DateTime endDate;&lt;br&gt;
  final List memberIds;&lt;/p&gt;

&lt;p&gt;factory Trip.fromJson(String id, Map json) {&lt;br&gt;
    return Trip(&lt;br&gt;
      id: id,&lt;br&gt;
      name: json['name'] as String,&lt;br&gt;
      destination: json['destination'] as String,&lt;br&gt;
      startDate: (json['startDate'] as Timestamp).toDate(),&lt;br&gt;
      endDate: (json['endDate'] as Timestamp).toDate(),&lt;br&gt;
      memberIds: List.from(json['memberIds'] as List? ?? []),&lt;br&gt;
    );&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;Map toJson() =&amp;gt; {&lt;br&gt;
        'name': name,&lt;br&gt;
        'destination': destination,&lt;br&gt;
        'startDate': Timestamp.fromDate(startDate),&lt;br&gt;
        'endDate': Timestamp.fromDate(endDate),&lt;br&gt;
        'memberIds': memberIds,&lt;br&gt;
      };&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Keep each checklist item and each expense as its own document in a subcollection, not as a list field on the trip. Two people editing the same list field overwrite each other; two people editing different documents never collide. That one decision removes most of the sync bugs a collaborative app can have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listen, do not fetch
&lt;/h2&gt;

&lt;p&gt;The defining feature of a collaborative app is that changes appear without anyone pressing refresh. In Firestore that means listening to a query with &lt;code&gt;snapshots()\&lt;/code&gt; and rebuilding the UI from the stream, instead of calling &lt;code&gt;get()\&lt;/code&gt; once and showing a stale copy:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
Stream&amp;lt;List&amp;lt;Trip&amp;gt;&amp;gt; watchTrips(String userId) {&lt;br&gt;
  return FirebaseFirestore.instance&lt;br&gt;
      .collection('trips')&lt;br&gt;
      .where('memberIds', arrayContains: userId)&lt;br&gt;
      .orderBy('startDate')&lt;br&gt;
      .snapshots()&lt;br&gt;
      .map((snapshot) =&amp;gt; snapshot.docs&lt;br&gt;
          .map((doc) =&amp;gt; Trip.fromJson(doc.id, doc.data()))&lt;br&gt;
          .toList());&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Wire that stream into a &lt;code&gt;StreamBuilder\&lt;/code&gt; and the screen updates the moment a travel companion adds a flight or checks off a bag. Two details matter. First, show pending writes: &lt;code&gt;snapshot.metadata.hasPendingWrites\&lt;/code&gt; tells you the device has local changes the server has not confirmed yet, so you can dim or badge them instead of pretending they are saved. Second, handle the error branch of the stream — a permission denial or a lost connection should show a clear message, not a spinner that never stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make shared checklists conflict-safe
&lt;/h2&gt;

&lt;p&gt;A packing checklist is the feature where two people tap at the same time, and the naive implementation loses one of the taps. If the checklist is a single array on the trip document, each phone reads the array, flips one item and writes the whole array back — the second write silently erases the first. The fix is to store every checklist item as its own document and toggle it with a single-field update:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
Future&amp;lt;void&amp;gt; toggleChecklistItem({&lt;br&gt;
  required String tripId,&lt;br&gt;
  required String itemId,&lt;br&gt;
  required bool packed,&lt;br&gt;
}) {&lt;br&gt;
  return FirebaseFirestore.instance&lt;br&gt;
      .collection('trips')&lt;br&gt;
      .doc(tripId)&lt;br&gt;
      .collection('checklist')&lt;br&gt;
      .doc(itemId)&lt;br&gt;
      .update({'packed': packed, 'packedBy': currentUserId});&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A single-field update touches only that field, so two people packing different items never conflict, and even two people toggling the same item resolve to the last write — which is the correct answer for a checkbox. Adding items is just as safe: each new item is a new document with an auto-generated ID, so two people adding at the same moment create two items instead of overwriting one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split expenses with transactions
&lt;/h2&gt;

&lt;p&gt;Split expenses are the one place where last-write-wins is not good enough. If two people add expenses at the same time and both read the running total, add their amount and write it back, one expense disappears from the total. Firestore transactions solve this: the transaction re-reads the document and retries if it changed underneath you:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
Future addExpense({&lt;br&gt;
  required String tripId,&lt;br&gt;
  required Expense expense,&lt;br&gt;
}) async {&lt;br&gt;
  final db = FirebaseFirestore.instance;&lt;br&gt;
  final tripRef = db.collection('trips').doc(tripId);&lt;/p&gt;

&lt;p&gt;await db.runTransaction((transaction) async {&lt;br&gt;
    final tripSnap = await transaction.get(tripRef);&lt;br&gt;
    final currentTotal = (tripSnap.data()?['totalSpent'] as num?) ?? 0;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transaction.set(tripRef.collection('expenses').doc(), expense.toJson());
transaction.update(tripRef, {
  'totalSpent': currentTotal + expense.amount,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Even simpler, when all you need is a running total, &lt;code&gt;FieldValue.increment()\&lt;/code&gt; applies the change on the server without reading first:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
tripRef.update({'totalSpent': FieldValue.increment(expense.amount)});&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Use the transaction when the write depends on the current value in a way increment cannot express — for example, capping the total or splitting by shares — and use &lt;code&gt;FieldValue.increment()\&lt;/code&gt; everywhere else. Either way, never compute a shared total on the client from a value you read seconds ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep it working offline
&lt;/h2&gt;

&lt;p&gt;A travel app is used in airports, on planes and in places with no signal, so offline behaviour is a feature, not an edge case. Firestore on Android and iOS persists its cache and queues writes by default, which means the app keeps working without a connection: listeners keep serving cached data, writes queue locally, and everything syncs when the connection returns.&lt;/p&gt;

&lt;p&gt;Design the UI around that reality. Show a small "offline — changes will sync" state when the device has no connection, and use &lt;code&gt;snapshot.metadata.hasPendingWrites\&lt;/code&gt; to mark queued items. Avoid patterns that break offline: do not require a server round trip before showing a screen, and do not treat a failed write as lost data when it may simply be queued. The same offline-first thinking applies to other apps too, and the full pattern is covered in the guide to &lt;a href="https://www.uzairxdev.online/blog/flutter-privacy-first-offline-app" rel="noopener noreferrer"&gt;privacy-first offline apps&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock each trip to its members
&lt;/h2&gt;

&lt;p&gt;A shared trip is also a private trip: the itinerary, the expenses and the packing list belong to the members and nobody else. Firestore security rules enforce that on the server, so a client bug can never leak another group's trip:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`&lt;br&gt;
rules_version = '2';&lt;br&gt;
service cloud.firestore {&lt;br&gt;
  match /databases/{database}/documents {&lt;br&gt;
    match /trips/{tripId} {&lt;br&gt;
      allow read, write: if request.auth != null&lt;br&gt;
        &amp;amp;&amp;amp; request.auth.uid in resource.data.memberIds;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  match /{subcollection}/{docId} {
    allow read, write: if request.auth != null
      &amp;amp;&amp;amp; request.auth.uid in get(/databases/$(database)/documents/trips/$(tripId)).data.memberIds;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Note the rule for adding a member: the new member is not in &lt;code&gt;memberIds\&lt;/code&gt; yet, so the invite itself has to be handled by a trusted path — a Cloud Function or a dedicated invites collection with its own rules — rather than a direct write from the invited user. The setup for the auth side of this is covered in the &lt;a href="https://www.uzairxdev.online/blog/flutter-firebase-authentication-email-google-phone" rel="noopener noreferrer"&gt;Firebase authentication guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the sync with two devices
&lt;/h2&gt;

&lt;p&gt;Real-time features cannot be tested on one emulator. Run the app on two devices or emulators signed in as two different members of the same trip, then exercise the cases that break naive implementations: toggle the same checklist item on both at once, add two expenses simultaneously, and kill the network on one device, make edits, and reconnect. Watch for the failure modes this article was designed around: a toggle that reverts, a total that drops an expense, or a screen that goes blank offline.&lt;/p&gt;

&lt;p&gt;Keep the data layer behind a repository so the screens never talk to Firestore directly. That separation is what lets you test the sync logic with fake streams in widget tests, and it is the same pattern described in the guide to the &lt;a href="https://www.uzairxdev.online/blog/flutter-repository-pattern-stop-putting-api-calls-in-widgets" rel="noopener noreferrer"&gt;repository pattern&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;A collaborative travel planner is not a harder app to build than a single-user one — it is a different set of defaults. Model shared things as documents, not list fields. Listen instead of fetching. Update single fields instead of whole objects. Use transactions or server-side increments for shared totals. Let the offline cache do its job, lock each trip to its members with security rules, and test everything with two devices before you ship.&lt;/p&gt;

&lt;p&gt;That is how Turnie works: one shared plan for the whole group, with itineraries, split expenses and packing checklists that stay in sync wherever the trip takes you. You can see the app in the &lt;a href="https://www.uzairxdev.online/projects/turnie" rel="noopener noreferrer"&gt;Turnie case study&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.uzairxdev.online/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; and let's talk about your project.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/flutter-collaborative-travel-planner-firestore" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firestore</category>
      <category>firebase</category>
      <category>realtime</category>
    </item>
    <item>
      <title>Synthesis: How AI Humanizors Are Passing the Turing Test</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Thu, 12 Nov 2026 03:01:26 +0000</pubDate>
      <link>https://dev.to/uzairxdev/synthesis-how-ai-humanizors-are-passing-the-turing-test-4apf</link>
      <guid>https://dev.to/uzairxdev/synthesis-how-ai-humanizors-are-passing-the-turing-test-4apf</guid>
      <description>&lt;h1&gt;
  
  
  Synthesis: How AI Humanizors Are Passing the Turing Test
&lt;/h1&gt;

&lt;p&gt;The Turing Test, proposed by Alan Turing in 1950, has long been the gold standard for measuring artificial intelligence. If a machine can converse with a human without being detected as a machine, it passes. Today, with the advent of &lt;a href="https://dev.to/blog/evolution-of-ai-text-humanizors"&gt;sophisticated AI humanizors&lt;/a&gt;, this is no longer a hypothetical scenario—it's a daily occurrence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Role of AI Humanizors
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developer.apple.com/design/human-interface-guidelines/generative-ai" rel="noopener noreferrer"&gt;Standard language models&lt;/a&gt; prioritize safety, neutrality, and factual accuracy. This results in a distinct "AI voice" that is easily detectable. AI humanizors counteract this by deliberately introducing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conversational Asymmetry:&lt;/strong&gt; Real humans don't always answer questions directly; they tangent, they pause, they reflect. Humanizors simulate this cognitive meandering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual Imperfection:&lt;/strong&gt; Slight structural irregularities make text feel grounded and less "plastic."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep Personalization:&lt;/strong&gt; &lt;a href="https://dev.to/blog/crafting-content-with-humanizor-ai"&gt;Adapting tone&lt;/a&gt; to match specific demographics or individual personas.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Ethical Implications
&lt;/h2&gt;

&lt;p&gt;As AI text becomes indistinguishable from human writing, we face new challenges. How do we verify authenticity? What does this mean for academic integrity, journalism, and personal communication? The technology is here, and the conversation around how to use it responsibly is just beginning.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/passing-the-turing-test-ai-synthesis" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Build a Period Tracker App in Flutter</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Thu, 12 Nov 2026 03:00:19 +0000</pubDate>
      <link>https://dev.to/uzairxdev/build-a-period-tracker-app-in-flutter-1k49</link>
      <guid>https://dev.to/uzairxdev/build-a-period-tracker-app-in-flutter-1k49</guid>
      <description>&lt;p&gt;A period tracker looks simple on the surface: a calendar, a few logged dates, a prediction. Underneath it is one of the most demanding apps a Flutter developer can build. The dates have to be right — a wrong prediction erodes trust faster than any crash. The data is some of the most sensitive health information a person carries, so privacy cannot be an afterthought. And cycles are irregular by nature, so the math has to adapt instead of assuming a textbook 28-day cycle.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://www.uzairxdev.online/projects/hercycle" rel="noopener noreferrer"&gt;HerCycle&lt;/a&gt;, a privacy-first period tracker with a menstrual calendar, an irregular-cycle prediction algorithm, ovulation and fertile-window estimates, symptom and mood logging, and health reports — all stored on-device, account-free and fully offline, with a PIN and biometric app lock. This guide walks through building that kind of app in Flutter, with working Dart code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model cycle data as dates, not strings
&lt;/h2&gt;

&lt;p&gt;Everything in a period tracker derives from dates: when periods started, how long cycles lasted, when the next one is due. Keep one entry per logged day, with the date normalized to midnight and stored as an ISO 8601 string. It survives app restarts, database migrations and time zone changes without ambiguity, and &lt;code&gt;DateTime.parse&lt;/code&gt; reads it back exactly.&lt;/p&gt;

&lt;p&gt;Model each day as an immutable class with &lt;code&gt;fromJson&lt;/code&gt; and &lt;code&gt;toJson&lt;/code&gt;, so the storage layer stays boring and the logic layer works with real &lt;code&gt;DateTime&lt;/code&gt; values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;FlowLevel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;medium&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heavy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spotting&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Mood&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;great&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;okay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;irritable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DayEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;DayEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symptoms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// always normalized to midnight&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;FlowLevel&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;symptoms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Mood&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="nc"&gt;DayEntry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DayEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'flow'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
          &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FlowLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'flow'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;symptoms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'symptoms'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'mood'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
          &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Mood&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'mood'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toIso8601String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'flow'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'symptoms'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;symptoms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'mood'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalizing to midnight is the detail that saves you from the classic bug in this kind of app: two &lt;code&gt;DateTime&lt;/code&gt; values for the same calendar day that are not equal because one carries a time component. Strip the time the moment a date enters the app, and run every comparison, set lookup and difference calculation through normalized dates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="nf"&gt;normalizeDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only remember one rule from this article, remember this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compute cycle length from real history
&lt;/h2&gt;

&lt;p&gt;A cycle length is the number of days between two consecutive period start dates. Deriving it from logged data — rather than asking the user to type in a number — is what makes the app feel smart and keeps the predictions honest. Given a list of period starts, the lengths are the gaps between neighbours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;cycleLengths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;periodStarts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;periodStarts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;lengths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;difference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two edge cases matter. A brand-new user has no history, so there is nothing to average — the app must say so plainly and ask for at least two logged periods before predicting anything, instead of inventing a number. And a user can log a start date by mistake, so validate input: reject future dates and reject a start date inside an already logged period. Bad data in means wrong predictions out, and the user will blame the algorithm, not the typo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Predict the next period, and handle irregular cycles
&lt;/h2&gt;

&lt;p&gt;The naive prediction adds a fixed 28 days to the last period start. That is wrong for most people, most of the time. Real cycles vary, and a tracker that assumes regularity will be confidently wrong again and again. HerCycle predicts periods with an irregular-cycle algorithm that adapts to cycle variations, and the simplest robust version of that idea is the median of recent cycle lengths: it ignores outliers, so one unusually short or long cycle does not drag the prediction with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;predictNextPeriod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;periodStarts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;lengths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cycleLengths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;periodStarts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
      &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sublist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isOdd&lt;/span&gt;
      &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;~/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;~/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;~/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;~/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;lastStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;periodStarts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;normalizeDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastStart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using only the last six cycles keeps the prediction responsive to recent change. Present the result as a predicted window, not a promise: "expected around 22 December" holds up far better than a countdown that claims certainty it does not have. Predictions are estimates, and the app should say so, with a clear medical disclaimer that estimates are not medical advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estimate ovulation and the fertile window
&lt;/h2&gt;

&lt;p&gt;Once the next period is predicted, ovulation and the fertile window follow from it. The common estimation rule is that ovulation happens roughly 14 days before the next period starts, and the fertile window covers the five days before ovulation plus ovulation day itself. It is an estimate built on an estimate, so it inherits all of the uncertainty of the period prediction — another reason to show a range rather than a single day.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;ovulation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;fertileStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;fertileEnd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;fertileWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;predictedNextPeriod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ovulation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;predictedNextPeriod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ovulation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ovulation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fertileStart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ovulation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fertileEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ovulation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HerCycle refines these estimates with optional inputs: basal body temperature, cervical mucus observations, and a pregnancy due date. Each is a separate, optional signal the user can add when they have it and ignore when they do not — never require data the user may not have, and never let a missing input break the core flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the calendar UI
&lt;/h2&gt;

&lt;p&gt;The calendar is the home screen of a period tracker, and it has one job: make the state of the cycle readable at a glance. Colour-code each day by its type — logged period days, the predicted next period, the fertile window, ovulation day, and today — and keep the legend to four or five colours at most. A calendar with twelve shades of pink communicates nothing.&lt;/p&gt;

&lt;p&gt;Derive the colour from a pure function that maps a date to a day type, so the UI stays dumb and the logic stays testable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;DayType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictedPeriod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fertile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ovulation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;none&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;DayType&lt;/span&gt; &lt;span class="nf"&gt;dayType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loggedPeriodDays&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;predictedStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;fertileStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;fertileEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ovulation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loggedPeriodDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DayType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predictedStart&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;normalizeDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predictedStart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DayType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predictedPeriod&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ovulation&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;normalizeDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ovulation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DayType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ovulation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fertileStart&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;fertileEnd&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fertileStart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fertileEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DayType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fertile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DayType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make this work. The set of logged period days must contain normalized dates, or &lt;code&gt;contains&lt;/code&gt; will miss days that are actually logged. And the order of the checks is the priority order: a logged period day beats a prediction, and ovulation beats the fertile window it sits inside. Add a next-period countdown chip above the calendar — "next period in 6 days" is the single most checked number in the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log symptoms, mood and flow, then turn them into a report
&lt;/h2&gt;

&lt;p&gt;Logging is where a tracker earns its keep between periods. HerCycle logs symptoms, PMS and mood alongside flow, and turns that history into cycle health reports users can share with their doctor. The logging UI should be fast: one tap per symptom, a small set of mood options, a flow selector — because nobody will fill in a form every day, but many people will tap three chips.&lt;/p&gt;

&lt;p&gt;Store the entries keyed by normalized date so each day holds at most one entry, and build the report from the same data the calendar already uses: recent cycle lengths and their variation, average flow days, the most frequently logged symptoms, and the mood trend across the cycle. Generate the report as a plain summary the user can share from the app — the numbers that help a doctor are the recent history, not a lifetime of raw data. The same on-device patterns apply to &lt;a href="https://www.uzairxdev.online/blog/flutter-health-vitals-tracker" rel="noopener noreferrer"&gt;a health vitals tracker&lt;/a&gt; too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remind without a server
&lt;/h2&gt;

&lt;p&gt;A tracker that never reminds is a tracker the user forgets to open. The reminder for a predicted period belongs on the device, scheduled locally — there is no reason to involve a server, and every reason not to. Use &lt;code&gt;flutter_local_notifications&lt;/code&gt; with timezone-aware scheduling, and schedule the reminder a day before the predicted start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nc"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;schedulePeriodReminder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;predictedStart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;reminderDay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;predictedStart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;flutterLocalNotificationsPlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Period expected tomorrow'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Your next period is predicted to start tomorrow.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TZDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reminderDay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;NotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AndroidNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cycle'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Cycle reminders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;androidScheduleMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AndroidScheduleMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exactAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uiLocalNotificationDateInterpretation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nc"&gt;UILocalNotificationDateInterpretation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;absoluteTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reschedule reminders whenever the prediction changes — a newly logged period moves everything, so cancel the old notification and schedule a fresh one in the same step. The full setup, including the timezone package and the Android exact-alarm permission, is covered in the &lt;a href="https://www.uzairxdev.online/blog/flutter-local-notifications-guide" rel="noopener noreferrer"&gt;local notifications guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy is the architecture, not a setting
&lt;/h2&gt;

&lt;p&gt;This is the section that decides whether people trust the app. Cycle data is intimate health data, and the only privacy promise that holds up is a structural one: everything is stored on-device, there is no account, no registration, and no server sync. HerCycle works fully offline for exactly this reason, and that architecture is not a marketing line — it is the product. The full pattern is covered in the guide to &lt;a href="https://www.uzairxdev.online/blog/flutter-privacy-first-offline-app" rel="noopener noreferrer"&gt;privacy-first offline apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On-device storage only holds if the phone itself cannot be picked up and read by anyone else, which is why the app ships a PIN and biometric app lock. Gate the app behind &lt;code&gt;local_auth&lt;/code&gt; before any cycle data renders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;didAuthenticate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;LocalAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;localizedReason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Unlock HerCycle to view your cycle data'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;AuthenticationOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;biometricOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If authentication fails or is unavailable, fall back to the PIN screen — never to an unlocked view. And keep the analytics honest: crash reporting that uploads device data by default quietly breaks the promise the app just made, so leave it out or make it opt-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the algorithm with fixed dates
&lt;/h2&gt;

&lt;p&gt;Date math is exactly the kind of code that looks right and is wrong, so test it with fixed dates. A small test file with known histories pins down the median and the empty-history case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'predicts the next period from the median cycle length'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;starts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;predictNextPeriod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'returns null with fewer than two logged periods'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;predictNextPeriod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isNull&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the first test by hand: the gaps are 28, 29 and 27 days, so the median is 28, and 24 November plus 28 days is 22 December. Add a test with one wildly irregular cycle — a 45-day gap among 28-day cycles — and confirm the median ignores it while a plain average would not. That test is the whole argument for the median in one assertion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;A period tracker is a small set of well-separated pieces: a date model normalized to midnight, cycle lengths derived from logged history, a median-based prediction that adapts to irregular cycles, an ovulation and fertile-window estimate built on that prediction, a calendar that renders a pure day-type function, fast per-day logging that feeds a shareable health report, local reminders, and on-device storage behind a PIN or biometric lock. Each piece is easy to test on its own, and none of them needs a backend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.uzairxdev.online/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; and let's talk about your project.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/flutter-period-tracker-app" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>healthapps</category>
      <category>privacy</category>
    </item>
    <item>
      <title>From Senior Engineer to Tech Lead: Skills You Need</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Wed, 11 Nov 2026 03:01:00 +0000</pubDate>
      <link>https://dev.to/uzairxdev/from-senior-engineer-to-tech-lead-skills-you-need-14c2</link>
      <guid>https://dev.to/uzairxdev/from-senior-engineer-to-tech-lead-skills-you-need-14c2</guid>
      <description>&lt;h1&gt;
  
  
  From Senior Engineer to Tech Lead: The Skills Code Doesn't Teach You
&lt;/h1&gt;

&lt;p&gt;For the first five to eight years of a software engineer's career, the measure of success is straightforward: &lt;strong&gt;How much high-quality code can you ship?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You master design patterns, commit thousands of lines of well-tested code, optimize build pipelines, and squash edge-case bugs. You feel unstoppable.&lt;/p&gt;

&lt;p&gt;Then, you receive the promotion: &lt;strong&gt;Tech Lead&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suddenly, the skills that got you here are no longer sufficient. If you attempt to lead by simply writing twice as much code, you will quickly burn out, create bottlenecks, and disempower your teammates.&lt;/p&gt;

&lt;p&gt;Here is what the transition from Senior Engineer to Tech Lead actually requires, and how to become a genuine force multiplier.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Escaping the "Individual Contributor Trap"
&lt;/h2&gt;

&lt;p&gt;The most difficult psychological shift for a new Tech Lead is stepping back from the keyboard.&lt;/p&gt;

&lt;p&gt;As a Senior Engineer, your contribution is &lt;strong&gt;linear&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Your Output = Your Hours × Your Technical Skill&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;As a Tech Lead, your contribution must be &lt;strong&gt;multiplicative&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Your Output = Team Velocity × Architectural Clarity × Unblocked Decisions&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you assign yourself the most critical, complex architectural feature on the critical path, you become the team's single point of failure. While you are heads-down coding for six hours, three other developers are blocked &lt;a href="https://dev.to/blog/mastering-code-reviews-empathy"&gt;waiting for code reviews&lt;/a&gt;, requirements clarification, or API contracts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Golden Rule:&lt;/strong&gt; The Tech Lead's primary job is not to write all the code, but to ensure that the best code gets written.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Architectural Stewardship &amp;amp; Technical Design Documents (RFCs)
&lt;/h2&gt;

&lt;p&gt;A Tech Lead sets the technical direction without dictating every line. The primary tool for this is the &lt;strong&gt;RFC (Request for Comments) / Technical Design Document&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before kicking off a multi-sprint initiative, write a concise 2-to-3 page design doc covering:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Problem Statement:&lt;/strong&gt; What business or user pain are we solving?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-Goals:&lt;/strong&gt; What are we explicitly &lt;em&gt;not&lt;/em&gt; building in this iteration?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proposed Architecture:&lt;/strong&gt; System diagrams, data contracts, &lt;a href="https://dev.to/blog/state-management-architecture"&gt;state management patterns&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Considered Alternatives &amp;amp; Tradeoffs:&lt;/strong&gt; Why did we choose Approach A over Approach B?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure Modes &amp;amp; Migration Strategy:&lt;/strong&gt; How does the system degrade gracefully when things break?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Circulate the document among your team. Encourage junior and mid-level engineers to challenge assumptions. When the team collaboratively reviews the blueprint before writing code, implementation velocity accelerates dramatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Defending Developer Flow
&lt;/h2&gt;

&lt;p&gt;A major source of engineering friction is context switching: spontaneous meetings, vague ticket specifications, and shifting priorities.&lt;/p&gt;

&lt;p&gt;As a Tech Lead, you are the &lt;strong&gt;buffer&lt;/strong&gt; between external business turbulence and your team's focus time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Partner closely with your Product Manager to ensure user stories meet the "Definition of Ready" before sprint kickoff.&lt;/li&gt;
&lt;li&gt;Aggressively decline unnecessary meetings on behalf of your team. Protect long, uninterrupted blocks of maker time.&lt;/li&gt;
&lt;li&gt;Translate ambiguous business requirements into precise, actionable technical tasks.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Cultivating Psychological Safety &amp;amp; Blameless Culture
&lt;/h2&gt;

&lt;p&gt;High-performing teams are not teams that never experience bugs; they are teams that learn exponentially from failures without fear.&lt;/p&gt;

&lt;p&gt;When a critical production bug slips through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never ask:&lt;/strong&gt; &lt;em&gt;"Who wrote this code?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always ask:&lt;/strong&gt; &lt;em&gt;"What gap in our automated test suite, &lt;a href="https://dev.to/blog/automating-flutter-workflows-cicd-2026"&gt;CI/CD pipeline&lt;/a&gt;, or review process allowed this regression to reach production?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lead &lt;strong&gt;Blameless Postmortems&lt;/strong&gt;. Document the incident timeline, the root cause, the immediate mitigation, and the preventive action items. When engineers know they won't be penalized for honest mistakes, they build with greater confidence, innovate faster, and take ownership of systemic quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Mentorship as an Architectural Investment
&lt;/h2&gt;

&lt;p&gt;The ultimate test of a Tech Lead's efficacy is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you take a two-week vacation completely off the grid, does the engineering team continue to ship features smoothly without you?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, you haven't built a team—you have built a dependency on yourself.&lt;/p&gt;

&lt;p&gt;Invest proactively in your team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pair Programming:&lt;/strong&gt; Don't just point out flaws in pull requests; jump on a 20-minute pairing session to demonstrate problem-solving workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strategic Delegation:&lt;/strong&gt; Give challenging architectural modules to mid-level engineers with your guidance, rather than hoarding the fun work for yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constructive PR Reviews:&lt;/strong&gt; Frame feedback as inquiries (&lt;em&gt;"How does this handle &lt;a href="https://developer.android.com/topic/performance/memory" rel="noopener noreferrer"&gt;low-memory scenarios&lt;/a&gt;?"&lt;/em&gt;) rather than commands (&lt;em&gt;"Change this"&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Transition Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mindset&lt;/th&gt;
&lt;th&gt;Senior Engineer&lt;/th&gt;
&lt;th&gt;Tech Lead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary Focus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;My code &amp;amp; my tickets&lt;/td&gt;
&lt;td&gt;Team velocity &amp;amp; system health&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solving Problems&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"I will fix it myself"&lt;/td&gt;
&lt;td&gt;"I will empower the team to solve it"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Communication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Technical precision&lt;/td&gt;
&lt;td&gt;Strategic empathy &amp;amp; business alignment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Success Metric&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Features shipped by me&lt;/td&gt;
&lt;td&gt;Capabilities unlocked across the team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Leading technical teams is challenging, but seeing an entire engineering team thrive, grow, and deliver exceptional products together is the most rewarding milestone in a software engineer's career.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/senior-to-tech-lead-engineering-journey" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>leadership</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Build a Quiz App in Flutter: Full Guide</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Wed, 11 Nov 2026 02:59:48 +0000</pubDate>
      <link>https://dev.to/uzairxdev/build-a-quiz-app-in-flutter-full-guide-5ffe</link>
      <guid>https://dev.to/uzairxdev/build-a-quiz-app-in-flutter-full-guide-5ffe</guid>
      <description>&lt;p&gt;Quiz apps are one of the most useful projects a Flutter developer can build. The pattern shows up everywhere: exam prep, language learning, driving theory, onboarding quizzes, and trivia games. Underneath the friendly interface, a quiz app is a compact engineering problem — a question bank, a fair way to pick and order questions, state that survives taps and rotations, scoring, and progress that persists between sessions.&lt;/p&gt;

&lt;p&gt;I built the practice test engine for &lt;a href="https://www.uzairxdev.online/projects/us-citizenship-test" rel="noopener noreferrer"&gt;AI US Citizenship Test 2026&lt;/a&gt;, an app that prepares green card holders for the US naturalization interview. The app covers 100 civics questions, the English test, and the N-400 interview, with an AI tutor that explains answers in plain words, realistic mock interviews, and an audio study mode. The quiz engine at its core is the same one any quiz app needs, and this guide walks through building it from scratch with working Dart code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the question bank first
&lt;/h2&gt;

&lt;p&gt;Every quiz starts with data, not widgets. A question needs its text, a list of options, the index of the correct option, and an optional explanation shown after the user answers. Model it as an immutable class with a &lt;code&gt;fromJson\&lt;/code&gt; factory so the bank can live in a JSON file:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
class Question {&lt;br&gt;
  const Question({&lt;br&gt;
    required this.text,&lt;br&gt;
    required this.options,&lt;br&gt;
    required this.correctIndex,&lt;br&gt;
    this.explanation,&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;final String text;&lt;br&gt;
  final List options;&lt;br&gt;
  final int correctIndex;&lt;br&gt;
  final String? explanation;&lt;/p&gt;

&lt;p&gt;factory Question.fromJson(Map json) {&lt;br&gt;
    return Question(&lt;br&gt;
      text: json['text'] as String,&lt;br&gt;
      options: List.from(json['options'] as List),&lt;br&gt;
      correctIndex: json['correctIndex'] as int,&lt;br&gt;
      explanation: json['explanation'] as String?,&lt;br&gt;
    );&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Keeping the bank in JSON has a practical advantage: content and code stay separate. A subject-matter expert can add questions without touching Dart, and the same file can feed a practice mode, a timed test, and a review screen. Validate the data on load — every question should have at least two options, and &lt;code&gt;correctIndex\&lt;/code&gt; must point at a real option. A single bad index crashes the scoring logic later, so fail loudly at load time instead of silently during a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load the bank from an asset
&lt;/h2&gt;

&lt;p&gt;Ship the questions as a JSON asset bundled with the app. That keeps the quiz working offline, which matters for a study app — users revise on the train, in waiting rooms, and anywhere else with no signal. Load it once when the quiz screen starts:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
Future&amp;lt;List&amp;lt;Question&amp;gt;&amp;gt; loadQuestions(String assetPath) async {&lt;br&gt;
  final raw = await rootBundle.loadString(assetPath);&lt;br&gt;
  final decoded = jsonDecode(raw) as List;&lt;br&gt;
  return decoded&lt;br&gt;
      .map((item) =&amp;gt; Question.fromJson(item as Map&amp;lt;String, dynamic&amp;gt;))&lt;br&gt;
      .toList();&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Register the file under &lt;code&gt;assets:\&lt;/code&gt; in &lt;code&gt;pubspec.yaml\&lt;/code&gt;. For a large bank, split it by category or chapter into several files and load only the set the user picked. A civics bank with hundreds of questions loads fast either way, but a 10 MB JSON file parsed on the main isolate will drop frames — for very large banks, parse on a background isolate with &lt;code&gt;compute\&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shuffle fairly, and never repeat a question
&lt;/h2&gt;

&lt;p&gt;A quiz that shows the same questions in the same order every time stops testing anything after the second run. Shuffle a copy of the bank, take the first N questions, and keep the original list untouched:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
List&amp;lt;Question&amp;gt; buildQuiz(&lt;br&gt;
  List&amp;lt;Question&amp;gt; bank, {&lt;br&gt;
  int count = 10,&lt;br&gt;
  int? seed,&lt;br&gt;
}) {&lt;br&gt;
  final questions = List&amp;lt;Question&amp;gt;.of(bank)..shuffle(Random(seed));&lt;br&gt;
  return questions.take(count).toList();&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Two details matter here. First, copy before shuffling: &lt;code&gt;List.of(bank)\&lt;/code&gt; creates a new list, so the bank keeps its original order for the review screen and for the next session. Second, shuffle once per quiz, not once per build. If the shuffle lives inside &lt;code&gt;build()\&lt;/code&gt;, every rebuild reorders the questions under the user's finger. Do it in &lt;code&gt;initState\&lt;/code&gt; or in the controller that owns the quiz state.&lt;/p&gt;

&lt;p&gt;Passing a seed makes a shuffle reproducible, which is handy for tests and for a mock interview mode where the teacher and the student need the same question order. In production, leave the seed out and let &lt;code&gt;Random()\&lt;/code&gt; draw from the system entropy source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hold quiz state in a small controller
&lt;/h2&gt;

&lt;p&gt;A quiz has a clear lifecycle: ready, answering, finished. Encoding that as an enum keeps the UI honest — the results screen cannot appear while a question is still unanswered, and the answer buttons cannot be tapped after the quiz ends. A &lt;code&gt;ChangeNotifier\&lt;/code&gt; holds the current index, the answers map, and the score:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
enum QuizPhase { ready, answering, finished }&lt;/p&gt;

&lt;p&gt;class QuizController extends ChangeNotifier {&lt;br&gt;
  QuizController(this.questions);&lt;/p&gt;

&lt;p&gt;final List questions;&lt;br&gt;
  final Map answers = {};&lt;br&gt;
  int currentIndex = 0;&lt;br&gt;
  QuizPhase phase = QuizPhase.ready;&lt;/p&gt;

&lt;p&gt;Question get current =&amp;gt; questions[currentIndex];&lt;/p&gt;

&lt;p&gt;void answer(int optionIndex) {&lt;br&gt;
    answers[currentIndex] = optionIndex;&lt;br&gt;
    notifyListeners();&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;void next() {&lt;br&gt;
    if (currentIndex &amp;lt; questions.length - 1) {&lt;br&gt;
      currentIndex++;&lt;br&gt;
    } else {&lt;br&gt;
      phase = QuizPhase.finished;&lt;br&gt;
    }&lt;br&gt;
    notifyListeners();&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;int get score =&amp;gt; answers.entries&lt;br&gt;
      .where((entry) =&amp;gt; questions[entry.key].correctIndex == entry.value)&lt;br&gt;
      .length;&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Store answers as a map from question index to chosen option index, not as a list of booleans. The map makes the review screen trivial: iterate the questions, look up each answer, and show the correct option next to the one the user picked. It also lets the user go back and change an answer before finishing, which a boolean list cannot express.&lt;/p&gt;

&lt;p&gt;Expose the controller with a &lt;code&gt;ChangeNotifierProvider\&lt;/code&gt; above the quiz screen. Widgets that only show the question text can listen to it; the progress bar and the score display rebuild when the state changes, and nothing else does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Score at the end, not during the run
&lt;/h2&gt;

&lt;p&gt;Score the quiz when it finishes, from the answers map, instead of incrementing a counter on every tap. The two approaches agree when everything works, but the derived score stays correct when a user changes an answer, when a question is skipped, and when the app restores a half-finished session. Derived state cannot drift out of sync with its source.&lt;/p&gt;

&lt;p&gt;Show the result as more than a number. A pass mark — for example, 6 out of 10 — tells the user whether they are ready, and a per-question review tells them what to study next. Include the explanation from the question model on the review screen: a wrong answer without an explanation is a missed teaching moment, and for a test-prep app the explanation is the product. In the citizenship app, the AI tutor takes this further and explains civics answers in plain words when the short explanation is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save progress on the device
&lt;/h2&gt;

&lt;p&gt;Users abandon a quiz halfway and come back the next day. Save the best score and the last completed set with &lt;code&gt;shared_preferences\&lt;/code&gt;, and only write when the new score beats the stored one:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;dart&lt;br&gt;
Future&amp;lt;void&amp;gt; saveBestScore(int score) async {&lt;br&gt;
  final prefs = await SharedPreferences.getInstance();&lt;br&gt;
  final best = prefs.getInt('best_score') ?? 0;&lt;br&gt;
  if (score &amp;gt; best) {&lt;br&gt;
    await prefs.setInt('best_score', score);&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For richer state — the answers of an unfinished quiz, a streak of study days, per-category scores — use a local database such as Drift or Isar instead of piling keys into preferences. The rule of thumb: preferences for a handful of settings and best scores, a database for anything shaped like a table.&lt;/p&gt;

&lt;p&gt;Keep everything on-device unless the app genuinely needs an account. The citizenship app stores practice progress locally, and the same approach fits most quiz apps: no login wall, no sync conflicts, and the quiz works the moment the app opens. If you later add cloud backup, make it opt-in and sync only after the local write succeeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a per-question timer for exam mode
&lt;/h2&gt;

&lt;p&gt;A practice mode can be untimed, but a mock exam needs a clock. A &lt;code&gt;Timer.periodic\&lt;/code&gt; ticks once a second, and when it reaches zero the question counts as unanswered and the quiz moves on:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
Timer? _timer;&lt;br&gt;
int secondsLeft = 30;&lt;/p&gt;

&lt;p&gt;void _startTimer() {&lt;br&gt;
  _timer?.cancel();&lt;br&gt;
  secondsLeft = 30;&lt;br&gt;
  _timer = Timer.periodic(const Duration(seconds: 1), (timer) {&lt;br&gt;
    secondsLeft--;&lt;br&gt;
    if (secondsLeft &amp;lt;= 0) {&lt;br&gt;
      timer.cancel();&lt;br&gt;
      next(); // Unanswered counts as wrong.&lt;br&gt;
    }&lt;br&gt;
    notifyListeners();&lt;br&gt;
  });&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Cancel the timer in &lt;code&gt;dispose\&lt;/code&gt; and restart it on every &lt;code&gt;next()\&lt;/code&gt;. A timer that keeps firing after the screen is gone leaks, and a timer that is not restarted gives the second question the first question's remaining seconds. Show the countdown as a thin progress bar above the question — a number alone is easy to miss, and a shrinking bar communicates urgency without stealing attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes to avoid
&lt;/h2&gt;

&lt;p&gt;The most common bug in a first quiz app is shuffling inside &lt;code&gt;build()\&lt;/code&gt;, which reorders the questions on every rebuild. The second is storing the selected answer in the widget's local state instead of the controller: the selection vanishes on rotation or when the user navigates to the review screen and back. The third is scoring by incrementing a counter in the tap handler, which breaks the moment an answer can change.&lt;/p&gt;

&lt;p&gt;A subtler one is loading the question bank on every screen. Load it once, pass the list down, and keep a single source of truth. And validate the bank at startup in debug mode with an assert: every question needs at least two options, a valid &lt;code&gt;correctIndex\&lt;/code&gt;, and a non-empty question text. Bad content is far cheaper to catch in development than in a user's test session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;A quiz app is a small set of well-separated pieces: a question model and a JSON bank, a shuffle that runs once, a controller holding an explicit phase and an answers map, a score derived from that map, local persistence for progress, and an optional timer for exam mode. Each piece is easy to test on its own, and none of them needs a backend.&lt;/p&gt;

&lt;p&gt;Start with ten questions in a single JSON file and the controller above. Add categories, review screens, and timed mock exams one at a time, testing each against the real question bank. The same structure scales from a trivia game to a serious exam-prep tool — the citizenship app's practice test engine, mock interviews, and audio study mode all sit on top of this exact foundation. If you want to round out the experience, pair the quiz with &lt;a href="https://www.uzairxdev.online/blog/flutter-zip-code-autofill" rel="noopener noreferrer"&gt;ZIP-code autofill&lt;/a&gt; for location-specific questions, &lt;a href="https://www.uzairxdev.online/blog/flutter-offline-audio-playback" rel="noopener noreferrer"&gt;offline audio playback&lt;/a&gt; for a study mode that works without a connection, and &lt;a href="https://www.uzairxdev.online/blog/flutter-local-notifications-guide" rel="noopener noreferrer"&gt;local notifications&lt;/a&gt; for daily study reminders.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.uzairxdev.online/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; and let's talk about your project.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/flutter-quiz-app-question-bank-scoring" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>quizapp</category>
      <category>dart</category>
      <category>statemanagement</category>
    </item>
    <item>
      <title>Production Mobile Security: Hardening Apps Against Attacks</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Tue, 10 Nov 2026 03:01:52 +0000</pubDate>
      <link>https://dev.to/uzairxdev/production-mobile-security-hardening-apps-against-attacks-1e1h</link>
      <guid>https://dev.to/uzairxdev/production-mobile-security-hardening-apps-against-attacks-1e1h</guid>
      <description>&lt;h1&gt;
  
  
  Production Mobile Security: Hardening Cross-Platform Apps Against Reverse Engineering
&lt;/h1&gt;

&lt;p&gt;When developers transition from web backend engineering to mobile app development, they frequently carry a dangerous assumption: &lt;em&gt;that client-side code executes in a trusted, controlled environment.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On mobile devices, this assumption is completely false.&lt;/p&gt;

&lt;p&gt;Your APK or IPA binary lives entirely on the user's hardware. Anyone with basic tools like &lt;code&gt;apktool\&lt;/code&gt;, &lt;code&gt;jadx\&lt;/code&gt;, &lt;code&gt;ghidra\&lt;/code&gt;, or an HTTP proxy like Charles or Proxyman can unpack your binary, inspect your strings, intercept your network traffic, and manipulate runtime memory using dynamic instrumentation frameworks like &lt;strong&gt;Frida&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is an architectural guide to hardening cross-platform mobile applications against real-world vulnerabilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Threat Modeling the Mobile Client
&lt;/h2&gt;

&lt;p&gt;Before writing security code, understand what an attacker can easily do to an unhardened app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static Analysis:&lt;/strong&gt; Decompiling the application package to find hardcoded API secrets, private endpoints, or insecure encryption keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Man-In-The-Middle (MITM) Attacks:&lt;/strong&gt; Installing a custom Root CA certificate on an emulator or rooted device to inspect and alter HTTPS payloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Hooking (Frida/Objection):&lt;/strong&gt; Overriding Dart or native C++ method return values at runtime (e.g., forcing &lt;code&gt;isUserAuthenticated()\&lt;/code&gt; to always return &lt;code&gt;true\&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data-at-Rest Extraction:&lt;/strong&gt; Pulling unencrypted databases, token caches, and session identifiers from local application sandboxes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Cryptographic Storage: Zero Plaintext on Disk
&lt;/h2&gt;

&lt;p&gt;Never store sensitive credentials, session tokens, or PII (Personally Identifiable Information) in standard storage mechanisms like Android's &lt;code&gt;SharedPreferences\&lt;/code&gt; or iOS's &lt;code&gt;NSUserDefaults\&lt;/code&gt;. These files are plain XML and plist files that can be read with a single adb command.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution:
&lt;/h3&gt;

&lt;p&gt;Use hardware-backed security modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android:&lt;/strong&gt; Uses the Android Keystore system with AES-256 GCM encryption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS:&lt;/strong&gt; Uses the Secure Enclave and iOS Keychain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Flutter, leverage &lt;a href="https://pub.dev/packages/flutter_secure_storage" rel="noopener noreferrer"&gt;&lt;code&gt;flutter_secure_storage\&lt;/code&gt;&lt;/a&gt; configured with strict hardware security options:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';&lt;/p&gt;

&lt;p&gt;const secureStorage = FlutterSecureStorage(&lt;br&gt;
  aOptions: AndroidOptions(&lt;br&gt;
    encryptedSharedPreferences: true,&lt;br&gt;
    keyCipherAlgorithm: KeyCipherAlgorithm.RSA_ECB_PKCS1Padding,&lt;br&gt;
    storageCipherAlgorithm: StorageCipherAlgorithm.AES_GCM_NoPadding,&lt;br&gt;
  ),&lt;br&gt;
  iOptions: IOSOptions(&lt;br&gt;
    accessibility: KeychainAccessibility.first_unlock,&lt;br&gt;
    synchronizable: false,&lt;br&gt;
  ),&lt;br&gt;
);&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For local databases (Drift / SQLite), always encrypt the entire database file using &lt;strong&gt;SQLCipher&lt;/strong&gt; with a 256-bit key dynamically derived from the hardware Keystore.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Network Hardening: Enforcing SSL/TLS Pinning
&lt;/h2&gt;

&lt;p&gt;By default, an app trusts any certificate signed by a Certificate Authority installed in the device's system trust store. If a user installs a custom proxy certificate, they can read &lt;a href="https://dev.to/blog/flutter-rest-api-integration-from-json-to-ui"&gt;every API request&lt;/a&gt; in plaintext.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSL Pinning&lt;/strong&gt; forces the app to accept only known certificates or public keys specifically hardcoded into the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Practice: Public Key Pinning (SPKI)
&lt;/h3&gt;

&lt;p&gt;Pin the SHA-256 hash of the server's Subject Public Key Info (SPKI) rather than the leaf certificate. When your leaf certificate renews annually, your public key remains unchanged, eliminating the risk of accidental app lockouts. Always provide at least one backup pin.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
// Example: SSL Pinning with Dio and SecurityContext&lt;br&gt;
Dio createSecureDioClient() {&lt;br&gt;
  final dio = Dio();&lt;/p&gt;

&lt;p&gt;final securityContext = SecurityContext(withTrustedRoots: false);&lt;br&gt;
  // Import your organization's trusted root / intermediate cert&lt;br&gt;
  securityContext.setTrustedCertificatesBytes(rawCertBytes);&lt;/p&gt;

&lt;p&gt;final httpClient = HttpClient(context: securityContext);&lt;br&gt;
  httpClient.badCertificateCallback = (cert, host, port) {&lt;br&gt;
    // Strictly reject any certificate that fails validation&lt;br&gt;
    return false;&lt;br&gt;
  };&lt;/p&gt;

&lt;p&gt;(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () =&amp;gt; httpClient;&lt;br&gt;
  return dio;&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Binary Obfuscation and Symbol Stripping
&lt;/h2&gt;

&lt;p&gt;When you &lt;a href="https://dev.to/blog/reduce-flutter-app-size-guide"&gt;build a release Flutter binary&lt;/a&gt; without flags, class names, field identifiers, and symbol tables remain largely readable in the compiled snapshot.&lt;/p&gt;

&lt;p&gt;Enable &lt;a href="https://docs.flutter.dev/deployment/obfuscate" rel="noopener noreferrer"&gt;full Dart symbol obfuscation&lt;/a&gt; during your release build pipeline:&lt;br&gt;
&lt;code&gt;\&lt;/code&gt;`bash&lt;br&gt;
flutter build appbundle --release \&lt;br&gt;
  --obfuscate \&lt;br&gt;
  --split-debug-info=./build/symbols/android&lt;/p&gt;

&lt;p&gt;flutter build ipa --release \&lt;br&gt;
  --obfuscate \&lt;br&gt;
  --split-debug-info=./build/symbols/ios&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This strips human-readable symbols from the output binary and generates a mapping file that you securely archive in your CI/CD storage to &lt;a href="https://dev.to/blog/flutter-firebase-crashlytics-setup"&gt;de-obfuscate production crash stack traces&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Runtime Integrity &amp;amp; Anti-Tampering
&lt;/h2&gt;

&lt;p&gt;For high-stakes applications (banking, healthcare, fintech), client-side heuristics are insufficient. Implement server-verified attestation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://developer.android.com/google/play/integrity/overview" rel="noopener noreferrer"&gt;Google Play Integrity API&lt;/a&gt; (Android):&lt;/strong&gt; Verifies that the app binary was installed through Google Play and that the device is running a genuine, certified Android build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple App Attest (iOS):&lt;/strong&gt; Uses the Secure Enclave to generate a cryptographic key pair that attests the authenticity of the app to your backend before issuing sensitive tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen Privacy Protection:&lt;/strong&gt; Prevent confidential data from leaking in the recent apps switcher:

&lt;ul&gt;
&lt;li&gt;On Android, set &lt;code&gt;FLAG_SECURE\&lt;/code&gt; on the window to block screenshots and screen recording.&lt;/li&gt;
&lt;li&gt;On iOS, render a blurred visual overlay when &lt;code&gt;applicationWillResignActive\&lt;/code&gt; triggers.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Golden Principle: The Backend is the Gatekeeper
&lt;/h2&gt;

&lt;p&gt;No amount of client-side hardening makes a mobile device 100% impenetrable. A determined reverse engineer with physical access and a hardware debugger can eventually bypass client checks.&lt;/p&gt;

&lt;p&gt;Therefore, &lt;strong&gt;never trust the client for business rules, authorization, or rate limiting&lt;/strong&gt;. Treat mobile security hardening as defense-in-depth: raising the cost and complexity of attacks so high that malicious actors look elsewhere.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/mobile-app-security-hardening-guide" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>cybersecurity</category>
      <category>mobile</category>
      <category>security</category>
    </item>
    <item>
      <title>Flutter Isolates: Heavy Work Without Jank</title>
      <dc:creator>Muhammad Uzair</dc:creator>
      <pubDate>Tue, 10 Nov 2026 03:00:46 +0000</pubDate>
      <link>https://dev.to/uzairxdev/flutter-isolates-heavy-work-without-jank-egl</link>
      <guid>https://dev.to/uzairxdev/flutter-isolates-heavy-work-without-jank-egl</guid>
      <description>&lt;p&gt;Flutter apps feel fast until they do not. A scroll stutters, a button tap waits half a second, an animation drops frames — and the cause is rarely the widget tree. More often, the app is doing heavy work on the same thread that draws the UI. Dart runs your code on a single thread, called the main isolate, and while that thread is busy parsing JSON or crunching numbers, it cannot build frames. The fix is to move the heavy work somewhere else: a second isolate.&lt;/p&gt;

&lt;p&gt;An isolate is Dart's version of a thread, with one important difference: isolates do not share memory. Each one has its own heap and its own event loop, so they communicate only by sending messages. That sounds restrictive, but it is exactly what makes them safe. There are no data races, no locks, and no shared mutable state to corrupt. You send a value in, you get a value back, and the UI thread keeps drawing frames in the meantime.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much work is too much for the main isolate
&lt;/h2&gt;

&lt;p&gt;Flutter targets 60 frames per second, which gives the main isolate about 16 milliseconds per frame. Any single task that takes longer than that risks a missed frame. Parsing a large JSON response, decoding a big image, sorting thousands of records, or running a compression routine can easily take 50 to 500 milliseconds. Users notice that as a freeze.&lt;/p&gt;

&lt;p&gt;A useful rule: if the work takes more than a few milliseconds and you cannot break it into small chunks, move it to another isolate. If the work takes less than a millisecond, the cost of spawning an isolate and copying the data across is higher than just doing it inline. Measure first — the DevTools timeline shows long tasks on the UI thread clearly — then isolate the real offenders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolate.run: the simplest option
&lt;/h2&gt;

&lt;p&gt;Dart 2.19 introduced &lt;code&gt;Isolate.run\&lt;/code&gt;, the easiest way to run a function on a background isolate. You pass it a function, it runs that function on a new isolate, and it returns a Future with the result:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
import 'dart:isolate';&lt;/p&gt;

&lt;p&gt;Future&amp;gt; loadProducts(String rawJson) async {&lt;br&gt;
  // Parsing happens on a background isolate.&lt;br&gt;
  // The UI thread stays free to draw frames.&lt;br&gt;
  return Isolate.run(() =&amp;gt; parseProducts(rawJson));&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;List parseProducts(String rawJson) {&lt;br&gt;
  final decoded = jsonDecode(rawJson) as List;&lt;br&gt;
  return decoded&lt;br&gt;
      .map((item) =&amp;gt; Product.fromJson(item as Map))&lt;br&gt;
      .toList();&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A few details matter here. &lt;code&gt;Isolate.run\&lt;/code&gt; creates a new isolate, runs the function, sends the result back, and shuts the isolate down. That is perfect for one-off jobs such as parsing a network response or decoding a file. The function you pass must be a top-level function, a static method, or a closure that captures only values that can be sent between isolates. A closure that captures a &lt;code&gt;BuildContext\&lt;/code&gt;, a controller, or any object holding native resources will fail at runtime, so keep the function self-contained and pass everything it needs as arguments.&lt;/p&gt;

&lt;p&gt;The result must also be sendable. Plain data — numbers, strings, lists, maps, and objects made only of those — crosses the boundary fine. Objects that hold sockets, file handles, or platform channels do not, and the isolate will throw when you try to send them.&lt;/p&gt;

&lt;h2&gt;
  
  
  compute: the Flutter shortcut
&lt;/h2&gt;

&lt;p&gt;Flutter ships a convenience wrapper called &lt;code&gt;compute\&lt;/code&gt; in &lt;code&gt;package:flutter/foundation.dart\&lt;/code&gt;. It does the same job as &lt;code&gt;Isolate.run\&lt;/code&gt; for a single function call, and it is the form you will see most often in Flutter code:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
import 'package:flutter/foundation.dart';&lt;/p&gt;

&lt;p&gt;Future&amp;gt; loadProducts(String rawJson) async {&lt;br&gt;
  return compute(parseProducts, rawJson);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// Must be a top-level or static function.&lt;br&gt;
List parseProducts(String rawJson) {&lt;br&gt;
  final decoded = jsonDecode(rawJson) as List;&lt;br&gt;
  return decoded&lt;br&gt;
      .map((item) =&amp;gt; Product.fromJson(item as Map))&lt;br&gt;
      .toList();&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;compute\&lt;/code&gt; takes the function and a single argument. If your function needs several inputs, wrap them in a record or a small data class and pass that as the one argument. Under the hood, &lt;code&gt;compute\&lt;/code&gt; spawns an isolate, runs the function, returns the result, and tears the isolate down — the same lifecycle as &lt;code&gt;Isolate.run\&lt;/code&gt;, with a Flutter-friendly API. For one-off background work in a Flutter app, &lt;code&gt;compute\&lt;/code&gt; is usually the right default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolate.spawn: when the work never stops
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Isolate.run\&lt;/code&gt; and &lt;code&gt;compute\&lt;/code&gt; are built for single jobs: spawn, run, return, shut down. Some work does not fit that shape. A background worker that processes a stream of images, a worker that keeps a connection open, or a long-running computation that reports progress all need an isolate that stays alive. That is what &lt;code&gt;Isolate.spawn\&lt;/code&gt; is for.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;Isolate.spawn\&lt;/code&gt;, you create the isolate yourself and talk to it through ports. A &lt;code&gt;SendPort\&lt;/code&gt; sends messages; a &lt;code&gt;ReceivePort\&lt;/code&gt; listens for them:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
import 'dart:isolate';&lt;/p&gt;

&lt;p&gt;Future main() async {&lt;br&gt;
  final receivePort = ReceivePort();&lt;br&gt;
  await Isolate.spawn(_workerEntry, receivePort.sendPort);&lt;/p&gt;

&lt;p&gt;// The worker sends its own SendPort back first.&lt;br&gt;
  final sendPort = await receivePort.first as SendPort;&lt;/p&gt;

&lt;p&gt;// Ask the worker to do something.&lt;br&gt;
  sendPort.send('process this image');&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;void _workerEntry(SendPort mainSendPort) {&lt;br&gt;
  final workerPort = ReceivePort();&lt;br&gt;
  mainSendPort.send(workerPort.sendPort);&lt;/p&gt;

&lt;p&gt;workerPort.listen((message) {&lt;br&gt;
    // Heavy work happens here, on the worker isolate.&lt;br&gt;
    final result = doHeavyWork(message as String);&lt;br&gt;
    mainSendPort.send(result);&lt;br&gt;
  });&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The handshake at the start is the pattern to remember: the spawned isolate creates its own &lt;code&gt;ReceivePort\&lt;/code&gt;, sends that port's &lt;code&gt;SendPort\&lt;/code&gt; back to the main isolate, and from then on the two sides exchange messages. The worker isolate stays alive until you call &lt;code&gt;isolate.kill()\&lt;/code&gt; or it exits on its own, so remember to shut it down when the screen that owns it is disposed, or you will leak an isolate for the life of the app.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Isolate.spawn\&lt;/code&gt; is more code than &lt;code&gt;compute\&lt;/code&gt;, and that is the point: you only reach for it when a single-shot call is not enough. If you find yourself spawning an isolate for every tap, you are paying the spawn cost repeatedly for no benefit — use &lt;code&gt;compute\&lt;/code&gt; or keep one worker isolate alive instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can cross the isolate boundary
&lt;/h2&gt;

&lt;p&gt;Messages between isolates are copied, not shared. That has two consequences worth knowing. First, sending a large object costs time: a 10 MB list is serialized, copied, and deserialized on the other side. For very large payloads, that copy can eat the benefit of the background work, so prefer sending compact data — a file path instead of the file bytes, or an ID instead of a full object — and let the worker read what it needs itself.&lt;/p&gt;

&lt;p&gt;Second, only certain values can be sent: null, numbers, booleans, strings, lists and maps of sendable values, &lt;code&gt;SendPort\&lt;/code&gt; instances, and objects whose fields are all sendable. Anything holding a native resource — a database connection, an open file, a platform channel, a &lt;code&gt;BuildContext\&lt;/code&gt; — cannot cross. If you get an error about an un-sendable object, the fix is almost always to move the creation of that resource inside the worker, or to pass a plain description of the work instead of the resource itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;p&gt;Three mistakes show up again and again. The first is isolating work that is too small: spawning an isolate for a task that takes one millisecond costs more than it saves, and the app gets slower, not faster. The second is capturing UI objects in the background function — a closure that quietly holds a controller or a context will fail when the message is sent, often far from the line that caused it. The third is forgetting that isolates cannot touch the UI at all: there is no &lt;code&gt;setState\&lt;/code&gt; from a background isolate, no platform channel call, and no access to plugins that rely on the main isolate. The worker computes; the main isolate updates the screen with the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete example: parsing without the freeze
&lt;/h2&gt;

&lt;p&gt;Putting it together, here is the pattern most apps end up with: fetch on the main isolate, parse on a background isolate, render when the result arrives.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`dart&lt;br&gt;
import 'package:flutter/foundation.dart';&lt;br&gt;
import 'package:http/http.dart' as http;&lt;/p&gt;

&lt;p&gt;class ProductListScreen extends StatefulWidget {&lt;br&gt;
  const ProductListScreen({super.key});&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/override"&gt;@override&lt;/a&gt;&lt;br&gt;
  State createState() =&amp;gt; _ProductListScreenState();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;class _ProductListScreenState extends State {&lt;br&gt;
  late final Future&amp;gt; _products;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/override"&gt;@override&lt;/a&gt;&lt;br&gt;
  void initState() {&lt;br&gt;
    super.initState();&lt;br&gt;
    _products = _fetchProducts();&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;Future&amp;gt; _fetchProducts() async {&lt;br&gt;
    final response = await http.get(Uri.parse('&lt;a href="https://api.example.com/products')" rel="noopener noreferrer"&gt;https://api.example.com/products')&lt;/a&gt;);&lt;br&gt;
    // The expensive parse runs off the main isolate.&lt;br&gt;
    return compute(parseProducts, response.body);&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/override"&gt;@override&lt;/a&gt;&lt;br&gt;
  Widget build(BuildContext context) {&lt;br&gt;
    return FutureBuilder&amp;gt;(&lt;br&gt;
      future: _products,&lt;br&gt;
      builder: (context, snapshot) {&lt;br&gt;
        if (!snapshot.hasData) {&lt;br&gt;
          return const Center(child: CircularProgressIndicator());&lt;br&gt;
        }&lt;br&gt;
        final products = snapshot.data!;&lt;br&gt;
        return ListView.builder(&lt;br&gt;
          itemCount: products.length,&lt;br&gt;
          itemBuilder: (context, index) {&lt;br&gt;
            return ListTile(title: Text(products[index].name));&lt;br&gt;
          },&lt;br&gt;
        );&lt;br&gt;
      },&lt;br&gt;
    );&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Nothing about the UI changed. The fetch, the parse, and the list are the same as before — the only difference is that &lt;code&gt;compute\&lt;/code&gt; moves the parse to a background isolate, so the loading spinner animates smoothly while a large response is being decoded. That is the whole point of isolates: the user should never be able to tell that heavy work is happening at all.&lt;/p&gt;

&lt;p&gt;If you want to go deeper into how Dart handles work that waits — network calls, timers, and streams — read &lt;a href="https://www.uzairxdev.online/blog/dart-streams-vs-futures-flutter" rel="noopener noreferrer"&gt;Dart Streams vs Futures: When to Use Each&lt;/a&gt;. And if the heavy work in your app comes from an on-device AI model, the same off-thread thinking applies; see &lt;a href="https://www.uzairxdev.online/blog/flutter-on-device-ai-tensorflow-lite" rel="noopener noreferrer"&gt;Flutter On-Device AI With TensorFlow Lite&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Isolates are not a performance trick. They are the way Dart is designed to handle heavy work: keep the main isolate free to draw frames, send plain data to a worker, and bring the result back. Start with &lt;code&gt;compute\&lt;/code&gt; for one-off jobs, reach for &lt;code&gt;Isolate.spawn\&lt;/code&gt; when the worker needs to stay alive, and measure before and after — the frame timeline in DevTools will show the difference immediately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.uzairxdev.online/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; and let's talk about your project.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.uzairxdev.online/blog/flutter-isolates-heavy-work-without-jank" rel="noopener noreferrer"&gt;uzairxdev.online&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>isolates</category>
      <category>dart</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
