DEV Community

Ge Ji
Ge Ji

Posted on

Flutter Lesson 12: Network Requests and JSON Parsing

In modern mobile application development, nearly all apps need to interact with backend servers to fetch remote data and display it to users. Flutter provides various ways to handle network requests and data parsing. This lesson will detail how to make network requests, process response data, and parse JSON-formatted data in Flutter.

I. HTTP Library Selection: dio Installation and Basic Usage

While Flutter officially provides the http package for network requests, the dio library is widely used in practical development due to its more powerful features and cleaner API. dio is a robust Dart HTTP client that supports interceptors, FormData, request cancellation, timeout settings, and other advanced features.

1. Installing dio

Add the dependency to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.9.0  # Use the latest version
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to install the dependency.

2. Basic dio Usage

First, import the dio package:

import 'package:dio/dio.dart';
Enter fullscreen mode Exit fullscreen mode

Create a dio instance:

// Create a default instance
Dio dio = Dio();

// Or configure an instance with BaseOptions
BaseOptions options = BaseOptions(
  baseUrl: 'https://api.example.com',
  connectTimeout: const Duration(milliseconds: 5000),
  receiveTimeout: const Duration(milliseconds: 3000),
  headers: {
    'Content-Type': 'application/json',
  },
);

Dio dio = Dio(options);
Enter fullscreen mode Exit fullscreen mode

II. Making GET/POST Requests and Parameter Handling

1. Making GET Requests

GET requests are typically used to retrieve data from a server:

// Simple GET request
Future<void> fetchData() async {
  try {
    Response response = await dio.get('/users');
    print('Response data: ${response.data}');
    print('Status code: ${response.statusCode}');
  } catch (e) {
    print('Error: $e');
  }
}

