Подключаем микроконтроллер ESP32 к Flutter-приложению

b211ba2e1b1a40d02c2194e23fdebd99.png

Всем привет! Это Мурат Насиров, Flutter-разработчик в Friflex. Мы разрабатываем высоконагруженные мобильные приложения для бизнеса и специализируемся на Flutter. 

Сегодня расскажу, как использовать микроконтроллер ESP32 в связке с Flutter-приложением. В качестве примера покажу, как снимать показания температуры и влажности с помощью датчика DHT11 и передавать эти данные через Firebase Realtime Database в приложение.

Взаимодействие систем и общий план

Идея приложения заключается в следующем — создается Flutter-проект, к нему подключается Firebase, а также необходимые библиотеки для управления состоянием. Затем происходит настройка в сервисе Firebase базы данных Realtime Database, которая в реальном времени считывает показания с датчика и через микроконтроллер отправляет их в БД, где затем из этой базы данные считывает само приложение. Помимо прочего, будет реализована функция авторизации, которая позволит только созданным в Firebase аккаунтам управлять и мониторировать данными в БД.

Так как взаимодействие происходит между тремя системами, представлю вид того, как это будет выглядеть.

A — приложение, B — микроконтроллер ESP32

A — приложение, B — микроконтроллер ESP32

В целом, это все что надо знать о взаимодействии систем. Приступим к созданию проекта в Firebase.

Создаем проект в Firebase

Начинаем с регистрации проекта в Firebase Console. Следуем инструкции с картинками. Проходим авторизацию через Google-аккаунт и нажимаем Add Project.

ef9a6614788dabbb17fe3ac4f7b8dbbb.jpg

Задаем имя проекта.

a5bd7f50ca79e8b748f14b709f7e971c.jpg

Отключаем сервис Google Analytics. В этом проекте он нам не нужен.

22e990667f2c05ec6fe7f902ff20b59d.jpg

Готово. Теперь нужно подключить Firebase к Flutter-приложению. Сделаем это через Firebase CLI. Выбираем иконку Flutter.

8ad675c831dc5fc3d6b19e4bbb66559e.jpg

Следуем инструкции. Перед этим не забудьте создать Flutter-проект.

a0b1467e110e27f7149f09890210ee02.jpg

После подключения Firebase на главной должны появиться проекты для iOS и Android, если вы выбрали их при установке Firebase.

abf8aced47148fb1cfe901ea804ca59e.jpg

Добавляем авторизацию через электронную почту и пароль.

399abccd17bd5ecfc111646c128e14ef.jpg

Выбираем только Email/Password.

eec9f2d62525ac314c0a87b01214bc82.jpg

Добавляем двух пользователей: user@u.com — это пользователь, который будет проходить авторизацию в приложении. И device@d.com — это микроконтроллер ESP32. 

Учетные записи решают вопрос с безопасностью. Мы позволяем записывать данные в Realtime Database только пользователям с особыми правами. 

Обязательно сохраните учетные данные, они еще пригодятся.

cb6b4c845b3a9ecc3a65d3a98da3f4bc.jpg

Изучаем схему подключения микроконтроллера

Рассмотрим схему подключения.

Подключение DHT11 к ESP32

Подключение DHT11 к ESP32

Все достаточно просто. Схема состоит из микроконтроллера ESP32, а также датчика температуры и влажности DHT11. Пин подключения сигнального провода можно выбрать любой из доступных вам для вашей модели микроконтроллера (они бывают с разным количеством выходов, то есть, пинов).

Описание пинов

Описание пинов

У меня микроконтроллер выглядит вот так.

Микроконтроллер на макетной плате

Микроконтроллер на макетной плате

Пишем код для микроконтроллера

Теперь переходим к коду для микроконтроллера. Написать его можно в Arduino IDE или с помощью плагина PlatformIO для VS Code. Второй способ интереснее, поэтому я воспользуюсь им.

