Flutter Base Architecture Plugin

A robust and scalable Flutter plugin that provides a solid foundation for building production-ready apps. Includes API calls, BLoC state management, navigation, dependency injection, and localization out of the box. Perfect for developers looking to save time and follow best practices in Flutter app development.

🚀 Features

  • BLoC State Management: Built-in base classes for BLoC pattern implementation
  • API Integration: Pre-configured REST API client with Dio
  • Dependency Injection: Singleton-based DI system for plugin services
  • Localization: Multi-language support with easy localization management
  • Navigation: Enhanced navigation extensions with authentication checks
  • Error Handling: Comprehensive error handling and network monitoring
  • Logging: Configurable logging with interceptors
  • Network Info: Real-time connectivity status monitoring
  • Base Components: Reusable base classes for screens, states, and controllers

📦 Installation

Add this to your package's pubspec.yaml file:

dependencies:
  flutter_base_architecture_plugin:
    path: ../flutter_base_architecture_plugin # For local development
    # git: https://github.com/your-repo/flutter_base_architecture_plugin.git # For git dependency

For dependency injection in your app, also add:

dependencies:
  get_it: ^7.6.0
  injectable: ^2.3.2

dev_dependencies:
  injectable_generator: ^2.4.1
  build_runner: ^2.4.7

🛠️ Setup

1. Initialize the Plugin

In your main.dart:

import 'package:flutter/material.dart';
import 'package:flutter_base_architecture_plugin/core/base_state.dart';
import 'your_injector.dart'; // Your app's injector

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Setup dependency injection
  await Injector.setup();

  runApp(const MyApp());
}

2. Create Your App's Injector

Create lib/inject/injector.dart:

import 'package:flutter_base_architecture_plugin/inject/base_injector.dart';
import 'package:flutter_base_architecture_plugin/base_arch_controller/base_arch_controller.dart';
import 'package:flutter_base_architecture_plugin/imports/core_imports.dart';
import 'package:flutter_base_architecture_plugin/services/app_routes/app_routes_service.dart';
import 'package:flutter_base_architecture_plugin/services/localization/localization_service.dart';
import 'package:flutter_base_architecture_plugin/core/network/network_info.dart';
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';

import '../api/home/home_api/home_api.dart';
import '../service/home/home_service.dart';
import 'injector.config.dart';

final getIt = GetIt.instance;

@InjectableInit()
void configureDependencies() => getIt.init();

// Module to register plugin and app-specific dependencies
@module
abstract class PluginModule {
  @singleton
  HomeApi get homeApi => HomeApi(BaseInjector.restApiClient);

  @singleton
  HomeService get homeService => HomeService(getIt<HomeApi>());

  @singleton
  LocalizationService get localizationService =>
      BaseInjector.localizationService;

  @singleton
  AppRoutesService get appRoutesService => BaseInjector.appRoutesService;

  @singleton
  BaseArchController get baseArchController =>
      BaseInjector.baseArchController;

  @singleton
  EventBus get eventBus => BaseInjector.eventBus;

  @singleton
  NetworkInfoImpl get networkInfoImpl =>
      BaseInjector.networkInfoImpl;
}

class Injector {
  static Future<bool> setup() async {
    // Configure GetIt dependencies
    configureDependencies();

    // Set external resolver for BaseState
    BaseState.setExternalResolver(Injector.resolve);
    return true;
  }

  static T resolve<T extends Object>() => getIt<T>();
}

3. Generate Dependency Injection Code

Run the code generator:

dart run build_runner build

4. Configure Your App

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends BaseState<YourMainBloc, MyApp> {
  @override
  void initState() {
    super.initState();
    
    // Configure the base architecture
    bloc.add(SetRestApiConfiguration(
      baseUrl: 'https://your-api.com/api/',
      connectTimeout: 30,
      receiveTimeout: 30,
    ));
    
    bloc.add(SetLocalizationEvent(
      localizationList: [
        EnglishLocalization(),
        // Add more localizations
      ],
    ));
    
    bloc.add(SetProtectedRoutesEvent(
      isAuthenticated: false, // Set based on your auth state
      protectedRoutesList: [
        '/profile',
        '/settings',
        // Add protected routes
      ],
    ));
  }

  @override
  Widget build(BuildContext context) {
    return BlocProvider<YourMainBloc>(
      create: (context) => bloc,
      child: BlocBuilder<YourMainBloc, YourMainData>(
        builder: (context, state) {
          return MaterialApp(
            title: 'Your App',
            home: YourHomeScreen(),
            // Configure other MaterialApp properties
          );
        },
      ),
    );
  }
}

📱 Usage Examples

Creating a BLoC

import 'package:injectable/injectable.dart';
import 'package:flutter_base_architecture_plugin/imports/core_imports.dart';

@injectable
class HomeBloc extends BaseBloc<HomeEvent, HomeData> {
  HomeBloc(this._yourService, this._localizationService) : super(initState) {
    on<LoadDataEvent>(_loadData);
  }

