DEV Community

Sean Atukorala
Sean Atukorala

Posted on

Building a Calorie Tracker App in Flutter

In order to get a firm understanding of any new programming language we might be learning, it is best to build a simple application with that language in order to actively learn its pros/cons and its intricacies. 
What better way to learn Flutter than to build a Calorie Tracker app with it! In this tutorial post I will cover how to build a Calorie Tracker app using Flutter, so let's get started!

What We Will Build

Here are some screenshots of the Calorie Tracker application that we will build:
Flutter 1
Flutter 2
Flutter 3
Flutter 4
Flutter 5

Figure 1: Screenshots of the Calorie Tracker App we're about to build

Setup

GitHub Starter Template Here: https://github.com/ShehanAT/flutter_calorie_tracker_app/tree/starter-template

In order to follow along in this tutorial I highly recommend using the starter template project posted above as this will quicken the development process for all of us.
Then, make sure to install Flutter at the following link
Finally, we'll need a Firebase project for the application we're building so make sure to head over to the Firebase Console and create a new project if you haven't already.
As for the development environment, I'll be using VSCode(download link here) so if you don't have it installed, you can do so now. Alternatively, using Android Studio(download link here) is also possible.

Setting up Firebase 

Once you have a Firebase project up and running the project creation process will allow you to download a google-services.json file. Make sure to place this file in the {root_dir}/android/app directory as this is how our Flutter application is able to connect with our Firebase project. 
We will be using the Firestore database as our data source for this application so let's create a Firestore database instance.
In the Firebase Console, click on the 'Firestore Database' tab and then click on 'Create Database' as shown in this screenshot:
Firebase Firestore<br>
    Database

Then select 'Start in Test Mode' in the modal pop-up, select a region closed to you and create the database.

Installing Packages

First, visit the starter template link above and clone the starter-template branch into your local machine. Then, open it up in VSCode and run the following command in a Git Bash terminal instance while at the root directory of this project:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

This command is used to install all the necessary packages used in this application, most notably the charts_flutter library.

Adding code to files

Now comes the development part!
First, let's add the following code to the main.dart file:

import 'package:calorie_tracker_app/src/model/food_track_task.dart';
import 'package:calorie_tracker_app/src/page/day-view/day-view.dart';
import 'package:calorie_tracker_app/src/page/settings/settings_screen.dart';
import 'package:flutter/material.dart';
import 'src/page/history/history_screen.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:provider/provider.dart';
import 'package:calorie_tracker_app/src/providers/theme_notifier.dart';
import 'package:calorie_tracker_app/src/services/shared_preference_service.dart';
import 'package:calorie_tracker_app/helpers/theme.dart';
import 'package:calorie_tracker_app/routes/router.dart';
import 'package:firebase_database/firebase_database.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  await SharedPreferencesService().init();
  runApp(CalorieTrackerApp());
}

class CalorieTrackerApp extends StatefulWidget {
  @override
  _CalorieTrackerAppState createState() => _CalorieTrackerAppState();
}

class _CalorieTrackerAppState extends State<CalorieTrackerApp> {
  DarkThemeProvider themeChangeProvider = DarkThemeProvider();
  late Widget homeWidget;
  late bool signedIn;

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

  void checkFirstSeen() {
    final bool _firstLaunch = true;

    if (_firstLaunch) {
      homeWidget = Homepage();
    }
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<DarkThemeProvider>(
      create: (_) {
        return themeChangeProvider;
      },
      child: Consumer<DarkThemeProvider>(
        builder:
            (BuildContext context, DarkThemeProvider value, Widget? child) {
          return GestureDetector(
              onTap: () => hideKeyboard(context),
              child: MaterialApp(
                  debugShowCheckedModeBanner: false,
                  builder: (_, Widget? child) => ScrollConfiguration(
                      behavior: MyBehavior(), child: child!),
                  theme: themeChangeProvider.darkTheme ? darkTheme : lightTheme,
                  home: homeWidget,
                  onGenerateRoute: RoutePage.generateRoute));
        },
      ),
    );
  }

  void hideKeyboard(BuildContext context) {
    final FocusScopeNode currentFocus = FocusScope.of(context);
    if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
      FocusManager.instance.primaryFocus!.unfocus();
    }
  }
}

class Homepage extends StatefulWidget {
  const Homepage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: FlatButton(
          onPressed: () {
            // Navigate back to homepage
          },
          child: const Text('Go Back!'),
        ),
      ),
    );
  }

  @override
  State<StatefulWidget> createState() {
    return _Homepage();
  }
}

class _Homepage extends State<Homepage> with SingleTickerProviderStateMixin {
  @override
  void initState() {
    super.initState();
  }

  void onClickHistoryScreenButton(BuildContext context) {
    Navigator.of(context)
        .push(MaterialPageRoute(builder: (context) => HistoryScreen()));
  }

  void onClickSettingsScreenButton(BuildContext context) {
    Navigator.of(context)
        .push(MaterialPageRoute(builder: (context) => SettingsScreen()));
  }

  void onClickDayViewScreenButton(BuildContext context) {
    Navigator.of(context)
        .push(MaterialPageRoute(builder: (context) => DayViewScreen()));
  }

  @override
  Widget build(BuildContext context) {
    final ButtonStyle buttonStyle =
        ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20));

    return Scaffold(
        appBar: AppBar(
          title: Text(
            "Flutter Calorie Tracker App",
            style: TextStyle(
                color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
          ),
        ),
        body: new Column(
          children: <Widget>[
            new ListTile(
                leading: const Icon(Icons.food_bank),
                title: new Text("Welcome To Calorie Tracker App!",
                    textAlign: TextAlign.center,
                    style: const TextStyle(fontWeight: FontWeight.bold))),
            new ElevatedButton(
                onPressed: () {
                  onClickDayViewScreenButton(context);
                },
                child: Text("Day View Screen")),
            new ElevatedButton(
                onPressed: () {
                  onClickHistoryScreenButton(context);
                },
                child: Text("History Screen")),
            new ElevatedButton(
                onPressed: () {
                  onClickSettingsScreenButton(context);
                },
                child: Text("Settings Screen")),
          ],
        ));
  }
}

class MyBehavior extends ScrollBehavior {
  @override
  Widget buildViewportChrome(
      BuildContext context, Widget child, AxisDirection axisDirection) {
    return child;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now for an explanation of the important parts of the above code:

  • Future<void> main() async: Instead of the standard void main() method we have to specify the return type as Future<void> because we are using the async and await keywords in this method. Since these keywords are asynchronous, we have to adjust the return type of the method accordingly.
  • class CalorieTrackerApp: This class is the main entrypoint of the application. It responsible for rendering the Homepage widget when the app is first launched. Its build() method does the rendering and uses the ChangeNotifierProvider provider-wrapper class to set a dark theme for the entire application, with the help of DarkThemeProvider
  • class Homepage: This class is the home screen for the application. This class renders three buttons for navigating to the Day View, History and Settings screens of the application. We use the Navigator.of(context).push(MaterialPageRoute(builder: (context) => DayViewScreen())) statement to switch to the desired screen

Now let's build out the model files, starting with lib\src\model\food_track_task.dart:

import 'package:json_annotation/json_annotation.dart';
import 'package:calorie_tracker_app/src/utils/uuid.dart';
import 'package:firebase_database/firebase_database.dart';

@JsonSerializable()
class FoodTrackTask {
  String id;
  String food_name;
  num calories;
  num carbs;
  num fat;
  num protein;
  String mealTime;
  DateTime createdOn;
  num grams;

  FoodTrackTask({
    required this.food_name,
    required this.calories,
    required this.carbs,
    required this.protein,
    required this.fat,
    required this.mealTime,
    required this.createdOn,
    required this.grams,
    String? id,
  }) : this.id = id ?? Uuid().generateV4();

  factory FoodTrackTask.fromSnapshot(DataSnapshot snap) => FoodTrackTask(
      food_name: snap.child('food_name').value as String,
      calories: snap.child('calories') as int,
      carbs: snap.child('carbs').value as int,
      fat: snap.child('fat').value as int,
      protein: snap.child('protein').value as int,
      mealTime: snap.child('mealTime').value as String,
      grams: snap.child('grams').value as int,
      createdOn: snap.child('createdOn').value as DateTime);

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'mealTime': mealTime,
      'food_name': food_name,
      'calories': calories,
      'carbs': carbs,
      'protein': protein,
      'fat': fat,
      'grams': grams,
      'createdOn': createdOn
    };
  }