Заходим на сайт VS Code, скачиваем редактор кода для своей операционной системы. В разделе Extensions находим и устанавливаем Platformio IDE.

2673d3243692ebd89d890bc169231559.jpg

Во время установки оказалось, что у меня нет Python-интерпретатора. На моем компьютере установлена операционная система Ubuntu. Поэтому открываю консоль и устанавливаю последнюю версию:

sudo apt install python3-venv

В появившемся окне в VS Code выбираем Try again.

bfefae3fc698ef72a211ae3235f4d1df.png

Теперь можно создавать проект. Выбираем удобную папку для проекта. В PlatformIO нажимаем Create New Project — New Project.

e595b300dd548ed4d5b862f2877a5dbc.jpg

Задаем имя и в качестве Board выбираем Espressif ESP32 Dev Module.

4887dfa6c33e66c20d1f65803f115517.jpg

В готовый проект устанавливаем библиотеки. Для этого переходим в раздел Libraries.

831e1bbfaf003fe36e1f2e8185fbc4d1.jpg

По очереди находим и устанавливаем DHT sensor library, Adafruit Unified Sensor и Firebase ESP32 Client.

c8d4704ce0cbf14a1341ce20d320c8da.jpg

В файле platformio.ini добавим пару строк скорости чтения и записи данных с платы:

upload_speed = 921600
monitor_speed = 115200

Пора кодить. Напишем скрипты для работы с Wi-Fi и датчиком. Создадим в папке lib папки: firebase, sensors и wifi.

Начнем с wifi, создадим в этой папке два файла DeviceWiFi.cpp и DeviceWiFi.h, где первый это файл с исполняемым кодом, а второй заголовочный файл. Такое разбиение на модули позволит затем удобно импортировать код в main.cpp.

DeviceWiFi.h

#include 

/*!
Подключение к Wi-Fi
\param[in] ssid Имя точки доступа (Wi-Fi)
\param[in] pass Пароль точки доступа (Wi-Fi)
*/
void connectToWiFi(const char *ssid, const char *pass);

/*!
Переподключение к Wi-Fi, если соединение разорвано
\param[in] ssid Имя точки доступа (Wi-Fi)
\param[in] pass Пароль точки доступа (Wi-Fi)
*/
void loopReconnectToWiFi(const char *ssid, const char *pass);

DeviceWiFi.cpp

#include "DeviceWiFi.h"
void connectToWiFi(const char *ssid, const char *pass)
{
  Serial.println("Устанавливается соединение с ");
  Serial.print(ssid);
  // Регистрация подключения на основе введенных данных
  WiFi.begin(ssid, pass);
  // Проверка подключения
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(1000);
    Serial.print(".");
  }
  // Установка соединения, если все успешно
  Serial.println("");
  Serial.println("Соединение установлено!"); 
  Serial.print("IP-адрес: ");
  Serial.println(WiFi.localIP());
}

void loopReconnectToWiFi(const char *ssid, const char *pass)
{
  // Если соединение разорвано происходит переподключение со всеми настройками
  while (WiFi.status() != WL_CONNECTED)
  {
    connectToWiFi(ssid, pass);
  }
}

Теперь напишем модуль с датчиком. Нам необходимо списать показания с DHT11 и обрабатывать их в ESP32. В папке sensors создаем файлы DeviceSensors.cpp и DeviceSensors.h

DeviceSensors.h

#include "DeviceFirebase.h"

/* Отправляет данные с датчиков в DeviceFirebase#loopSendDataToRTDB */
void loopSendSensorsData();

DeviceSensors.cpp

#include "DeviceSensors.h"
#include 

/* Номер пина датчика температуры и влажности */
uint8_t DHTPin = 27;
#define DHTTYPE DHT11
DHT dht(DHTPin, DHTTYPE);

void loopSendSensorsData()
{
  int temp = dht.readTemperature();
  int hum = dht.readHumidity();
  loopSendDataToRTDB(temp, hum);
}

