Работа с сетевыми подключениями во Flutter

Автор Дэвид Адегоке✏️

Введение

Три, два, один — действие! Возьмите телефон, откройте свое любимое приложение, нажмите на значок приложения, оно открывается, регистрирует вас, а затем бум … оно продолжает загружаться. Вы, вероятно, думаете, что оно все еще получает данные, поэтому даете ему минуту, а затем один превращается в два, два в три, три в пять — все еще загружается, никакой информации, никакой ошибки, только загрузка. Разочаровавшись, вы закрываете приложение и либо ищете альтернативу, либо, возможно, даете ему еще одну попытку, прежде чем сдаться.

Подключение невероятно важно, особенно для тех частей нашего приложения, которые сильно зависят от состояния соединения. Мы, разработчики, должны хорошо справляться с этими аспектами нашего приложения. Отслеживая интернет-соединение пользователя, мы можем запустить сообщение, информирующее пользователя о проблемах с подключением, и, что самое важное, запустить функцию, которая загрузит необходимые данные после восстановления интернет-соединения, обеспечив пользователю бесперебойную работу, к которой мы стремимся.

Мы не хотим, чтобы шаткое соединение стало крахом нашего приложения — хотя качество интернет-соединения наших пользователей не всегда находится под нашим контролем — но мы можем установить некоторые проверки, которые проинформируют наших пользователей об этой проблеме, и принять меры в зависимости от состояния соединения. Мы рассмотрим это практически в следующем разделе и увидим, как отслеживать состояние интернет-соединения приложения, а также делать звонок при его восстановлении.

Под «состоянием соединения» здесь подразумевается активное соединение, не в сети, нестабильное и т.д. Давайте погрузимся в это, да?

Реализация обработчика подключений

Пример приложения, который мы создадим в этом разделе, пришел спасти положение (не зря же он называется Superhero API). Мы будем получать данные из Superhero API, а затем отображать их пользователю.

Давайте сделаем паузу. Наша цель — отслеживать подключение, верно?

Это верно, но между этими потоками нам нужно отслеживать интернет-соединение устройства. Когда соединение прерывается, мы должны вывести сообщение пользователю, информируя его о ситуации, а когда соединение восстанавливается, мы должны немедленно сделать вызов API и получить наши данные.

Для того чтобы наше приложение не продолжало постоянно получать данные при каждом изменении состояния соединения, мы введем дополнительную переменную, которая будет информировать приложение о том, вызвали ли мы функцию, загружающую наши данные.

Настройка API супергероя

Прежде чем мы приступим к работе над кодом, нам необходимо установить несколько вещей на нашем сайте-образце, прежде чем мы сможем использовать Superhero API.

Прежде всего, перейдите на сайт Superhero API. Вам необходимо войти в систему Facebook, чтобы получить маркер доступа, который мы будем использовать для запросов к API.

После входа в систему вы можете скопировать маркер доступа и использовать его в приложении.

Второе, что нужно сделать, это выбрать персонажа. Супермен? Определенно.

Как видно из документации, Superhero API предоставляет нам ID для каждого супергероя. Этот ID затем используется в нашем запросе API и возвращает информацию о конкретном герое. ID для Супермена — 644, поэтому запишите его.

Выполнив эти два действия, мы можем приступать к выполнению запросов к API.

Настройка проекта

Выполните следующую команду, чтобы создать новую кодовую базу для проекта.

flutter create handling_network_connectivity
Войти в полноэкранный режим Выйдите из полноэкранного режима

Импортируйте следующие зависимости в наш файл pubspec.yaml:

  • http: чтобы сделать запрос GET к Superhero API и получить данные о персонаже для выбранного нами супергероя.
  • stacked: Это архитектурное решение, которое мы будем использовать в этом пакете, которое использует Provider под капотом и дает нам доступ к некоторым действительно классным классам, чтобы разнообразить наш процесс разработки
  • stacked_services: Готовые к использованию сервисы, предоставляемые пакетом stacked
  • build_runner: Предоставляет доступ к командам запуска для автоматической генерации файлов из аннотаций
  • stacked_generator: Генерирует файлы из сложенных в стопку аннотаций
  • logger: Выводит важную информацию в консоль отладки