  FoodTrackTask.fromJson(Map<dynamic, dynamic> json)
      : id = json['id'],
        mealTime = json['mealTime'],
        calories = json['calories'],
        createdOn = DateTime.parse(json['createdOn']),
        food_name = json['food_name'],
        carbs = json['carbs'],
        fat = json['fat'],
        protein = json['protein'],
        grams = json['grams'];

  Map<dynamic, dynamic> toJson() => <dynamic, dynamic>{
        'id': id,
        'mealTime': mealTime,
        'createdOn': createdOn.toString(),
        'food_name': food_name,
        'calories': calories,
        'carbs': carbs,
        'fat': fat,
        'protein': protein,
        'grams': grams,
      };
}
Enter fullscreen mode Exit fullscreen mode

So this model class is the primary class that will hold the information of each food tracking instance. The mealTime field defines the time in which the food was consumed, the createdOn field defines the time in which it was tracked and the carbs, fat, protein and grams fields convey the quality and nutritional value of the food consumed.

Next is a relatively minor model class: lib\src\model\food_track_entry.dart:

class FoodTrackEntry {
  DateTime date;
  int calories;

  FoodTrackEntry(this.date, this.calories);
}
Enter fullscreen mode Exit fullscreen mode

This class will be used as entry points for the charts_flutter Time Series chart that we'll be developing in the History Screen.

Next up is the developing of the services folder. We'll start with the lib/src/services/database.dart file:

import 'package:calorie_tracker_app/src/model/food_track_task.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class DatabaseService {
  final String uid;
  final DateTime currentDate;
  DatabaseService({required this.uid, required this.currentDate});

  final DateTime today =
      DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
  final DateTime weekStart = DateTime(2020, 09, 07);
  // collection reference
  final CollectionReference foodTrackCollection =
      FirebaseFirestore.instance.collection('foodTracks');

  Future addFoodTrackEntry(FoodTrackTask food) async {
    return await foodTrackCollection
        .doc(food.createdOn.millisecondsSinceEpoch.toString())
        .set({
      'food_name': food.food_name,
      'calories': food.calories,
      'carbs': food.carbs,
      'fat': food.fat,
      'protein': food.protein,
      'mealTime': food.mealTime,
      'createdOn': food.createdOn,
      'grams': food.grams
    });
  }

  Future deleteFoodTrackEntry(FoodTrackTask deleteEntry) async {
    print(deleteEntry.toString());
    return await foodTrackCollection
        .doc(deleteEntry.createdOn.millisecondsSinceEpoch.toString())
        .delete();
  }

  List<FoodTrackTask> _scanListFromSnapshot(QuerySnapshot snapshot) {
    return snapshot.docs.map((doc) {
      return FoodTrackTask(
        id: doc.id,
        food_name: doc['food_name'] ?? '',
        calories: doc['calories'] ?? 0,
        carbs: doc['carbs'] ?? 0,
        fat: doc['fat'] ?? 0,
        protein: doc['protein'] ?? 0,
        mealTime: doc['mealTime'] ?? "",
        createdOn: doc['createdOn'].toDate() ?? DateTime.now(),
        grams: doc['grams'] ?? 0,
      );
    }).toList();
  }

  Stream<List<FoodTrackTask>> get foodTracks {
    return foodTrackCollection.snapshots().map(_scanListFromSnapshot);
  }

  Future<List<dynamic>> getAllFoodTrackData() async {
    QuerySnapshot snapshot = await foodTrackCollection.get();
    List<dynamic> result = snapshot.docs.map((doc) => doc.data()).toList();
    return result;
  }

  Future<String> getFoodTrackData(String uid) async {
    DocumentSnapshot snapshot = await foodTrackCollection.doc(uid).get();
    return snapshot.toString();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now for an explanation for it:

  • This is the class used to interact with the Firebase Firestore instance we created in the previous steps
  • uid: This is the universal identifier for the Firestore instance. This value can be found by accessing the Firestore database in the Firebase Console
  • foodTrackCollection: This is the FirebaseFirestore instance that allows us to connect to the foodTracks collection in the Firestore database we previously created
  • addFoodTrackEntry(FoodTrackTask food): This is the method used to create a new record in the foodTracks Firestore collection. Notice that the record identifier is the millisecondsSinceEpoch value based on the FoodTrackTask instance's createdOn field. This is done to ensure uniqueness in the collection
  • deleteFoodTrackEntry(FoodTrackTask deleteEntry): This is the method used to delete a record in the foodTracks Firestore collection. It uses the millisecondsSinceEpoch value based on the createdOn field from the FoodTrackTask instance that is passed in as a parameter to identify the record to be deleted
  • _scanListFromSnapshot(QuerySnapshot snapshot): This method is used to convert the data in the QuerySnapshot response object from Firestore to a List of FoodTrackTask instances. We use the popular map() function in order to do so
  • get foodtracks: This method only used by a StreamProvider instance in the Day View screen(we'll get to building it soon) to provide a list of FoodTrackTask instances that will be displayed in a list format
  • getAllFoodTrackData(): This method is used to fetch all foodTrack records from the database and return them in a List<dynamic> object. Note that we should avoid using the dynamic data type whenever possible, but it would be acceptable in this context because we're not 100% sure of what is being returned as a response from the database(More on the dynamic data type in the flutterbyexample.com site)
  • getFoodTrackData(String uid): This method is used to fetch a specific document from the Firestore database, based on its universal identifier. It can also provide the same result as the getAllFoodTrackData() method if the uid is set to the collection name

Ok making progress..
Next let's build out the lib/src/services/shared_preference_service.dart file:

import 'package:shared_preferences/shared_preferences.dart';

class SharedPreferencesService {
  static late SharedPreferences _sharedPreferences;

  Future<void> init() async {
    _sharedPreferences = await SharedPreferences.getInstance();
  }

  static String sharedPreferenceDarkThemeKey = 'DARKTHEME';

  static Future<bool> setDarkTheme({required bool to}) async {
    return _sharedPreferences.setBool(sharedPreferenceDarkThemeKey, to);
  }

  static bool getDarkTheme() {
    return _sharedPreferences.getBool(sharedPreferenceDarkThemeKey) ?? true;
  }
}
Enter fullscreen mode Exit fullscreen mode

Here is its explanation:

  • The shared_preferences package is used for the common tasks of storing and caching values on disk. This service class will serve to return instances of the SharedPreferences class and therefore follows the Singleton design pattern
  • On top of the above functionality, this class also assists in providing the dark theme functionality described in the main.dart file through the getDarkTheme() method

Ok on to the providers folder where we'll build the lib/src/providers/theme_notifier.dart file:

import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:calorie_tracker_app/src/services/shared_preference_service.dart';

class DarkThemeProvider with ChangeNotifier {
  // The 'with' keyword is similar to mixins in JavaScript, in that it is a way of reusing a class's fields/methods in a different class that is not a super class of the initial class.

  bool get darkTheme {
    return SharedPreferencesService.getDarkTheme();
  }

  set dartTheme(bool value) {
    SharedPreferencesService.setDarkTheme(to: value);
    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

This class will serve as a provider to the CalorieTrackerAppState class in the main.dart file while also enabling the dark theme that the application will use by default as of now. Providers are important because they help reduce inefficiencies having to do with re-rendering components whenever state changes occur. When using providers, the only widgets that have to be rebuilt whenever state changes occur are the ones that are assigned as consumers to their appropriate providers. More on Providers and Consumers in this great blog post

Moving on to the utils folder. Let's build the lib/src/utils/charts/datetime_series_chart.dart file now:

import 'package:charts_flutter/flutter.dart' as charts;
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:calorie_tracker_app/src/services/database.dart';
import 'package:calorie_tracker_app/src/model/food_track_task.dart';
import 'package:calorie_tracker_app/src/model/food-track-entry.dart';

class DateTimeChart extends StatefulWidget {
  @override
  _DateTimeChart createState() => _DateTimeChart();
}

class _DateTimeChart extends State<DateTimeChart> {
  List<charts.Series<FoodTrackEntry, DateTime>>? resultChartData = null;
  DatabaseService databaseService = new DatabaseService(
      uid: "calorie-tracker-b7d17", currentDate: DateTime.now());

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

    getAllFoodTrackData();
  }

  void getAllFoodTrackData() async {
    List<dynamic> foodTrackResults =
        await databaseService.getAllFoodTrackData();
    List<FoodTrackEntry> foodTrackEntries = [];

    for (var foodTrack in foodTrackResults) {
      if (foodTrack["createdOn"] != null) {
        foodTrackEntries.add(FoodTrackEntry(
            foodTrack["createdOn"].toDate(), foodTrack["calories"]));
      }
    }
    populateChartWithEntries(foodTrackEntries);
  }

  void populateChartWithEntries(List<FoodTrackEntry> foodTrackEntries) async {
    Map<String, int> caloriesByDateMap = new Map();
    if (foodTrackEntries != null) {
      var dateFormat = DateFormat("yyyy-MM-dd");
      for (var foodEntry in foodTrackEntries) {
        var trackedDateStr = foodEntry.date;
        DateTime dateNow = DateTime.now();
        var trackedDate = dateFormat.format(trackedDateStr);
        if (caloriesByDateMap.containsKey(trackedDate)) {
          caloriesByDateMap[trackedDate] =
              caloriesByDateMap[trackedDate]! + foodEntry.calories;
        } else {
          caloriesByDateMap[trackedDate] = foodEntry.calories;
        }
      }
      List<FoodTrackEntry> caloriesByDateTimeMap = [];
      for (var foodEntry in caloriesByDateMap.keys) {
        DateTime entryDateTime = DateTime.parse(foodEntry);
        caloriesByDateTimeMap.add(
            new FoodTrackEntry(entryDateTime, caloriesByDateMap[foodEntry]!));
      }

      caloriesByDateTimeMap.sort((a, b) {
        int aDate = a.date.microsecondsSinceEpoch;
        int bDate = b.date.microsecondsSinceEpoch;

        return aDate.compareTo(bDate);
      });
      setState(() {
        resultChartData = [
          new charts.Series<FoodTrackEntry, DateTime>(
              id: "Food Track Entries",
              colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
              domainFn: (FoodTrackEntry foodTrackEntry, _) =>
                  foodTrackEntry.date,
              measureFn: (FoodTrackEntry foodTrackEntry, _) =>
                  foodTrackEntry.calories,
              labelAccessorFn: (FoodTrackEntry foodTrackEntry, _) =>
                  '${foodTrackEntry.date}: ${foodTrackEntry.calories}',
              data: caloriesByDateTimeMap)
        ];
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (resultChartData != null) {
      return Scaffold(
        body: new Center(
            child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("Caloric Intake By Date Chart"),
            new Padding(
                padding: new EdgeInsets.all(32.0),
                child: new SizedBox(
                  height: 500.0,
                  child:
                      charts.TimeSeriesChart(resultChartData!, animate: true),
                ))
          ],
        )),
      );
    } else {
      return CircularProgressIndicator();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here is its explanation:

  • This class is responsible for displaying a Time Series chart that will be shown in the History screen
  • initState(): In the initState() lifecycle method we will call the getAllFoodTrackData() method in order to fetch all the FoodTrackEntry objects from the Firestore database
  • getAllFoodTrackData(): This method fetches all records from the 'foodTracks' collection, converts them into FoodTrackEntry instances and adds them to a List. Finally, it calls the populateChartWithEntries() method, as the name sounds, to populate the Time Series chart
  • populateChartWithEntries(): This method converts the List<FoodTrackEntry> list passed in as a parameter into another List<FoodTrackEntry> list that aggregates the calorie amounts based on the date. For example: if there are 3 FoodTrackEntry instances with a date value of 2022-03-25 and calorie values of 300, 400, and 500 respectively, then the new List<FoodTrackEntry> list would only contain one FoodTrackEntry instance with a date value of 2022-03-25 and a calorie value of 1200. Doing so allows us to chart the caloric intake of the user by date. Once the new List<FoodTrackEntry> has been created, the setState() method will reassign the value of the resultChartData variable. Consequently, the widget will be rebuilt and the charts.TimeSeriesChart() widget will display the updated chart data
  • build(): This method will render a Scaffold layout containing the title of the Time Series chart and the actual Time Series chart itself

Ok let's move on to some files where we will defined some constant values...

Here's lib/src/utils/constants.dart:

const DATABASE_UID = "<ENTER-YOUR-COLLECTION-ID-HERE>";
Enter fullscreen mode Exit fullscreen mode

The collection ID that this file requires can be found in the Firebase Console's Firestore Database page:
Firestore Database page showing Collection<br>
    ID
Figure 2: Firestore Database page showing Collection ID

and then here's lib/src/utils/theme_colors.dart:

const CARBS_COLOR = 0xffD83027;
const PROTEIN_COLOR = 0x9027D830;
const FAT_COLOR = 0xFF0D47A1;
Enter fullscreen mode Exit fullscreen mode

These color codes will be used to define the colors used in the Day View screen. Feel free to change them based on your preferences.

And to wrap up with the utils folder, let build out the lib/src/utils/uuid.dart file:

import 'dart:math';

class Uuid {
  final Random _random = Random();

  String generateV4() {
    final int special = 8 + _random.nextInt(4);

    return '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-'
        '${_bitsDigits(16, 4)}-'
        '4${_bitsDigits(12, 3)}-'
        '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-'
        '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}';
  }

  String _bitsDigits(int bitCount, int digitCount) =>
      _printDigits(_generateBits(bitCount), digitCount);

  int _generateBits(int bitCount) => _random.nextInt(1 << bitCount);

  String _printDigits(int value, int count) =>
      value.toRadixString(16).padLeft(count, '0');
}
Enter fullscreen mode Exit fullscreen mode

This class basically generates random universally unique identifiers, most frequently used as IDs when creating new instances of model classes.

Now we can start adding code to the files in the pages folder.

We are about to build the Day View screen so here is a screenshot of it:
Day View Screen
Figure 3: Day View Screen

Now that you have a better idea of how its supposed to look like, let's begin with the lib/src/page/day-view/calorie-stats.dart file:

import 'package:calorie_tracker_app/src/model/food_track_task.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:calorie_tracker_app/src/utils/theme_colors.dart';

class CalorieStats extends StatelessWidget {
  DateTime datePicked;
  DateTime today = DateTime.now();
  CalorieStats({required this.datePicked});

  num totalCalories = 0;
  num totalCarbs = 0;
  num totalFat = 0;
  num totalProtein = 0;
  num displayCalories = 0;

  bool dateCheck() {
    DateTime formatPicked =
        DateTime(datePicked.year, datePicked.month, datePicked.day);
    DateTime formatToday = DateTime(today.year, today.month, today.day);
    if (formatPicked.compareTo(formatToday) == 0) {
      return true;
    } else {
      return false;
    }
  }

  static List<num> macroData = [];

  @override
  Widget build(BuildContext context) {
    final DateTime curDate =
        new DateTime(datePicked.year, datePicked.month, datePicked.day);

    final foodTracks = Provider.of<List<FoodTrackTask>>(context);

    List findCurScans(List<FoodTrackTask> foodTracks) {
      List currentFoodTracks = [];
      foodTracks.forEach((foodTrack) {
        DateTime trackDate = DateTime(foodTrack.createdOn.year,
            foodTrack.createdOn.month, foodTrack.createdOn.day);
        if (trackDate.compareTo(curDate) == 0) {
          currentFoodTracks.add(foodTrack);
        }
      });
      return currentFoodTracks;
    }

    List currentFoodTracks = findCurScans(foodTracks);

    void findNutriments(List foodTracks) {
      foodTracks.forEach((scan) {
        totalCarbs += scan.carbs;
        totalFat += scan.fat;
        totalProtein += scan.protein;
        displayCalories += scan.calories;
      });
      totalCalories = 9 * totalFat + 4 * totalCarbs + 4 * totalProtein;
    }

    findNutriments(currentFoodTracks);

    // ignore: deprecated_member_use
    List<PieChartSectionData> _sections = <PieChartSectionData>[];

    PieChartSectionData _fat = PieChartSectionData(
      color: Color(FAT_COLOR),
      value: (9 * (totalFat) / totalCalories) * 100,
      title:
          '', // ((9 * totalFat / totalCalories) * 100).toStringAsFixed(0) + '%',
      radius: 50,
      // titleStyle: TextStyle(color: Colors.white, fontSize: 24),
    );

    PieChartSectionData _carbohydrates = PieChartSectionData(
      color: Color(CARBS_COLOR),
      value: (4 * (totalCarbs) / totalCalories) * 100,
      title:
          '', // ((4 * totalCarbs / totalCalories) * 100).toStringAsFixed(0) + '%',
      radius: 50,
      // titleStyle: TextStyle(color: Colors.black, fontSize: 24),
    );

    PieChartSectionData _protein = PieChartSectionData(
      color: Color(PROTEIN_COLOR),
      value: (4 * (totalProtein) / totalCalories) * 100,
      title:
          '', // ((4 * totalProtein / totalCalories) * 100).toStringAsFixed(0) + '%',
      radius: 50,
      // titleStyle: TextStyle(color: Colors.white, fontSize: 24),
    );

    _sections = [_fat, _protein, _carbohydrates];

    macroData = [displayCalories, totalProtein, totalCarbs, totalFat];

    totalCarbs = 0;
    totalFat = 0;
    totalProtein = 0;
    displayCalories = 0;

    Widget _chartLabels() {
      return Padding(
        padding: EdgeInsets.only(top: 78.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Row(
              children: <Widget>[
                Text('Carbs ',
                    style: TextStyle(
                      color: Color(CARBS_COLOR),
                      fontFamily: 'Open Sans',
                      fontSize: 16.0,
                      fontWeight: FontWeight.w500,
                    )),
                Text(macroData[2].toStringAsFixed(1) + 'g',
                    style: TextStyle(
                      color: Color.fromARGB(255, 0, 0, 0),
                      fontFamily: 'Open Sans',
                      fontSize: 16.0,
                      fontWeight: FontWeight.w500,
                    )),
              ],
            ),
            SizedBox(height: 3.0),
            Row(
              children: <Widget>[
                Text('Protein ',
                    style: TextStyle(
                      color: Color(0xffFA8925),
                      fontFamily: 'Open Sans',
                      fontSize: 16.0,
                      fontWeight: FontWeight.w500,
                    )),
                Text(macroData[1].toStringAsFixed(1) + 'g',
                    style: TextStyle(
                      color: Color.fromARGB(255, 0, 0, 0),
                      fontFamily: 'Open Sans',
                      fontSize: 16.0,
                      fontWeight: FontWeight.w500,
                    )),
              ],
            ),
            SizedBox(height: 3.0),
            Row(
              children: <Widget>[
                Text('Fat ',
                    style: TextStyle(
                      color: Color(0xff01B4BC),
                      fontFamily: 'Open Sans',
                      fontSize: 16.0,
                      fontWeight: FontWeight.w500,
                    )),
                Text(macroData[3].toStringAsFixed(1) + 'g',
                    style: TextStyle(
                      color: Color.fromARGB(255, 0, 0, 0),
                      fontFamily: 'Open Sans',
                      fontSize: 16.0,
                      fontWeight: FontWeight.w500,
                    )),
              ],
            ),
          ],
        ),
      );
    }

    Widget _calorieDisplay() {
      return Container(
        height: 74,
        width: 74,
        decoration: BoxDecoration(
          color: Color(0xff5FA55A),
          shape: BoxShape.circle,
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(macroData[0].toStringAsFixed(0),
                style: TextStyle(
                  fontSize: 22.0,
                  color: Colors.white,
                  fontFamily: 'Open Sans',
                  fontWeight: FontWeight.w500,
                )),
            Text('kcal',
                style: TextStyle(
                  fontSize: 14.0,
                  color: Colors.white,
                  fontFamily: 'Open Sans',
                  fontWeight: FontWeight.w500,
                )),
          ],
        ),
      );
    }

    if (currentFoodTracks.length == 0) {
      if (dateCheck()) {
        return Flexible(
          fit: FlexFit.loose,
          child: Text('Add food to see calorie breakdown.',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 40.0,
                fontWeight: FontWeight.w500,
              )),
        );
      } else {
        return Flexible(
          fit: FlexFit.loose,
          child: Text('No food added on this day.',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 40.0,
                fontWeight: FontWeight.w500,
              )),
        );
      }
    } else {
      return Container(
        child: Row(
          children: <Widget>[
            Stack(alignment: Alignment.center, children: <Widget>[
              AspectRatio(
                aspectRatio: 1,
                child: PieChart(
                  PieChartData(
                    sections: _sections,
                    borderData: FlBorderData(show: false),
                    centerSpaceRadius: 40,
                    sectionsSpace: 3,
                  ),
                ),
              ),
              _calorieDisplay(),
            ]),
            _chartLabels(),
          ],
        ),
      );
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here is the explanation for it:

  • The datePicked, totalCalories, totalCarbs, totalProtein, displayCalories variables store the nutritional data for the date picked
  • dateCheck(): This method checks if the datePicked DateTime value is equivalent to today's date. This method is used for prompting the user to add food in the current Day View page
  • macroData: This array is used to store the macro-nutritional values and quantity(carbs, protein, fat, and grams) of each type of food that is added in the Day View Screen
  • build(BuildContext context): This method renders the pie chart display of the macro-nutritional ratios and the macro quantities of the three macro-nutritional groups

Next up is the lib/src/page/day-view.dart file.

Here is its code:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:calorie_tracker_app/src/model/food_track_task.dart';
import 'package:calorie_tracker_app/component/iconpicker/icon_picker_builder.dart';
import 'package:calorie_tracker_app/src/utils/charts/datetime_series_chart.dart';
import 'calorie-stats.dart';
import 'package:provider/provider.dart';
import 'package:calorie_tracker_app/src/services/database.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'dart:math';
import 'package:calorie_tracker_app/src/utils/theme_colors.dart';
import 'package:calorie_tracker_app/src/utils/constants.dart';

class DayViewScreen extends StatefulWidget {
  DayViewScreen();

  @override
  State<StatefulWidget> createState() {
    return _DayViewState();
  }
}

class _DayViewState extends State<DayViewScreen> {
  String title = 'Add Food';
  double servingSize = 0;
  String dropdownValue = 'grams';
  DateTime _value = DateTime.now();
  DateTime today = DateTime.now();
  Color _rightArrowColor = Color(0xffC1C1C1);
  Color _leftArrowColor = Color(0xffC1C1C1);
  final _addFoodKey = GlobalKey<FormState>();

  DatabaseService databaseService = new DatabaseService(
      uid: "calorie-tracker-b7d17", currentDate: DateTime.now());

  late FoodTrackTask addFoodTrack;

  @override
  void initState() {
    super.initState();
    addFoodTrack = FoodTrackTask(
        food_name: "",
        calories: 0,
        carbs: 0,
        protein: 0,
        fat: 0,
        mealTime: "",
        createdOn: _value,
        grams: 0);
    databaseService.getFoodTrackData(DATABASE_UID);
  }

  void resetFoodTrack() {
    addFoodTrack = FoodTrackTask(
        food_name: "",
        calories: 0,
        carbs: 0,
        protein: 0,
        fat: 0,
        mealTime: "",
        createdOn: _value,
        grams: 0);
  }

  Widget _calorieCounter() {
    return Padding(
      padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0),
      child: new Container(
        decoration: BoxDecoration(
            color: Colors.white,
            border: Border(
                bottom: BorderSide(
              color: Colors.grey.withOpacity(0.5),
              width: 1.5,
            ))),
        height: 220,
        child: Row(
          children: <Widget>[
            CalorieStats(datePicked: _value),
          ],
        ),
      ),
    );
  }

  Widget _addFoodButton() {
    return IconButton(
      icon: Icon(Icons.add_box),
      iconSize: 25,
      color: Colors.white,
      onPressed: () async {
        setState(() {});
        _showFoodToAdd(context);
      },
    );
  }

  Future _selectDate() async {
    DateTime? picked = await showDatePicker(
      context: context,
      initialDate: _value,
      firstDate: new DateTime(2019),
      lastDate: new DateTime.now(),
      builder: (BuildContext context, Widget? child) {
        return Theme(
          data: ThemeData.light().copyWith(
            primaryColor: const Color(0xff5FA55A), //Head background
          ),
          child: child!,
        );
      },
    );
    if (picked != null) setState(() => _value = picked);
    _stateSetter();
  }

  void _stateSetter() {
    if (today.difference(_value).compareTo(Duration(days: 1)) == -1) {
      setState(() => _rightArrowColor = Color(0xffEDEDED));
    } else
      setState(() => _rightArrowColor = Colors.white);
  }

  checkFormValid() {
    if (addFoodTrack.calories != 0 &&
        addFoodTrack.carbs != 0 &&
        addFoodTrack.protein != 0 &&
        addFoodTrack.fat != 0 &&
        addFoodTrack.grams != 0) {
      return true;
    }
    return false;
  }

  _showFoodToAdd(BuildContext context) {
    return showDialog(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: Text(title),
            content: _showAmountHad(),
            actions: <Widget>[
              FlatButton(
                onPressed: () => Navigator.pop(context), // passing false
                child: Text('Cancel'),
              ),
              FlatButton(
                onPressed: () async {
                  if (checkFormValid()) {
                    Navigator.pop(context);
                    var random = new Random();
                    int randomMilliSecond = random.nextInt(1000);
                    addFoodTrack.createdOn = _value;
                    addFoodTrack.createdOn = addFoodTrack.createdOn
                        .add(Duration(milliseconds: randomMilliSecond));
                    databaseService.addFoodTrackEntry(addFoodTrack);
                    resetFoodTrack();
                  } else {
                    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                      content: Text(
                          "Invalid form data! All numeric fields must contain numeric values greater than 0"),
                      backgroundColor: Colors.white,
                    ));
                  }
                },
                child: Text('Ok'),
              ),
            ],
          );
        });
  }

  Widget _showAmountHad() {
    return new Scaffold(
      body: Column(children: <Widget>[
        _showAddFoodForm(),
        _showUserAmount(),
      ]),
    );
  }

  Widget _showAddFoodForm() {
    return Form(
      key: _addFoodKey,
      child: Column(children: [
        TextFormField(
          decoration: const InputDecoration(
              labelText: "Name *", hintText: "Please enter food name"),
          validator: (value) {
            if (value == null || value.isEmpty) {
              return "Please enter the food name";
            }
            return null;
          },
          onChanged: (value) {
            addFoodTrack.food_name = value;

            // addFood.calories = value;
          },
        ),
        TextFormField(
          decoration: const InputDecoration(
              labelText: "Calories *",
              hintText: "Please enter a calorie amount"),
          validator: (value) {
            if (value == null || value.isEmpty) {
              return "Please enter a calorie amount";
            }
            return null;
          },
          keyboardType: TextInputType.number,
          onChanged: (value) {
            try {
              addFoodTrack.calories = int.parse(value);
            } catch (e) {
              // return "Please enter numeric values"
              addFoodTrack.calories = 0;
            }

            // addFood.calories = value;
          },
        ),
        TextFormField(
          decoration: const InputDecoration(
              labelText: "Carbs *", hintText: "Please enter a carbs amount"),
          validator: (value) {
            if (value == null || value.isEmpty) {
              return "Please enter a carbs amount";
            }
            return null;
          },
          keyboardType: TextInputType.number,
          onChanged: (value) {
            try {
              addFoodTrack.carbs = int.parse(value);
            } catch (e) {
              addFoodTrack.carbs = 0;
            }
          },
        ),
        TextFormField(
          decoration: const InputDecoration(
              labelText: "Protein *",
              hintText: "Please enter a protein amount"),
          validator: (value) {
            if (value == null || value.isEmpty) {
              return "Please enter a calorie amount";
            }
            return null;
          },
          onChanged: (value) {
            try {
              addFoodTrack.protein = int.parse(value);
            } catch (e) {
              addFoodTrack.protein = 0;
            }
          },
        ),
        TextFormField(
          decoration: const InputDecoration(
              labelText: "Fat *", hintText: "Please enter a fat amount"),
          validator: (value) {
            if (value == null || value.isEmpty) {
              return "Please enter a fat amount";
            }
            return null;
          },
          onChanged: (value) {
            try {
              addFoodTrack.fat = int.parse(value);
            } catch (e) {
              addFoodTrack.fat = 0;
            }
          },
        ),
      ]),
    );
  }

  Widget _showUserAmount() {
    return new Expanded(
      child: new TextField(
          maxLines: 1,
          autofocus: true,
          decoration: new InputDecoration(
              labelText: 'Grams *',
              hintText: 'eg. 100',
              contentPadding: EdgeInsets.all(0.0)),
          keyboardType: TextInputType.number,
          inputFormatters: <TextInputFormatter>[
            FilteringTextInputFormatter.digitsOnly
          ],
          onChanged: (value) {
            try {
              addFoodTrack.grams = int.parse(value);
            } catch (e) {
              addFoodTrack.grams = 0;
            }
            setState(() {
              servingSize = double.tryParse(value) ?? 0;
            });
          }),
    );
  }

  Widget _showDatePicker() {
    return Container(
      width: 250,
      child: Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: <Widget>[
          IconButton(
            icon: Icon(Icons.arrow_left, size: 25.0),
            color: _leftArrowColor,
            onPressed: () {
              setState(() {
                _value = _value.subtract(Duration(days: 1));
                _rightArrowColor = Colors.white;
              });
            },
          ),
          TextButton(
            // textColor: Colors.white,
            onPressed: () => _selectDate(),
            // },
            child: Text(_dateFormatter(_value),
                style: TextStyle(
                  fontFamily: 'Open Sans',
                  fontSize: 18.0,
                  fontWeight: FontWeight.w700,
                )),
          ),
          IconButton(
              icon: Icon(Icons.arrow_right, size: 25.0),
              color: _rightArrowColor,
              onPressed: () {
                if (today.difference(_value).compareTo(Duration(days: 1)) ==
                    -1) {
                  setState(() {
                    _rightArrowColor = Color(0xffC1C1C1);
                  });
                } else {
                  setState(() {
                    _value = _value.add(Duration(days: 1));
                  });
                  if (today.difference(_value).compareTo(Duration(days: 1)) ==
                      -1) {
                    setState(() {
                      _rightArrowColor = Color(0xffC1C1C1);
                    });
                  }
                }
              }),
        ],
      ),
    );
  }

  String _dateFormatter(DateTime tm) {
    DateTime today = new DateTime.now();
    Duration oneDay = new Duration(days: 1);
    Duration twoDay = new Duration(days: 2);
    String month;

    switch (tm.month) {
      case 1:
        month = "Jan";
        break;
      case 2:
        month = "Feb";
        break;
      case 3:
        month = "Mar";
        break;
      case 4:
        month = "Apr";
        break;
      case 5:
        month = "May";
        break;
      case 6:
        month = "Jun";
        break;
      case 7:
        month = "Jul";
        break;
      case 8:
        month = "Aug";
        break;
      case 9:
        month = "Sep";
        break;
      case 10:
        month = "Oct";
        break;
      case 11:
        month = "Nov";
        break;
      case 12:
        month = "Dec";
        break;
      default:
        month = "Undefined";
        break;
    }

    Duration difference = today.difference(tm);

    if (difference.compareTo(oneDay) < 1) {
      return "Today";
    } else if (difference.compareTo(twoDay) < 1) {
      return "Yesterday";
    } else {
      return "${tm.day} $month ${tm.year}";
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            elevation: 0,
            bottom: PreferredSize(
              preferredSize: const Size.fromHeight(5.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: <Widget>[
                  _showDatePicker(),
                  _addFoodButton(),
                ],
              ),
            )),
        body: StreamProvider<List<FoodTrackTask>>.value(
          initialData: [],
          value: new DatabaseService(
                  uid: "calorie-tracker-b7d17", currentDate: DateTime.now())
              .foodTracks,
          child: new Column(children: <Widget>[
            _calorieCounter(),
            Expanded(
                child: ListView(
              children: <Widget>[FoodTrackList(datePicked: _value)],
            ))
          ]),
        ));
  }
}

