🌙
☀️ Dark

Volume 5: Networking & Data

Intermediate ⏱ 22 min read

Volume 5: Networking, APIs & Data

Learning Objectives

Why Does This Exist?

An app that doesn't talk to the internet is usually just a glorified calculator. You need to fetch user profiles, upload images, and save preferences. However, the network is fundamentally unreliable. The user might drive into a tunnel. The server might crash. If your app expects perfect network conditions, it will crash constantly.

The Problem Before the Solution

In older systems, fetching data meant writing raw socket connections or dealing with complex XML parsers. Today, we use HTTP and JSON. But JSON is dangerous. JSON is a dynamic map of Strings to anything (Map<String, dynamic>). If the backend suddenly changes the "age" field from an integer 25 to a string "25", and your Dart code is expecting an integer, the app will throw a fatal type error and crash the screen.

Mental Model: The Border Patrol

Think of your app as a secure country. The internet is the wild outside world. JSON is a mysterious package arriving at the border. You must never let raw JSON freely roam inside your app.

Instead, you build a Border Patrol Station (a Factory Constructor). The station opens the JSON box, checks if "name" is actually a String, checks if "age" is actually an integer, and if everything is safe, it repackages the data into a strongly-typed Dart Object (like a User class) and lets it into the app. If the data is corrupted, the Border Patrol stops it and raises a controlled alert, rather than letting it crash the entire UI.

Syntax: JSON Serialization

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

class User {
  final int id;
  final String name;

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

  // The Border Patrol: Map -> Dart Object
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      // We safely cast types. If it fails here, we catch the error 
      // BEFORE it reaches the UI.
      id: json['id'] as int,
      name: json['name'] as String,
    );
  }
}

// Fetching the data
Future<User> fetchUser() async {
  final response = await http.get(Uri.parse('https://api.example.com/user/1'));
  
  if (response.statusCode == 200) {
    // 1. Convert string to Map (JSON)
    final jsonMap = jsonDecode(response.body);
    // 2. Pass to Border Patrol
    return User.fromJson(jsonMap);
  } else {
    throw Exception('Failed to load user');
  }
}

Internal Working: The Event Loop

Dart is single-threaded. How does it wait for an HTTP request without freezing the UI? The Event Loop. When you call http.get(), Dart hands the network request off to the operating system, puts a sticky note on the Event Queue saying "Check back later", and immediately goes back to painting the UI at 60fps. When the OS finishes the download, it triggers the sticky note, and Dart executes the rest of your await code.

Local Persistence

For small data (tokens, theme preferences), we use SharedPreferences or FlutterSecureStorage (for passwords). For complex, relational data, we use local databases like SQLite or Isar.

dart
// Saving a token
final prefs = await SharedPreferences.getInstance();
await prefs.setString('jwt_token', 'abc123secret');

// Reading it later
final token = prefs.getString('jwt_token'); // returns null if not found

Break It & Debug It

The Bug: You fetch a list of 1000 items from an API. You try to parse the JSON, and the app freezes for 2 seconds. The animation stutters horribly.

The Reason: While the HTTP download happens in the background, jsonDecode() is CPU-intensive and runs synchronously on the main thread! Parsing 1000 items blocks the UI thread from painting the next frame.

The Fix: Use compute() or an Isolate. This spawns a completely separate worker thread to do the heavy JSON parsing math, leaving the main thread perfectly free to render smooth animations.

🔧 Mini Project ⏱ 20 min

Goal: The Chuck Norris API Client

Using the Dio package, fetch a random Chuck Norris joke from https://api.chucknorris.io/jokes/random. Parse the JSON response. Display the joke with a refresh button. Show a loading indicator while fetching. Show an error card with a retry button if the request fails (test by disabling WiFi).

💡 See One Approach (Mini Project)

One valid solution — yours may differ.

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

void main() => runApp(const MaterialApp(home: JokeScreen()));

class JokeScreen extends StatefulWidget {
  const JokeScreen({super.key});
  @override
  State<JokeScreen> createState() => _JokeScreenState();
}

class _JokeScreenState extends State<JokeScreen> {
  final _dio = Dio();
  String? _joke;
  bool _loading = false;
  String? _error;

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

  Future<void> _fetchJoke() async {
    setState(() { _loading = true; _error = null; });
    try {
      final res = await _dio.get('https://api.chucknorris.io/jokes/random');
      setState(() { _joke = res.data['value'] as String; _loading = false; });
    } on DioException catch (e) {
      setState(() {
        _error = e.type == DioExceptionType.connectionError
            ? 'No internet connection'
            : 'Error: ${e.message}';
        _loading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Chuck Norris Facts')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Center(
          child: _loading
            ? const CircularProgressIndicator()
            : _error != null
              ? Column(mainAxisSize: MainAxisSize.min, children: [
                  const Icon(Icons.wifi_off, size: 64, color: Colors.red),
                  const SizedBox(height: 16),
                  Text(_error!, textAlign: TextAlign.center),
                  const SizedBox(height: 16),
                  ElevatedButton.icon(onPressed: _fetchJoke, icon: const Icon(Icons.refresh), label: const Text('Retry')),
                ])
              : Column(mainAxisSize: MainAxisSize.min, children: [
                  const Text('💪', style: TextStyle(fontSize: 64)),
                  const SizedBox(height: 24),
                  Text(_joke ?? '', style: const TextStyle(fontSize: 18, height: 1.6), textAlign: TextAlign.center),
                  const SizedBox(height: 32),
                  FilledButton.icon(onPressed: _fetchJoke, icon: const Icon(Icons.refresh), label: const Text('New Joke')),
                ]),
        ),
      ),
    );
  }
}

🏗 Bigger Project ⏱ 1.5 hrs

Goal: Offline-First GitHub User Search

Build a GitHub user search app. Search bar that calls https://api.github.com/search/users?q={query}. Show avatar, login, and type. On tap, show detail screen with https://api.github.com/users/{login} data (name, bio, followers, public repos). Cache the last 20 search results in memory. Show cached results immediately while re-fetching. Handle rate limiting (403 response) with a friendly message.

💡 See One Approach (Bigger Project)

One valid solution — yours may differ.

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

void main() => runApp(const MaterialApp(home: GithubSearchScreen()));

class GitHubUser {
  final String login, avatarUrl, type;
  GitHubUser({required this.login, required this.avatarUrl, required this.type});
  factory GitHubUser.fromJson(Map j) => GitHubUser(
    login: j['login'], avatarUrl: j['avatar_url'], type: j['type'],
  );
}

class GithubSearchScreen extends StatefulWidget {
  const GithubSearchScreen({super.key});
  @override
  State<GithubSearchScreen> createState() => _GithubSearchScreenState();
}

class _GithubSearchScreenState extends State<GithubSearchScreen> {
  final _dio = Dio();
  final _ctrl = TextEditingController();
  final Map<String, List<GitHubUser>> _cache = {};
  List<GitHubUser> _results = [];
  bool _loading = false;
  String? _error;

  @override
  void dispose() { _ctrl.dispose(); super.dispose(); }

