Working with RESTful APIs in Flutter Applications

Flutter, Google’s UI toolkit, is widely used for building natively compiled applications for mobile, web, and desktop from a single codebase. One of the essential aspects of modern application development is interacting with RESTful APIs to fetch, post, update, and delete data. In this comprehensive guide, we will explore how to effectively work with RESTful APIs in Flutter applications, covering everything from setting up the HTTP client to handling data serialization and error handling.

What are RESTful APIs?

RESTful APIs (Representational State Transfer) are an architectural style for building networked applications. They rely on a stateless, client-server communication protocol, typically HTTP, to define operations through various HTTP methods (GET, POST, PUT, DELETE) on resources identified by URLs.

Why Use RESTful APIs in Flutter?

  • Data Integration: Fetch real-time data from backend services to populate the UI.
  • Scalability: Facilitate communication with scalable backend infrastructure.
  • Cross-Platform Compatibility: Interact with APIs that support multiple platforms.
  • Maintainability: Decouple the frontend UI from the backend data layer.

Setting Up Your Flutter Project

Before diving into the code, ensure you have Flutter installed and configured. Create a new Flutter project:

flutter create flutter_rest_api_example

Adding Dependencies

To work with RESTful APIs in Flutter, you will need an HTTP client package. The http package is a popular choice. Add it to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0 # Use the latest version

Then, run flutter pub get to install the dependency.

Making a GET Request

To fetch data from an API endpoint using a GET request, follow these steps:

Step 1: Import the HTTP Package

import 'package:http/http.dart' as http;
import 'dart:convert'; // For JSON encoding/decoding

Step 2: Create a Function to Fetch Data

Future<Map<String, dynamic>> fetchData() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'));

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return jsonDecode(response.body);
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load data');
  }
}

This function sends a GET request to the specified URL and parses the JSON response if the request is successful. If the request fails, it throws an exception.

Step 3: Use the Function in Your UI

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter REST API Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Future<Map<String, dynamic>> futureData;

  @override
  void initState() {
    super.initState();
    futureData = fetchData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('REST API Example'),
      ),
      body: Center(
        child: FutureBuilder<Map<String, dynamic>>(
          future: futureData,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return Text('Title: ${snapshot.data!['title']}');
            } else if (snapshot.hasError) {
              return Text('${snapshot.error}');
            }

            // By default, show a loading spinner.
            return CircularProgressIndicator();
          },
        ),
      ),
    );
  }
}

Here, the FutureBuilder widget is used to handle the asynchronous operation of fetching data. It displays a loading indicator while waiting for the data, and then displays the data or an error message.

Making a POST Request

To send data to an API endpoint using a POST request, follow these steps:

Step 1: Create a Function to Post Data

Future<http.Response> createAlbum(String title) async {
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/albums'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
      'userId': '1',
    }),
  );

  return response;
}

This function sends a POST request to the specified URL with a JSON payload containing the album title and userId.

Step 2: Use the Function in Your UI

import 'package:flutter/material.dart';
import 'dart:async';

class CreateData extends StatefulWidget {
  const CreateData({Key? key}) : super(key: key);

  @override
  _CreateDataState createState() => _CreateDataState();
}

class _CreateDataState extends State<CreateData> {
  final TextEditingController _controller = TextEditingController();
  Future<http.Response>? _futureAlbum;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Create Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Create Data Example'),
        ),
        body: Container(
          alignment: Alignment.center,
          padding: const EdgeInsets.all(8.0),
          child: (_futureAlbum == null)
              ? buildColumn()
              : buildFutureBuilder(),
        ),
      ),
    );
  }

  Column buildColumn() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        TextField(
          controller: _controller,
          decoration: const InputDecoration(hintText: 'Enter Title'),
        ),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _futureAlbum = createAlbum(_controller.text);
            });
          },
          child: const Text('Create Data'),
        ),
      ],
    );
  }

  FutureBuilder<http.Response> buildFutureBuilder() {
    return FutureBuilder<http.Response>(
      future: _futureAlbum,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text(snapshot.data!.body);
        } else if (snapshot.hasError) {
          return Text('${snapshot.error}');
        }

        return const CircularProgressIndicator();
      },
    );
  }
}

