Monday, June 5, 2023

Building a Clean Architecture: Flutter Demo API Integration for Seamless App Development


 Explore the power of clean architecture in Flutter as you seamlessly integrate a demo API into your app. Follow best practices to maintain a modular project structure, separate business logic from UI components, and ensure efficient data flow. By leveraging clean architecture principles, you can achieve a well-organized, scalable, and maintainable codebase while integrating API data effortlessly into your Flutter app.

Here's an example of integrating a Flutter app with a demo API using clean architecture principles:

First, let's define the project structure with clean architecture in mind:

- lib
  - core
    - errors
    - models
    - repositories
    - usecases
  - data
    - datasources
    - repositories
  - presentation
    - screens
    - widgets
  - main.dart

Define the core module:

  • errors: Contains custom error classes.
  • models: Contains the data models used in the app.
  • repositories: Defines interfaces for data repositories and use case repositories.
  • usecases: Contains the business logic use cases.

 Implement the data module:

  • datasources: Contains data source implementations for fetching data from the API.
  • repositories: Implements the repository interfaces defined in the core module to handle data retrieval.

Create the presentation module:

  • screens: Contains the UI screens/widgets for displaying the fetched data.
  • widgets: Contains reusable UI widgets.

Now, let's dive into the code:

 

// lib/core/models/user.dart
class User {
  final String id;
  final String name;
  final String email;

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

// lib/core/repositories/user_repository.dart
abstract class UserRepository {
  Future<List<User>> getUsers();
}

// lib/core/usecases/get_users_usecase.dart
import 'package:your_app/core/models/user.dart';
import 'package:your_app/core/repositories/user_repository.dart';

class GetUsersUseCase {
  final UserRepository userRepository;

  GetUsersUseCase(this.userRepository);

  Future<List<User>> execute() {
    return userRepository.getUsers();
  }
}

// lib/data/datasources/user_remote_datasource.dart
import 'package:your_app/core/models/user.dart';
import 'package:http/http.dart' as http;

class UserRemoteDataSource {
  final http.Client httpClient;

  UserRemoteDataSource({required this.httpClient});

  Future<List<User>> getUsersFromApi() async {
    final response = await httpClient.get(Uri.parse('https://api.example.com/users'));

    if (response.statusCode == 200) {
      final jsonData = jsonDecode(response.body) as List<dynamic>;
      return jsonData.map((userJson) => User(
        id: userJson['id'],
        name: userJson['name'],
        email: userJson['email'],
      )).toList();
    } else {
      throw Exception('Failed to fetch users');
    }
  }
}

// lib/data/repositories/user_repository_impl.dart
import 'package:your_app/core/models/user.dart';
import 'package:your_app/core/repositories/user_repository.dart';
import 'package:your_app/data/datasources/user_remote_datasource.dart';

class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource userRemoteDataSource;

  UserRepositoryImpl({required this.userRemoteDataSource});

  @override
  Future<List<User>> getUsers() {
    return userRemoteDataSource.getUsersFromApi();
  }
}

// lib/presentation/screens/users_screen.dart
import 'package:flutter/material.dart';
import 'package:your_app/core/models/user.dart';
import 'package:your_app/core/usecases/get_users_usecase.dart';
import 'package:your_app/data/repositories/user_repository_impl.dart';

class UsersScreen extends StatelessWidget {
  final GetUsersUseCase getUsersUseCase = GetUsersUseCase(
    UserRepositoryImpl(userRemoteDataSource: UserRemoteDataSource(httpClient: http.Client())),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Users'),
      ),
      body: FutureBuilder<List<User>>(
        future: getUsersUseCase.execute(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            final users = snapshot.data!;
            return ListView.builder(
              itemCount: users.length,
              itemBuilder: (context, index) {
                final user = users[index];
                return ListTile(
                  title: Text(user.name),
                  subtitle: Text(user.email),
                );
              },
            );
          } else if (snapshot.hasError) {
            return Center(
              child: Text('Error fetching users'),
            );
          } else {
            return Center(
              child: CircularProgressIndicator(),
            );
          }
        },
      ),
    );
  }
}

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:your_app/presentation/screens/users_screen.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'API Integration Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: UsersScreen(),
    );
  }
}

In this example, we have implemented clean architecture principles by separating concerns into different modules. The core module contains the core business logic, models, and repositories. The data module handles data retrieval from the API through a remote data source and implements the repository interfaces from the core module. Finally, the presentation module contains the UI screens and widgets responsible for displaying the fetched data.

In the UsersScreen widget, we use the GetUsersUseCase to fetch the users from the API through the UserRepositoryImpl. The UI is updated based on the state of the FutureBuilder. If the data is available, we display it using a ListView.builder. If there is an error, we display an error message, and while the data is being fetched, we show a loading indicator.

Remember to update the API URL and adjust the code to fit your specific needs.

This example demonstrates a simplified implementation of clean architecture principles for API integration in Flutter. You can further enhance the architecture by implementing dependency injection, abstracting data sources, and applying more SOLID principles as your app grows.

 

 

No comments:

Post a Comment

Enhancing Flutter Apps with BLoC State Management and REST APIs for Optimal Performance

 Develop a powerful Flutter app utilizing BLoC state management and REST APIs to enhance functionality and performance. Flutter app using th...