Теперь все собранные показания нужно отправить по Wi-Fi в Firebase. Создадим модуль, который будет отвечать за аутентификацию по уже ранее созданному пользователю в базе данных. В папке firebase создаем файлы DeviceFirebase.cpp и DeviceFirebase.h

DeviceFirebase.h

#include 
#include 

/*!
Инициализация сервиса Firebase
\param[in] deviceApiKey Уникальный ключ устройства (достаточно Web API Key)
\param[in] deviceDatabaseUrl Ссылка на Firebase Realtime Database для записи данных
\param[in] deviceEmail Email для авторизации устройства
\param[in] devicePass Пароль
*/
void initFirebaseService(String deviceApiKey, String deviceDatabaseUrl, String deviceEmail, String devicePass);

/*!
Отправка данных с сформированными путями в Realtime Database
\param[in] temp Температура
\param[in] hum Влажность
*/
void loopSendDataToRTDB(int temp, int hum);

DeviceFirebase.cpp

#include "DeviceFirebase.h"
#include 
#include 
#include 

FirebaseData fireData;
FirebaseConfig fireConfig;
FirebaseAuth fireAuth;

unsigned long epochTime;


// Получаем текущее время с сервера ntp
unsigned long getTime() {
 time_t now;
 struct tm timeinfo;
 if (!getLocalTime(&timeinfo)) {
   return(0);
 }
 time(&now);

 return now;
}

void initFirebaseService(String deviceApiKey, String deviceDatabaseUrl, String deviceEmail, String devicePass)
{
 Serial.printf("Firebase Client v%s\n\n", FIREBASE_CLIENT_VERSION);

 // Определение уникального ключа устройства и ссылки на Firebase Realtime Database
 fireConfig.api_key = deviceApiKey;
 fireConfig.database_url = deviceDatabaseUrl;

 // Определение учетных данных для микроконтроллера
 fireAuth.user.email = deviceEmail;
 fireAuth.user.password = devicePass;

 // Получение уникального токена микроконтроллера
 fireConfig.token_status_callback = tokenStatusCallback;

 // Регистрация работы сервиса
 Firebase.begin(&fireConfig, &fireAuth);

 // Получение UID и его отображение
 Serial.println("Получение UID микроконтроллера");
 while ((fireAuth.token.uid) == "")
 {
   Serial.print('.');
   delay(1000);
 }

 Serial.print("Успешно подключено к Firebase!");
}

void loopSendDataToRTDB(int temp, int hum)
{
 String basePath = "/devices";
 String tempPath = basePath + "/temperature";
 String humpPath = basePath + "/humidity";
 String timePath = basePath + "/timestamp";

 epochTime = getTime();

 if (Firebase.ready())
 {
   Firebase.setInt(fireData, tempPath, temp);
   Firebase.setInt(fireData, humpPath, hum);
   Firebase.setInt(fireData, timePath, epochTime);

   // Задержка в 1 минуту
   delay(60000);
 }
}

Для авторизации нам необходим Web API Key. Найти его можно с помощью кнопки справа от Project Overview — Project Settings — General. Этот API Key мы указываем в main.cpp вместе с логином и паролем устройства.

b41b5be74d4a8d5d11df97b5c0a2c32f.jpg

Открываем src/main.cpp и пишем:

main.cpp

#include "DeviceWiFi.h"
#include "DeviceFirebase.h"
#include "DeviceSensors.h"

// Подключение к WiFi
const char *wifiSsid = "wifi_name";
const char *wifiPass = "wifi_pass";

// Сервис для получения текущего времени в секундах
const char *ntpServer = "pool.ntp.org";

// Подключение к Firebase
/* Уникальный ключ микроконтроллера, определяемый в Google Cloud */
String deviceApiKey = "device_api_key";

/* Адрес Firebase Realtime Databse */
String deviceDatabaseUrl = "device_database_url";

/* Электронная почта микроконтроллера */
String deviceEmail = "device_email";

