SkillByAIOpen interactive version →

Lesson 19 / 25

HTTP, JSON and FutureBuilder

Fetching and modelling remote data.

From JSON to typed models

The http package offers simple get/post calls; dio adds interceptors, cancellation and timeouts. Responses arrive as strings: jsonDecode from dart:convert turns them into Map<String, dynamic>, which you convert into model classes with fromJson factories so the rest of the app is typed. For larger models, json_serializable generates that code via build_runner. In the UI, FutureBuilder shows loading, error and data states from a Future; create the Future once (in initState or your state layer), not inside build, or it will refetch on every rebuild. Always check status codes and handle timeouts and offline errors.

Network, disk and native code

Apps fetch JSON over HTTP, persist data locally and call native APIs through plugins and platform channels.

Figure 7.1 — Network, local storage and native platform access.

A model, a request and FutureBuilder

http plus a hand-written fromJson.

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

class Todo {
  final int id;
  final String title;
  final bool done;
  const Todo({required this.id, required this.title, required this.done});

  factory Todo.fromJson(Map<String, dynamic> json) => Todo(
        id: json['id'] as int,
        title: json['title'] as String,
        done: json['completed'] as bool,
      );
}

Future<List<Todo>> fetchTodos() async {
  final res = await http
      .get(Uri.parse('https://api.example.com/todos'))
      .timeout(const Duration(seconds: 10));
  if (res.statusCode != 200) {
    throw Exception('Failed: ${res.statusCode}');
  }
  final list = jsonDecode(res.body) as List<dynamic>;
  return list.map((e) => Todo.fromJson(e as Map<String, dynamic>)).toList();
}

class _TodosPageState extends State<TodosPage> {
  late final Future<List<Todo>> _todos = fetchTodos(); // created once

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Todo>>(
      future: _todos,
      builder: (context, snap) {
        if (snap.connectionState != ConnectionState.done) {
          return const Center(child: CircularProgressIndicator());
        }
        if (snap.hasError) return Text('Error: ${snap.error}');
        final todos = snap.data!;
        return ListView.builder(
          itemCount: todos.length,
          itemBuilder: (_, i) => Text(todos[i].title),
        );
      },
    );
  }
}

Generate the boilerplate

With json_serializable, annotate the class with @JsonSerializable(), add part 'todo.g.dart'; and run dart run build_runner build to generate _$TodoFromJson and _$TodoToJson.

Quick check: Why should the Future for a FutureBuilder not be created inside build()?

  • It would run on a different isolate
  • FutureBuilder only accepts Streams
  • Futures cannot be created in widgets
  • build can run many times, so the request would be repeated on every rebuild
Answer

build can run many times, so the request would be repeated on every rebuild — Create it once and pass the same Future on each build.