This widget includes a TextField for entering the album title and a button to trigger the POST request. The FutureBuilder then displays the response from the API.

Making a PUT Request

PUT requests are used to update a resource. Here’s how to make a PUT request in Flutter:

Future<http.Response> updateAlbum(int id, String title) async {
  final response = await http.put(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/$id'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, dynamic>{
      'id': id,
      'title': title,
      'userId': 1,
    }),
  );

  return response;
}

And the Widget to use this

class UpdateData extends StatefulWidget {
  const UpdateData({Key? key}) : super(key: key);

  @override
  _UpdateDataState createState() => _UpdateDataState();
}

class _UpdateDataState extends State<UpdateData> {
  final TextEditingController _controller = TextEditingController();
  int albumId = 1; // album ID you want to update
  Future<http.Response>? _futureAlbum;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Update Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Update Data Example'),
        ),
        body: Container(
          alignment: Alignment.center,
          padding: const EdgeInsets.all(8.0),
          child: (_futureAlbum == null)
              ? buildColumn()
              : buildFutureBuilder(),
        ),
      ),
    );
  }

  Column buildColumn() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        TextField(
          controller: _controller,
          decoration: const InputDecoration(hintText: 'Enter New Title'),
        ),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _futureAlbum = updateAlbum(albumId, _controller.text);
            });
          },
          child: const Text('Update Data'),
        ),
      ],
    );
  }

  FutureBuilder<http.Response> buildFutureBuilder() {
    return FutureBuilder<http.Response>(
      future: _futureAlbum,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text(snapshot.data!.body);
        } else if (snapshot.hasError) {
          return Text('${snapshot.error}');
        }

        return const CircularProgressIndicator();
      },
    );
  }
}

Similar to the POST example, the user inputs new text in the input field, then press update, so the data can be changed.

Making a DELETE Request

DELETE requests are used to delete a resource.

Future<http.Response> deleteAlbum(int id) async {
  final http.Response response = await http.delete(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/$id'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
  );

  return response;
}

And a widget using that

class DeleteData extends StatefulWidget {
  const DeleteData({Key? key}) : super(key: key);

  @override
  _DeleteDataState createState() => _DeleteDataState();
}

class _DeleteDataState extends State<DeleteData> {
  Future<http.Response>? _futureAlbum;
  int albumId = 1;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Delete Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Delete Data Example'),
        ),
        body: Center(
          child: FutureBuilder<http.Response>(
            future: _futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                if (snapshot.hasData) {
                  return Text('Album Deleted Successfully! Status Code: ${snapshot.data!.statusCode}');
                } else if (snapshot.hasError) {
                  return Text('Error deleting album: ${snapshot.error}');
                } else {
                  return Text('Deleting...');
                }
              } else {
                return ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _futureAlbum = deleteAlbum(albumId);
                    });
                  },
                  child: const Text('Delete Album'),
                );
              }
            },
          ),
        ),
      ),
    );
  }
}

Here a button is availabe on the loading process, when press the delete button, call the API to remove data from service.

Handling Errors

Proper error handling is crucial when working with APIs. Wrap your API calls in try-catch blocks to handle exceptions:

Future<Map<String, dynamic>> fetchData() async {
  try {
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'));

    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Failed to load data');
    }
  } catch (e) {
    print('Error: $e');
    throw Exception('Failed to load data');
  }
}

Conclusion

Working with RESTful APIs in Flutter involves fetching, posting, updating, and deleting data through HTTP requests. With the http package and proper error handling, you can create robust and scalable Flutter applications that interact with backend services effectively. This guide covered setting up your project, making various types of API requests, handling responses, and addressing errors, equipping you with the foundational knowledge to integrate RESTful APIs into your Flutter applications.