/* Пароль микроконтроллера */
String devicePass = "device_pass";

void setup()
{
  Serial.begin(115200);
  delay(10);

  connectToWiFi(wifiSsid, wifiPass);
  
  configTime(0, 0, ntpServer);

  initFirebaseService(deviceApiKey, deviceDatabaseUrl, deviceEmail, devicePass);
}

void loop()
{
  loopReconnectToWiFi(wifiSsid, wifiPass);
  loopSendSensorsData();
}

Помните, я говорил, что нам понадобятся электронная почта и пароль от учетной записи, которую вы создали в Firebase? Момент настал. Указываем эти данные в соответствующих полях. В качестве deviceApiKey указываем Web API Key, про который я также упоминал выше. 

Остается разобраться с deviceDatabaseUrl. Переходим в Firebase Console — Build — Realtime Database — Create Database. Выбираем сервер United States, затем Start in test mode. В этом режиме база данных (БД) будет существовать 30 дней. При необходимости можно увеличить срок.

d434161fb35abb2e3020a80c2e3c5d91.jpg

Получаем БД, ура! Пока что данных в ней нет, но это легко исправить. Подключаем микроконтроллер ESP32 к компьютеру, после чего переходим в PlatformIO и нажимаем build

cf867fa9a6bb7a5a08edff7a7b51d38d.jpg

Чтобы после успешного билда загрузить наши скрипты, выбираем Upload and Monitor. Возвращаемся в Firebase Console — Build — Realtime Database и любуемся результатом.

c29cb1c4908334ed024edb7fbf4b3cea.gif

Решаем возможные проблемы

Если ваш ESP32 не обнаруживается, возможно, нужно установить драйверы CH34x и CP210x. Проверьте работу USB-кабеля, вероятно, его необходимо заменить.

Чтобы проверить, опознается ли микроконтроллер, нужно подключить его к компьютеру и открыть раздел PlatformIO — Devices.

19d35aaf610ed26b617476ce33fe494a.jpg

Также можно пройтись по инструкции на сайте Espressif.

Создаем Flutter-проект и настраиваем взаимодействие

Нам осталось написать приложение, которое будет отображать данные с микроконтроллера. Во время настройки Firebase CLI я говорил, что нам понадобится Flutter-проект. В нем вы увидите сгенерированные файлы.

2b455d257247d99e91392fc28e036c5a.jpg

Лишь lib/firebase_options.dart нужно добавить в /.gitignore, а android/app/google-services.json и ios/firebase_app_id_file.json можно удалить. Либо их можно тоже добавить в /.gitignore, если нужно.

Я постараюсь максимально кратко описать, какие решения использовал в приложении, чтобы отобразить значения с микроконтроллера. Остальной код вы сможете найти в репозитории в конце статьи.

Начнем с пакетов. Нам будет достаточно использовать менеджер состояния, библиотеки Firebase для авторизации и БД, а также библиотеку для преобразования текущего времени с микроконтроллера. 

pubspec.yaml

dependencies:
intl: 0.19.0

#---NETWORK DATA---#
firebase_core: 2.25.3
firebase_auth: 4.17.3
firebase_database: 10.4.4

#---STATE MANAGEMENT---#
flutter_riverpod: 2.4.10

Инициализируем Firebase и Riverpod

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const ProviderScope(child: MyApp()));
}

Мы использовали Firebase CLI, поэтому в аргументе options обязательно указываем currentPlatform для платформ, которые мы выбрали при установке Firebase. 

В моем случае в lib/firebase_options.dart описывается попытка запуска на неподдерживаемой платформе. Для поддерживаемых платформ возвращаются сгенерированные ключи. Мы можем подкорректировать этот файл, потому что в моем случае запустить его можно только на Android, Linux и iOS.