dependencies:
 cupertino_icons: ^1.0.2
 flutter:
  sdk: flutter
 stacked: ^2.2.7
 stacked_services: ^0.8.15
 logger: ^1.1.0
dev_dependencies:
 build_runner: ^2.1.5
 flutter_lints: ^1.0.0
 flutter_test:
  sdk: flutter
 stacked_generator: ^0.5.6
flutter:
 uses-material-design: true
Вход в полноэкранный режим Выйти из полноэкранного режима

После этого мы можем приступить к разработке.

Настройка моделей

Из документации Superhero API мы видим, что вызов определенного идентификатора superheroId возвращает биографию супергероя, статистику силы, фон, внешний вид, изображение и многое другое.

В этой статье мы рассмотрим только поля биографии, статистики силы и изображения, но вы можете добавить больше данных, если захотите. Итак, нам понадобится создать модели для преобразования ответа JSON в наши данные Object.

Создайте папку в каталоге lib. Назовите папку models; все модели будут создаваться в этой папке. Создайте новый файл biography.dart, в котором мы создадим класс модели biography, используя пример ответа из документации.

class Biography {
  String? fullName;
  String? alterEgos;
  List<String>? aliases;
  String? placeOfBirth;
  String? firstAppearance;
  String? publisher;
  String? alignment;
  Biography(
      {this.fullName,
      this.alterEgos,
      this.aliases,
      this.placeOfBirth,
      this.firstAppearance,
      this.publisher,
      this.alignment});
  Biography.fromJson(Map<String, dynamic> json) {
    fullName = json['full-name'];
    alterEgos = json['alter-egos'];
    aliases = json['aliases'].cast<String>();
    placeOfBirth = json['place-of-birth'];
    firstAppearance = json['first-appearance'];
    publisher = json['publisher'];
    alignment = json['alignment'];
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = {};
    data['full-name'] = fullName;
    data['alter-egos'] = alterEgos;
    data['aliases'] = aliases;
    data['place-of-birth'] = placeOfBirth;
    data['first-appearance'] = firstAppearance;
    data['publisher'] = publisher;
    data['alignment'] = alignment;
    return data;
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Далее создайте модель Powerstats:

class Powerstats {
  String? intelligence;
  String? strength;
  String? speed;
  String? durability;
  String? power;
  String? combat;
  Powerstats(
      {this.intelligence,
      this.strength,
      this.speed,
      this.durability,
      this.power,
      this.combat});
  Powerstats.fromJson(Map<String, dynamic> json) {
    intelligence = json['intelligence'];
    strength = json['strength'];
    speed = json['speed'];
    durability = json['durability'];
    power = json['power'];
    combat = json['combat'];
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = {};
    data['intelligence'] = intelligence;
    data['strength'] = strength;
    data['speed'] = speed;
    data['durability'] = durability;
    data['power'] = power;
    data['combat'] = combat;
    return data;
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Следующая модель — модель Image:

class Image {
  String? url;
  Image({this.url});
  Image.fromJson(Map<String, dynamic> json) {
    url = json['url'];
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = {};
    data['url'] = url;
    return data;
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

И наконец, у нас есть общая модель SuperheroResponse, которая связывает все эти модели вместе.

import 'package:handling_network_connectivity/models/power_stats_model.dart';
import 'biography_model.dart';
import 'image_model.dart';
class SuperheroResponse {
  String? response;
  String? id;
  String? name;
  Powerstats? powerstats;
  Biography? biography;
  Image? image;
  SuperheroResponse(
      {this.response,
      this.id,
      this.name,
      this.powerstats,
      this.biography,
      this.image});
  SuperheroResponse.fromJson(Map<String, dynamic> json) {
    response = json['response'];
    id = json['id'];
    name = json['name'];
    powerstats = json['powerstats'] != null
        ? Powerstats.fromJson(json['powerstats'])
        : null;
    biography = json['biography'] != null
        ? Biography.fromJson(json['biography'])
        : null;
    image = json['image'] != null ? Image.fromJson(json['image']) : null;
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = {};
    data['response'] = response;
    data['id'] = id;
    data['name'] = name;
    if (powerstats != null) {
      data['powerstats'] = powerstats!.toJson();
    }
    if (biography != null) {
      data['biography'] = biography!.toJson();
    }
    if (image != null) {
      data['image'] = image!.toJson();
    }
    return data;
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

С этим мы можем перейти к следующему шагу — созданию сервисов, которые будут обрабатывать различные аспекты нашего приложения.

Регистрация зависимостей и маршрутов

Создайте новую папку в каталоге lib и назовите ее app. В этой папке создайте файл для хранения всех необходимых конфигураций, таких как маршруты, сервисы и логирование, и назовите его app.dart. Чтобы это работало, нам нужно создать базовую структуру папок для этих конфигураций, но мы полностью заполним их по ходу дела.

Теперь создайте новую папку UI. В нашем демонстрационном приложении будет один экран homeView, на котором будут отображаться данные.

Внутри каталога UI создайте две папки:

  1. shared, которая будет содержать наши общие компоненты пользовательского интерфейса, такие как snackbars, bottomsheets и т.д., которые мы будем использовать во всем приложении.
  2. views, который будет содержать фактические файлы представлений.

В директории view создайте новую папку homeView и создайте два новых файла, home_view.dart для бизнес-логики и функциональности, и home_viewmodel.dart для кода пользовательского интерфейса.

В классе home_viewmodel.dart создайте пустой класс, который расширяет BaseViewModel.

class HomeViewModel extends BaseViewModel{}
Войдите в полноэкранный режим Выход из полноэкранного режима

В файле home_view.dart создайте виджет stateless и верните функцию ViewModelBuilder.reactive() из пакета Stacked. Виджет stateless возвращает конструктор ViewModelBuilder.reactive(), который свяжет файл представления с viewmodel, предоставляя нам доступ к логике и функциям, которые мы объявили в файле viewmodel.

Вот homeView сейчас:

class HomeView extends StatelessWidget {
 const HomeView({Key? key}) : super(key: key);
 @override
 Widget build(BuildContext context) {
  return ViewModelBuilder<HomeViewModel>.reactive(
   viewModelBuilder: () => HomeViewModel(),
   onModelReady: (viewModel) => viewModel.setUp(),
   builder: (context, viewModel, child) {
    return Scaffold();
   },
  );
 }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Далее мы создадим базовую структуру наших сервисов. Создайте новую папку services в каталоге lib. В этой папке мы создадим три новых файла и их базовые структуры, мы будем дорабатывать их по ходу работы.

Мы предложим три сервиса:

  1. ApiService: обрабатывает все исходящие соединения из нашего приложения.

    class ApiService {}
    
  2. SuperheroService: обрабатывает вызов API Superhero, анализирует ответ, используя наши классы моделей, и возвращает данные в нашу viewmodel.

    class SuperheroService{}
    
  3. ConnectivityService: отвечает за мониторинг активного интернет-соединения пользователя.

    class ConnectivityService{}
    

Далее необходимо настроить наши маршруты и зарегистрировать сервисы. Мы воспользуемся аннотацией @StackedApp, которая берется из пакета Stacked. Эта аннотация предоставляет нам доступ к двум параметрам, маршрутам и зависимостям. Зарегистрируйте сервисы в блоке зависимостей, а маршруты объявите в блоке маршрутов.

Мы зарегистрируем SnackbarService и ConnectivityService как Singletons — а не LazySingleton, потому что мы хотим, чтобы они загружались, запускались и работали после запуска приложения, а не ждали до первого инстанцирования.

import 'package:handling_network_connectivity/services/api_service.dart';
import 'package:handling_network_connectivity/services/connectivity_service.dart';
import 'package:handling_network_connectivity/ui/home/home_view.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:stacked_services/stacked_services.dart';
@StackedApp(
  routes: [
    AdaptiveRoute(page: HomeView, initial: true),
  ],
  dependencies: [
    Singleton(classType: SnackbarService),
    Singleton(classType: ConnectivityService),
    LazySingleton(classType: ApiService),
    LazySingleton(classType: SuperheroService)
  ],
  logger: StackedLogger(),
)
class AppSetup {}
Вход в полноэкранный режим Выход из полноэкранного режима

Выполните приведенную ниже команду Flutter, чтобы сгенерировать необходимые файлы.

flutter pub run build_runner build --delete-conflicting-outputs
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта команда генерирует файлы app.locator.dart и app.router.dart, в которых регистрируются наши зависимости и маршруты.

Заполнение сервисов

Первый сервис, который необходимо настроить, это ApiService. Это довольно чистый класс, который мы будем использовать для обработки исходящих/удаленных соединений с помощью пакета http.

Импортируйте пакет http как http и создайте метод. Метод get принимает параметр url, который является url, на который мы направим наш запрос. Выполняем вызов к url с помощью пакета http, проверяем, что наш statusCode равен 200, и, если это так, возвращаем decodedResponse.

Затем мы обернем весь вызов блоком try-catch, чтобы перехватить любые исключения, которые могут быть брошены. Вот в принципе и все в нашем ApiService. Мы оставляем его простым и понятным, но вы можете изменить его по своему усмотрению.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
class ApiService {
  Future<dynamic> get(url) async {
    try {
      final response = await http.get(url);
      if (response.statusCode == 200) {
        return json.decode(response.body);
      }
    } on SocketException {
      rethrow;
    } on Exception catch (e) {
      throw Exception(e);
    }
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Далее по списку, создайте класс для обработки констант, связанных с вызовом API. Это значительно упростит работу, когда мы, наконец, выполним вызов.

В каталоге lib создайте новую папку utils и новый файл api_constants.dart. В нем будут храниться все константы, что сделает наши вызовы API чище и проще.

class ApiConstants {
  static const scheme = 'https';
  static const baseUrl = 'superheroapi.com';
  static const token = '1900121036863469';
  static const superHeroId = 644;
  static get getSuperhero =>
      Uri(host: baseUrl, scheme: scheme, path: '/api/$token/$superHeroId');
}
Вход в полноэкранный режим Выйдите из полноэкранного режима

После этого SuperheroesService, который выполняет вызов удаленного API, получает данные и анализирует их, используя модели, которые мы создали ранее.

import '../app/app.locator.dart';
import '../models/superhero_response_model.dart';
import '../utils/api_constant.dart';
import 'api_service.dart';
class SuperheroService {
  final _apiService = locator<ApiService>();

  Future<SuperheroResponseModel?> getCharactersDetails() async {
    try {
      final response = await _apiService.get(ApiConstants.getSuperhero);
      if (response != null) {
        final superheroData = SuperheroResponseModel.fromJson(response);
        return superheroData;
      }
    } catch (e) {
      rethrow;
    }
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, мы создаем ConnectivityService, который отслеживает наше соединение и предоставляет нам доступ к методам, с помощью которых мы можем проверить состояние соединения. Мы будем использовать функцию InternetAddress.lookup(), предоставляемую Dart для проверки соединения. При наличии стабильного интернет-соединения она возвращает ответ notEmpty, а также содержит rawAddress, связанный с переданным нами URL. При отсутствии интернет-соединения эти две функции не работают, и мы можем с уверенностью сказать, что в данный момент интернет-соединения нет.

Когда они проходят, мы устанавливаем переменную hasConnection в true; а когда они не проходят, мы устанавливаем ее в false. В качестве дополнительной проверки, когда возникает SocketException, что означает отсутствие подключения к Интернету, мы устанавливаем переменную hasConnection в false. Наконец, мы возвращаем hasConnection в качестве результата нашей функции.

import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
class ConnectivityService {
  Connectivity connectivity = Connectivity();
  bool hasConnection = false;
  ConnectivityResult? connectionMedium;
  StreamController<bool> connectionChangeController =
      StreamController.broadcast();
  Stream<bool> get connectionChange => connectionChangeController.stream;
  ConnectivityService() {
    checkInternetConnection();
  }
  Future<bool> checkInternetConnection() async {
    bool previousConnection = hasConnection;
    try {
      final result = await InternetAddress.lookup('google.com');
      if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
        hasConnection = true;
      } else {
        hasConnection = false;
      }
    } on SocketException catch (_) {
      hasConnection = false;
    }
    if (previousConnection != hasConnection) {
      connectionChangeController.add(hasConnection);
    }
    return hasConnection;
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Настройка закусочных панелей

Прежде чем создавать представление, давайте настроим наши пользовательские закуски. У нас будет два типа закусочных: успехи и ошибки. Для этого мы создадим перечисление SnackbarType, которое будет содержать эти два типа.

В папке utils внутри каталога lib создайте новый файл enums.dart. В этом файле мы объявим типы закусочных.

enum SnackbarType { positive, negative }
Вход в полноэкранный режим Выход из полноэкранного режима

Далее необходимо настроить пользовательский интерфейс закусочной (цвета, стилизация и т.д.). Внутри папки shared в каталоге UI создайте новый файл setup_snackbar_ui.dart. Он будет содержать две конфигурации, для типа success snackbar и типа error snackbar.

import 'package:flutter/material.dart';
import 'package:handling_network_connectivity/app/app.locator.dart';
import 'package:handling_network_connectivity/utils/enums.dart';
import 'package:stacked_services/stacked_services.dart';

Future<void> setupSnackBarUI() async {
  await locator.allReady();
  final service = locator<SnackbarService>();
  // Registers a config to be used when calling showSnackbar
  service.registerCustomSnackbarConfig(
    variant: SnackbarType.positive,
    config: SnackbarConfig(
      backgroundColor: Colors.green,
      textColor: Colors.white,
      snackPosition: SnackPosition.TOP,
      snackStyle: SnackStyle.GROUNDED,
      borderRadius: 48,
      icon: const Icon(
        Icons.info,
        color: Colors.white,
        size: 20,
      ),
    ),
  );
  service.registerCustomSnackbarConfig(
    variant: SnackbarType.negative,
    config: SnackbarConfig(
      backgroundColor: Colors.red,
      textColor: Colors.white,
      snackPosition: SnackPosition.BOTTOM,
      snackStyle: SnackStyle.GROUNDED,
      borderRadius: 48,
      icon: const Icon(
        Icons.info,
        color: Colors.white,
        size: 20,
      ),
    ),
  );
}
Вход в полноэкранный режим Выход из полноэкранного режима

Перейдите в файл main.dart и вызовите функции для настройки локатора и snackbarUI в главном блоке.

import 'package:flutter/material.dart';
import 'package:handling_network_connectivity/app/app.router.dart';
import 'package:handling_network_connectivity/ui/shared/snackbars/setup_snackbar_ui.dart';
import 'package:stacked_services/stacked_services.dart';
import 'app/app.locator.dart';
Future main() async {
  WidgetsFlutterBinding.ensureInitialized();
  setupLocator();
  await setupSnackBarUI();
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Connectivity',
      onGenerateRoute: StackedRouter().onGenerateRoute,
      navigatorKey: StackedService.navigatorKey,
    );
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

После этого мы можем приступать к созданию пользовательского интерфейса и мониторингу подключений.

Мониторинг интернет-соединения с помощью потоков

Мы хотим отслеживать интернет-соединение для экрана homeView и затем предпринимать действия, основанные на состоянии соединения. Поскольку мы хотим постоянно обновлять информацию об изменениях соединения, мы будем использовать поток.

Stacked предоставляет нам довольно удобный способ работы с потоками с помощью StreamViewModel. Мы привяжем наш поток к функции checkInternetConnectivity и будем использовать его для управления состоянием представления.

Выполните следующие шаги, чтобы связать поток для управления состоянием представления:

  1. Создайте поток, который мы будем прослушивать. Этот поток вызывает метод checkInternetConnectivity из класса ConnectivityService и затем выдает результат непрерывно как Stream из bool.
  2. Подключите поток, исходящий из этой функции, к переопределению потока модели представления, чтобы предоставить потоку доступ ко всем представлениям, подключенным к этой модели представления.
  3. Создайте булеву переменную connectionStatus, чтобы передать состояние соединения в каждой точке — фактическое состояние, а не поток состояний
  4. Создайте геттер с именем status для прослушивания потока
    1. Установите connectionState на событие, которое он получает, а затем вызовите notifyListeners, обновляя состояние connectionStatus в процессе.
    2. Еще одна важная вещь о геттере — когда нет соединения, приложение не будет загружать необходимые данные на homeView, но когда соединение восстановится, мы хотим, чтобы оно автоматически запускало вызов снова и получало данные, что гарантирует отсутствие разрыва в потоке операций.
  5. Чтобы гарантировать, что мы не будем постоянно пытаться получить данные после первого вызова, даже если после этого сеть будет колебаться, создайте булеву переменную hasCalled, установите ее по умолчанию в false, а затем, после успешного вызова, установите ее в true, чтобы предотвратить повторную выборку данных.
    1. В геттере мы проверяем переменную hasCalled, и если она false, мы запускаем повторную выборку.
  6. Наконец, создайте метод для вызова SuperheroService и получения данных. Присвойте данные экземпляру класса SuperheroResponseModel, который мы будем использовать в представлении для отображения данных.
    1. При успехе или ошибке мы выводим на экран закуску, информирующую пользователя о статусе.

Выполнив эти шаги, мы полностью закончили настройку модели представления и мониторинг сетевого подключения!

class HomeViewModel extends StreamViewModel {
  final _connectivityService = locator<ConnectivityService>();
  final _snackbarService = locator<SnackbarService>();
  final _superheroService = locator<SuperheroService>();
  final log = getLogger('HomeViewModel');

  //7
  SuperheroResponseModel? superHeroDetail;
  // 3
  bool connectionStatus = false;
  bool hasCalled = false;
  bool hasShownSnackbar = false;

  // 1
 Stream<bool> checkConnectivity() async* {
    yield await _connectivityService.checkInternetConnection();
  }

  // 2
  @override
  Stream get stream => checkConnectivity();

  // 4
  bool get status {
    stream.listen((event) {
      connectionStatus = event;
      notifyListeners();
  // 5 & 6
      if (hasCalled == false) getCharacters();
    });
    return connectionStatus;
  }

  Future<void> getCharacters() async {
    if (connectionStatus == true) {
      try {
        detail = await runBusyFuture(
          _superheroService.getCharactersDetails(),
          throwException: true,
        );
        // 6b:  We set the 'hasCalled' boolean to true only if the call is successful, which then prevents the app from re-fetching the data
        hasCalled = true;
        notifyListeners();
      } on SocketException catch (e) {
        hasCalled = true;
        notifyListeners();
        // 8
        _snackbarService.showCustomSnackBar(
          variant: SnackbarType.negative,
          message: e.toString(),
        );
      } on Exception catch (e) {
        hasCalled = true;
        notifyListeners();
        // 8
        _snackbarService.showCustomSnackBar(
          variant: SnackbarType.negative,
          message: e.toString(),
        );
      }
    } else {
      log.e('Internet Connectivity Error');
      if (hasShownSnackbar == false) {
      // 8
        _snackbarService.showCustomSnackBar(
          variant: SnackbarType.negative,
          message: 'Error: Internet Connection is weak or disconnected',
          duration: const Duration(seconds: 5),
        );
        hasShownSnackbar = true;
        notifyListeners();
      }
    }
  }

}
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте перейдем к построению представления.

Построение пользовательского интерфейса

Наконец, мы можем собрать все части вместе для создания пользовательского интерфейса. Для этого пользовательского интерфейса мы создадим две вещи:

  • панель приложения, которая меняет цвет и текст при изменении соединения.
  • Тело, которое отображает детали из API Superhero.

Поскольку ранее мы создали голые кости экрана пользовательского интерфейса, мы можем погрузиться в него прямо сейчас.

В виджете Scaffold создадим AppBar с backgroundColor, который меняется на основе булевой переменной status в модели представления.

 Scaffold(
            appBar: AppBar(
              backgroundColor: viewModel.status ? Colors.green : Colors.red,
              centerTitle: true,
              title: const Text(
                'Characters List',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 24,
                  color: Colors.black,
                ),
              ),
              actions: [
                Text(
                  viewModel.status ? "Online" : "Offline",
                  style: const TextStyle(color: Colors.black),
                )
              ],
            ),
        )
Вход в полноэкранный режим Выход из полноэкранного режима

Когда status становится true, цвет фона становится зеленым; когда он false, мы получаем красный. В дополнение к этому, мы вводим текстовое поле, которое показывает либо Online, либо Offline, основываясь на статусе соединения в данный момент.

В теле виджета Scaffold мы проверяем, является ли статус подключения false, и если это так, мы отображаем текстовое поле, сообщающее пользователю об отсутствии подключения к Интернету. Если нет, то мы отображаем наши данные.

viewModel.status == false
                  ? const Center(
                      child: Text(
                        'No Internet Connection',
                        style: TextStyle(fontSize: 24),
                      ),
                    )
                  : Column()
Вход в полноэкранный режим Выход из полноэкранного режима

Как только это будет сделано, приступайте к созданию пользовательского интерфейса для отображения данных, полученных из Superhero API. Вы можете проверить его в этом GitHub Gist.

Давайте запустим приложение и посмотрим, как все это работает.

Заключение

Наконец, мы полностью контролируем интернет-соединение на HomeView. Вы очень хорошо поработали, дойдя до этого момента. Вы успешно научились настраивать службу подключения, связывать ее с моделью представления для экрана, которым вы хотите управлять, и, наконец, передавать состояние представления в вашем приложении пользователям.

Ознакомьтесь с полным исходным кодом примера приложения. Если у вас есть вопросы или пожелания, обращайтесь ко мне в Twitter: @Blazebrain или LinkedIn: @Blazebrain.


LogRocket: Полная видимость ваших веб-приложений

LogRocket — это решение для мониторинга внешних приложений, которое позволяет воспроизводить проблемы так, как будто они происходят в вашем собственном браузере. Вместо того чтобы гадать, почему возникают ошибки, или просить пользователей предоставить скриншоты и дампы журналов, LogRocket позволяет воспроизвести сессию, чтобы быстро понять, что пошло не так. Он отлично работает с любым приложением, независимо от фреймворка, и имеет плагины для регистрации дополнительного контекста из Redux, Vuex и @ngrx/store.

Помимо регистрации действий и состояния Redux, LogRocket записывает журналы консоли, ошибки JavaScript, трассировку стека, сетевые запросы/ответы с заголовками + телами, метаданные браузера и пользовательские журналы. Он также использует DOM для записи HTML и CSS на странице, воссоздавая пиксельно идеальные видео даже самых сложных одностраничных и мобильных приложений.

Попробуйте бесплатно.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *