DEV Community

Cover image for Handling network connectivity in Flutter
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Handling network connectivity in Flutter

Written by David Adegoke✏️

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, 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

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. 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.

flutter create handling_network_connectivity
Enter fullscreen mode Exit fullscreen mode

Import the following dependencies in our pubspec.yaml file:

  • http: To make a GET request to the Superhero API and retrieve character data for our chosen superhero
  • stacked: This is the architectural solution we’ll use in this package, which makes use of Provider under the hood and gives us access to some really cool classes to spice up our development process
  • stacked_services: Ready-to-use services made available by the stacked package
  • build_runner: Gives access to run commands for auto-generating files from annotations
  • stacked_generator: Generates files from stacked annotations
  • logger: Prints important information to the debug console
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
Enter fullscreen mode Exit fullscreen mode

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

Setting up models

From the Superhero API documentation, 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

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.

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, create the Powerstats model:

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Next model is the Image model:

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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.

class HomeViewModel extends BaseViewModel{}
Enter fullscreen mode Exit fullscreen mode

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:

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();
   },
  );
 }
}
Enter fullscreen mode Exit fullscreen mode

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

    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

    class SuperheroService{}
    
  3. The ConnectivityService: is responsible for monitoring the user’s active internet connection

    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.

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 {}
Enter fullscreen mode Exit fullscreen mode

Run the Flutter command below to generate the files needed.

flutter pub run build_runner build --delete-conflicting-outputs
Enter fullscreen mode Exit fullscreen mode

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.

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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');
}
Enter fullscreen mode Exit fullscreen mode

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

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

enum SnackbarType { positive, negative }
Enter fullscreen mode Exit fullscreen mode

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.

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,
      ),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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. 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!

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

}
Enter fullscreen mode Exit fullscreen mode

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.

 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),
                )
              ],
            ),
        )
Enter fullscreen mode Exit fullscreen mode

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.

viewModel.status == false
                  ? const Center(
                      child: Text(
                        'No Internet Connection',
                        style: TextStyle(fontSize: 24),
                      ),
                    )
                  : Column()
Enter fullscreen mode Exit fullscreen mode

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.

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

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 for the sample app. If you have any questions or inquiries, feel free to reach out to me on Twitter: @Blazebrain or LinkedIn: @Blazebrain.


LogRocket: Full visibility into your web apps

LogRocket signup

LogRocket 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.

Top comments (0)