Автор Дэвид Адегоке✏️
Введение
Три, два, один — действие! Возьмите телефон, откройте свое любимое приложение, нажмите на значок приложения, оно открывается, регистрирует вас, а затем бум … оно продолжает загружаться. Вы, вероятно, думаете, что оно все еще получает данные, поэтому даете ему минуту, а затем один превращается в два, два в три, три в пять — все еще загружается, никакой информации, никакой ошибки, только загрузка. Разочаровавшись, вы закрываете приложение и либо ищете альтернативу, либо, возможно, даете ему еще одну попытку, прежде чем сдаться.
Подключение невероятно важно, особенно для тех частей нашего приложения, которые сильно зависят от состояния соединения. Мы, разработчики, должны хорошо справляться с этими аспектами нашего приложения. Отслеживая интернет-соединение пользователя, мы можем запустить сообщение, информирующее пользователя о проблемах с подключением, и, что самое важное, запустить функцию, которая загрузит необходимые данные после восстановления интернет-соединения, обеспечив пользователю бесперебойную работу, к которой мы стремимся.
Мы не хотим, чтобы шаткое соединение стало крахом нашего приложения — хотя качество интернет-соединения наших пользователей не всегда находится под нашим контролем — но мы можем установить некоторые проверки, которые проинформируют наших пользователей об этой проблеме, и принять меры в зависимости от состояния соединения. Мы рассмотрим это практически в следующем разделе и увидим, как отслеживать состояние интернет-соединения приложения, а также делать звонок при его восстановлении.
Под «состоянием соединения» здесь подразумевается активное соединение, не в сети, нестабильное и т.д. Давайте погрузимся в это, да?
Реализация обработчика подключений
Пример приложения, который мы создадим в этом разделе, пришел спасти положение (не зря же он называется 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
создайте две папки:
-
shared
, которая будет содержать наши общие компоненты пользовательского интерфейса, такие какsnackbars
,bottomsheets
и т.д., которые мы будем использовать во всем приложении. -
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
. В этой папке мы создадим три новых файла и их базовые структуры, мы будем дорабатывать их по ходу работы.
Мы предложим три сервиса:
-
ApiService
: обрабатывает все исходящие соединения из нашего приложения.class ApiService {}
-
SuperheroService
: обрабатывает вызов API Superhero, анализирует ответ, используя наши классы моделей, и возвращает данные в нашуviewmodel
.class SuperheroService{}
-
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
и будем использовать его для управления состоянием представления.
Выполните следующие шаги, чтобы связать поток для управления состоянием представления:
- Создайте поток, который мы будем прослушивать. Этот поток вызывает метод
checkInternetConnectivity
из классаConnectivityService
и затем выдает результат непрерывно какStream
изbool
. - Подключите поток, исходящий из этой функции, к переопределению потока модели представления, чтобы предоставить потоку доступ ко всем представлениям, подключенным к этой модели представления.
- Создайте булеву переменную
connectionStatus
, чтобы передать состояние соединения в каждой точке — фактическое состояние, а не поток состояний - Создайте геттер с именем
status
для прослушивания потока- Установите
connectionState
на событие, которое он получает, а затем вызовитеnotifyListeners
, обновляя состояниеconnectionStatus
в процессе. - Еще одна важная вещь о геттере — когда нет соединения, приложение не будет загружать необходимые данные на homeView, но когда соединение восстановится, мы хотим, чтобы оно автоматически запускало вызов снова и получало данные, что гарантирует отсутствие разрыва в потоке операций.
- Установите
- Чтобы гарантировать, что мы не будем постоянно пытаться получить данные после первого вызова, даже если после этого сеть будет колебаться, создайте булеву переменную
hasCalled
, установите ее по умолчанию вfalse
, а затем, после успешного вызова, установите ее вtrue
, чтобы предотвратить повторную выборку данных.- В геттере мы проверяем переменную
hasCalled
, и если онаfalse
, мы запускаем повторную выборку.
- В геттере мы проверяем переменную
- Наконец, создайте метод для вызова
SuperheroService
и получения данных. Присвойте данные экземпляру классаSuperheroResponseModel
, который мы будем использовать в представлении для отображения данных.- При успехе или ошибке мы выводим на экран закуску, информирующую пользователя о статусе.
Выполнив эти шаги, мы полностью закончили настройку модели представления и мониторинг сетевого подключения!
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 на странице, воссоздавая пиксельно идеальные видео даже самых сложных одностраничных и мобильных приложений.
Попробуйте бесплатно.