  final YourService _yourService;
  final LocalizationService _localizationService;

  static HomeData get initState => HomeDataBuilder()
    ..state = ScreenState.loading
    ..build();

  Future<void> _loadData(LoadDataEvent event, Emitter<HomeData> emit) async {
    try {
      emit(state.rebuild((b) => b..state = ScreenState.loading));
      
      final data = await _yourService.fetchData();
      
      emit(state.rebuild((b) => b
        ..state = ScreenState.content
        ..data = data));
        
    } catch (error) {
      emit(state.rebuild((b) => b
        ..state = ScreenState.error
        ..errorMessage = error.toString()));
    }
  }
}

Creating a Screen

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

class _HomeScreenState extends BaseState<HomeBloc, HomeScreen> {
  @override
  void initState() {
    super.initState();
    bloc.add(LoadDataEvent());
  }

  @override
  Widget build(BuildContext context) {
    return BlocProvider<HomeBloc>(
      create: (context) => bloc,
      child: BlocBuilder<HomeBloc, HomeData>(
        builder: (context, state) {
          switch (state.state) {
            case ScreenState.loading:
              return const Center(child: CircularProgressIndicator());
              
            case ScreenState.content:
              return _buildContent(state);
              
            case ScreenState.error:
              return _buildError(state);
              
            default:
              return Container();
          }
        },
      ),
    );
  }

  Widget _buildContent(HomeData state) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Column(
        children: [
          // Your content here
          ElevatedButton(
            onPressed: () => context.pushNamed('/details'),
            child: Text('Navigate to Details'),
          ),
        ],
      ),
    );
  }

  Widget _buildError(HomeData state) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Error: ${state.errorMessage}'),
            ElevatedButton(
              onPressed: () => bloc.add(LoadDataEvent()),
              child: Text('Retry'),
            ),
          ],
        ),
      ),
    );
  }
}

API Integration

@injectable
class YourApi {
  YourApi(this._restApiClient);
  
  final RestApiClient _restApiClient;

  Future<List<YourModel>> fetchData() async {
    final response = await _restApiClient.request(
      path: '/data',
      requestMethod: RequestMethod.GET,
      data: RequestData(),
    );
    
    if (response.data != null) {
      return (response.data!['results'] as List)
          .map((json) => YourModel.fromJson(json))
          .toList();
    }
    
    throw Exception('Failed to fetch data');
  }
}
// Navigate with automatic auth check
context.pushNamedWithAuthCheck('/protected-route');

// Regular navigation
context.pushNamed('/public-route');

// Navigation with parameters
context.pushNamed('/details', arguments: {'id': 123});

Localization

// Create your localization class
class AppEnglishLocalization extends BaseLocalization {
  @override
  String get appName => 'My App';
  
  @override
  String get welcome => 'Welcome';
  
  // Add more translations
}

// Use in widgets
Text(LocalizationService.currentLocalization().welcome)

🏗️ Architecture Overview

├── Core Components
│   ├── BaseBloc - Base class for all BLoCs
│   ├── BaseState - Base class for all StatefulWidgets
│   ├── BaseArchController - Main app controller
│   └── Event Bus - App-wide event communication
│
├── Network Layer
│   ├── RestApiClient - HTTP client wrapper
│   ├── NetworkInfo - Connectivity monitoring
│   └── Error Handling - Centralized error management
│
├── Dependency Injection
│   ├── BaseInjector - Plugin's DI system (Singleton-based)
│   └── External Injector - App's DI system (GetIt Injectable)
│
├── Services
│   ├── LocalizationService - Multi-language support
│   ├── AppRoutesService - Route management
│   └── Custom Services - Your app-specific services
│
└── Extensions
    ├── Navigation Extensions - Enhanced navigation
    ├── Context Extensions - Utility extensions
    └── String Extensions - String utilities

🔧 Configuration

REST API Configuration

bloc.add(SetRestApiConfiguration(
  baseUrl: 'https://api.example.com/',
  connectTimeout: 30,
  receiveTimeout: 30,
  defaultHeaders: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  },
));

Protected Routes

bloc.add(SetProtectedRoutesEvent(
  isAuthenticated: userIsLoggedIn,
  protectedRoutesList: [
    '/profile',
    '/settings',
    '/admin',
  ],
));

🚧 Migration from Kiwi to GetIt Injectable

If you're migrating from an older version that used Kiwi, follow these steps:

  1. Remove Kiwi dependencies from your app's pubspec.yaml
  2. Add GetIt dependencies as shown in installation
  3. Update your injector to use the new pattern shown above
  4. Run code generation with dart run build_runner build
  5. Update BaseState initialization in your main.dart

The plugin now uses a singleton-based approach internally while supporting modern GetIt injectable for app-level dependencies.

📚 Additional Resources

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.