Architecture des Applications Flutter
Gad Ntenta

Gad Ntenta

Développeur Full Stack

•

Architecture des Applications Flutter

FlutterArchitectureClean ArchitectureState Management

Découvrez les meilleures pratiques d'architecture pour vos applications Flutter.

Architecture des Applications Flutter

Introduction

Une bonne architecture est essentielle pour créer des applications Flutter maintenables et évolutives. Dans cet article, nous allons explorer les différentes approches d'architecture et les meilleures pratiques.

Clean Architecture

1. Structure des Couches

// Core Layer
abstract class UseCase<Type, Params> {
  Future<Type> call(Params params);
}

// Domain Layer
abstract class UserRepository {
  Future<User> getUser(String id);
  Future<void> saveUser(User user);
}

// Data Layer
class UserRepositoryImpl implements UserRepository {
  final ApiClient apiClient;
  final LocalStorage localStorage;

  UserRepositoryImpl(this.apiClient, this.localStorage);

  @override
  Future<User> getUser(String id) async {
    try {
      final user = await apiClient.getUser(id);
      await localStorage.saveUser(user);
      return user;
    } catch (e) {
      return localStorage.getUser(id);
    }
  }

  @override
  Future<void> saveUser(User user) async {
    await apiClient.saveUser(user);
    await localStorage.saveUser(user);
  }
}

// Presentation Layer
class UserBloc extends Bloc<UserEvent, UserState> {
  final GetUserUseCase getUserUseCase;
  final SaveUserUseCase saveUserUseCase;

  UserBloc(this.getUserUseCase, this.saveUserUseCase) : super(UserInitial()) {
    on<GetUserEvent>(_onGetUser);
    on<SaveUserEvent>(_onSaveUser);
  }

  Future<void> _onGetUser(GetUserEvent event, Emitter<UserState> emit) async {
    emit(UserLoading());
    try {
      final user = await getUserUseCase(event.userId);
      emit(UserLoaded(user));
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }

  Future<void> _onSaveUser(SaveUserEvent event, Emitter<UserState> emit) async {
    emit(UserLoading());
    try {
      await saveUserUseCase(event.user);
      emit(UserSaved());
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }
}

2. Injection de Dépendances

class DependencyInjection {
  static final GetIt getIt = GetIt.instance;

  static void init() {
    // Core
    getIt.registerLazySingleton(() => ApiClient());
    getIt.registerLazySingleton(() => LocalStorage());

    // Repositories
    getIt.registerLazySingleton<UserRepository>(
      () => UserRepositoryImpl(getIt(), getIt()),
    );

    // Use Cases
    getIt.registerLazySingleton(() => GetUserUseCase(getIt()));
    getIt.registerLazySingleton(() => SaveUserUseCase(getIt()));

    // BLoCs
    getIt.registerFactory(() => UserBloc(getIt(), getIt()));
  }
}

Feature-First Architecture

1. Structure des Modules

// Auth Module
class AuthModule extends Module {
  @override
  List<Bind> get binds => [
    Bind.singleton((i) => AuthRepository(i())),
    Bind.singleton((i) => LoginUseCase(i())),
    Bind.singleton((i) => RegisterUseCase(i())),
    Bind.factory((i) => AuthBloc(i(), i())),
  ];

  @override
  List<ModularRoute> get routes => [
    ChildRoute('/login', child: (_, __) => LoginPage()),
    ChildRoute('/register', child: (_, __) => RegisterPage()),
  ];
}

// Home Module
class HomeModule extends Module {
  @override
  List<Bind> get binds => [
    Bind.singleton((i) => HomeRepository(i())),
    Bind.singleton((i) => GetPostsUseCase(i())),
    Bind.factory((i) => HomeBloc(i())),
  ];

  @override
  List<ModularRoute> get routes => [
    ChildRoute('/', child: (_, __) => HomePage()),
    ChildRoute('/post/:id', child: (_, __) => PostDetailPage()),
  ];
}

2. Navigation Modulaire

class AppModule extends Module {
  @override
  List<ModularRoute> get routes => [
    ModuleRoute('/auth', module: AuthModule()),
    ModuleRoute('/home', module: HomeModule()),
  ];
}

class AppWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: ModularRouteInformationParser(),
      routerDelegate: ModularRouterDelegate(),
    );
  }
}

State Management

1. BLoC Pattern

// Events
abstract class UserEvent {}

class GetUserEvent extends UserEvent {
  final String userId;
  GetUserEvent(this.userId);
}

class SaveUserEvent extends UserEvent {
  final User user;
  SaveUserEvent(this.user);
}

// States
abstract class UserState {}

