## Learning Objectives

By the end of this chapter, you will be able to:

- Build a complete Flutter app that fetches data from a REST API
- Create proper data models with JSON serialization
- Implement error handling and loading states
- Structure a Flutter project with proper separation of concerns
- Use FutureBuilder and ListView to display dynamic data
- Implement pull-to-refresh and search functionality

1

## Project Overview

### Project Goal

Build a "Users Directory" app that fetches user data from JSONPlaceholder API (https://jsonplaceholder.typicode.com/users), displays it in a list, and allows users to view details and search.

### Features to Implement

- Fetch users from API on app launch
- Display users in a scrollable list
- Show user details in a detail screen
- Pull-to-refresh functionality
- Search/filter users by name
- Loading and error states

2

## Project Setup

### Step 1: Create Project

    flutter create users_directory
    cd users_directory

### Step 2: Add Dependencies

Update `pubspec.yaml`:

### pubspec.yaml

    name: users_directory
    description: A Flutter app that fetches and displays users from API
    version: 1.0.0+1

    environment:
      sdk: '>=3.0.0 <4.0.0'

    dependencies:
      flutter:
        sdk: flutter
      http: ^1.1.0
      cupertino_icons: ^1.0.6

    dev_dependencies:
      flutter_test:
        sdk: flutter
      flutter_lints: ^3.0.0

    flutter:
      uses-material-design: true

### Step 3: Install Dependencies

    flutter pub get

3

## Project Structure

### Suggested Folder Layout

    lib/
      main.dart
      models/
        user.dart
        address.dart
        company.dart
      services/
        api_service.dart
      screens/
        home_screen.dart
        user_detail_screen.dart
      widgets/
        user_card.dart
        loading_widget.dart
        error_widget.dart
      utils/
        constants.dart

4

## Creating Data Models

### User Model

Create `lib/models/user.dart`:

### lib/models/user.dart

    class User {
      final int id;
      final String name;
      final String username;
      final String email;
      final String phone;
      final String website;
      final Address address;
      final Company company;

      User({
        required this.id,
        required this.name,
        required this.username,
        required this.email,
        required this.phone,
        required this.website,
        required this.address,
        required this.company,
      });

      factory User.fromJson(Map json) {
        return User(
          id: json['id'] as int,
          name: json['name'] as String,
          username: json['username'] as String,
          email: json['email'] as String,
          phone: json['phone'] as String,
          website: json['website'] as String,
          address: Address.fromJson(json['address'] as Map),
          company: Company.fromJson(json['company'] as Map),
        );
      }

      Map toJson() {
        return {
          'id': id,
          'name': name,
          'username': username,
          'email': email,
          'phone': phone,
          'website': website,
          'address': address.toJson(),
          'company': company.toJson(),
        };
      }
    }

    class Address {
      final String street;
      final String suite;
      final String city;
      final String zipcode;
      final Geo geo;

      Address({
        required this.street,
        required this.suite,
        required this.city,
        required this.zipcode,
        required this.geo,
      });

      factory Address.fromJson(Map json) {
        return Address(
          street: json['street'] as String,
          suite: json['suite'] as String,
          city: json['city'] as String,
          zipcode: json['zipcode'] as String,
          geo: Geo.fromJson(json['geo'] as Map),
        );
      }

      Map toJson() {
        return {
          'street': street,
          'suite': suite,
          'city': city,
          'zipcode': zipcode,
          'geo': geo.toJson(),
        };
      }

      String get fullAddress => '$street, $suite, $city $zipcode';
    }

    class Geo {
      final String lat;
      final String lng;

      Geo({required this.lat, required this.lng});

      factory Geo.fromJson(Map json) {
        return Geo(
          lat: json['lat'] as String,
          lng: json['lng'] as String,
        );
      }

      Map toJson() {
        return {
          'lat': lat,
          'lng': lng,
        };
      }
    }

    class Company {
      final String name;
      final String catchPhrase;
      final String bs;

      Company({
        required this.name,
        required this.catchPhrase,
        required this.bs,
      });

      factory Company.fromJson(Map json) {
        return Company(
          name: json['name'] as String,
          catchPhrase: json['catchPhrase'] as String,
          bs: json['bs'] as String,
        );
      }

      Map toJson() {
        return {
          'name': name,
          'catchPhrase': catchPhrase,
          'bs': bs,
        };
      }
    }

5

## Creating API Service

### API Service Class

Create `lib/services/api_service.dart`:

### lib/services/api_service.dart

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

    class ApiService {
      static const String baseUrl = 'https://jsonplaceholder.typicode.com';

      Future> fetchUsers() async {
        try {
          final response = await http.get(
            Uri.parse('$baseUrl/users'),
          ).timeout(Duration(seconds: 10));

          if (response.statusCode == 200) {
            final List jsonList = jsonDecode(response.body);
            return jsonList.map((json) => User.fromJson(json)).toList();
          } else {
            throw Exception('Failed to load users: ${response.statusCode}');
          }
        } catch (e) {
          throw Exception('Error fetching users: $e');
        }
      }

      Future fetchUserById(int id) async {
        try {
          final response = await http.get(
            Uri.parse('$baseUrl/users/$id'),
          ).timeout(Duration(seconds: 10));

          if (response.statusCode == 200) {
            final json = jsonDecode(response.body);
            return User.fromJson(json);
          } else {
            throw Exception('Failed to load user: ${response.statusCode}');
          }
        } catch (e) {
          throw Exception('Error fetching user: $e');
        }
      }
    }

6

## Creating Constants

### lib/utils/constants.dart

    class AppConstants {
      static const String appName = 'Users Directory';
      static const String apiBaseUrl = 'https://jsonplaceholder.typicode.com';
    }

7

## Creating Reusable Widgets

### Loading Widget

Create `lib/widgets/loading_widget.dart`:

### lib/widgets/loading_widget.dart

    import 'package:flutter/material.dart';

    class LoadingWidget extends StatelessWidget {
      const LoadingWidget({Key? key}) : super(key: key);

      @override
      Widget build(BuildContext context) {
        return Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CircularProgressIndicator(),
              SizedBox(height: 16),
              Text(
                'Loading users...',
                style: TextStyle(fontSize: 16),
              ),
            ],
          ),
        );
      }
    }

### Error Widget

Create `lib/widgets/error_widget.dart`:

### lib/widgets/error_widget.dart

    import 'package:flutter/material.dart';

    class ErrorDisplayWidget extends StatelessWidget {
      final String message;
      final VoidCallback? onRetry;

      const ErrorDisplayWidget({
        Key? key,
        required this.message,
        this.onRetry,
      }) : super(key: key);

      @override
      Widget build(BuildContext context) {
        return Center(
          child: Padding(
            padding: EdgeInsets.all(24),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(
                  Icons.error_outline,
                  size: 64,
                  color: Colors.red,
                ),
                SizedBox(height: 16),
                Text(
                  'Error',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                SizedBox(height: 8),
                Text(
                  message,
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 14),
                ),
                if (onRetry != null) ...[
                  SizedBox(height: 24),
                  ElevatedButton.icon(
                    onPressed: onRetry,
                    icon: Icon(Icons.refresh),
                    label: Text('Retry'),
                  ),
                ],
              ],
            ),
          ),
        );
      }
    }

### User Card Widget

Create `lib/widgets/user_card.dart`:

### lib/widgets/user_card.dart

    import 'package:flutter/material.dart';
    import '../models/user.dart';

    class UserCard extends StatelessWidget {
      final User user;
      final VoidCallback onTap;

      const UserCard({
        Key? key,
        required this.user,
        required this.onTap,
      }) : super(key: key);

      @override
      Widget build(BuildContext context) {
        return Card(
          margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: ListTile(
            leading: CircleAvatar(
              backgroundColor: Theme.of(context).primaryColor,
              child: Text(
                user.name[0].toUpperCase(),
                style: TextStyle(color: Colors.white),
              ),
            ),
            title: Text(
              user.name,
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            subtitle: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                SizedBox(height: 4),
                Text(user.email),
                Text(user.phone),
              ],
            ),
            trailing: Icon(Icons.chevron_right),
            onTap: onTap,
          ),
        );
      }
    }

8

## Creating Home Screen

### lib/screens/home_screen.dart

    import 'package:flutter/material.dart';
    import '../models/user.dart';
    import '../services/api_service.dart';
    import '../widgets/user_card.dart';
    import '../widgets/loading_widget.dart';
    import '../widgets/error_widget.dart';
    import 'user_detail_screen.dart';

    class HomeScreen extends StatefulWidget {
      @override
      _HomeScreenState createState() => _HomeScreenState();
    }

    class _HomeScreenState extends State {
      final ApiService _apiService = ApiService();
      List _users = [];
      List _filteredUsers = [];
      bool _isLoading = false;
      String? _error;
      final TextEditingController _searchController = TextEditingController();

      @override
      void initState() {
        super.initState();
        _loadUsers();
        _searchController.addListener(_filterUsers);
      }

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

      Future _loadUsers() async {
        setState(() {
          _isLoading = true;
          _error = null;
        });

        try {
          final users = await _apiService.fetchUsers();
          setState(() {
            _users = users;
            _filteredUsers = users;
            _isLoading = false;
          });
        } catch (e) {
          setState(() {
            _error = e.toString();
            _isLoading = false;
          });
        }
      }

      void _filterUsers() {
        final query = _searchController.text.toLowerCase();
        setState(() {
          if (query.isEmpty) {
            _filteredUsers = _users;
          } else {
            _filteredUsers = _users.where((user) {
              return user.name.toLowerCase().contains(query) ||
                     user.email.toLowerCase().contains(query);
            }).toList();
          }
        });
      }

      void _navigateToDetail(User user) {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => UserDetailScreen(user: user),
          ),
        );
      }

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Users Directory'),
            actions: [
              if (!_isLoading && _error == null)
                IconButton(
                  icon: Icon(Icons.refresh),
                  onPressed: _loadUsers,
                  tooltip: 'Refresh',
                ),
            ],
          ),
          body: Column(
            children: [
              // Search bar
              if (!_isLoading && _error == null)
                Padding(
                  padding: EdgeInsets.all(16),
                  child: TextField(
                    controller: _searchController,
                    decoration: InputDecoration(
                      hintText: 'Search users by name or email...',
                      prefixIcon: Icon(Icons.search),
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(12),
                      ),
                    ),
                  ),
                ),
              // Content
              Expanded(
                child: _buildBody(),
              ),
            ],
          ),
        );
      }

      Widget _buildBody() {
        if (_isLoading) {
          return LoadingWidget();
        }

        if (_error != null) {
          return ErrorDisplayWidget(
            message: _error!,
            onRetry: _loadUsers,
          );
        }

        if (_filteredUsers.isEmpty) {
          return Center(
            child: Text(
              _searchController.text.isEmpty
                  ? 'No users found'
                  : 'No users match your search',
              style: TextStyle(fontSize: 16),
            ),
          );
        }

        return RefreshIndicator(
          onRefresh: _loadUsers,
          child: ListView.builder(
            itemCount: _filteredUsers.length,
            itemBuilder: (context, index) {
              final user = _filteredUsers[index];
              return UserCard(
                user: user,
                onTap: () => _navigateToDetail(user),
              );
            },
          ),
        );
      }
    }

9

## Creating User Detail Screen

### lib/screens/user_detail_screen.dart

    import 'package:flutter/material.dart';
    import '../models/user.dart';

    class UserDetailScreen extends StatelessWidget {
      final User user;

      const UserDetailScreen({Key? key, required this.user}) : super(key: key);

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(user.name),
          ),
          body: SingleChildScrollView(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _buildSection(
                  'Personal Information',
                  [
                    _buildInfoRow('Name', user.name),
                    _buildInfoRow('Username', user.username),
                    _buildInfoRow('Email', user.email),
                    _buildInfoRow('Phone', user.phone),
                    _buildInfoRow('Website', user.website),
                  ],
                ),
                SizedBox(height: 24),
                _buildSection(
                  'Address',
                  [
                    _buildInfoRow('Street', user.address.street),
                    _buildInfoRow('Suite', user.address.suite),
                    _buildInfoRow('City', user.address.city),
                    _buildInfoRow('Zipcode', user.address.zipcode),
                    _buildInfoRow('Full Address', user.address.fullAddress),
                    _buildInfoRow('Coordinates', '${user.address.geo.lat}, ${user.address.geo.lng}'),
                  ],
                ),
                SizedBox(height: 24),
                _buildSection(
                  'Company',
                  [
                    _buildInfoRow('Name', user.company.name),
                    _buildInfoRow('Catch Phrase', user.company.catchPhrase),
                    _buildInfoRow('Business', user.company.bs),
                  ],
                ),
              ],
            ),
          ),
        );
      }

      Widget _buildSection(String title, List children) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(height: 12),
            Card(
              child: Padding(
                padding: EdgeInsets.all(16),
                child: Column(
                  children: children,
                ),
              ),
            ),
          ],
        );
      }

      Widget _buildInfoRow(String label, String value) {
        return Padding(
          padding: EdgeInsets.symmetric(vertical: 8),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              SizedBox(
                width: 120,
                child: Text(
                  '$label:',
                  style: TextStyle(
                    fontWeight: FontWeight.w600,
                    color: Colors.grey[700],
                  ),
                ),
              ),
              Expanded(
                child: Text(
                  value,
                  style: TextStyle(fontSize: 14),
                ),
              ),
            ],
          ),
        );
      }
    }

10

## Updating Main.dart

### lib/main.dart

    import 'package:flutter/material.dart';
    import 'screens/home_screen.dart';

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

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Users Directory',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            primarySwatch: Colors.blue,
            useMaterial3: true,
          ),
          home: HomeScreen(),
        );
      }
    }

11

## Running the App

### Commands

    # Run the app
    flutter run

    # Run in release mode
    flutter run --release

    # Build APK
    flutter build apk

12

## Project Checklist

### Implementation Checklist

- ✅ Project created and dependencies added
- ✅ Folder structure organized
- ✅ User model with nested Address and Company models
- ✅ API service with error handling
- ✅ Loading widget for async states
- ✅ Error widget with retry functionality
- ✅ User card widget for list items
- ✅ Home screen with search and pull-to-refresh
- ✅ User detail screen with complete information
- ✅ Main.dart configured

13

## Enhancement Ideas

### Optional Enhancements

- Add local caching using SharedPreferences
- Implement pagination for large datasets
- Add favorite users functionality
- Implement dark mode support
- Add user avatar images
- Create a map view showing user locations
- Add sorting options (by name, email, etc.)

14

## Exercises

### 1. Complete Implementation

Implement the entire project following the structure and code provided. Test all features including search, pull-to-refresh, and navigation to detail screen.

### 2. Add Caching

Implement local caching using SharedPreferences. Save fetched users locally and load them on app start while fetching fresh data in the background.

### 3. Add Favorites

Add a favorite button to each user card. Store favorite user IDs locally and show a separate favorites screen with only favorited users.