class DefaultFirebaseOptions {
  static FirebaseOptions get currentPlatform {

  switch (defaultTargetPlatform) {
    case TargetPlatform.android:
      return android;
    case TargetPlatform.iOS:
      return ios;
    case TargetPlatform.linux:
      throw UnsupportedError(
        'DefaultFirebaseOptions have not been configured for linux - '
        'you can reconfigure this by running the FlutterFire CLI again.',
      );
    default:
      throw UnsupportedError(
        'DefaultFirebaseOptions are not supported for this platform.',
      );
    }
  }

  static const FirebaseOptions android = FirebaseOptions(
    apiKey: 'apiKey',
    appId: 'appId',
    messagingSenderId: 'messagingSenderId',
    projectId: 'projectId',
    storageBucket: 'storageBucket',
  );

  static const FirebaseOptions ios = FirebaseOptions(
    apiKey: 'apiKey',
    appId: 'appId',
    messagingSenderId: 'messagingSenderId',
    projectId: 'projectId',
    storageBucket: 'storageBucket',
    iosBundleId: 'iosBundleId',
  );
}

Придадим нашему проекту следующую структуру.

0dd54d4e7560e7198c69e45b23da40ab.jpg

Начнём с авторизации. Для этого создадим сервис, который будет выполнять функцию входа и выхода в приложение. Вход можно отследить при помощи значения id в модели User. Установим проверку, которая будет переключать на экран устройства при успешном входе. Либо на экран авторизации, если войти не удалось.

lib/auth/data/services/auth_service.dart

class AuthService {
  /// Если аутентификация, либо авторизация прошла успешно, значение этого
  /// геттера становится заполненная модель [User]
  User? get user => FirebaseAuth.instance.currentUser;

  String? get userId => user?.uid;

  String? get userEmail => user?.email;

  bool get isAlreadyLoggedIn => userId != null;

  Future login({
    required String email,
    required String password,
  }) async {
    return FirebaseAuth.instance.signInWithEmailAndPassword(
      email: email,
      password: password,
    );
  }

  Future logout() async => FirebaseAuth.instance.signOut();
}

Для управления состоянием нужна логика. Она будет отображать в UI нужные виджеты, в зависимости от состояния. Это простой запрос в сеть, поэтому у нас четыре состояния: инициализация, загрузка, ошибка, успех. В арсенале у Riverpod есть StateNotifier. Используя его вместе с AsyncValue (тоже из этого пакета), получаем следующую картину:

lib/auth/domain/auth_riverpod.dart

class AuthStateNotifier extends StateNotifier> {
  AuthStateNotifier({required AuthService service})
: _service = service,
  // В super объявляется изначальное состояние (initial). Так как данных
  // нет, ставим null
  super(const AsyncData(null)) {
    // Если пользователь уже был авторизован, просто меняем состояние на
    // успешное
    if (service.isAlreadyLoggedIn) state = AsyncData(service.user);
  }

  final AuthService _service;

  Future logout() async {
    if (state.isLoading) return;

    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await _service.logout();

      // После выхода делаем User равным null, так как от id внутри этой модели
      // определяется состояние авторизованности в приложении
      return null;
    });
  }

  Future login({required String email, required String password}) async {
    if (state.isLoading) return;
    state = const AsyncLoading();

    // AsyncValue.guard используется для удобства. По факту получается
    // state = AsyncData(response.user) в случае успеха, либо
    // state = AsyncError(error, stackTrace) в случае ошибки
    state = await AsyncValue.guard(() async {
      final response = await _service.login(
        email: email,
        password: password,
      );
      return response.user;
    });
  }
}

Теперь создаем несколько провайдеров для манипуляций состояниями в UI-слое. Нам нужны провайдеры для:

  • текстовых полей логина и пароля;

  • определения статуса авторизации в приложении;

  • модели пользователя (User).

Создадим lib/auth/domain/auth_scope_providers.dart и добавим несколько провайдеров.

final loginAuthProvider = Provider.autoDispose((ref) {
  final cotroller = TextEditingController();

  // onDispose срабатывает в местах, где использовался
  // ref.watch(loginAuthProvider). После выхода из экрана этот текстовый
  // контроллер закрывается
  ref.onDispose(() => controller.dispose());
  return controller;
});

