Flutter Tutorial: How to Integrate APIs with a State Management Approach

Written by jitenpatel | Published 2021/11/08
Tech Story Tags: flutter | api-integration | flutter-for-mobile-app | flutter-tutorial | flutter-trends | mobile-app-development | state-management

TLDRWe are gonna create a normal shopping application which displays a list of products. All the products are fetched over the internet and displayed in our Flutter app. We will be integrating [Fakestore API] which is a free online REST API that you can use whenever you need Pseudo-real data for your e-commerce or shopping app without running any server-side code. GetX is a fast, lightweight, and powerful microframework, and using this, we can easily manage states.via the TL;DR App

Handling network requests and integrating APIs is one of the problems faced by Flutter beginners. Even I have faced these problems while developing Flutter applications. Converting JSON to dart objects, making a network call, state management, are a few things we have to consider while integrating APIs in a Flutter app. I want to cover all those things in this article so anyone who's a beginner in Flutter can get going quickly and easily with their API network calls on your projects. So, without any further ado, let's get started.

Here's a quick demo of what we are going to build.

We are gonna create a normal shopping application which displays a list of products. All the products are fetched over the internet and displayed in our Flutter app.

We will be integrating Fakestore API. It is a free online REST API that you can use whenever you need Pseudo-real data for your e-commerce or shopping app without running any server-side code. Before diving into a code let me give you a short intro about the packages we are gonna use.

Getting Started

Here's the folder structure of our app.

HTTP Service

For making communication with a remote server we use various APIs which need some type of HTTP methods to get executed. So we are going to create a HttpService class, which will help us to communicate with our server. Inside the services folder, create a file called http_service.dart

import 'package:http/http.dart' as http;

class HttpService {
  static Future<List<ProductsModel>> fetchProducts() async {
    var response =
        await http.get(Uri.parse("https://fakestoreapi.com/products"));
    if (response.statusCode == 200) {
      var data = response.body;
      return productsModelFromJson(data);
    } else {
      throw Exception();
    }
  }
}

The code is self-explanatory. We created an HttpService class and we created a static method called fetchProducts(). We are making an HTTP GET request to our URL endpoint which is our Fakestore API. If we successfully hit the endpoint then we will get a response code (statusCode) as 200 and will return dart objects or else we will throw an Exception. Okay, Jiten I get to know about the class and function and what it does but what is List<ProductionModel> and productsModelFromJson()?

Note: You can also wrap this response code in a try-catch block. But for simplicity and for understanding will throw Exception.

Model Class

While dealing with APIs, we may get a large number of data and which may have numerous fields so coding each and every JSON field into Dart Objects (this is also called JSON parsing ) will come in handy. To deal with this problem will use a famous website called quicktype.io which converts our JSON response to Dart Objects.

Create a file called products_model.dart inside the folder models and hit the Fakestore API with the postman or just copy-pasting the link into your browser. You will get a JSON response just copy the whole response and paste it into quicktype.io

// To parse this JSON data, do
//
//     final productsModel = productsModelFromJson(jsonString);

import 'dart:convert';

List<ProductsModel> productsModelFromJson(String str) =>
    List<ProductsModel>.from(
        json.decode(str).map((x) => ProductsModel.fromJson(x)));

String productsModelToJson(List<ProductsModel> data) =>
    json.encode(List<dynamic>.from(data.map((x) => x.toJson())));

class ProductsModel {
  ProductsModel({
    this.id,
    this.title,
    this.price,
    this.description,
    this.category,
    this.image,
    this.rating,
  });

  final int? id;
  final String? title;
  final double? price;
  final String? description;
  final String? category;
  final String? image;
  final Rating? rating;

  factory ProductsModel.fromJson(Map<String, dynamic> json) => ProductsModel(
        id: json["id"],
        title: json["title"],
        price: json["price"].toDouble(),
        description: json["description"],
        category: json["category"],
        image: json["image"],
        rating: Rating.fromJson(json["rating"]),
      );

  Map<String, dynamic> toJson() => {
        "id": id,
        "title": title,
        "price": price,
        "description": description,
        "category": category,
        "image": image,
        "rating": rating!.toJson(),
      };
}

class Rating {
  Rating({
    this.rate,
    this.count,
  });

  final double? rate;
  final int? count;

  factory Rating.fromJson(Map<String, dynamic> json) => Rating(
        rate: json["rate"].toDouble(),
        count: json["count"],
      );