class FoodTrackList extends StatelessWidget {
  final DateTime datePicked;
  FoodTrackList({required this.datePicked});

  @override
  Widget build(BuildContext context) {
    final DateTime curDate =
        new DateTime(datePicked.year, datePicked.month, datePicked.day);

    final foodTracks = Provider.of<List<FoodTrackTask>>(context);

    List findCurScans(List foodTrackFeed) {
      List curScans = [];
      foodTrackFeed.forEach((foodTrack) {
        DateTime scanDate = DateTime(foodTrack.createdOn.year,
            foodTrack.createdOn.month, foodTrack.createdOn.day);
        if (scanDate.compareTo(curDate) == 0) {
          curScans.add(foodTrack);
        }
      });
      return curScans;
    }

    List curScans = findCurScans(foodTracks);

    return ListView.builder(
      scrollDirection: Axis.vertical,
      physics: ClampingScrollPhysics(),
      shrinkWrap: true,
      itemCount: curScans.length + 1,
      itemBuilder: (context, index) {
        if (index < curScans.length) {
          return FoodTrackTile(foodTrackEntry: curScans[index]);
        } else {
          return SizedBox(height: 5);
        }
      },
    );
  }
}

class FoodTrackTile extends StatelessWidget {
  final FoodTrackTask foodTrackEntry;
  DatabaseService databaseService = new DatabaseService(
      uid: "calorie-tracker-b7d17", currentDate: DateTime.now());

  FoodTrackTile({required this.foodTrackEntry});

  List macros = CalorieStats.macroData;

  @override
  Widget build(BuildContext context) {
    return ExpansionTile(
      leading: CircleAvatar(
        radius: 25.0,
        backgroundColor: Color(0xff5FA55A),
        child: _itemCalories(),
      ),
      title: Text(foodTrackEntry.food_name,
          style: TextStyle(
            fontSize: 16.0,
            fontFamily: 'Open Sans',
            fontWeight: FontWeight.w500,
          )),
      subtitle: _macroData(),
      children: <Widget>[
        _expandedView(context),
      ],
    );
  }

  Widget _itemCalories() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(foodTrackEntry.calories.toStringAsFixed(0),
            style: TextStyle(
              fontSize: 16.0,
              color: Colors.white,
              fontFamily: 'Open Sans',
              fontWeight: FontWeight.w500,
            )),
        Text('kcal',
            style: TextStyle(
              fontSize: 10.0,
              color: Colors.white,
              fontFamily: 'Open Sans',
              fontWeight: FontWeight.w500,
            )),
      ],
    );
  }

  Widget _macroData() {
    return Row(
      children: <Widget>[
        Container(
          width: 200,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Row(
                children: <Widget>[
                  Container(
                    height: 8,
                    width: 8,
                    decoration: BoxDecoration(
                      color: Color(CARBS_COLOR),
                      shape: BoxShape.circle,
                    ),
                  ),
                  Text(' ' + foodTrackEntry.carbs.toStringAsFixed(1) + 'g    ',
                      style: TextStyle(
                        fontSize: 12.0,
                        color: Colors.white,
                        fontFamily: 'Open Sans',
                        fontWeight: FontWeight.w400,
                      )),
                  Container(
                    height: 8,
                    width: 8,
                    decoration: BoxDecoration(
                      color: Color(PROTEIN_COLOR),
                      shape: BoxShape.circle,
                    ),
                  ),
                  Text(
                      ' ' + foodTrackEntry.protein.toStringAsFixed(1) + 'g    ',
                      style: TextStyle(
                        fontSize: 12.0,
                        color: Colors.white,
                        fontFamily: 'Open Sans',
                        fontWeight: FontWeight.w400,
                      )),
                  Container(
                    height: 8,
                    width: 8,
                    decoration: BoxDecoration(
                      color: Color(FAT_COLOR),
                      shape: BoxShape.circle,
                    ),
                  ),
                  Text(' ' + foodTrackEntry.fat.toStringAsFixed(1) + 'g',
                      style: TextStyle(
                        fontSize: 12.0,
                        color: Colors.white,
                        fontFamily: 'Open Sans',
                        fontWeight: FontWeight.w400,
                      )),
                ],
              ),
              Text(foodTrackEntry.grams.toString() + 'g',
                  style: TextStyle(
                    fontSize: 12.0,
                    color: Colors.white,
                    fontFamily: 'Open Sans',
                    fontWeight: FontWeight.w300,
                  )),
            ],
          ),
        )
      ],
    );
  }

  Widget _expandedView(BuildContext context) {
    return Padding(
      padding: EdgeInsets.fromLTRB(20.0, 0.0, 15.0, 0.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          expandedHeader(context),
          _expandedCalories(),
          _expandedCarbs(),
          _expandedProtein(),
          _expandedFat(),
        ],
      ),
    );
  }

  Widget expandedHeader(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Text('% of total',
            style: TextStyle(
              fontSize: 14.0,
              color: Colors.white,
              fontFamily: 'Open Sans',
              fontWeight: FontWeight.w400,
            )),
        IconButton(
            icon: Icon(Icons.delete),
            iconSize: 16,
            onPressed: () async {
              print("Delete button pressed");
              databaseService.deleteFoodTrackEntry(foodTrackEntry);
            }),
      ],
    );
  }

  Widget _expandedCalories() {
    double caloriesValue = 0;
    if (!(foodTrackEntry.calories / macros[0]).isNaN) {
      caloriesValue = foodTrackEntry.calories / macros[0];
    }
    return Padding(
      padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0),
      child: Row(
        children: <Widget>[
          Container(
            height: 10.0,
            width: 200.0,
            child: LinearProgressIndicator(
              value: caloriesValue,
              backgroundColor: Color(0xffEDEDED),
              valueColor: AlwaysStoppedAnimation<Color>(Color(0xff5FA55A)),
            ),
          ),
          Text('      ' + ((caloriesValue) * 100).toStringAsFixed(0) + '%'),
        ],
      ),
    );
  }

  Widget _expandedCarbs() {
    double carbsValue = 0;
    if (!(foodTrackEntry.carbs / macros[2]).isNaN) {
      carbsValue = foodTrackEntry.carbs / macros[2];
    }
    return Padding(
      padding: EdgeInsets.fromLTRB(0.0, 15.0, 0.0, 0.0),
      child: Row(
        children: <Widget>[
          Container(
            height: 10.0,
            width: 200.0,
            child: LinearProgressIndicator(
              value: carbsValue,
              backgroundColor: Color(0xffEDEDED),
              valueColor: AlwaysStoppedAnimation<Color>(Color(0xffFA5457)),
            ),
          ),
          Text('      ' + ((carbsValue) * 100).toStringAsFixed(0) + '%'),
        ],
      ),
    );
  }

  Widget _expandedProtein() {
    double proteinValue = 0;
    if (!(foodTrackEntry.protein / macros[1]).isNaN) {
      proteinValue = foodTrackEntry.protein / macros[1];
    }
    return Padding(
      padding: EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 0.0),
      child: Row(
        children: <Widget>[
          Container(
            height: 10.0,
            width: 200.0,
            child: LinearProgressIndicator(
              value: proteinValue,
              backgroundColor: Color(0xffEDEDED),
              valueColor: AlwaysStoppedAnimation<Color>(Color(0xffFA8925)),
            ),
          ),
          Text('      ' + ((proteinValue) * 100).toStringAsFixed(0) + '%'),
        ],
      ),
    );
  }

  Widget _expandedFat() {
    double fatValue = 0;
    if (!(foodTrackEntry.fat / macros[3]).isNaN) {
      fatValue = foodTrackEntry.fat / macros[3];
    }
    return Padding(
      padding: EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 10.0),
      child: Row(
        children: <Widget>[
          Container(
            height: 10.0,
            width: 200.0,
            child: LinearProgressIndicator(
              value: (foodTrackEntry.fat / macros[3]),
              backgroundColor: Color(0xffEDEDED),
              valueColor: AlwaysStoppedAnimation<Color>(Color(0xff01B4BC)),
            ),
          ),
          Text('      ' + ((fatValue) * 100).toStringAsFixed(0) + '%'),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Now for its explanation:

  • createState(): This method creates a mutable state for the DayViewScreen widget
  • _value: This DateTime value holds the current date that the Day View screen is set to. The left and right arrow buttons allow the user to switch dates accordingly, which will be updated in the _value variable
  • databaseService: This is the DatebaseService instance used to fetch and add records to the foodTracks collection in the Firestore database we setup in previous steps
  • initState(): This lifecycle method initializes the addFoodTrack variable to an empty FoodTrackTask instance. Then databaseService.getFoodTrackData() is called to fetch all the FoodTrack instances from the Firestore database
  • resetFoodTrack(): This method is used to reset the addFoodTrack variable to an empty FoodTrack instance after adding a new FoodTrack instance in the Add Food modal
  • The _addFoodButton(), _showFoodToAdd(), _showAmountHad(), _showAddFoodForm() and _showUserAmount() methods are used to render the Add Food Modal that popups when tapping the Add Food + button
  • _showDatePicker(): This method renders the Date toggling mechanism on the top of the screen
  • build(): In this render method we render the Add Food button and the Date picker widget onto the appBar. Then in the body, the DatabaseService class is used to fetch all foodTrack instances which will be listed using the StreamProvider class(more on the StreamProvider class in the Flutter documentation). The FoodTrackList class will render the listings. It will required the _value argument(which is the date picked by the user), which it will use to filter out the foodTrack instances that match that date to within a 24 hour time period and render those instances according. The FoodTrackTile class is used to render single items in the listing that the FoodTrackList class will render. These items will show the calorie amount, macro-nutritional amounts, and percentages of those values in comparison to the overall day's values in a chart format. Lastly, it will also render a delete button to delete any foodTrack instances

Whew! Now that we have gotten through building the Day View screen, let's build the History screen!

Here is a screenshot of how it looks:
History Screen
Figure 4: History Screen

Add the following code to the lib/src/page/history/history-screen.dart file:

import 'package:flutter/material.dart';
import 'package:calorie_tracker_app/src/model/food_track_task.dart';
import 'package:calorie_tracker_app/component/iconpicker/icon_picker_builder.dart';
import 'package:calorie_tracker_app/src/utils/charts/datetime_series_chart.dart';

class HistoryScreen extends StatefulWidget {
  HistoryScreen();

  @override
  State<StatefulWidget> createState() {
    return _HistoryScreenState();
  }
}

class _HistoryScreenState extends State<HistoryScreen> {
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
  bool _isBack = true;
  @override
  void initState() {
    super.initState();
  }

  void onClickBackButton() {
    print("Back Button");
    Navigator.of(context).pop();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(
            "History Screen",
            style: TextStyle(
                color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
          ),
        ),
        body: Container(
          child: DateTimeChart(),
        ));
  }
}
Enter fullscreen mode Exit fullscreen mode

The History screen just renders a simple Time Series chart using the charts_flutter library. It uses the DateTimeChart widget, covered in an earlier section of this post, to accomplish this.

Last but not least, we'll build the Settings screen. We won't be providing any tangible functionality for it and will only be looking at building its UI.

Here is a screenshot of what it looks like:
Settings Screen
Figure 5: Settings Screen

And here its code which we'll add to the lib/src/page/settings/settings_screen.dart file:

import 'package:flutter/material.dart';
import 'package:calorie_tracker_app/src/model/food_track_task.dart';
import 'package:calorie_tracker_app/component/iconpicker/icon_picker_builder.dart';
import 'package:settings_ui/settings_ui.dart';

class SettingsScreen extends StatefulWidget {
  SettingsScreen();

  @override
  State<StatefulWidget> createState() {
    return _SettingsScreenState();
  }
}

class _SettingsScreenState extends State<SettingsScreen> {
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
  bool _isBack = true;

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

  @override
  Widget build(BuildContext context) {
    // const IconData computer = IconData(0xe185, fontFamily: 'MaterialIcons');
    return SettingsList(
      sections: [
        SettingsSection(
          title: Text('Settings',
              textAlign: TextAlign.center,
              style: const TextStyle(fontWeight: FontWeight.bold)),
          tiles: [
            SettingsTile(
              title: Text('Language',
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontWeight: FontWeight.bold)),
              value: Text('English',
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontWeight: FontWeight.bold)),
              leading: Icon(Icons.language),
              onPressed: (BuildContext context) {},
            ),
            SettingsTile(
              title: Text('Environment',
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontWeight: FontWeight.bold)),
              // subtitle: 'English',
              value: Text('Development',
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontWeight: FontWeight.bold)),
              leading: Icon(Icons.computer),
              onPressed: (BuildContext context) {},
            ),
            SettingsTile(
              title: Text('Environment',
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontWeight: FontWeight.bold)),
              // subtitle: 'English',
              value: Text('Development',
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontWeight: FontWeight.bold)),
              leading: Icon(Icons.language),
              onPressed: (BuildContext context) {},
            ),
          ],
        ),
        SettingsSection(
          title: Text('Account',
              textAlign: TextAlign.center,
              style: const TextStyle(fontWeight: FontWeight.bold)),
          tiles: [
            SettingsTile(
                title: Text('Phone Number',
                    textAlign: TextAlign.center,
                    style: const TextStyle(fontWeight: FontWeight.bold)),
                leading: Icon(Icons.local_phone)),
            SettingsTile(
                title: Text('Email',
                    textAlign: TextAlign.center,
                    style: const TextStyle(fontWeight: FontWeight.bold)),
                leading: Icon(Icons.email)),
            SettingsTile(
                title: Text('Sign out',
                    textAlign: TextAlign.center,
                    style: const TextStyle(fontWeight: FontWeight.bold)),
                leading: Icon(Icons.logout)),
          ],
        ),
        SettingsSection(
          title: Text('Misc',
              textAlign: TextAlign.center,
              style: const TextStyle(fontWeight: FontWeight.bold)),
          tiles: [
            SettingsTile(
                title: Text('Terms of Service',
                    textAlign: TextAlign.center,
                    style: const TextStyle(fontWeight: FontWeight.bold)),
                leading: Icon(Icons.document_scanner)),
            SettingsTile(
                title: Text('Open source licenses',
                    textAlign: TextAlign.center,
                    style: const TextStyle(fontWeight: FontWeight.bold)),
                leading: Icon(Icons.collections_bookmark)),
          ],
        )
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Not much to explain here other than that we are using the settings_ui library(see its GitHub page here) to provide us with the SettingsSection, SettingsList, and SettingsTile widgets to create a typical setting screen, as seen in most modern mobile applications.

Ok, we have finished developing our application now! Give yourself a pat on the back if you made it this far.

Now I give an brief summary of Routing in Flutter and how we chose to implement routing for this application:

Flutter has two types of routing: (1) Imperative routing via the Navigator widget and (2) Idiomatic declarative routing via the Router widget. Traditionally, most web applications use idiomatic declarative routing while most mobile applications used some sort of imperative routing mechanism.
As a general guideline, it is best to use Imperative routing for smaller Flutter applications and Idiomatic declarative routing for larger apps. Accordingly, we have chosen the Imperative routing mechanism for this application, as evident by the Navigator.of(context).push() calls that are commonplace throughout this application.

Running And Testing The App

Now all that's left is to run the application via the VSCode Debugger, if you're developing on VSCode, or using the Run Icon if you're developing on Android Studio.

Here's an informative blog post on how to run Flutter applications on VSCode if you need help with that and here is one for running Flutter apps on Android Studio, if you need it.

Here are some screenshots of what the app should look like:
Flutter 1
Flutter 2
Flutter 4
Flutter 5
Figure 6: Screenshots of the Calorie Tracker App we've just built

Conclusion

If you managed to follow along, Congrats! You now know how to build a Calorie Tracker application on Flutter. If not, look into my GitHub source code repo for this app and feel free to clone it and replicate it as you wish.

