0
0
Fluttermobile~20 mins

Mock dependencies in Flutter - Mini App: Build & Ship

Choose your learning style9 modes available
Build: User Profile Screen
This screen shows a user's profile information fetched from a service. We want to build it so that the data fetching service can be replaced with a mock for testing.
Target UI
-------------------------
|      User Profile      |
|-----------------------|
| Name: Loading...       |
| Email: Loading...      |
|                       |
| [Refresh]             |
-------------------------
Create a UserProfileScreen that shows user name and email.
Use a UserService to fetch user data asynchronously.
Inject the UserService dependency so it can be replaced.
Add a Refresh button to reload user data.
Create a mock UserService that returns fixed user data instantly.
Demonstrate switching between real and mock UserService.
Starter Code
Flutter
import 'package:flutter/material.dart';

class User {
  final String name;
  final String email;
  User(this.name, this.email);
}

abstract class UserService {
  Future<User> fetchUser();
}

class RealUserService implements UserService {
  @override
  Future<User> fetchUser() async {
    // Simulate network delay
    await Future.delayed(Duration(seconds: 2));
    return User('Alice', 'alice@example.com');
  }
}

class UserProfileScreen extends StatefulWidget {
  final UserService userService;
  const UserProfileScreen({super.key, required this.userService});

  @override
  State<UserProfileScreen> createState() => _UserProfileScreenState();
}

class _UserProfileScreenState extends State<UserProfileScreen> {
  User? user;
  bool loading = true;

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

  Future<void> _loadUser() async {
    setState(() {
      loading = true;
    });
    // TODO: Use widget.userService to fetch user
    // TODO: Update user and loading state
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('User Profile')),
      body: Center(
        child: loading
            ? CircularProgressIndicator()
            : Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text('Name: ${user?.name ?? ''}'),
                  Text('Email: ${user?.email ?? ''}'),
                  SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: _loadUser,
                    child: Text('Refresh'),
                  ),
                ],
              ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: UserProfileScreen(userService: RealUserService()),
  ));
}
Task 1
Task 2
Task 3
Solution
Flutter
import 'package:flutter/material.dart';

class User {
  final String name;
  final String email;
  User(this.name, this.email);
}

abstract class UserService {
  Future<User> fetchUser();
}

class RealUserService implements UserService {
  @override
  Future<User> fetchUser() async {
    await Future.delayed(Duration(seconds: 2));
    return User('Alice', 'alice@example.com');
  }
}

class MockUserService implements UserService {
  @override
  Future<User> fetchUser() async {
    // Return instantly with fixed data
    return User('Mock Bob', 'mockbob@example.com');
  }
}

class UserProfileScreen extends StatefulWidget {
  final UserService userService;
  const UserProfileScreen({super.key, required this.userService});

  @override
  State<UserProfileScreen> createState() => _UserProfileScreenState();
}

class _UserProfileScreenState extends State<UserProfileScreen> {
  User? user;
  bool loading = true;

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

  Future<void> _loadUser() async {
    setState(() {
      loading = true;
    });
    User fetchedUser = await widget.userService.fetchUser();
    setState(() {
      user = fetchedUser;
      loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('User Profile')),
      body: Center(
        child: loading
            ? CircularProgressIndicator()
            : Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text('Name: ${user?.name ?? ''}'),
                  Text('Email: ${user?.email ?? ''}'),
                  SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: _loadUser,
                    child: Text('Refresh'),
                  ),
                ],
              ),
      ),
    );
  }
}

void main() {
  // Switch between RealUserService and MockUserService here
  bool useMock = true;
  runApp(MaterialApp(
    home: UserProfileScreen(
      userService: useMock ? MockUserService() : RealUserService(),
    ),
  ));
}

We created a MockUserService that implements UserService and returns fixed user data immediately. This allows us to test the UI without waiting for network delays.

In UserProfileScreen, we use the injected userService to fetch user data asynchronously in _loadUser(). We update the loading state to show a spinner while fetching.

The main() function demonstrates switching between the real and mock services by toggling the useMock boolean. This shows how dependency injection helps us replace real services with mocks easily.

Final Result
Completed Screen
-------------------------
|      User Profile      |
|-----------------------|
| Name: Mock Bob         |
| Email: mockbob@example.com |
|                       |
| [Refresh]             |
-------------------------
When the screen loads, it shows a spinner for a moment then displays the user name and email from the service.
Pressing the Refresh button reloads the user data and updates the display.
Switching the 'useMock' flag in main() changes the displayed user data between real and mock.
Stretch Goal
Add a toggle switch in the app bar to switch between RealUserService and MockUserService at runtime.
💡 Hint
Use a StatefulWidget to hold the toggle state and rebuild UserProfileScreen with the selected service when toggled.