Build a Period Tracker with Flutter

Build a PMOS-aware period tracker app with Flutter on an Android emulator.

Introduction

30 Second Summary

Millions of women live with PMOS (Polyendocrine Metabolic Ovarian Syndrome), a condition that affects hormones, metabolism, and menstrual cycles. Most period tracking apps ignore this complexity and treat every cycle the same.

In this project, you will build a period tracking app designed specifically for women with PMOS using Flutter, Google's cross-platform mobile framework. Your app will calculate cycle phases, display a visual calendar of period days, and save everything locally so data survives app restarts.

What You'll Build

Open your finished app on an Android emulator, log a period start date on the calendar, and watch the dashboard instantly recalculate your cycle day, phase, and countdown.

By the end of this project, you'll have:

  • A dashboard showing your current cycle day, phase name, and countdown to your next period. Each phase includes a PMOS-specific description of its hormonal and metabolic impact.
  • A visual calendar with period days highlighted in pink. Tap any date to log it as a new period start and watch the dashboard recalculate instantly.
  • Persistent local storage so your cycle history survives app restarts and is always ready when you reopen the app.
  • Secret Mission: Add a PMOS symptom logger that lets you track common symptoms like bloating, fatigue, and mood changes per day. Logged symptoms appear as color-coded markers on the calendar.

Are there any prerequisites?

None. This project walks you through installing every tool you need, including Flutter, Android Studio, and the VS Code extensions. All you need to start is a Windows computer with VS Code or Cursor already installed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

You're creating a period tracking app designed specifically for women with PMOS (Polyendocrine Metabolic Ovarian Syndrome), a condition that affects 1 in 8 women worldwide.

Set Up Your Flutter Development Environment

Your PMOS tracker is a Flutter app. Flutter lets you build mobile apps for both Android and iOS from a single codebase. Before you can write any Flutter code, you need three things installed: the Flutter SDK, Android Studio (for its emulator), and the Flutter extension for VS Code.

This step installs all of them from scratch and verifies everything works together. If you use Cursor instead of VS Code, every instruction in this project works the same way.

In this step, get ready to:
  • Install Git for Windows and the Flutter SDK.
  • Set up Android Studio with an Android emulator.
  • Verify your setup with flutter doctor.
Install Git for Windows and the Flutter SDK

Flutter uses Git behind the scenes to download and update itself. Before installing the Flutter SDK, you need Git on your machine.

  • Press the Windows key, type VS Code, and press Enter to open it.
  • Click Terminal in the top menu bar, then click New Terminal.
  • Check if Git is already installed by running this command:
git --version

✔️ I see a version number

Git is ready. Move on to installing the Flutter extension below.

ⓧ 'git' is not recognized

You need to install Git for Windows.

  • Download Git for Windows from the official Git website.
  • Run the downloaded installer.
  • Click Next through each screen, accepting all default settings.
  • Click Install, then click Finish when installation completes.
  • Go back to VS Code. Click Terminal in the top menu bar, then click New Terminal to open a fresh terminal.
  • Confirm Git is installed by running:
git --version

You should see a version number like 2.55.0 displayed.

Still not recognized?

Make sure you opened a new terminal after installing Git. The old terminal does not pick up PATH changes.

If the issue persists, close VS Code completely and reopen it, then try again.

Help me troubleshoot Git installation on Windows.

Now install the Flutter and Dart extensions for VS Code. These give VS Code the tools it needs to create and run Flutter apps.

  • Press Ctrl+Shift+X to open the Extensions sidebar.
  • Type Flutter in the search bar at the top.
  • Find the extension named Flutter published by Dart Code.
  • Click Install.

You should see both Flutter and Dart listed as installed in your Extensions sidebar. The Dart extension is installed automatically alongside the Flutter extension.

What do these extensions do?

The Flutter extension adds device selection, hot reload, and project creation tools to VS Code. The Dart extension provides syntax highlighting, code completion, and debugging for Dart, the programming language Flutter uses.

The extensions are ready. Now download the Flutter SDK through VS Code's built-in installer.

  • Press Ctrl+Shift+P to open the Command Palette.
  • Type flutter in the search bar.
  • Select Flutter: New Project from the dropdown.

What happens next?

VS Code detects that no Flutter SDK is installed and shows a prompt asking you to locate an existing SDK or download one. You want the download option.

  • Click Download SDK in the prompt that appears.
  • VS Code opens a folder picker. Navigate to C:\ and click Select Folder to Install Flutter To.

Why install at C:\Flutter?

Flutter creates deeply nested file paths during builds. Installing at a short path like C:\Flutter avoids Windows "Filename too long" errors that can break the build process.

  • Click Clone Flutter to start the download. This takes a few minutes. You will see a progress notification in the bottom-right corner of VS Code.
  • When VS Code shows a notification to add the SDK to your PATH, click Add SDK to PATH.
  • Close all open terminals in VS Code by clicking the trash can icon on each terminal tab.
  • Close VS Code completely by clicking the X in the top-right corner.
  • Reopen VS Code by pressing the Windows key, typing VS Code, and pressing Enter.

Now verify that Flutter is installed and available on your PATH.

  • Open a new terminal by clicking Terminal in the top menu bar, then clicking New Terminal.
  • Check your Flutter version by running:
flutter --version

✔️ I see version 3.47 or higher

Flutter is installed and ready. Move on to setting up Android Studio below.

ⓧ I see an older version

You have a previous Flutter installation that is outdated. Upgrade it by running this command:

flutter upgrade

After the upgrade completes, run flutter --version again to confirm you see 3.47 or higher.

Still seeing an old version?

Your old Flutter installation may be shadowing the new one on your PATH. Delete the old SDK folder, then re-run the VS Code install flow above.

Help me fix my Flutter version on Windows.

ⓧ 'flutter' is not recognized

The Flutter SDK was not added to your system PATH. Try these fixes:

  • Make sure you clicked Add SDK to PATH during the installation.
  • Make sure you closed and reopened VS Code after the install.
  • If you missed the PATH prompt, add C:\Flutter\bin to your system PATH manually through System Properties > Environment Variables.

After fixing the PATH, close and reopen VS Code, open a new terminal, and run flutter --version again.

Still not recognized?

Help me add Flutter to my PATH on Windows.

Install Android Studio and create an emulator