final passAuthProvider = Provider.autoDispose((ref) {
  final controller = TextEditingController();
  ref.onDispose(() => controller.dispose());
  return controller;
});

final userProvider = Provider(
  (ref) => ref.watch(authStateProvider).value,
);

/// Провайдер текущего состояния авторизованности
final authStateProvider =
StateNotifierProvider>(
  (_) => AuthStateNotifier(service: AuthService()),
);

После определения всех провайдеров можно приступать к вёрстке экрана. В экране будет отображаться простой стилизованный вид ввода учётных данных.

Я не буду описывать весь код UI, а лишь покажу интересные на мой взгляд моменты. Создадим lib/auth/presentation/screens/auth_screen.dart. В Riverpod есть возможность следить за определенным провайдером прямо в build методе, поэтому объявим наши TextEditingController для текстовых полей.

// Получаем значение TextEditingController, которое используем для
   // текстового поля. ref.watch будет перестраивать виджет каждый раз, когда
   // его значение меняется, например, при вызове у этого
   // TextEditingController метода dispose
   final loginController = ref.watch(loginAuthProvider);
   final passController = ref.watch(passAuthProvider);
   _listenAuthState(context, ref);

И установим для каждого текстового поля.

TextField(
 controller: loginController,
 decoration: const InputDecoration(hintText: 'Email'),
 textInputAction: TextInputAction.next,
 keyboardType: TextInputType.emailAddress,
 onEditingComplete: () => FocusScope.of(context).nextFocus(),
)

Также нарисуем кнопку, которая будет принимать значения с контроллеров текстовых полей.

ElevatedButton(
 onPressed: () {
   _completeInput(
     context,
     ref,
     email: loginController.text.trim(),
     pass: passController.text.trim(),
   );
 },
...
)

Эта кнопка выполняет функцию входа.

/// Метод завершения ввода логина и пароля
void _completeInput(
 BuildContext context,
 WidgetRef ref, {
 required String email,
 required String pass,
}) {
 // Если у пользователя открыта клавиатура - скрываем ее
 final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 1;
 if (isKeyboardVisible) FocusScope.of(context).unfocus();

 // Вызываем функцию для входа
 ref.read(authStateProvider.notifier).login(email: email, password: pass);
}

Выше я упоминал функцию _listenAuthState. Опишем ее.

/// Отслеживает состояние авторизованности пользователя
void _listenAuthState(BuildContext context, WidgetRef ref) {
 ref.listen>(authStateProvider, (previous, next) async {
   ...

   // В случае успешной авторизации переходим на экран устройств
   if (next.hasValue && context.mounted) {
     Navigator.of(context).pushReplacement(
       MaterialPageRoute(builder: (_) => const DevicesScreen()),
     );
   }
 });
}

Со входом мы разобрались. Теперь приступаем к экрану с показаниями датчика DHT11. Начнем с модели. В видео выше было показано, что данные хранятся в Firebase Realtime Database. Это просто Map с набором ключ или значение. Поэтому нам нужно сделать простой маппинг этих данных.

lib/devices/domain/model/devices_data.dar

@immutable
class DevicesData {
 const DevicesData({
   required this.humidity,
   required this.temperature,
   required this.readingDateTime,
 });

 /// Влажность
 final int? humidity;

 /// Температура
 final int? temperature;

 /// Время считывания
 final DateTime? readingDateTime;

 @override
 bool operator ==(Object other) =>
     identical(this, other) ||
     other is DevicesData &&
         runtimeType == other.runtimeType &&
         humidity == other.humidity &&
         temperature == other.temperature &&
         readingDateTime == other.readingDateTime;

 @override
 int get hashCode =>
     humidity.hashCode ^ temperature.hashCode ^ readingDateTime.hashCode;