// GET request with query parameters
Future<void> fetchUserData() async {
  try {
    // Method 1: Add parameters directly in the URL
    Response response1 = await dio.get('/users?userId=123&name=John');

    // Method 2: Use queryParameters
    Response response2 = await dio.get(
      '/users',
      queryParameters: {
        'userId': 123,
        'name': 'John',
      },
    );

    print('Response data: ${response2.data}');
  } catch (e) {
    print('Error: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Making POST Requests

POST requests are typically used to submit data to a server:

// Submit JSON data
Future<void> submitData() async {
  try {
    Response response = await dio.post(
      '/users',
      data: {
        'name': 'John Doe',
        'email': 'john@example.com',
        'age': 30,
      },
    );
    print('Response data: ${response.data}');
  } catch (e) {
    print('Error: $e');
  }
}

// Submit FormData
Future<void> uploadForm() async {
  try {
    FormData formData = FormData.fromMap({
      'name': 'John Doe',
      'avatar': await MultipartFile.fromFile(
        '/path/to/avatar.jpg',
        filename: 'avatar.jpg',
      ),
      'hobbies': [
        'reading',
        'sports',
      ],
    });

    Response response = await dio.post(
      '/user/profile',
      data: formData,
    );
    print('Response data: ${response.data}');
  } catch (e) {
    print('Error: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Custom Request Headers

You can set custom headers for individual requests:

Future<void> fetchWithHeaders() async {
  try {
    Response response = await dio.get(
      '/protected/data',
      options: Options(
        headers: {
          'Authorization': 'Bearer your_token_here',
          'Custom-Header': 'custom_value',
        },
      ),
    );
    print('Response data: ${response.data}');
  } catch (e) {
    print('Error: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Handling Request Timeouts

You can set timeout times for individual requests:

Future<void> fetchWithTimeout() async {
  try {
    Response response = await dio.get(
      '/slow/endpoint',
      options: Options(
        sendTimeout: const Duration(seconds: 2),
        receiveTimeout: const Duration(seconds: 5),
      ),
    );
    print('Response data: ${response.data}');
  } on DioException catch (e) {
    if (e.type == DioExceptionType.connectionTimeout) {
      print('Connection timeout');
    } else if (e.type == DioExceptionType.receiveTimeout) {
      print('Receive timeout');
    } else {
      print('Other error: $e');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

III. JSON Data Parsing

Data returned from servers is usually in JSON format. Flutter provides several ways to parse JSON data.

1. Manual JSON Parsing

Dart has a built-in dart:convert library that can manually parse JSON data:

import 'dart:convert';

// Assume the server returns the following JSON data:
// {
//   "id": 1,
//   "name": "John Doe",
//   "email": "john@example.com",
//   "age": 30,
//   "hobbies": ["reading", "sports"]
// }

// Create a model class
class User {
  final int id;
  final String name;
  final String email;
  final int age;
  final List<String> hobbies;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.age,
    required this.hobbies,
  });

  // Create a User instance from a JSON map
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
      age: json['age'],
      hobbies: List<String>.from(json['hobbies']),
    );
  }

  // Convert to a JSON map
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'age': age,
      'hobbies': hobbies,
    };
  }
}

// Parse JSON data
Future<void> parseUserJson() async {
  try {
    Response response = await dio.get('/users/1');

    // Convert JSON string to Map
    Map<String, dynamic> jsonData = response.data;

    // Convert to User object
    User user = User.fromJson(jsonData);

    print('User name: ${user.name}');
    print('User email: ${user.email}');
  } catch (e) {
    print('Error: $e');
  }
}

// Parse JSON array
Future<void> parseUsersJson() async {
  try {
    Response response = await dio.get('/users');

    // Convert JSON array to List
    List<dynamic> jsonList = response.data;

    // Convert to List of User objects
    List<User> users = jsonList.map((json) => User.fromJson(json)).toList();

    print('Number of users: ${users.length}');
    print('First user: ${users[0].name}');
  } catch (e) {
    print('Error: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode

Manual parsing is simple and straightforward without additional dependencies, but it becomes tedious and error-prone when dealing with complex JSON structures or many fields.

2. Using json_serializable for Automatic Code Generation

json_serializable is an automated source code generator that generates code for JSON serialization and deserialization, reducing the work of writing parsing code manually.

Install Dependencies

Add dependencies to pubspec.yaml:

dependencies:
  # ... other dependencies
  json_annotation: ^4.8.1  # Annotation package

dev_dependencies:
  # ... other dev dependencies
  build_runner: ^2.4.4     # Build tool
  json_serializable: ^6.7.1 # Code generator
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to install dependencies.

Create Model Classes

import 'package:json_annotation/json_annotation.dart';

// Generated code will be in user.g.dart
part 'user.g.dart';

@JsonSerializable()
class User {
  final int id;
  final String name;

  // Use @JsonKey annotation when JSON field name differs from class property
  @JsonKey(name: 'email_address')
  final String email;

  final int age;

  // Ignore this field in serialization/deserialization
  @JsonKey(ignore: true)
  final String? token;

  final List<String> hobbies;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.age,
    this.token,
    required this.hobbies,
  });

  // Create a User instance from a JSON map
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  // Convert to a JSON map
  Map<String, dynamic> toJson() => _$UserToJson(this);
}
Enter fullscreen mode Exit fullscreen mode

Generate Code

Run the following command in the project root directory to generate serialization code:

flutter pub run build_runner build
Enter fullscreen mode Exit fullscreen mode

For automatic code generation during development (when model classes change), use watch mode:

flutter pub run build_runner watch
Enter fullscreen mode Exit fullscreen mode

After successful execution, a user.g.dart file will be generated containing automatically generated serialization and deserialization code.

Using Generated Code

Future<void> useGeneratedCode() async {
  try {
    Response response = await dio.get('/users/1');

    // Parse using the automatically generated fromJson method
    User user = User.fromJson(response.data);

    print('User name: ${user.name}');
    print('User email: ${user.email}');

    // Serialization example
    Map<String, dynamic> userJson = user.toJson();
    print('Serialized user: $userJson');
  } catch (e) {
    print('Error: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode

The advantage of json_serializable is that it reduces the work of writing parsing code manually, improving code reliability and maintainability, especially for complex JSON structures.


IV. Network State Handling

In practical applications, network requests typically have several states: loading, success, and error. We need to display different UIs based on these states.

1. Create Network State Management Class

enum NetworkStatus { initial, loading, success, error }

class NetworkResult<T> {
  final NetworkStatus status;
  final T? data;
  final String? errorMessage;

  NetworkResult.initial()
      : status = NetworkStatus.initial,
        data = null,
        errorMessage = null;

  NetworkResult.loading()
      : status = NetworkStatus.loading,
        data = null,
        errorMessage = null;

  NetworkResult.success(this.data)
      : status = NetworkStatus.success,
        errorMessage = null;

  NetworkResult.error(this.errorMessage)
      : status = NetworkStatus.error,
        data = null;
}
Enter fullscreen mode Exit fullscreen mode

2. Display Different UIs Based on State

class DataScreen extends StatefulWidget {
  const DataScreen({super.key});

  @override
  State<DataScreen> createState() => _DataScreenState();
}

class _DataScreenState extends State<DataScreen> {
  final Dio _dio = Dio();
  NetworkResult<List<User>> _result = NetworkResult.initial();

  @override
  void initState() {
    super.initState();
    fetchUsers();
  }

  Future<void> fetchUsers() async {
    setState(() {
      _result = NetworkResult.loading();
    });

    try {
      Response response = await _dio.get('https://api.example.com/users');
      List<User> users = (response.data as List)
          .map((json) => User.fromJson(json))
          .toList();

      setState(() {
        _result = NetworkResult.success(users);
      });
    } catch (e) {
      setState(() {
        _result = NetworkResult.error(e.toString());
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User List')),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    switch (_result.status) {
      case NetworkStatus.initial:
        return const Center(child: Text('Tap to load data'));
      case NetworkStatus.loading:
        return const Center(child: CircularProgressIndicator());
      case NetworkStatus.success:
        return _buildUserList(_result.data!);
      case NetworkStatus.error:
        return Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Error: ${_result.errorMessage}'),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: fetchUsers,
                child: const Text('Retry'),
              ),
            ],
          ),
        );
    }
  }

  Widget _buildUserList(List<User> users) {
    return ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) {
        User user = users[index];
        return ListTile(
          title: Text(user.name),
          subtitle: Text(user.email),
          trailing: Text('Age: ${user.age}'),
        );
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

V. dio Interceptors

dio provides interceptor functionality that can handle requests before they're sent or responses after they're received, such as adding authentication tokens or handling errors.

1. Request Interceptor

// Add request interceptor
dio.interceptors.add(
  InterceptorsWrapper(
    onRequest: (options, handler) {
      // Do something before request is sent
      print('Request: ${options.method} ${options.uri}');

      // Add authentication token
      options.headers['Authorization'] = 'Bearer your_token_here';

      // Continue with the request
      return handler.next(options);

      // If you want to cancel the request, call handler.reject()
      // return handler.reject(DioException(requestOptions: options, type: DioExceptionType.cancel));
    },
  ),
);
Enter fullscreen mode Exit fullscreen mode

2. Response Interceptor

// Add response interceptor
dio.interceptors.add(
  InterceptorsWrapper(
    onResponse: (response, handler) {
      // Do something with response data
      print('Response: ${response.statusCode} ${response.data}');

      // Continue with the response
      return handler.next(response);
    },
  ),
);
Enter fullscreen mode Exit fullscreen mode

3. Error Interceptor

// Add error interceptor
dio.interceptors.add(
  InterceptorsWrapper(
    onError: (DioException e, handler) {
      // Handle error
      print('Error: ${e.message}');

      // Unified handling of 401 unauthorized errors
      if (e.response?.statusCode == 401) {
        // You can navigate to login page here
        print('Unauthorized, redirecting to login');
      }

      // Continue with the error
      return handler.next(e);

      // If you want to mask the error, return a successful response
      // return handler.resolve(Response(requestOptions: e.requestOptions, data: {}));
    },
  ),
);
Enter fullscreen mode Exit fullscreen mode

4. Log Interceptor

dio provides a built-in log interceptor for debugging:

import 'package:dio/io.dart';

dio.interceptors.add(LogInterceptor(
  request: true,  // Print request information
  requestHeader: true,  // Print request headers
  requestBody: true,  // Print request body
  responseHeader: true,  // Print response headers
  responseBody: true,  // Print response body
  error: true,  // Print error information
  logPrint: (object) {
    print('Dio Log: $object');
  },
));
Enter fullscreen mode Exit fullscreen mode

VI. Example: Fetch and Display News List with Open API

We'll implement a complete example that fetches and displays a news list using a public news API.

1. Create News Model Classes

import 'package:json_annotation/json_annotation.dart';

part 'news.g.dart';

@JsonSerializable()
class NewsArticle {
  @JsonKey(name: 'source')
  final NewsSource source;

  @JsonKey(name: 'author')
  final String? author;

  @JsonKey(name: 'title')
  final String title;

  @JsonKey(name: 'description')
  final String? description;

  @JsonKey(name: 'url')
  final String url;

  @JsonKey(name: 'urlToImage')
  final String? urlToImage;

  @JsonKey(name: 'publishedAt')
  final String publishedAt;

  @JsonKey(name: 'content')
  final String? content;

  NewsArticle({
    required this.source,
    this.author,
    required this.title,
    this.description,
    required this.url,
    this.urlToImage,
    required this.publishedAt,
    this.content,
  });

  factory NewsArticle.fromJson(Map<String, dynamic> json) =>
      _$NewsArticleFromJson(json);

  Map<String, dynamic> toJson() => _$NewsArticleToJson(this);
}

@JsonSerializable()
class NewsSource {
  @JsonKey(name: 'id')
  final String? id;

  @JsonKey(name: 'name')
  final String name;

  NewsSource({
    this.id,
    required this.name,
  });

  factory NewsSource.fromJson(Map<String, dynamic> json) =>
      _$NewsSourceFromJson(json);

  Map<String, dynamic> toJson() => _$NewsSourceToJson(this);
}

@JsonSerializable()
class NewsResponse {
  @JsonKey(name: 'status')
  final String status;

  @JsonKey(name: 'totalResults')
  final int totalResults;

  @JsonKey(name: 'articles')
  final List<NewsArticle> articles;

  NewsResponse({
    required this.status,
    required this.totalResults,
    required this.articles,
  });

  factory NewsResponse.fromJson(Map<String, dynamic> json) =>
      _$NewsResponseFromJson(json);

  Map<String, dynamic> toJson() => _$NewsResponseToJson(this);
}
Enter fullscreen mode Exit fullscreen mode

Run the code generation command:

flutter pub run build_runner build
Enter fullscreen mode Exit fullscreen mode

2. Create News Service Class

import 'package:dio/dio.dart';

class NewsService {
  final Dio _dio = Dio();
  final String _apiKey = 'your_news_api_key'; // Replace with your API Key
  final String _baseUrl = 'https://newsapi.org/v2';

  NewsService() {
    // Configure dio
    _dio.options.baseUrl = _baseUrl;
    _dio.options.connectTimeout = const Duration(seconds: 5);
    _dio.options.receiveTimeout = const Duration(seconds: 3);

    // Add log interceptor
    _dio.interceptors.add(LogInterceptor(responseBody: true));
  }

  // Get top headlines
  Future<NewsResponse> getTopHeadlines({String country = 'us'}) async {
    try {
      Response response = await _dio.get(
        '/top-headlines',
        queryParameters: {
          'country': country,
          'apiKey': _apiKey,
        },
      );
      return NewsResponse.fromJson(response.data);
    } on DioException catch (e) {
      print('News API error: ${e.message}');
      throw Exception('Failed to fetch news: ${e.message}');
    }
  }

  // Search news
  Future<NewsResponse> searchNews(String query) async {
    try {
      Response response = await _dio.get(
        '/everything',
        queryParameters: {
          'q': query,
          'apiKey': _apiKey,
        },
      );
      return NewsResponse.fromJson(response.data);
    } on DioException catch (e) {
      print('News API error: ${e.message}');
      throw Exception('Failed to search news: ${e.message}');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: You need to register at News API to get an API Key.

3. Implement News List Page

class NewsListScreen extends StatefulWidget {
  const NewsListScreen({super.key});

  @override
  State<NewsListScreen> createState() => _NewsListScreenState();
}

class _NewsListScreenState extends State<NewsListScreen> {
  final NewsService _newsService = NewsService();
  NetworkResult<List<NewsArticle>> _newsResult = NetworkResult.initial();
  final TextEditingController _searchController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _fetchTopHeadlines();
  }

  Future<void> _fetchTopHeadlines() async {
    setState(() {
      _newsResult = NetworkResult.loading();
    });

    try {
      NewsResponse response = await _newsService.getTopHeadlines();
      setState(() {
        _newsResult = NetworkResult.success(response.articles);
      });
    } catch (e) {
      setState(() {
        _newsResult = NetworkResult.error(e.toString());
      });
    }
  }

  Future<void> _searchNews() async {
    String query = _searchController.text.trim();
    if (query.isEmpty) return;

    setState(() {
      _newsResult = NetworkResult.loading();
    });

    try {
      NewsResponse response = await _newsService.searchNews(query);
      setState(() {
        _newsResult = NetworkResult.success(response.articles);
      });
    } catch (e) {
      setState(() {
        _newsResult = NetworkResult.error(e.toString());
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Latest News'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              controller: _searchController,
              decoration: InputDecoration(
                hintText: 'Search news...',
                suffixIcon: IconButton(
                  icon: const Icon(Icons.search),
                  onPressed: _searchNews,
                ),
                border: const OutlineInputBorder(),
              ),
              onSubmitted: (value) => _searchNews(),
            ),
          ),
          Expanded(
            child: _buildNewsContent(),
          ),
        ],
      ),
    );
  }

  Widget _buildNewsContent() {
    switch (_newsResult.status) {
      case NetworkStatus.initial:
        return const Center(child: Text('Loading news...'));
      case NetworkStatus.loading:
        return const Center(child: CircularProgressIndicator());
      case NetworkStatus.success:
        return _buildNewsList(_newsResult.data!);
      case NetworkStatus.error:
        return Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Error: ${_newsResult.errorMessage}'),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _fetchTopHeadlines,
                child: const Text('Retry'),
              ),
            ],
          ),
        );
    }
  }

  Widget _buildNewsList(List<NewsArticle> articles) {
    if (articles.isEmpty) {
      return const Center(child: Text('No news found'));
    }

    return ListView.builder(
      itemCount: articles.length,
      itemBuilder: (context, index) {
        NewsArticle article = articles[index];
        return Card(
          margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
          child: Column(
            children: [
              if (article.urlToImage != null)
                Image.network(
                  article.urlToImage!,
                  height: 180,
                  width: double.infinity,
                  fit: BoxFit.cover,
                  errorBuilder: (context, error, stackTrace) {
                    return Container(
                      height: 180,
                      color: Colors.grey[200],
                      child: const Center(child: Icon(Icons.image_not_supported)),
                    );
                  },
                ),
              Padding(
                padding: const EdgeInsets.all(12.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      article.source.name,
                      style: TextStyle(
                        color: Colors.grey[600],
                        fontSize: 12,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      article.title,
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    if (article.description != null)
                      Text(
                        article.description!,
                        style: const TextStyle(fontSize: 14),
                        maxLines: 3,
                        overflow: TextOverflow.ellipsis,
                      ),
                    const SizedBox(height: 8),
                    Text(
                      _formatDate(article.publishedAt),
                      style: TextStyle(
                        color: Colors.grey[600],
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  String _formatDate(String dateString) {
    DateTime date = DateTime.parse(dateString);
    return DateFormat.yMMMd().add_jm().format(date);
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: You need to add the intl dependency for date formatting. Add intl: ^0.18.1 to pubspec.yaml and run flutter pub get.

4. Configure Network Permissions

For Android, add network permissions in android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
Enter fullscreen mode Exit fullscreen mode

For iOS, add in ios/Runner/Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>
Enter fullscreen mode Exit fullscreen mode

VII. Network Request Best Practices

  1. Encapsulate network layer: Encapsulate network request logic in dedicated service classes, separating it from the UI layer to improve code reusability and maintainability.
  2. Unified error handling: Use interceptors to handle network errors uniformly, such as timeouts, no network, and authentication failures.
  3. Manage request states properly: Clearly display loading, success, error, and other states to provide a good user experience.
  4. Data caching: Implement local caching for infrequently changing data to reduce network requests and improve app performance.
  5. Request cancellation: Cancel unfinished network requests when a page is destroyed to avoid memory leaks and unnecessary resource consumption.
  6. Image handling: Use libraries like cached_network_image to handle network images, implementing caching and placeholder functionality.
  7. Avoid complex tasks on UI thread: Ensure network requests execute on asynchronous threads to avoid blocking the UI.
  8. Add logging: Add detailed network logs in the development environment for debugging; disable or simplify logs in the production environment.

Top comments (0)