Android Studio provides the Android emulator where you will test your app. You only need Android Studio for its emulator and SDK tools. All your coding stays in VS Code.

  • Download Android Studio from the official Android developer website.
  • Run the downloaded installer.
  • Click Next through each screen, accepting all default settings, then click Install.
  • When installation finishes, check the Start Android Studio checkbox and click Finish.

What happens on first launch?

Android Studio runs a setup wizard on first launch to download Android SDK components. Accept the default settings and wait for it to finish. This may take several minutes.

Android Studio opens to its welcome screen. Now create the virtual phone you will use to test your app.

Why do I need Android Studio if I code in VS Code?

Android Studio bundles the Android SDK and emulator tools that Flutter needs to compile and run Android apps. You will not write code in Android Studio. You only need it for its Device Manager to create and run virtual Android devices.

  • On the Android Studio welcome screen, click More Actions, then select Virtual Device Manager.
  • Click the + button, then select Create Virtual Device.
  • Select Phone as the category on the left.
  • Select Pixel 7 from the device list, then click Next.
  • Find the VanillaIceCream system image (Android 15). If you see a Download link next to it, click it and wait for the download to complete.
  • Select the VanillaIceCream image, then click Next.
  • On the Verify Configuration screen, make sure Graphics is set to Hardware - GLES 2.0 for hardware acceleration.
  • Click Finish.

Your Pixel 7 virtual device now appears in the Device Manager list.

  • Click the Start button (play icon) next to your new Pixel 7 device to launch the emulator.

The Android emulator launches and shows a virtual phone screen. The first boot may take a minute or two.

Emulator not starting?

Make sure hardware virtualization (Intel VT-x or AMD-V) is enabled in your BIOS/UEFI settings. If you see a virtualization error, restart your computer, enter BIOS, and enable the setting.

Help me fix the Android emulator not starting on Windows.

Accept Android licenses and verify your setup

Two final commands will finish your environment setup. Switch back to VS Code for these.

  • Accept Android SDK licenses by running this command in your VS Code terminal:
flutter doctor --android-licenses
  • Press y and then Enter each time you are prompted to accept a license. You will see this prompt several times.

What are these licenses?

The Android SDK has open-source licenses that you must accept before building Android apps. This is a one-time step.

Before you run the final verification, think about what you expect to see. You installed Git, the Flutter SDK, Android Studio, and created an emulator. Do you think flutter doctor will show all green checkmarks?

  • Verify your entire setup by running:
flutter doctor -v

You should see green checkmarks next to the Flutter and Android toolchain sections. These confirm that Flutter can find the SDK and your Android emulator.

What about other sections?

You may see warnings or X marks for Chrome, Visual Studio, or Linux sections. Those are for web and desktop development. For this project, you only need the Flutter and Android toolchain sections to pass.

Seeing issues in flutter doctor?

If the Android toolchain shows a license error, re-run flutter doctor --android-licenses and accept all licenses.

If Flutter shows a path issue, confirm that C:\Flutter\bin is in your system PATH.

Help me resolve flutter doctor issues on Windows.

Your development environment is fully set up. Next up, you will create your first Flutter app and build the PMOS dashboard.

Create the App and Build the Dashboard

Your Flutter environment is ready, and your Android emulator is running. Now you need an actual app to run on it.

In this step, you will create a new Flutter project and build a dashboard screen that shows cycle day, phase name, and a countdown to the next period. Starting with a visible home screen gives you something real to look at before you add more features.

In this step, get ready to:
  • Create a new Flutter project using the flutter create command.
  • Build a dashboard screen that displays cycle day, phase name, and countdown.
  • Run the app on the Android emulator to see the dashboard in action.
Create a new Flutter project