Well that’s all for today, I hope you found this article helpful. Thanks so much for reading my article! Feel free to follow me on Twitter and GitHub, connect with me on LinkedIn and subscribe to my YouTube channel.

Top comments (31)

Collapse
 
pomoich profile image
pomoich

I consider that to be a wonderful idea. Everyone needs something like this to maintain health and keep fit. I used to stay on a lot of diets when I was younger. I wanted a lot to lose weight because I had some health problems. The first attempts were unsuccessful, and I wanted to give up on dieting. Then one of my friends encouraged me and even started dieting with me to motivate me. He even bought a bathroom scale so we could keep track of the weight we lost. It was a very useful tool, and I can say that it was one of the most important parts of our dieting process.

Collapse
 
mykola_taran profile image
Taran Mykola

Combining online gaming with cryptocurrency can be a dream come true for many enthusiasts, and I found that perfect fusion at crypto casino . Offering a wide variety of games and an easy-to-use interface, I enjoyed an incredible gaming experience on this site. Moreover, the added security and privacy provided by using cryptocurrency for transactions made it an even more attractive option.

Collapse
 
aarondelatorre1 profile image
Aaron Delatorre

Досліджуючи величезний цифровий ландшафт, онлайн-гравці постійно перебувають у пошуках унікальних і корисних вражень. Бонуси, що пропонуються на сайті slotscity.casino/bonusy/ , суттєво допомагають у цьому плані. Ці бонуси не тільки додають додатковий рівень захоплення до ігрового процесу, але й надають фантастичну можливість максимізувати потенційний прибуток. Різноманітність бонусів гарантує, що кожен знайде щось для себе. Від новачка, який шукає привабливий вітальний бонус, до досвідченого гравця, який прагне заробити на бонусах за лояльність, - на сайті ви знайдете все, що потрібно.