 static DevicesData fromJson(Map json) {
   // Так как отсчет времени происходит с начала эпохи (01.01.1970) в секундах, здесь происходит преобразование этого времени в понятный вид
   final timestamp = json['timestamp'];
   final readingDateTime = timestamp != null
       ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
       : null;

   return DevicesData(
     temperature: json['temperature'],
     humidity: json['humidity'],
     readingDateTime: readingDateTime,
   );
 }
}

В логике мы сразу после получения данных смаппим их в эту модели.

lib/devices/domain/devices_riverpod.dart

class DevicesStateNotifier extends StateNotifier> {
 DevicesStateNotifier({required DatabaseReference database})
     : _database = database,
       super(const AsyncData(null));

 final DatabaseReference _database;

 late StreamSubscription _subscription;

 Future getDevicesData() async {
   state = const AsyncLoading();
   // Обновление данных происходит в реальном времени
   _subscription = _database.onValue.listen((event) {
     final data = event.snapshot.child('devices').value as Map?;
     if (data == null) return;

     state = AsyncData(DevicesData.fromJson(data));
   }, onError: (e, s) => state = AsyncError(e, s));
 }

 @override
 void dispose() {
   _subscription.cancel();
   super.dispose();
 }
}

Порядок, осталось сделать провайдер.

lib/devices/domain/devices_scope_providers.dart

final devicesStateProvider = StateNotifierProvider.autoDispose<
   DevicesStateNotifier, AsyncValue>((_) {
 return DevicesStateNotifier(database: FirebaseDatabase.instance.ref())
   // При входе на экран, где используется этот провайдер сразу же начнется
   // запрос в сеть посредством вызова getDevicesData
   ..getDevicesData();
});

Мы близки к завершению, осталось лишь написать простой UI для отображения показаний датчика. Создав lib/devices/presentation/screens/devices_screen.dart в функции build нужно реагировать на состояние, когда пользователь вышел из аккаунта.

final user = ref.watch(userProvider);
   // authStateProvider провайдер вызовется по нажатию кнопки выхода. В теле
   // этого слушателя стоит проверка
   ref.listen>(authStateProvider, (prev, next) {
     final uid = next.value?.uid;

     // Если пользователь вышел - заменяем текущий экран на экран авторизации
     if (uid == null) {
       Navigator.of(context).pushReplacement(
         MaterialPageRoute(builder: (_) => const AuthScreen()),
       );
     }
   });

Так как получение данных это асинхронная операция, в параметре body виджета Scaffold удобнее использовать функцию when, которую предоставляет Riverpod.

Scaffold(
 ...
 // Метод when в асинхронных операциях в Riverpod позволяет удобно
 // распределять события загрузки, ошибки и успеха на нужные виджеты,
 // которые описаны здесь
 body: deviceData.when(
   loading: () {...},
   error: (error, stackTrace) {...},
   data: (data) {...},
 )

В аргументе data находятся показания температуры и влажности, а также timestamp, которые меняются динамически, раз в 1 минуту. Достаточно просто отобразить эти данные в UI.

Осталось самое главное.

lib/main.dart

Future main() async {
 WidgetsFlutterBinding.ensureInitialized();
 await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
 runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends ConsumerWidget {
 const MyApp({super.key});

 @override
 Widget build(BuildContext context, WidgetRef ref) {
   return MaterialApp(
     title: 'Flutter Arduino',
     debugShowCheckedModeBanner: false,
     home: Consumer(
       builder: (context, ref, child) {
         final uid = ref.watch(userProvider)?.uid;
         if (uid != null) return const DevicesScreen();

         return const AuthScreen();
       },
     ),
   );
 }
}

Все готово! Можно смотреть на красоту :)

aecc63f193fc9e5d8dbeb75a0e3cf9c1.gif

Надеюсь, вам было интересно. Если появились вопросы, пишите в комментариях. Всем пока!

Исходники: код микроконтроллера, код Flutter-проекта.

© Habrahabr.ru