Flutter's CLI includes a flutter create command that scaffolds a complete project for you. The --empty flag gives you a minimal starting point without the default counter app boilerplate.

  • Open the terminal in VS Code by pressing Ctrl+`.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the Flutter project and move into it by running these commands:
flutter create pmos_tracker --empty
cd pmos_tracker
  • Confirm the project was created by running:
ls lib/

You should see main.dart listed in the output. This is the entry point Flutter created for your app.

Not seeing main.dart?

Make sure you ran flutter create pmos_tracker --empty (with two dashes before empty). Also confirm you ran cd pmos_tracker before running ls lib/.

help me troubleshoot my Flutter project creation

  • Open the pmos_tracker project folder in VS Code. Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the pmos_tracker folder, and click Select Folder.

What did flutter create build?

The flutter create command generated a full project structure including lib/main.dart (your app code), pubspec.yaml (your dependencies), and platform folders like android/ and ios/.

You will only edit files inside the lib/ folder. Flutter handles the platform-specific folders automatically.

Build the dashboard screen

The dashboard is the first screen users see when they open the app. It calculates which cycle phase you are in based on your last period start date, and displays that information in styled cards.

You will build this screen in a separate file called dashboard_screen.dart. Keeping each screen in its own file is standard practice in Flutter projects.

  • In the VS Code Explorer sidebar, right-click the lib folder.
  • Click New File and name it dashboard_screen.dart.
  • Start building the DashboardScreen class by pasting this code into the new file:
import 'package:flutter/material.dart';

class DashboardScreen extends StatelessWidget {
  // Accept a list of period start dates from the parent widget
  final List<DateTime> periodStartDates;

  const DashboardScreen({super.key, required this.periodStartDates});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('PMOS Tracker'),
        centerTitle: true,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        // Show a message if no data exists, otherwise build the dashboard
        child: periodStartDates.isEmpty
            ? const Center(
                child: Text(
                  'No period data logged yet.\nGo to the Calendar tab to log your first period start date.',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 16),
                ),
              )
            : _buildDashboardContent(context),
      ),
    );
  }

What does this code do?

DashboardScreen is a StatelessWidget. It receives periodStartDates as a parameter from its parent, so it does not need to manage its own state.

The build method checks if the list is empty. If there is no data, it shows a placeholder message. Otherwise, it calls _buildDashboardContent which you will add next.

  • Below the build method's closing brace, add the start of the _buildDashboardContent method with the cycle calculation logic:
  Widget _buildDashboardContent(BuildContext context) {
    // Get the most recent period start date
    final lastPeriod = periodStartDates.last;
    final today = DateTime.now();
    // Calculate how many days since the last period started
    final daysSinceLastPeriod =
        DateTime(today.year, today.month, today.day)
            .difference(DateTime(lastPeriod.year, lastPeriod.month, lastPeriod.day))
            .inDays;

    const averageCycleLength = 28;
    const periodLength = 5;

    // Cycle day starts at 1, not 0
    final cycleDay = daysSinceLastPeriod + 1;
    final daysUntilNextPeriod = averageCycleLength - daysSinceLastPeriod;

    String phaseName;
    String phaseDescription;
    Color phaseColor;
  • Directly below, add the phase determination logic that assigns a name, description, and colour to each cycle phase:
    // Determine which cycle phase based on the current day
    if (cycleDay <= periodLength) {
      phaseName = 'Menstrual Phase';
      phaseDescription =
          'Your body is shedding the uterine lining. With PMOS, periods may be irregular or heavier. Rest and hydration are important.';
      phaseColor = const Color(0xFFE91E63);
    } else if (cycleDay <= 13) {
      phaseName = 'Follicular Phase';
      phaseDescription =
          'Estrogen rises as follicles develop. With PMOS, this phase can be extended due to hormonal imbalances. Energy typically increases.';
      phaseColor = const Color(0xFF4CAF50);
    } else if (cycleDay <= 16) {
      phaseName = 'Ovulatory Phase';
      phaseDescription =
          'An egg is released. With PMOS, ovulation may be irregular or absent. Tracking symptoms helps identify patterns.';
      phaseColor = const Color(0xFF2196F3);
    } else {
      phaseName = 'Luteal Phase';
      phaseDescription =
          'Progesterone rises after ovulation. With PMOS, this phase may bring stronger PMS symptoms. Monitor mood, cravings, and energy.';
      phaseColor = const Color(0xFFFF9800);
    }

How do the cycle phases work?

The app uses a standard 28-day cycle model split into four phases: Menstrual (days 1 to 5), Follicular (days 6 to 13), Ovulatory (days 14 to 16), and Luteal (days 17 to 28). Each phase gets a PMOS-specific description acknowledging how the condition affects that phase.

The cycleDay is calculated as the number of days since the last period plus one. So if your last period started today, you are on day 1.

  • Now add the return statement that builds the first card showing the big cycle day number:
    return SingleChildScrollView(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Card 1: Big cycle day number and phase name
          Card(
            color: phaseColor.withValues(alpha: 0.1),
            child: Padding(
              padding: const EdgeInsets.all(20.0),
              child: Column(
                children: [
                  Text(
                    'Day $cycleDay',
                    style: TextStyle(
                      fontSize: 48,
                      fontWeight: FontWeight.bold,
                      color: phaseColor,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    phaseName,
                    style: TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.w600,
                      color: phaseColor,
                    ),
                  ),
                ],
              ),
            ),
          ),
  • Directly below, add the phase description card:
          const SizedBox(height: 16),
          // Card 2: PMOS-specific phase description
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'About This Phase (PMOS)',
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    phaseDescription,
                    style: const TextStyle(fontSize: 14, height: 1.5),
                  ),
                ],
              ),
            ),
          ),
  • Add the third card with quick stats, then close the return, Column, SingleChildScrollView, and _buildDashboardContent method:
          const SizedBox(height: 16),
          // Card 3: Quick stats row
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  _buildInfoColumn(
                    'Next Period',
                    daysUntilNextPeriod > 0
                        ? '$daysUntilNextPeriod days'
                        : 'Due now',
                    Icons.event,
                  ),
                  _buildInfoColumn(
                    'Cycle Length',
                    '$averageCycleLength days',
                    Icons.repeat,
                  ),
                  _buildInfoColumn(
                    'Last Period',
                    '${lastPeriod.day}/${lastPeriod.month}',
                    Icons.history,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
  • Finally, add the _buildInfoColumn helper method below _buildDashboardContent, then close the class with a final }:
  // Helper to build each stat column in the info row
  Widget _buildInfoColumn(String label, String value, IconData icon) {
    return Column(
      children: [
        Icon(icon, size: 28, color: const Color(0xFFE91E63)),
        const SizedBox(height: 4),
        Text(
          value,
          style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
        ),
        Text(
          label,
          style: const TextStyle(fontSize: 12, color: Colors.grey),
        ),
      ],
    );
  }
}

What do the three cards display?

  • Card 1 shows the current cycle day as a large number with the phase name below it. The card background tints to match the phase colour.
  • Card 2 shows a PMOS-specific description explaining what is happening in this phase and how it may differ for someone with PMOS.
  • Card 3 shows three quick stats in a row: days until next period, the average cycle length, and the date of the last logged period.
  • Save dashboard_screen.dart.
  • Confirm the file exists by running this in your terminal:
ls lib/

You should see both main.dart and dashboard_screen.dart listed.

✔️ Awesome, I've got everything!

Great. Make sure you have saved dashboard_screen.dart before moving on.

ⓧ I'd like to double check the full code

Scroll up through your dashboard_screen.dart file and confirm these sections are present in order:

  • The import statement and DashboardScreen class with its build method (the first chunk you pasted).
  • The _buildDashboardContent method opening with date calculations and phase variable declarations.
  • The if/else if/else block setting phaseName, phaseDescription, and phaseColor.
  • The return SingleChildScrollView with three Card widgets inside a Column.
  • The _buildInfoColumn helper method and the closing } for the class.

Seeing red underlines in the file?

Red underlines under _buildDashboardContent or _buildInfoColumn mean those methods are being called before they are defined in the file. Make sure all seven code blocks are pasted in order from top to bottom. The file should start with the import and end with the closing } of the class.

help me fix errors in my dashboard_screen.dart file

The dashboard screen is ready. Now you need to wire it into the app. The target main.dart imports two other files that do not exist yet: one for the calendar screen (built in Step 3) and one for the data service (built in Step 5). You will create temporary placeholder files so the app compiles.

  • Right-click the lib folder in the Explorer sidebar, click New File, and name it calendar_screen.dart.
  • Paste this placeholder code into the file:
import 'package:flutter/material.dart';

// Placeholder - will be fully built in Step 3
class CalendarScreen extends StatefulWidget {
  final List<DateTime> periodStartDates;
  final Future<void> Function(DateTime) onPeriodLogged;

  const CalendarScreen({
    super.key,
    required this.periodStartDates,
    required this.onPeriodLogged,
  });

  @override
  State<CalendarScreen> createState() => _CalendarScreenState();
}

class _CalendarScreenState extends State<CalendarScreen> {
  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(child: Text('Calendar coming in Step 3')),
    );
  }
}
  • Save calendar_screen.dart.
  • Create another new file in lib named cycle_data_service.dart.
  • Paste this placeholder code:
// Placeholder - will be fully built in Step 5
class CycleDataService {
  Future<void> savePeriodDates(List<DateTime> dates) async {
    // Does nothing yet
  }

  Future<List<DateTime>> loadPeriodDates() async {
    // Return a hardcoded date so the dashboard has data to display
    return [DateTime.now().subtract(const Duration(days: 14))];
  }
}
  • Save cycle_data_service.dart.
  • Confirm all four files exist by running:
ls lib/

You should see calendar_screen.dart, cycle_data_service.dart, dashboard_screen.dart, and main.dart listed.

Why create placeholder files?

In Dart, every import statement must point to a real file. The app's main.dart will import calendar_screen.dart and cycle_data_service.dart even though you have not built them yet. Without these placeholders, Dart would refuse to compile.

The CycleDataService stub returns a hardcoded period start date from 14 days ago. This gives the dashboard real data to display right away. You will replace both stubs with full implementations in later steps.

Now you can build the main app shell. This is the main.dart file that Flutter runs first. It sets up the theme, creates a home screen with navigation, and passes data to your dashboard.

  • Open lib/main.dart in the editor. Select all existing code and delete it.
  • Paste the app's imports and root widget:
import 'package:flutter/material.dart';
import 'calendar_screen.dart';
import 'cycle_data_service.dart';
import 'dashboard_screen.dart';

void main() {
  runApp(const PMOSTrackerApp());
}

// Root widget with the app's theme
class PMOSTrackerApp extends StatelessWidget {
  const PMOSTrackerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PMOS Tracker',
      theme: ThemeData(
        // Pink/rose color scheme for the period tracker
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFFE91E63),
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      home: const HomeScreen(),
    );
  }
}

What does PMOSTrackerApp do?

PMOSTrackerApp is a StatelessWidget that wraps your entire app in a MaterialApp. The ColorScheme.fromSeed call generates a full colour palette from a single pink seed colour (0xFFE91E63).

Setting useMaterial3: true enables Material Design 3 styling across all widgets. The home property points to HomeScreen, which you will add next.

  • Below PMOSTrackerApp, add the HomeScreen widget with its state and data loading methods:
class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _selectedIndex = 0;
  // Store period start dates shared between screens
  List<DateTime> _periodStartDates = [];
  final CycleDataService _dataService = CycleDataService();
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  // Load saved period dates when the app starts
  Future<void> _loadData() async {
    final dates = await _dataService.loadPeriodDates();
    setState(() {
      _periodStartDates = dates;
      _isLoading = false;
    });
  }

What is a StatefulWidget?

A StatefulWidget is a widget that can change over time. The HomeScreen needs to track which screen is selected and which period dates have been logged. When that data changes, setState tells Flutter to rebuild the UI with the new values.

When the app starts, initState runs automatically and calls _loadData to fetch any saved period dates from CycleDataService. Right now the stub returns a hardcoded date, but later it will load from real storage.

  • Continue adding the method to log new period dates, plus the start of the build method:
  // Add a new period date, sort the list, and save
  Future<void> _addPeriodDate(DateTime date) async {
    setState(() {
      _periodStartDates.add(date);
      _periodStartDates.sort();
    });
    await _dataService.savePeriodDates(_periodStartDates);
  }

  @override
  Widget build(BuildContext context) {
    // Show a loading spinner while data loads
    if (_isLoading) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }

    // Both screens receive the same period data
    final screens = [
      DashboardScreen(periodStartDates: _periodStartDates),
      CalendarScreen(
        periodStartDates: _periodStartDates,
        onPeriodLogged: _addPeriodDate,
      ),
    ];
  • Complete the build method by adding the bottom navigation bar and closing the class:
    return Scaffold(
      body: screens[_selectedIndex],
      // Bottom navigation to switch between Dashboard and Calendar
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: 'Dashboard',
          ),
          NavigationDestination(
            icon: Icon(Icons.calendar_month_outlined),
            selectedIcon: Icon(Icons.calendar_month),
            label: 'Calendar',
          ),
        ],
      ),
    );
  }
}

How does the navigation work?

The NavigationBar widget shows a Dashboard tab and a Calendar tab at the bottom of the screen. When the user taps a tab, onDestinationSelected fires and updates _selectedIndex with setState.

The screens list holds both screen widgets. screens[_selectedIndex] picks the correct one to display. Both screens share the same _periodStartDates list so changes in one screen are visible in the other.

  • Save main.dart.

✔️ Awesome, I've got everything!

Great. Confirm you have saved both main.dart and dashboard_screen.dart before running the app.

ⓧ I'd like to double check the full code

Scroll through your main.dart and confirm these sections appear in order:

  • Four import statements at the top (material.dart, calendar_screen.dart, cycle_data_service.dart, dashboard_screen.dart).
  • The main() function and PMOSTrackerApp class with the pink theme and home: const HomeScreen().
  • The HomeScreen StatefulWidget and _HomeScreenState with _selectedIndex, _periodStartDates, _dataService, and _isLoading.
  • The _loadData and _addPeriodDate methods.
  • The build method with the loading check, screens list, and NavigationBar with two NavigationDestination items.
Run the app on the emulator

All four files are in place. Time to launch the app on your Android emulator and see the dashboard for the first time.

  • Make sure your Android emulator is still running from Step 1. You should see the emulator window on screen.
  • In VS Code, press Ctrl+Shift+P to open the Command Palette.
  • Type Flutter: Select Device and select your running Android emulator from the list.

Before you launch the app, what do you think the dashboard will show? Remember the stub returns a period start date from 14 days ago.

  • Press F5 to start debugging. The first build takes a minute or two as Flutter compiles for Android.

The app launches on the emulator. You should see the PMOS Tracker dashboard with a large "Day 15" in blue, the phase name "Ovulatory Phase", a description about ovulation and PMOS, and a stats row showing days until next period, cycle length, and last period date.

Why Day 15 and Ovulatory Phase?

The stub CycleDataService returns a date 14 days in the past. That makes daysSinceLastPeriod equal to 14, and cycleDay equal to 15 (14 + 1). Day 15 falls in the Ovulatory Phase (days 14 to 16), which is why the card shows blue.

App not launching on the emulator?

  • Check the bottom-right status bar in VS Code. It should show your emulator name. If it shows "No Device", press Ctrl+Shift+P and run Flutter: Select Device again.
  • If you see red error text in the terminal, check that all four files in lib/ are saved. An unsaved file with a missing closing brace will block compilation.
  • Make sure the Android emulator is fully booted (you should see the Android home screen, not just a black window).

help me fix my Flutter app that is not launching on the emulator

  • Try tapping the Calendar tab at the bottom of the app. You should see the placeholder text "Calendar coming in Step 3". Tap Dashboard to switch back.

Your dashboard is live on the emulator. Next up, you will build a real calendar screen so you can see your cycle history across months.

Add the Calendar View with Navigation

Your dashboard is live and showing cycle phase information. That is a great start, but it only tells you about today.

To spot patterns and see your cycle history across weeks and months, you need a calendar view. In this step, you will add an interactive calendar screen with period days highlighted in pink, and connect it to the dashboard with a bottom navigation bar.

In this step, get ready to:
  • Add the table_calendar package for a pre-built calendar widget.
  • Build a calendar screen that highlights period days in pink circles.
  • Navigate between the dashboard and calendar using a bottom navigation bar.
Add the table_calendar package

Building a calendar from scratch would eat most of your project time without teaching you Flutter fundamentals. The table_calendar package gives you a ready-made calendar widget with day selection, custom styling, and month navigation built in.

  • Add the table_calendar package to your project by running this command in the VS Code terminal:
flutter pub add table_calendar
  • You should see output ending with Changed X dependencies! confirming the package was added to your pubspec.yaml.

Seeing a network error?

Make sure you are connected to the internet. The command downloads the package from pub.dev.

Also confirm your terminal is inside the pmos_tracker folder. Run cd pmos_tracker if needed.

Help me troubleshoot the flutter pub add command

Build the calendar screen

The calendar screen is a StatefulWidget that wraps the TableCalendar widget. It receives period start dates from the parent, expands each into a 5-day range, and highlights those days in pink.

  • In the VS Code Explorer sidebar, right-click the lib folder.
  • Click New File and name it calendar_screen.dart.
  • Build the file section by section. Paste each of the following code blocks in order, one below the previous.
  • Start with the imports, widget class, and the opening of the state class:
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';

// Receives period start dates and a callback for logging new ones
class CalendarScreen extends StatefulWidget {
  final List<DateTime> periodStartDates;
  final Future<void> Function(DateTime) onPeriodLogged;

  const CalendarScreen({
    super.key,
    required this.periodStartDates,
    required this.onPeriodLogged,
  });

  @override
  State<CalendarScreen> createState() => _CalendarScreenState();
}

class _CalendarScreenState extends State<CalendarScreen> {
  // Track which day the calendar shows and which the user tapped
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;
  CalendarFormat _calendarFormat = CalendarFormat.month;
  • Below that, paste the period-day helper methods:

  // Expand each period start date into a 5-day range
  Set<DateTime> get _periodDays {
    final days = <DateTime>{};
    for (final startDate in widget.periodStartDates) {
      for (int i = 0; i < 5; i++) {
        final day = startDate.add(Duration(days: i));
        days.add(DateTime(day.year, day.month, day.day));
      }
    }
    return days;
  }

  // Check whether a given day falls inside any period range
  bool _isPeriodDay(DateTime day) {
    final normalized = DateTime(day.year, day.month, day.day);
    return _periodDays.contains(normalized);
  }
  • Continue with the start of the build method and the TableCalendar configuration:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Cycle Calendar'),
        centerTitle: true,
      ),
      body: Column(
        children: [
          // Configure the interactive calendar widget
          TableCalendar(
            firstDay: DateTime.utc(2020, 1, 1),
            lastDay: DateTime.utc(2030, 12, 31),
            focusedDay: _focusedDay,
            calendarFormat: _calendarFormat,
            selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
            onDaySelected: (selectedDay, focusedDay) {
              setState(() {
                _selectedDay = selectedDay;
                _focusedDay = focusedDay;
              });
              _showLogDialog(selectedDay);
            },
  • Add the format callbacks and the custom day builders that render period days as pink circles:
            onFormatChanged: (format) {
              setState(() {
                _calendarFormat = format;
              });
            },
            onPageChanged: (focusedDay) {
              _focusedDay = focusedDay;
            },
            // Style period days as pink circles
            calendarBuilders: CalendarBuilders(
              defaultBuilder: (context, day, focusedDay) {
                if (_isPeriodDay(day)) {
                  return Container(
                    margin: const EdgeInsets.all(4.0),
                    decoration: const BoxDecoration(
                      color: Color(0xFFE91E63),
                      shape: BoxShape.circle,
                    ),
                    child: Center(
                      child: Text(
                        '${day.day}',
                        style: const TextStyle(color: Colors.white),
                      ),
                    ),
                  );
                }
                return null;
              },
  • Add the builder for today's date when it also falls on a period day:
              // Also style today if it falls on a period day
              todayBuilder: (context, day, focusedDay) {
                if (_isPeriodDay(day)) {
                  return Container(
                    margin: const EdgeInsets.all(4.0),
                    decoration: BoxDecoration(
                      color: const Color(0xFFE91E63),
                      shape: BoxShape.circle,
                      border: Border.all(color: Colors.blue, width: 2),
                    ),
                    child: Center(
                      child: Text(
                        '${day.day}',
                        style: const TextStyle(color: Colors.white),
                      ),
                    ),
                  );
                }
                return null;
              },
            ),
  • Add the default calendar styles, the selected-day hint text, and close the build method:
            // Set default styles for today and selected days
            calendarStyle: const CalendarStyle(
              todayDecoration: BoxDecoration(
                color: Colors.blue,
                shape: BoxShape.circle,
              ),
              selectedDecoration: BoxDecoration(
                color: Color(0xFFAD1457),
                shape: BoxShape.circle,
              ),
            ),
          ),
          const SizedBox(height: 16),
          if (_selectedDay != null)
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16.0),
              child: Text(
                _isPeriodDay(_selectedDay!)
                    ? 'This day is marked as a period day.'
                    : 'Tap a day to log it as a period start date.',
                style: const TextStyle(fontSize: 14, color: Colors.grey),
              ),
            ),
        ],
      ),
    );
  }
  • Finish the file by pasting the period logging dialog and the closing brace:

  void _showLogDialog(DateTime selectedDay) {
    final normalized = DateTime(selectedDay.year, selectedDay.month, selectedDay.day);
    final alreadyLogged = widget.periodStartDates.any(
      (d) => d.year == normalized.year && d.month == normalized.month && d.day == normalized.day,
    );
    if (alreadyLogged) return;
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Log Period Start'),
        content: Text(
          'Log ${normalized.day}/${normalized.month}/${normalized.year} as a period start date?',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          FilledButton(
            onPressed: () {
              widget.onPeriodLogged(normalized);
              Navigator.pop(context);
            },
            child: const Text('Confirm'),
          ),
        ],
      ),
    );
  }
}

What does this code do?

  • CalendarScreen is a StatefulWidget that receives the list of period start dates and a callback to log new ones from its parent.
  • _periodDays expands each start date into a 5-day window (the typical period length) so every day in that range gets highlighted, not just the start day.
  • calendarBuilders uses defaultBuilder and todayBuilder to draw period days as pink circles. Today gets an extra blue border so you can always spot it.
  • _showLogDialog pops up an AlertDialog when you tap any day. If you confirm, the date is passed to the parent widget via the onPeriodLogged callback.
  • Save calendar_screen.dart.

Seeing red underlines in the file?

Check that all seven code blocks were pasted in order with no gaps or duplicated lines. A missing closing brace or a duplicated line causes cascading errors.

Make sure the file is named exactly calendar_screen.dart inside the lib folder. The import in main.dart expects this exact name.

Help me fix syntax errors in my calendar screen file

✔️ Awesome, I've got everything!

Great. Make sure you have saved calendar_screen.dart before continuing.

ⓧ I'd like to double check the full code

Scroll up and confirm these sections appear in your file, in this order:

  • Imports and the CalendarScreen StatefulWidget class.
  • The _CalendarScreenState class with _focusedDay, _selectedDay, and _calendarFormat fields.
  • The _periodDays getter and _isPeriodDay helper method.
  • The build method opening with Scaffold, AppBar, and TableCalendar.
  • The calendarBuilders with defaultBuilder and todayBuilder rendering pink circles.
  • The calendarStyle, SizedBox, and the conditional selected-day text.
  • The _showLogDialog method and the final closing brace.
Verify navigation between screens

Your main.dart already has a NavigationBar wired up from the previous step. Now that calendar_screen.dart exists, the app can finally compile with both screens.

How does the navigation work?

The HomeScreen widget in main.dart keeps a _selectedIndex variable. A NavigationBar at the bottom has two destinations: Dashboard and Calendar.

When you tap a destination, onDestinationSelected updates the index with setState, and the build method swaps which screen is displayed. Both screens receive the same _periodStartDates list so they always show consistent data.

Before you restart the app, predict: what will you see when you tap the Calendar icon at the bottom of the screen?

  • Hot restart the app by pressing Ctrl+Shift+F5 or clicking the green restart button in the debug toolbar.
  • The dashboard loads first. Tap the Calendar icon in the bottom navigation bar.

You should see a full calendar displaying the current month with today's date highlighted in blue. No period days are highlighted yet because you have not logged any.

  • Tap any day on the calendar.

A dialog pops up asking if you want to log that day as a period start date. Tap Confirm. You should see that day and the next four days turn into pink circles on the calendar.

  • Tap the Dashboard icon in the bottom navigation bar.

The dashboard now shows a cycle day, phase name, and countdown based on the date you just logged. Both screens share the same data.

Calendar not showing up?

If the app shows a blank screen or crashes, check the VS Code debug console for error messages. A common issue is a missing closing bracket in calendar_screen.dart. Compare your file against the section checklist in the double-check tab above.

If the bottom navigation bar does not appear, make sure you hot restarted (not just hot reloaded). New files require a full restart.

Help me debug why my calendar screen is not rendering

Your tracker now has two screens connected by a bottom navigation bar. You can view your cycle on a calendar and log period start dates with a tap. Next up, you will make period logging truly interactive and connect the data between both screens so every change is reflected everywhere.

Make Period Logging Work

Your app has a dashboard and a calendar, but they are not truly connected yet. The dashboard can display cycle information, and the calendar can show days. Without a way to log your own period dates and have the dashboard respond, the app is just a static display.

In this step, you will test the full period logging flow end to end. You will tap a day on the calendar, confirm it as a period start, and watch the dashboard recalculate your cycle day, phase, and countdown in real time. Then you will discover a critical gap that the next step will fix.

In this step, get ready to:
  • Log a period start date using the calendar's dialog.
  • Verify the dashboard updates live based on your logged data.
  • Discover what happens to your data when the app restarts.
Log a period start date

The calendar screen you built in the last step already has a logging dialog wired up. When you tap any day, the _showLogDialog method fires and shows an AlertDialog. Let's look at what that method does, then test it live.

  • In VS Code, open lib/calendar_screen.dart and scroll to the _showLogDialog method at the bottom of the file.
  • Read through the method. Here is what it looks like:
void _showLogDialog(DateTime selectedDay) {
  final normalized = DateTime(
    selectedDay.year, selectedDay.month, selectedDay.day,
  );

  // Skip if this date is already logged
  final alreadyLogged = widget.periodStartDates.any(
    (d) => d.year == normalized.year &&
           d.month == normalized.month &&
           d.day == normalized.day,
  );

  if (alreadyLogged) return;

  // Show a confirmation dialog
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Log Period Start'),
      content: Text(
        'Log ${normalized.day}/${normalized.month}/${normalized.year}'
        ' as a period start date?',
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('Cancel'),
        ),
        FilledButton(
          onPressed: () {
            widget.onPeriodLogged(normalized);
            Navigator.pop(context);
          },
          child: const Text('Confirm'),
        ),
      ],
    ),
  );
}

What does this code do?

  • normalized strips the time from the selected day so dates compare correctly (just year, month, day).
  • alreadyLogged checks whether this date is already in the list. If it is, the dialog does not appear.
  • showDialog displays the AlertDialog with the date and two buttons.
  • When the learner taps Confirm, widget.onPeriodLogged(normalized) sends the date back up to the parent widget (HomeScreen).

Time to test it on the emulator.

  • Switch to the running app on the Android emulator.
  • Tap the Calendar icon in the bottom navigation bar.
  • Tap any day a few days in the past.

An AlertDialog titled "Log Period Start" should pop up, showing the date you tapped and asking you to confirm.

  • Tap Confirm.

The day you tapped and the next four days should now appear as pink circles on the calendar. The _periodDays getter generates 5 days from each start date, matching a typical 5-day period.

Don't see the dialog or pink circles?

  • Make sure the app is running on the emulator. If you stopped it, press F5 in VS Code to start debugging again.
  • Try tapping a different day. If you already logged that date, the dialog will not appear because alreadyLogged blocks duplicate entries.
  • Check that onDaySelected in the TableCalendar calls _showLogDialog(selectedDay).

Why isn't the period logging dialog appearing when I tap a day?

Verify the dashboard updates live

The logged date travels from the calendar screen back to the dashboard through a pattern called state lifting. The HomeScreen widget owns the _periodStartDates list and passes it down to both screens. When a new date is logged, both screens rebuild with the updated data.

  • In lib/main.dart, find the _addPeriodDate method. Here is the flow it triggers:
Future<void> _addPeriodDate(DateTime date) async {
  setState(() {
    // Add the new date to the shared list
    _periodStartDates.add(date);
    // Sort so the most recent date is always last
    _periodStartDates.sort();
  });
  await _dataService.savePeriodDates(_periodStartDates);
}

How does state lifting work here?

When the learner taps Confirm in the dialog, widget.onPeriodLogged calls _addPeriodDate in HomeScreen. setState tells Flutter to rebuild the widget tree.

Because both DashboardScreen and CalendarScreen receive _periodStartDates as a constructor argument, they both get the updated list when HomeScreen rebuilds. One source of truth, two consumers.

Now test whether the dashboard actually picks up your logged data.

  • In the emulator, tap the Dashboard icon in the bottom navigation bar.

The dashboard should now show a cycle day number, the current phase name (like "Follicular Phase" or "Luteal Phase"), a PMOS-specific description, and a countdown to your next expected period. All of these values are calculated from the date you just logged.

Dashboard still shows 'No period data logged yet'?

  • Go back to the Calendar tab and confirm you see pink circles on the days you logged. If not, the logging did not succeed.
  • Check that DashboardScreen in lib/main.dart receives periodStartDates: _periodStartDates as an argument.

Why isn't my dashboard updating after I log a period date?

Experience what happens on restart

Your period logging works. The calendar and dashboard share data and update in real time. But where does that data actually live?

The _periodStartDates list is a variable inside _HomeScreenState. It exists only in the app's memory. Before you run the next command, think about what will happen to that variable when the app process restarts from scratch. Do you think your logged dates will still be there?

  • Hot-restart the app by pressing Ctrl+Shift+F5 in VS Code, or click the green circular arrow in the debug toolbar.

The dashboard now shows "No period data logged yet." All your logged period dates are gone.

Why did the data disappear?

A hot-restart destroys the entire widget tree and creates it fresh. The _HomeScreenState object is rebuilt from scratch, and _periodStartDates starts as an empty list again.

This is the gap. A health tracker that forgets your cycle history every time you close the app is not useful. The data needs to be saved somewhere that survives restarts.

Your period logging flow is working perfectly. The calendar and dashboard communicate through state lifting, and the cycle calculations update in real time. The one missing piece is persistence. In the next step, you will save your data to the device's local storage so it survives any restart.

Persist Cycle Data Locally

In the last step, you made period logging interactive and experienced the painful reality of in-memory data. You logged a date, watched the dashboard update, then lost everything on restart.

For a health tracker, losing cycle data is unacceptable. In this step, you'll add local persistence using the shared_preferences package so your period dates survive app restarts and device reboots.

In this step, get ready to:
  • Add the shared_preferences package to your project.
  • Create a CycleDataService class that saves and loads period dates to disk.
  • Verify that logged period data persists after stopping and relaunching the app.
Add the shared_preferences package

Your app needs a way to write data to disk so it sticks around between sessions. The shared_preferences package provides a simple key-value store that persists data on both Android and iOS.

  • Add the package to your project by running this command in the VS Code terminal:
flutter pub add shared_preferences
  • Confirm the command output shows shared_preferences being added to your pubspec.yaml.

Command not found?

Make sure your terminal is inside the pmos_tracker folder. Run cd pmos_tracker if you're in the parent directory.

help me fix the flutter pub add command

Why shared_preferences over a database?

For storing a small list of dates, shared_preferences is the lightest option with zero setup. It stores simple key-value pairs on the device's file system.

A full database like SQLite would be overkill for this use case. You'd reach for one when your data has relationships or needs complex queries.

Create the cycle data service

Your main.dart already references a CycleDataService class for loading and saving period dates. Now you need to create the actual file that makes those methods persist data to disk.

  • Create a new file called cycle_data_service.dart inside the lib folder of your pmos_tracker project.
  • In the VS Code Explorer sidebar, right-click the lib folder and select New File.
  • Name it cycle_data_service.dart and press Enter.
  • Paste the following code into the new file:
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';

class CycleDataService {
  // Key used to store period dates in shared_preferences
  static const _periodDatesKey = 'period_start_dates';

  // Convert DateTime list to JSON strings and save to disk
  Future<void> savePeriodDates(List<DateTime> dates) async {
    final prefs = await SharedPreferencesAsync();
    final dateStrings = dates.map((d) => d.toIso8601String()).toList();
    await prefs.setString(_periodDatesKey, jsonEncode(dateStrings));
  }

  // Load JSON strings from disk and parse back to DateTime objects
  Future<List<DateTime>> loadPeriodDates() async {
    final prefs = await SharedPreferencesAsync();
    final jsonString = await prefs.getString(_periodDatesKey);
    if (jsonString == null) return [];
    final List<dynamic> dateStrings = jsonDecode(jsonString);
    return dateStrings.map((s) => DateTime.parse(s as String)).toList();
  }
}

What does this code do?

  • _periodDatesKey is the storage key. Think of it like a label on a box where the dates go on the device's file system.
  • savePeriodDates converts each DateTime to an ISO 8601 string (like 2026-08-15T00:00:00.000), wraps the list in JSON, and writes it to disk.
  • loadPeriodDates reads that JSON string back from disk and parses each string into a DateTime object. If nothing has been saved yet, it returns an empty list.
  • SharedPreferencesAsync is the modern API for the package. It handles all the platform-specific storage under the hood.
  • Save cycle_data_service.dart.
  • Press Shift+R in the terminal (or click the green restart button in the debug toolbar) to hot-restart the app.
  • The app should load on the emulator without any errors. If you have no previously logged dates, you'll see the empty dashboard message.

Seeing a compilation error?

  • Check that the file is named exactly cycle_data_service.dart and sits directly inside the lib folder. The import in main.dart expects this exact path.
  • Make sure you ran flutter pub add shared_preferences in the previous substep. Without the package, the import line will fail.

help me fix a compilation error in my Flutter project after creating cycle_data_service.dart

✔️ Awesome, I've got everything!

Great. Make sure you've saved cycle_data_service.dart and that the app loads without errors on the emulator.

ⓧ I'd like to double check the full code

Here is the complete lib/cycle_data_service.dart file. Make sure yours matches exactly.

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';

class CycleDataService {
  static const _periodDatesKey = 'period_start_dates';

  Future<void> savePeriodDates(List<DateTime> dates) async {
    final prefs = await SharedPreferencesAsync();
    final dateStrings = dates.map((d) => d.toIso8601String()).toList();
    await prefs.setString(_periodDatesKey, jsonEncode(dateStrings));
  }

  Future<List<DateTime>> loadPeriodDates() async {
    final prefs = await SharedPreferencesAsync();
    final jsonString = await prefs.getString(_periodDatesKey);
    if (jsonString == null) return [];
    final List<dynamic> dateStrings = jsonDecode(jsonString);
    return dateStrings.map((s) => DateTime.parse(s as String)).toList();
  }
}
Verify data persists across restarts

The service file is ready. Your main.dart already calls CycleDataService in two places, so the integration is already wired up from the previous step.

How does main.dart use CycleDataService?

  • _loadData() runs when the app starts (inside initState). It calls loadPeriodDates() to read saved dates from disk and populate the dashboard and calendar.
  • _addPeriodDate() is called whenever you confirm a new period start date. It updates the in-memory list with setState and then calls savePeriodDates() to write the updated list to disk.

This means every date you log gets saved immediately, and every app launch loads from the last save. No data loss.

Time to prove it works. First, log a period start date so there is data to persist.

  • Make sure the app is running on the emulator.
  • Tap the Calendar tab in the bottom navigation bar.
  • Tap any date on the calendar.
  • In the dialog that appears, tap Confirm to log it as a period start date.
  • Tap the Dashboard tab. You should see cycle day, phase name, and days-until-next-period updated based on the date you just logged.

Now for the real test. Before you stop the app, do you think the dashboard will still show the same cycle data when you relaunch?

  • Press the Stop button in the VS Code debug toolbar (the red square) to fully stop the app.
  • Press F5 to relaunch the app from scratch.

The dashboard loads with the same cycle day, phase name, and countdown as before. Your logged period date survived the restart. No more vanishing data.

  • Tap the Calendar tab to confirm the pink circles are still marking your logged period days.

Dashboard is empty after restart?

  • Make sure you used Stop (red square) and then F5 to relaunch. A hot-restart (Shift+R) should also preserve data, but fully stopping and relaunching is the definitive test.
  • Open cycle_data_service.dart and verify the class name is exactly CycleDataService. A mismatch with the class name in main.dart would cause a compile error, not silent data loss.
  • Check that savePeriodDates calls prefs.setString with jsonEncode(dateStrings). A missing await before prefs.setString can cause the save to not complete before the app closes.

my period data is not persisting after app restart in my Flutter project

Your PMOS period tracker is fully functional. It calculates cycle phases, displays a visual calendar, and persists data locally. Next up, there's an optional Secret Mission waiting to push your app even further.

Secret mission

Add a PMOS Symptom Logger

Period tracking alone does not capture the full picture of PMOS. In this mission, you will add a symptom logger that lets you track common symptoms like bloating, fatigue, mood changes, and acne per day, then see them as color-coded dot markers on the calendar.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your machine with no cloud resources or ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The pmos_tracker project folder on your Desktop.
  • The Flutter SDK folder (e.g., C:\Flutter).
  • Android Studio and the Android emulator virtual device.
  • The Flutter SDK entry in your system PATH environment variable.

Keep everything running

No action needed. Choose this if you are still actively building or plan to continue with more Flutter projects.

  • Flutter, Android Studio, and your pmos_tracker project stay installed and ready to use.
  • Your Android emulator configuration is saved and can be launched again from Android Studio at any time.

Pause - I'll come back to this later

Shut down running processes to free up memory while keeping your files and configurations on disk.

  • Stop the app debug session in VS Code by pressing the Stop button in the debug toolbar.
  • Close the Android emulator window.
  • Close Android Studio if it is open.
  • Your project files, Flutter SDK, and emulator configuration remain on disk for when you return.

Delete - I don't want to use this again

Remove all project files, the Flutter SDK, and Android Studio from your machine.

  • Delete the pmos_tracker project folder by running this command in your VS Code terminal:
Remove-Item -Recurse -Force ~\Desktop\pmos_tracker
  • Delete the Flutter SDK folder by running:
Remove-Item -Recurse -Force C:\Flutter

Installed Flutter elsewhere?

Replace C:\Flutter with the actual path you chose during setup.

  • Open Windows Settings.
  • Select Apps, then Installed apps.
  • Find Android Studio, click the three-dot menu, and select Uninstall.

Finally, remove the Flutter SDK from your system PATH.

  • Press the Windows key and search for Environment Variables.
  • Select Edit the system environment variables, then click Environment Variables.
  • Under User variables, select Path and click Edit.
  • Find the entry containing Flutter\bin and click Delete.
  • Click OK on all open dialogs to save your changes.

Nice Work!

Nice Work!

Nice work! You just built a fully functional PMOS period tracker from scratch using Flutter and deployed it to an Android emulator.

You've learned how to:

  • Built a multi-screen PMOS period tracker app with a dashboard showing cycle day, phase name, and PMOS-specific phase descriptions, a calendar view with period days highlighted in pink, and a cycle logging form for recording period start dates.
  • Learned Flutter fundamentals including StatefulWidget, setState, navigation with BottomNavigationBar, and widget composition to build a real mobile app from a single Dart codebase.
  • Implemented local data persistence with the shared_preferences package so cycle data survives app restarts, and used the table_calendar package to build an interactive calendar with custom day styling and event markers.
  • Secret Mission: Added a PMOS symptom logger with color-coded markers on the calendar, giving you a richer health tracking view.

Ready to quiz yourself?