  Map<String, dynamic> toJson() => {
        "rate": rate,
        "count": count,
      };
}

Note, that the code generated will differ from the code I have given because while generating code quicktype.io generates for an older version of dart language until the time of writing this article. The above code is with sound null-safety. To ensure null safety in your flutter code you can refer to this article.

State Management

Whenever data changes we have to update our app UI accordingly. For example, when we make a network request we must show a user progress indicator until the network request is complete, once completed, we must show appropriate UI. If the request fails we must show the appropriate message to the user.

In our app, we will show two states, a progress indicator while making a network call, once the network request completes will show our fetched data and update the UI.

Okay, now how we are gonna do it? Let's create a file called product_controller.dart inside the folder controllers.

import 'package:get/get.dart';

class ProductController extends GetxController {
  var isLoading = true.obs;
  var productList = [].obs;
  

  @override
  void onInit() {
    fetchProducts();
    super.onInit();
  }

  void fetchProducts() async {
    try {
      isLoading(true);
      var products = await HttpService.fetchProducts();
      if (products != null) {
        productList.value = products;
      }
    } finally {
      isLoading(false);
    }
  }
}

As I mentioned, we are using GetX for state management, we extend the GetxController class which provides us onInit() method.

GetX is a fast, lightweight, and powerful microframework, and using this, we can easily manage states. It is beginner-friendly and you don't have to worry about the boilerplate code for managing states. GetX does it all for you. Although, you can use any state management approach like BLOC, Provider, MobX., etc. But for this app, we will stick to GetX.

It is the method first called whenever we instantiate the object of the class. We have declared two variables, one is boolean (isLoading) and another one is a list (productList). Note that we have added .obs to the variable which means observable. That means these two variables may be updated and we have to observe that. It's all GetX stuff you don't have to understand what is going under the hood. GetX does all of this for us. We wrote fetchProducts() method in which we are making a network request as shown on line number 17.

Before making a network request we are updating our boolean variable isLoading to true so that our UI can show a progress indicator and in finally block we update the same variable to false so that our UI can know the data has been fetched from the internet and update the UI accordingly.

It's All About Views

Last step but not least. Let's create a UI.

class HomePageView extends StatelessWidget {
  final ProductController productController = Get.put(ProductController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        elevation: 0,
        leading: const Icon(Icons.arrow_back_ios),
        actions: [
          IconButton(
            onPressed: () {},
            icon: const Icon(Icons.shopping_cart),
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                const Expanded(
                  child: Text(
                    "ShopMe",
                    style: TextStyle(
                        fontFamily: "avenir",
                        fontSize: 32,
                        fontWeight: FontWeight.w900),
                  ),
                ),
              ],
            ),
          ),
          Expanded(
            child: Obx(
              () {
                if (productController.isLoading.value) {
                  return Center(child: CircularProgressIndicator());
                } else
                  return StaggeredGridView.countBuilder(
                    crossAxisCount: 2,
                    itemCount: productController.productList.length,
                    crossAxisSpacing: 16,
                    mainAxisSpacing: 16,
                    itemBuilder: (context, index) {
                      return ProductTile(productController.productList[index]);
                    },
                    staggeredTileBuilder: (index) => StaggeredTile.fit(1),
                  );
              },
            ),
          ),
        ],
      ),
    );
  }
}

The code is very basic, the key things to explain here are on line number 2 and line number 36–49. On line number 2, we have instantiated our controller and added dependency injection.

Note that on line number 36 we have used Obx(), remember we have added .obs to our controller variables? when you want to show observable variables and update the screen whenever the values changes, we simply use Obx().

On line number 38, we are checking if our isLoading variable is true or not. If it is true that means we are having a network call and the data is being fetched. It will show a CircularProgressIndicator() and once the data has been fetched isLoading variable will become false then will show a staggered grid view.

Conclusion

To sum up, we have integrated the FakestoreAPI with a simple state management approach. Still, there are most of the changes we can do in this code like making abstraction classes, handling network requests and errors according to HTTP Response Code, etc.

If you like to improve my code feel free to check out my Github repo of this project. Have any questions? Let's get connected on LinkedIn, Twitter, & Instagram

Happy Coding :)


This article was also published here


Written by jitenpatel | Machine learning eng. and Flutter developer. Learning from mistakes and delivering from experience
Published by HackerNoon on 2021/11/08