class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
  final User user;
  UserLoaded(this.user);
}
class UserError extends UserState {
  final String message;
  UserError(this.message);
}

// BLoC
class UserBloc extends Bloc<UserEvent, UserState> {
  final GetUserUseCase getUserUseCase;
  final SaveUserUseCase saveUserUseCase;

  UserBloc(this.getUserUseCase, this.saveUserUseCase) : super(UserInitial()) {
    on<GetUserEvent>(_onGetUser);
    on<SaveUserEvent>(_onSaveUser);
  }

  Future<void> _onGetUser(GetUserEvent event, Emitter<UserState> emit) async {
    emit(UserLoading());
    try {
      final user = await getUserUseCase(event.userId);
      emit(UserLoaded(user));
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }

  Future<void> _onSaveUser(SaveUserEvent event, Emitter<UserState> emit) async {
    emit(UserLoading());
    try {
      await saveUserUseCase(event.user);
      emit(UserSaved());
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }
}

2. Provider Pattern

class UserProvider extends ChangeNotifier {
  final GetUserUseCase getUserUseCase;
  final SaveUserUseCase saveUserUseCase;

  UserProvider(this.getUserUseCase, this.saveUserUseCase);

  User? _user;
  bool _isLoading = false;
  String? _error;

  User? get user => _user;
  bool get isLoading => _isLoading;
  String? get error => _error;

  Future<void> getUser(String id) async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      _user = await getUserUseCase(id);
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  Future<void> saveUser(User user) async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      await saveUserUseCase(user);
      _user = user;
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

Navigation

1. Gestion des Routes

class AppRouter {
  static const String home = '/';
  static const String login = '/login';
  static const String register = '/register';
  static const String profile = '/profile';
  static const String settings = '/settings';

  static Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case home:
        return MaterialPageRoute(builder: (_) => HomePage());
      case login:
        return MaterialPageRoute(builder: (_) => LoginPage());
      case register:
        return MaterialPageRoute(builder: (_) => RegisterPage());
      case profile:
        return MaterialPageRoute(builder: (_) => ProfilePage());
      case settings:
        return MaterialPageRoute(builder: (_) => SettingsPage());
      default:
        return MaterialPageRoute(
          builder: (_) => Scaffold(
            body: Center(
              child: Text('Route not found!'),
            ),
          ),
        );
    }
  }
}

2. Navigation avec Paramètres

class NavigationService {
  static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  static Future<dynamic> navigateTo(String routeName, {Object? arguments}) {
    return navigatorKey.currentState!.pushNamed(routeName, arguments: arguments);
  }

  static Future<dynamic> navigateToReplacement(String routeName, {Object? arguments}) {
    return navigatorKey.currentState!.pushReplacementNamed(routeName, arguments: arguments);
  }

  static Future<dynamic> navigateToAndClear(String routeName, {Object? arguments}) {
    return navigatorKey.currentState!.pushNamedAndRemoveUntil(
      routeName,
      (Route<dynamic> route) => false,
      arguments: arguments,
    );
  }

  static void goBack() {
    return navigatorKey.currentState!.pop();
  }
}

// Utilisation
class UserProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final userId = ModalRoute.of(context)!.settings.arguments as String;
    return Scaffold(
      appBar: AppBar(
        title: Text('Profile'),
        leading: IconButton(
          icon: Icon(Icons.arrow_back),
          onPressed: () => NavigationService.goBack(),
        ),
      ),
      body: UserProfileContent(userId: userId),
    );
  }
}

Meilleures Pratiques

1. Organisation du Code

  • SĂ©parer les responsabilitĂ©s
  • Utiliser des modules
  • Suivre les principes SOLID
  • Maintenir la cohĂ©rence

2. Gestion des Erreurs

  • ImplĂ©menter une gestion d'erreurs centralisĂ©e
  • Utiliser des types d'erreurs personnalisĂ©s
  • Logger les erreurs
  • GĂ©rer les erreurs de rĂ©seau

3. Tests

  • Écrire des tests unitaires
  • Écrire des tests d'intĂ©gration
  • Écrire des tests de widget
  • Automatiser les tests

Conclusion

Une bonne architecture est la clé du succès d'une application Flutter. En suivant ces principes et en utilisant les bonnes pratiques, vous pouvez créer des applications maintenables, évolutives et de haute qualité.

N'oubliez pas de :

  • Choisir l'architecture adaptĂ©e Ă  votre projet
  • Maintenir la cohĂ©rence du code
  • Documenter votre architecture
  • Évoluer avec votre application

Avec ces connaissances, vous êtes prêt à créer des applications Flutter bien structurées !