Handling network connectivity in Flutter
Tags: flutter
Published: 2022-02-23 15:17:58

**Written by [David Adegoke](https://blog.logrocket.com/author/davidadegoke/)✏️**

## Introduction

Three, two, one — action! Pick up your phone, open up your favorite app, click the app icon, it opens up, logs you in, then boom … it keeps loading. You probably think it’s still fetching data, so you give it a minute, and then one turns to two, two to three, three to five — still loading, no info, no error, just loading. Out of frustration, you close the app and either look for an alternative or possibly give it another trial before giving up.

Connectivity is incredibly important, especially for the specific parts of our application that depend heavily on the connection state. It’s proper for us as developers to handle those aspects of our application well. By monitoring the user’s internet connection, we can trigger a message that informs the user of the issues with their connection — and, most importantly, triggers a function that loads the needed data once the internet connection is back, giving the user the seamless experience we aim for.

We don’t want a shaky connection to be the downfall of our app — even though the quality of our users’ internet connection isn’t necessarily under our control — but we can put some checks in place that inform our users of this issue, and take action based on the connection state. We’ll go into this practically in the next section and see how to monitor the internet connectivity of an application as well as make a call when it’s recovered.

“Connection states” here refer to active connection, offline, unstable, etc. Let’s dive into it, yeah?

## Implementing a connectivity handler

The sample application we’ll build in this section has come to save the day (well, it’s the Superhero API for a reason). We’ll fetch data from the [Superhero API](https://superheroapi.com/), then display this data to the user.

Let’s pause there. Our goal is to monitor connectivity, right?

While that is correct, in between that flow, we need to monitor the device’s internet connection. When the connection is off, we need to display a message to the user informing them of the situation, and, when the internet connectivity is restored, we must immediately make a call to the API and get our data.

As a way of ensuring our app does not keep fetching the data continuously on every change in the connection status, we’ll also introduce an additional variable whose duty is to inform the app whether or not we’ve called the function that loads our data.

## Superhero API setup

Before we launch into the code, there are a few things we need to put in place on our sample site before we can make use of the Superhero API.

First of all, head over to the Superhero API site. You are required to sign-in with Facebook in order to get the access token that we’ll use to query the API.

![Log into the Superhero API using Facebook to gain the access token](https://blog.logrocket.com/wp-content/uploads/2022/01/superhero-api-access-token.png)

After logging in, you can copy the access token and and use it in the app.

Second thing to do is pick a character. Superman? Definitely.

As seen in the docs, the Superhero API provides us an [ID for each superhero](https://superheroapi.com/ids.html). This ID is then used in our API query and returns information on that particular hero. The ID for Superman is `644`, so note that down.

With these two things done, we are free to start querying the API.

## Project setup

Run the following command to create a new codebase for the project.

```BASH
flutter create handling_network_connectivity
```

Import the following dependencies in our `pubspec.yaml` file:

*   [`http`](https://pub.dev/packages/http): To make a `GET` request to the [Superhero API](https://superheroapi.com/?ref=apilist.fun) and retrieve character data for our chosen superhero
*   [`stacked`](https://pub.dev/packages/stacked): This is the architectural solution we’ll use in this package, which [makes use of Provider](https://blog.logrocket.com/provider-vs-riverpod-comparing-state-managers-in-flutter) under the hood and gives us access to some really cool classes to spice up our development process
*   [`stacked_services`](https://pub.dev/packages/stacked_services): Ready-to-use services made available by the stacked package
*   [`build_runner`](https://pub.dev/packages/build_runner): Gives access to run commands for auto-generating files from annotations
*   [`stacked_generator`](https://pub.dev/packages/stacked_generator): Generates files from stacked annotations
*   [`logger`](https://pub.dev/packages/logger): Prints important information to the debug console

```plaintext
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
```

With this out of the way, we are set to begin actual development.

## Setting up models

From the [Superhero API documentation](https://superheroapi.com/?ref=apilist.fun#id), we see that a call to a particular `superheroId` returns that superhero’s biography, powers-stats, background, appearance, image, and more.

![A list of the available fields you can fetch in the Superhero API](https://blog.logrocket.com/wp-content/uploads/2022/01/available-fields-superhero-api.png)

In this article, we will only deal with the biography, power-stats, and image fields, but you can decide to add more data if you want. Thus, we’ll need to create models to convert the JSON response into our `Object` data.

Create a folder in the `lib` directory. Name the folder `models`; all models will be created in this folder. Create a new file named `biography.dart`, into which we will create the `biography` model class using the sample response from the documentation.

```dart
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;
  }
}
```

Next, create the `Powerstats` model:

```dart
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;
  }
}
```

Next model is the `Image` model:

```dart
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;
  }
}
```

Lastly, we have the overall `SuperheroResponse` model, which links all of these models together.

```dart
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;
  }
}
```

With this in place, we can move forward to the next step, which is creating the services that will handle various aspects of our app.

## Registering dependencies and routes

Create a new folder in the `lib` directory and name it `app`. In this folder, create a file to hold all of our necessary configurations, like routes, services, and logging, and name it `app.dart`. For this to work, we need to create the basic folder structure for these configurations, but we’ll fully flesh them out as we proceed.

Now, create a new folder called `UI`. We’ll have a single screen in our demo app, the `homeView`, which will display the data.

Inside the `UI` directory, create two folders:

1.  `shared`, which will contain our shared UI components, like `snackbars`, `bottomsheets` etc., that we’ll use throughout the app
2.  `views`, which will contain the actual view files

![The Shared and Views folders in our UI directory](https://blog.logrocket.com/wp-content/uploads/2022/01/shared-views-ui-directory.png)

Within the `view` directory, create a new folder named `homeView` and create two new files, `home_view.dart` for the business logic and functionalities, and `home_viewmodel.dart`, for the UI code.

Within the `home_viewmodel.dart` class, create an empty class that extends the `BaseViewModel`.

```dart
class HomeViewModel extends BaseViewModel{}
```

In the `home_view.dart` file, create a stateless widget and return the `ViewModelBuilder.reactive()` function from the Stacked package. The stateless widget returns the `ViewModelBuilder.reactive()` constructor, which will bind the view file with the `viewmodel`, granting us access to the logic and functions we declared in the `viewmodel` file.

Here is the `homeView` now:

```dart
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();
   },
  );
 }
}
```

Next, we’ll create the base structure of our services. Create a new folder called `services` in the `lib` directory. In this folder, we create the three new files and their base structures, we will flesh out all of them as we proceed.

We’ll offer three services:

1.  The `ApiService`: handles all outbound connections from our application

    ```dart
class ApiService {}
 ```

2.  The `SuperheroService`: handles the call to the Superhero API, parses the response using our model classes, and returns the data to our `viewmodel`

    ```dart
class SuperheroService{}
```

3.  The `ConnectivityService`: is responsible for monitoring the user’s active internet connection

    ```dart
class ConnectivityService{}
```

Next is to set up our routes and register the services. We’ll make use of the `@StackedApp` annotation, which comes from the Stacked package. This annotation grants us access to two parameters, routes and dependencies. Register the services in the dependencies block, and declare the routes in the route block.

We’ll register the `SnackbarService` and `ConnectivityService` as `Singletons` — and not `LazySingleton` because we want them loaded, up, and running once the app starts and not waiting until first instantiation.

```dart
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 {}
```

Run the Flutter command below to generate the files needed.

```dart
flutter pub run build_runner build --delete-conflicting-outputs
```

This command generates the `app.locator.dart` and `app.router.dart` files into which our dependencies and routes are registered.

## Filling out the services

The first service to setup is the `ApiService`. It’s a pretty clean class that we’ll use to handle our outbound/remote connections using the `http` package.

Import the http package as `http` and create a method. The get method accepts a `url` parameter, which is the `url` to which we’ll point our request. Make the call to the `url` using the `http` package, check if our `statusCode` is `200`, and, if it’s true, we return the `decodedResponse`.

We then wrap the entire call with a `try-catch` block in order to catch any exceptions that might be thrown. That's basically everything in our `ApiService`. We’re keeping it sweet and simple, but you can definitely adjust as you see fit.

```dart
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);
    }
  }
}
```

Next on the list, create a class to handle the constants relating to the API call. This will make things much easier when we finally make the calls.

In the `lib` directory, create a new folder named `utils` and a new file titled `api_constants.dart`. This will hold all constants, making our API calls cleaner and easier.

```dart
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');
}
```

After this, the `SuperheroesService`, which makes the call to the remote API, gets the data and parses it using the models we created earlier.

```dart
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;
    }
  }
}
```

Finally, we create the `ConnectivityService`, which monitors our connection and gives us access to methods through which we can check the connection state. We’ll make use of an `InternetAddress.lookup()` function that’s provided by Dart to check for connectivity. When there is a stable internet connection, it returns a `notEmpty` response and also contains the `rawAddress` related to the URL we passed. If there is no internet connection, these two functions fail and we can safely say there is no internet connection at the moment.

When they pass, we set the `hasConnection` variable to `true`; and when they fail, we set it to `false`. As an additional check, when there is a `SocketException`, which signifies no internet connection, we set the `hasConnection` variable to `false`. Finally, we return `hasConnection` as the result of our function.

```dart
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;
  }
}
```

## Setting up our snackbars

Before we build the view, let’s set up our custom snackbars. We’ll have two types of snackbars: successes and errors. For this, we’ll create an enum of `SnackbarType`, which holds these two types.

In the `utils` folder inside the `lib` directory, create a new file called `enums.dart`. We’ll declare the snackbar types in this file.

```dart
enum SnackbarType { positive, negative }
```

Next is to actually configure the snackbar UI (colors, styling, etc.). Inside the `shared` folder in the `UI` directory, create the a new file called `setup_snackbar_ui.dart`. It will hold two config registrations, for the `success` snackbar type and the `error` snackbar type.

```dart
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,
      ),
    ),
  );
}
```

Head over to the `main.dart` file and call the functions to setup the locator and the `snackbarUI` in the main block.

```dart
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,
    );
  }
}
```

With this done, we are good to go and can actually start building the UI and monitoring connections.

## Monitoring internet connectivity using streams

We want to monitor the internet connection for the `homeView` screen and then take action based on the connection state. Since we want it to be constantly updated on connection changes, we’ll make use of a stream.

Stacked provides us a pretty handy way to handle streams using the [`StreamViewModel`](https://pub.dev/documentation/stacked/latest/stacked/StreamViewModel-class.html). We link our stream to the `checkInternetConnectivity` function and use it to control the state of the view.

Follow these steps to link the stream to control the state of the view:

1.  Create the stream we’ll be listening to. This stream calls the `checkInternetConnectivity` method from the `ConnectivityService` class and then yields the result continually as a `Stream` of `bool`
2.  Hook the stream coming from this function to the stream override of the view model to grant the stream access to all views connected to this view model
3.  Create a boolean variable named `connectionStatus` to give the state of the connection at each point — the actual state, not a Stream of states
4.  Create a getter named `status` to listen to the stream
    1.  Set the `connectionState` to the event that it receives, and then call `notifyListeners`, updating the `connectionStatus` state in the process
    2.  One more important thing about the getter — when there is no connection, the app won’t load essential data needed on the homeView, but when the connection returns, we want it to automatically run the call again and fetch the data, which ensures there isn’t a break in the operation flow
5.  To ensure that we don’t continually try to fetch the data after the first call, even if the network fluctuates after, create a boolean variable named `hasCalled`, set it to `false` by default, and then, after a call has successfully been made, set it to `true` to prevent the app from re-fetching
    1.  In the getter, we check the `hasCalled` variable and if it’s `false`, we trigger a re-fetch
6.  Lastly, create the method to call the `SuperheroService` and get the data. Assign the data to an instance of the `SuperheroResponseModel` class, which we will use in the view to display the data
    1.  On success or error, we display a snackbar to the user informing them of the status

With these steps done, we are fully done with setting up our view model and monitoring network connectivity!

```dart
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();
      }
    }
  }

}
```

Let’s proceed to build the view.

## Building the user interface

Finally, we can bring the pieces together to build the UI. We will build two things for this UI:

*   The app bar, which changes color and text when the connection changes
*   The body, which displays the details from the Superhero API

Since we built the bare bones of the UI screen earlier, we can dive right in now.

In the `Scaffold` widget, let's create an `AppBar` with a `backgroundColor` that changes based on the `status` boolean variable in the view model.

```dart
 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),
                )
              ],
            ),
        )
```

Once the `status` is `true`, the background color will turn green; when it’s false, we get red. In addition to that, we introduce a text box that shows either `Online` or `Offline` based on the connection status at that point.

In the body of the `Scaffold` widget, we check if the connection status is `false`, and if it is, we display a text box to the user telling them there is no internet connection. If not, we then display our data.

```dart
viewModel.status == false
                  ? const Center(
                      child: Text(
                        'No Internet Connection',
                        style: TextStyle(fontSize: 24),
                      ),
                    )
                  : Column()
```

Once this is done, go ahead and create the UI to display the details drawn from the Superhero API. You can [check it out in this GitHub Gist](https://gist.github.com/Blazebrain/0fdfeae52da857321825fa4a7b969e4e).

Let's run the app and see how it all comes together.

<iframe title="YouTube video player" src="https://www.youtube.com/embed/TwxQvhvKJ1A" width="560" height="315" frameborder="0" allowfullscreen="allowfullscreen"></iframe>

## Conclusion

Finally, we are fully monitoring the internet connection on the homeView. You’ve done really well getting to this point. You have successfully learned how to set up your connectivity service, link it to the view model for the screen you want to control, and then, how to finally communicate the view state in your application to your users.

Check out the [complete source code](https://github.com/Blazebrain/handling_network_connectivity) for the sample app. If you have any questions or inquiries, feel free to reach out to me on Twitter: [@Blazebrain](https://www.twitter.com/Blazebrain01) or LinkedIn: [@Blazebrain](https://www.linkedin.com/in/david-adegoke).


---

## [LogRocket](https://logrocket.com/signup/): Full visibility into your web apps

![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)

[LogRocket](https://logrocket.com/signup/) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

[Try it for free](https://logrocket.com/signup/).