Для того чтобы получить твердое понимание любого нового языка программирования, который мы изучаем, лучше всего создать простое приложение на этом языке, чтобы активно изучать его плюсы/минусы и тонкости.
Что может быть лучше для изучения Flutter, чем создание на нем приложения для отслеживания калорий! В этом учебном посте я расскажу, как создать приложение для отслеживания калорий с помощью Flutter, так что давайте начнем!
Что мы будем создавать
Вот несколько скриншотов приложения Calorie Tracker, которое мы создадим:
Рисунок 1: Скриншоты приложения для отслеживания калорий, которое мы собираемся создать
Установка
Стартовый шаблон на GitHub здесь: https://github.com/ShehanAT/flutter_calorie_tracker_app/tree/starter-template
Для того чтобы следовать этому руководству, я настоятельно рекомендую использовать проект стартового шаблона, размещенный выше, так как это ускорит процесс разработки для всех нас.
Затем обязательно установите Flutter по следующей ссылке.
Наконец, нам понадобится проект Firebase для приложения, которое мы создаем, поэтому обязательно зайдите в Firebase Console и создайте новый проект, если вы еще этого не сделали.
Что касается среды разработки, я буду использовать VSCode (ссылка для загрузки здесь), поэтому если у вас он еще не установлен, вы можете сделать это прямо сейчас. В качестве альтернативы можно использовать Android Studio(ссылка для скачивания здесь).
Настройка Firebase
Как только вы создадите и запустите проект Firebase, процесс создания проекта позволит вам загрузить файл google-services.json
. Обязательно поместите этот файл в директорию {root_dir}/android/app
, поскольку именно так наше приложение Flutter сможет соединиться с проектом Firebase.
Мы будем использовать базу данных Firestore в качестве источника данных для этого приложения, поэтому давайте создадим экземпляр базы данных Firestore.
В консоли Firebase Console перейдите на вкладку ‘Firestore Database’, а затем нажмите ‘Create Database’, как показано на этом скриншоте:
Затем выберите ‘Start in Test Mode’ во всплывающем модальном окне, выберите закрытый для вас регион и создайте базу данных.
Установка пакетов
Сначала перейдите по ссылке на стартовый шаблон выше и клонируйте ветку starter-template
на свою локальную машину. Затем откройте его в VSCode и выполните следующую команду в терминале Git Bash
, находясь в корневом каталоге этого проекта:
flutter pub get
Эта команда используется для установки всех необходимых пакетов, используемых в этом приложении, в первую очередь библиотеки charts_flutter.
Добавление кода в файлы
Теперь наступает часть разработки!
Сначала добавим следующий код в файл main.dart:
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;
}
}
Теперь объясним важные части приведенного выше кода:
Теперь давайте создадим файлы моделей, начиная с libsrcmodelfood_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,
};
}
Таким образом, этот класс модели является основным классом, который будет хранить информацию о каждом экземпляре отслеживания еды. Поле mealTime
определяет время потребления пищи, поле createdOn
определяет время, в которое она была отслежена, а поля carbs
, fat
, protein
и grams
передают качество и пищевую ценность потребленной пищи.
Далее следует относительно небольшой класс моделей: libsrcmodelfood_track_entry.dart
:
class FoodTrackEntry {
DateTime date;
int calories;
FoodTrackEntry(this.date, this.calories);
}
Этот класс будет использоваться как точка входа для графика временного ряда charts_flutter
, который мы будем разрабатывать на экране истории.
Далее следует разработка папки services. Мы начнем с файла lib/src/services/database.dart
:
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();
}
}
Теперь объяснение:
- Это класс, используемый для взаимодействия с экземпляром Firebase Firestore, который мы создали в предыдущих шагах.
Хорошо, прогресс достигнут…
Далее давайте создадим файл lib/src/services/shared_preference_service.dart
:
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;
}
}
Вот его объяснение:
- Пакет
shared_preferences
используется для общих задач хранения и кэширования значений на диске. Этот сервисный класс будет служить для возврата экземпляров классаSharedPreferences
и поэтому следует шаблону проектирования Singleton. - В дополнение к вышеуказанной функциональности, этот класс также помогает в обеспечении функциональности темной темы, описанной в файле
main.dart
с помощью методаgetDarkTheme()
.
Перейдите в папку providers
, где мы создадим файл lib/src/providers/theme_notifier.dart
:
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();
}
}
Этот класс будет служить провайдером для класса CalorieTrackerAppState
в файле main.dart
, а также включит темную тему, которую приложение будет использовать по умолчанию. Провайдеры важны, потому что они помогают уменьшить неэффективность, связанную с повторным отображением компонентов при каждом изменении состояния. При использовании провайдеров единственные виджеты, которые должны перестраиваться при изменении состояния, это те, которые назначены потребителями для соответствующих провайдеров. Подробнее о провайдерах и потребителях в этой замечательной статье блога
Переходим к папке utils
. Давайте теперь построим файл lib/src/utils/charts/datetime_series_chart.dart
:
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();
}
}
}
Вот его объяснение:
- Этот класс отвечает за отображение графика временных рядов, который будет показан на экране истории.
Хорошо, давайте перейдем к некоторым файлам, где мы определим некоторые постоянные значения…
Вот lib/src/utils/constants.dart
:
const DATABASE_UID = "<ENTER-YOUR-COLLECTION-ID-HERE>";
ID коллекции, который требуется этому файлу, можно найти на странице Firebase Console «Firestore Database»:
Рисунок 2: Страница базы данных Firestore, показывающая ID коллекции
а затем вот lib/src/utils/theme_colors.dart
:
const CARBS_COLOR = 0xffD83027;
const PROTEIN_COLOR = 0x9027D830;
const FAT_COLOR = 0xFF0D47A1;
Эти цветовые коды будут использоваться для определения цветов, используемых на экране Day View. Не стесняйтесь изменять их в зависимости от ваших предпочтений.
И чтобы завершить работу с папкой utils
, давайте построим файл lib/src/utils/uuid.dart
:
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');
}
Этот класс в основном генерирует случайные универсальные уникальные идентификаторы, которые чаще всего используются в качестве идентификаторов при создании новых экземпляров классов моделей.
Теперь мы можем начать добавлять код в файлы в папке pages
.
Мы собираемся создать экран Day View, поэтому вот его скриншот:
Рисунок 3: Экран просмотра дня
Теперь, когда вы лучше представляете, как он должен выглядеть, давайте начнем с файла lib/src/page/day-view/calorie-stats.dart
:
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(),
],
),
);
}
}
}
Вот объяснение этому:
- Переменные
datePicked
,totalCalories
,totalCarbs
,totalProtein
,displayCalories
хранят данные о питании для выбранной даты.
Следующим идет файл lib/src/page/day-view.dart
.
Вот его код:
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) + '%'),
],
),
);
}
}
Теперь объяснение:
- Методы
_addFoodButton()
,_showFoodToAdd()
,_showAmountHad()
,_showAddFoodForm()
и_showUserAmount()
используются для отображения модала добавления еды, который появляется при нажатии на кнопкуAdd Food +
.
Ух ты! Теперь, когда мы разобрались с созданием экрана просмотра дня, давайте создадим экран истории!
Вот скриншот того, как он выглядит:
Рисунок 4: Экран истории
Добавьте следующий код в файл lib/src/page/history/history-screen.dart
:
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(),
));
}
}
Экран истории просто отображает простой график временного ряда, используя библиотеку charts_flutter
. Для этого используется виджет DateTimeChart
, о котором говорилось в одном из предыдущих разделов этой статьи.
И последнее, но не менее важное, мы создадим экран настроек. Мы не будем предоставлять никакой ощутимой функциональности для него и рассмотрим только создание его пользовательского интерфейса.
Вот скриншот того, как он выглядит:
Рисунок 5: Экран настроек
А вот его код, который мы добавим в файл lib/src/page/settings/settings_screen.dart
:
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)),
],
)
],
);
}
}
Здесь не нужно много объяснять, кроме того, что мы используем библиотеку settings_ui
(см. ее страницу на GitHub здесь), чтобы предоставить нам виджеты SettingsSection
, SettingsList
и SettingsTile
для создания типичного экрана настроек, который можно увидеть в большинстве современных мобильных приложений.
Итак, мы закончили разработку нашего приложения! Похлопайте себе по спине, если вы дошли до этого.
Теперь я кратко расскажу о маршрутизации во Flutter и о том, как мы решили реализовать маршрутизацию для этого приложения:
Flutter имеет два типа маршрутизации: (1) императивная маршрутизация через виджет Navigator
и (2) идиоматическая декларативная маршрутизация через виджет Router
. Традиционно большинство веб-приложений используют идиоматическую декларативную маршрутизацию, в то время как большинство мобильных приложений используют какой-либо механизм императивной маршрутизации.
В качестве общего руководства лучше использовать императивную маршрутизацию для небольших приложений Flutter и идиоматическую декларативную маршрутизацию для больших приложений. Соответственно, для данного приложения мы выбрали императивный механизм маршрутизации, о чем свидетельствуют вызовы Navigator.of(context).push()
, которые часто встречаются в этом приложении.
Запуск и тестирование приложения
Теперь осталось запустить приложение через отладчик VSCode, если вы разрабатываете в VSCode, или с помощью иконки Run Icon, если вы разрабатываете в Android Studio.
Вот информативная статья в блоге о том, как запускать приложения Flutter на VSCode, если вам нужна помощь в этом, и вот статья о запуске приложений Flutter на Android Studio, если вам это нужно.
Вот несколько скриншотов того, как должно выглядеть приложение:
Рисунок 6: Скриншоты приложения для отслеживания калорий, которое мы только что создали
Заключение
Если вам удалось проследить за всем этим, поздравляем! Теперь вы знаете, как создать приложение для отслеживания калорий на Flutter. Если нет, загляните в мой репозиторий исходного кода этого приложения на GitHub и не стесняйтесь клонировать его и копировать по своему усмотрению.
Ну вот и все на сегодня, надеюсь, эта статья была вам полезна. Большое спасибо, что прочитали мою статью! Не стесняйтесь следить за мной на Twitter и GitHub, общаться со мной на LinkedIn и подписываться на мой канал YouTube.