  Future<void> _search(String query) async {
    if (query.isEmpty) return;
    // Show cache immediately
    if (_cache.containsKey(query)) setState(() => _results = _cache[query]!);
    setState(() { _loading = true; _error = null; });
    try {
      final res = await _dio.get(
        'https://api.github.com/search/users',
        queryParameters: {'q': query, 'per_page': 20},
      );
      final users = (res.data['items'] as List).map((j) => GitHubUser.fromJson(j)).toList();
      _cache[query] = users;
      setState(() { _results = users; _loading = false; });
    } on DioException catch (e) {
      setState(() {
        _loading = false;
        _error = e.response?.statusCode == 403
            ? 'Rate limit exceeded. Wait a minute and try again.'
            : 'Network error: ${e.message}';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('GitHub Search')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: SearchBar(
              controller: _ctrl,
              hintText: 'Search GitHub users...',
              onSubmitted: _search,
              trailing: [if (_loading) const Padding(padding: EdgeInsets.all(12), child: SizedBox(width:20, height:20, child: CircularProgressIndicator(strokeWidth: 2)))],
            ),
          ),
          if (_error != null) Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Card(color: Colors.red.shade50, child: Padding(
              padding: const EdgeInsets.all(12),
              child: Text(_error!, style: const TextStyle(color: Colors.red)),
            )),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _results.length,
              itemBuilder: (_, i) => ListTile(
                leading: CircleAvatar(backgroundImage: NetworkImage(_results[i].avatarUrl)),
                title: Text(_results[i].login),
                subtitle: Text(_results[i].type),
                onTap: () => Navigator.push(context, MaterialPageRoute(
                  builder: (_) => UserDetailScreen(login: _results[i].login, dio: _dio),
                )),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class UserDetailScreen extends StatelessWidget {
  final String login;
  final Dio dio;
  const UserDetailScreen({super.key, required this.login, required this.dio});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(login)),
      body: FutureBuilder(
        future: dio.get('https://api.github.com/users/$login'),
        builder: (_, snapshot) {
          if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
          final d = snapshot.data!.data as Map;
          return ListView(padding: const EdgeInsets.all(24), children: [
            CircleAvatar(radius: 48, backgroundImage: NetworkImage(d['avatar_url'])),
            const SizedBox(height: 16),
            Text(d['name'] ?? login, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
            if (d['bio'] != null) Text(d['bio'], textAlign: TextAlign.center, style: const TextStyle(color: Colors.grey)),
            const SizedBox(height: 24),
            Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
              _Stat('Repos', '${d['public_repos']}'),
              _Stat('Followers', '${d['followers']}'),
              _Stat('Following', '${d['following']}'),
            ]),
          ]);
        },
      ),
    );
  }
}

class _Stat extends StatelessWidget {
  final String label, value;
  const _Stat(this.label, this.value);
  @override
  Widget build(BuildContext context) => Column(children: [
    Text(value, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
    Text(label, style: const TextStyle(color: Colors.grey)),
  ]);
}

🎯 Interview Questions

Answer these before revealing. These appear in real Flutter/Dart interviews.

🔍 Easy: What is the difference between `http` and `dio` packages in Flutter?

`http` is Flutter's official, minimal HTTP client — simple API but lacks built-in features. `dio` is a third-party package that adds: interceptors (for logging, auth token injection), request cancellation, form data, file download with progress, timeout configuration, and a cleaner error handling API (DioException with typed error types like connectionTimeout, badResponse). For production apps, Dio is almost always preferred.

🔍 Medium: What is an interceptor and how would you use one to automatically add an auth token to every request?

An interceptor is middleware that runs before a request is sent or after a response is received. In Dio: `dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) { options.headers['Authorization'] = 'Bearer ${getToken()}'; handler.next(options); }))`. This runs before every request, injecting the token. You can also intercept responses to catch 401 errors and automatically refresh the token before retrying the original request — a production pattern called token refresh interceptor.

🔍 Hard: Explain an offline-first architecture for a Flutter app. How do you handle sync conflicts?

Offline-first means the app reads from and writes to local storage first, then syncs with the backend. Architecture: 1. User action → write to SQLite/Drift immediately → UI updates instantly. 2. Background sync service (periodic or on connectivity change) pushes local changes to the server. 3. Server responses update local DB. Conflict resolution strategies: Last-Write-Wins (use timestamps — simple but can lose data), Server-Wins (server is source of truth — safe for most apps), Client-Wins (user's change always takes precedence), CRDT (Conflict-free Replicated Data Types — mathematical merge that works for collaborative editing). In Flutter: use `connectivity_plus` to detect connectivity changes, `WorkManager` for background sync, and a `sync_status` column in SQLite to track which records are pending upload.

✅ I can fetch, parse, and cache API data using Dio, handle errors gracefully, and design offline-first data flows.