Collapse
 
aarondelatorre1 profile image
Aaron Delatorre

Если вы ищете первоклассные онлайн-игры, загляните на сайт sityslots.com/ru/slots-siti-kazino... . Это рай для любителей игровых автоматов, где представлено множество игр, каждая из которых может похвастаться захватывающей графикой. Акцент сайта на безопасную игру и честную игру добавляет положительных впечатлений. Исключительное обслуживание клиентов является бонусом, что делает этот сайт достойной рекомендацией для любого любителя азартных игр.

Collapse
 
aarondelatorre1 profile image
Aaron Delatorre

Мои поиски полноценного игрового опыта привели меня на сайт sitislot.com/ru/ . Множество игровых автоматов и потрясающие визуальные эффекты сразу же привлекли мое внимание. Но больше всего меня впечатлило стремление сайта обеспечить безопасную игровую среду. Его приверженность честной игре и превосходному обслуживанию клиентов заслуживает похвалы. Эта платформа, безусловно, установила высокую планку для онлайн-игр.

Collapse
 
mykola_taran profile image
Taran Mykola

Мои поиски полноценного игрового опыта привели меня на сайт sitislot.com/ru/ . Множество игровых автоматов и потрясающие визуальные эффекты сразу же привлекли мое внимание. Но больше всего меня впечатлило стремление сайта обеспечить безопасную игровую среду. Его приверженность честной игре и превосходному обслуживанию клиентов заслуживает похвалы. Эта платформа, безусловно, установила высокую планку для онлайн-игр.

Collapse
 
mykola_taran profile image
Taran Mykola

The importance of a solid foundation for any startup cannot be overstated, and the professionals at corpsoft.io/service/startup-develo... understand this well. Their comprehensive suite of services ensures that every startup receives the attention, guidance, and expertise it deserves to make a lasting impact in the market.

Collapse
 
mykola_taran profile image
Taran Mykola

Приветствую вас, автовладельцы! Если вам нужны новые шины или диски для вашего автомобиля, у меня есть идеальное решение. Я нашел этот замечательный интернет-магазин goroshina.ua/ru , который предлагает широкий ассортимент высококачественной продукции. Удобный веб-сайт и подробная информация о товаре превращают покупки в удовольствие. Я уже долгое время являюсь их довольным клиентом и не могу рекомендовать их!

Collapse
 
mykola_taran profile image
Taran Mykola

I was browsing online for some elegant dinnerware and stumbled upon these beautiful crystal plates. They're northamericancrystal.com/crystal-p... so stunning and unique, I couldn't resist ordering a few for my upcoming dinner party. The intricate detailing and sparkly finish truly make them stand out. I can't wait to serve my guests on these plates and impress them with my taste in tableware. The quality of these crystal plates is exceptional and I know they will last for years to come. I highly recommend them to anyone looking for a touch of sophistication in their dining experience.

Collapse
 
mykola_taran profile image
Taran Mykola

Overall, I highly recommend checking out this site onevisiongames.com/casinos/5-depos... if you're a Canadian player looking for a high-quality $5 deposit casino. They've got everything you need to make an informed decision, and their commitment to safety and fairness is truly impressive. Give it a try and see for yourself – I think you'll be just as impressed as I am!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.