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:
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:
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
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;
}
}
Now for an explanation of the important parts of the above code:
-
Future<void> main() async
: Instead of the standardvoid main()
method we have to specify the return type asFuture<void>
because we are using theasync
andawait
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 theHomepage
widget when the app is first launched. Itsbuild()
method does the rendering and uses theChangeNotifierProvider
provider-wrapper class to set a dark theme for the entire application, with the help ofDarkThemeProvider
-
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 theNavigator.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,
};
}
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);
}
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();
}
}
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 theFirebaseFirestore
instance that allows us to connect to thefoodTracks
collection in the Firestore database we previously created -
addFoodTrackEntry(FoodTrackTask food)
: This is the method used to create a new record in thefoodTracks
Firestore collection. Notice that the record identifier is themillisecondsSinceEpoch
value based on theFoodTrackTask
instance'screatedOn
field. This is done to ensure uniqueness in the collection -
deleteFoodTrackEntry(FoodTrackTask deleteEntry)
: This is the method used to delete a record in thefoodTracks
Firestore collection. It uses themillisecondsSinceEpoch
value based on thecreatedOn
field from theFoodTrackTask
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 ofFoodTrackTask
instances. We use the popularmap()
function in order to do so -
get foodtracks
: This method only used by aStreamProvider
instance in the Day View screen(we'll get to building it soon) to provide a list ofFoodTrackTask
instances that will be displayed in a list format -
getAllFoodTrackData()
: This method is used to fetch allfoodTrack
records from the database and return them in aList<dynamic>
object. Note that we should avoid using thedynamic
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 thedynamic
data type in theflutterbyexample.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 thegetAllFoodTrackData()
method if theuid
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;
}
}
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 theSharedPreferences
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 thegetDarkTheme()
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();
}
}
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();
}
}
}
Here is its explanation:
- This class is responsible for displaying a Time Series chart that will be shown in the History screen
-
initState()
: In theinitState()
lifecycle method we will call thegetAllFoodTrackData()
method in order to fetch all theFoodTrackEntry
objects from the Firestore database -
getAllFoodTrackData()
: This method fetches all records from the 'foodTracks' collection, converts them intoFoodTrackEntry
instances and adds them to a List. Finally, it calls thepopulateChartWithEntries()
method, as the name sounds, to populate the Time Series chart -
populateChartWithEntries()
: This method converts theList<FoodTrackEntry>
list passed in as a parameter into anotherList<FoodTrackEntry>
list that aggregates the calorie amounts based on the date. For example: if there are 3FoodTrackEntry
instances with a date value of2022-03-25
and calorie values of300
,400
, and500
respectively, then the newList<FoodTrackEntry>
list would only contain oneFoodTrackEntry
instance with a date value of2022-03-25
and a calorie value of1200
. Doing so allows us to chart the caloric intake of the user by date. Once the newList<FoodTrackEntry>
has been created, thesetState()
method will reassign the value of theresultChartData
variable. Consequently, the widget will be rebuilt and thecharts.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>";
The collection ID that this file requires can be found in the Firebase Console's Firestore Database page:
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;
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');
}
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:
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(),
],
),
);
}
}
}
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 thedatePicked
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) + '%'),
],
),
);
}
}
Now for its explanation:
-
createState()
: This method creates a mutable state for theDayViewScreen
widget -
_value
: ThisDateTime
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 theDatebaseService
instance used to fetch and add records to thefoodTracks
collection in the Firestore database we setup in previous steps -
initState()
: This lifecycle method initializes theaddFoodTrack
variable to an emptyFoodTrackTask
instance. ThendatabaseService.getFoodTrackData()
is called to fetch all theFoodTrack
instances from the Firestore database -
resetFoodTrack()
: This method is used to reset theaddFoodTrack
variable to an emptyFoodTrack
instance after adding a newFoodTrack
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 theAdd 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 theappBar
. Then in the body, theDatabaseService
class is used to fetch allfoodTrack
instances which will be listed using theStreamProvider
class(more on theStreamProvider
class in the Flutter documentation). TheFoodTrackList
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 thefoodTrack
instances that match that date to within a 24 hour time period and render those instances according. TheFoodTrackTile
class is used to render single items in the listing that theFoodTrackList
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 anyfoodTrack
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:
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(),
));
}
}
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:
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)),
],
)
],
);
}
}
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:
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 (32)
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.
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.
Досліджуючи величезний цифровий ландшафт, онлайн-гравці постійно перебувають у пошуках унікальних і корисних вражень. Бонуси, що пропонуються на сайті slotscity.casino/bonusy/ , суттєво допомагають у цьому плані. Ці бонуси не тільки додають додатковий рівень захоплення до ігрового процесу, але й надають фантастичну можливість максимізувати потенційний прибуток. Різноманітність бонусів гарантує, що кожен знайде щось для себе. Від новачка, який шукає привабливий вітальний бонус, до досвідченого гравця, який прагне заробити на бонусах за лояльність, - на сайті ви знайдете все, що потрібно.
Если вы ищете первоклассные онлайн-игры, загляните на сайт sityslots.com/ru/slots-siti-kazino... . Это рай для любителей игровых автоматов, где представлено множество игр, каждая из которых может похвастаться захватывающей графикой. Акцент сайта на безопасную игру и честную игру добавляет положительных впечатлений. Исключительное обслуживание клиентов является бонусом, что делает этот сайт достойной рекомендацией для любого любителя азартных игр.
Мои поиски полноценного игрового опыта привели меня на сайт sitislot.com/ru/ . Множество игровых автоматов и потрясающие визуальные эффекты сразу же привлекли мое внимание. Но больше всего меня впечатлило стремление сайта обеспечить безопасную игровую среду. Его приверженность честной игре и превосходному обслуживанию клиентов заслуживает похвалы. Эта платформа, безусловно, установила высокую планку для онлайн-игр.
Мои поиски полноценного игрового опыта привели меня на сайт sitislot.com/ru/ . Множество игровых автоматов и потрясающие визуальные эффекты сразу же привлекли мое внимание. Но больше всего меня впечатлило стремление сайта обеспечить безопасную игровую среду. Его приверженность честной игре и превосходному обслуживанию клиентов заслуживает похвалы. Эта платформа, безусловно, установила высокую планку для онлайн-игр.
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.
Приветствую вас, автовладельцы! Если вам нужны новые шины или диски для вашего автомобиля, у меня есть идеальное решение. Я нашел этот замечательный интернет-магазин goroshina.ua/ru , который предлагает широкий ассортимент высококачественной продукции. Удобный веб-сайт и подробная информация о товаре превращают покупки в удовольствие. Я уже долгое время являюсь их довольным клиентом и не могу рекомендовать их!
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.
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.