LayerKit

Where MVVM meets Clean Architecture

LayerKit is a powerful Flutter framework that combines the best aspects of the MVVM (Model-View-ViewModel) pattern with Clean Architecture principles to create a robust, maintainable, and scalable application structure.

pub package License: MIT

Layer Kit

Features

  • Clean Project Structure - Organized folder structure that follows both MVVM and Clean Architecture principles
  • Feature Generation - CLI tools to quickly scaffold new features with all necessary components
  • Project Generation - Quickly bootstrap an entire project with best practices built-in
  • Error Handling - Standardized exceptions and failures for consistent error management
  • Configuration System - Flexible configuration for different environments
  • Extension Methods - Utility extensions for common types to reduce boilerplate code
  • UI Utilities - Helper methods for common UI tasks such as decorations
  • Logging - Development-friendly logging system

Table of Contents

Installation

Add LayerKit to your pubspec.yaml:

dependencies:
  layer_kit:

Run:

flutter pub get

Getting Started

Generating a New Project

Create a new Flutter project, then run:

⚠️ IMPORTANT: Run this command only immediately after creating a new project. If you already have files and folders inside your lib directory, you will lose them as this command overwrites the lib folder with the package template. To avoid data loss, take a ZIP backup of your lib folder before running the command.

dart run layer_kit --project

This will generate a properly structured project with all necessary files and configurations.

Generating a New Feature

To add a new feature to your project:

⚠️ NOTE: Run this command only if you have created your project structure using LayerKit. Running it in projects without the LayerKit structure may not work as expected.

dart run layer_kit --feature feature_name

This command will:

  1. Create a feature directory in the src folder
  2. Generate all necessary files following MVVM and Clean Architecture principles
  3. Update routes to include the new feature

Project Structure

LayerKit organizes your project into a well-defined structure:

lib/
├── config/                         # Configuration files
│   ├── data/                       # Data-related configuration
│   ├── lang/                       # Localization
│   ├── routes/                     # Routing
│   └── theme/                      # Theme configuration
├── core/                           # Core components
│   ├── callbacks/                  # Callback interfaces
│   ├── common/                     # Common utilities
│   │   └── widgets/                # Reusable widgets
│   ├── constants/                  # Constants
│   ├── extensions/                 # Extension methods
│   ├── helper/                     # Helper methods 
│   ├── network/                    # Network service 
│   └── utils/                      # Utilities
├── src/                            # Features
│   ├── feature1/                   # Feature module
│   │   ├── datasource/             # Data sources
│   │   │   ├── models/             # Data models
│   │   │   │   ├── body_models/    # UI display models (used in screens)
│   │   │   │   ├── requests/       # API request models
│   │   │   │   └── response/       # API response models
│   │   │   └── repo/               # Repository implementation
│   │   ├── providers/              # Feature providers
│   │   └── screens/                # UI screens
│   └── feature2/                   # Another feature module
├── defaults.dart                   # Default values used across the app
├── di_container.dart               # Dependency Injection imports (part of di main)
├── di_container.main.dart          # DI main file - injection of all dependencies (using 'Get It')
└── main.dart                       # Entry point

Configuration

LayerKit provides a flexible configuration system:

// main.dart
  await AppLocalization.init();
  await AppTheme.init();

  runApp(AppLocalization(
    child: AppTheme(
      child: MyApp(),
    ),
  ));
  
/// ...code....  

@override
Widget build(BuildContext context) {
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
    // DeviceOrientation.landscapeRight,
    // DeviceOrientation.landscapeLeft,
  ]);

  /// FOR THEME 
  ThemeConfig.init(context);
  /// FOR ROUTING (SEE https://pub.dev/packages/flutter_easy_routing)
  RouteConfig.setDefaultTransition(TransitionType.fade);
  
  return ToastificationWrapper(
    child: MaterialApp(
      title: AppConsts.appName,
      navigatorKey: AppRouter.navigatorKey,
      debugShowCheckedModeBanner: false,
      onGenerateRoute: (s) => AppRouter.generateRoute(s, SplashScreen()),
      scrollBehavior: const StretchScrollBehavior(),
      initialRoute: Routes.splash.path,
      localizationsDelegates: context.localizationDelegates,
      supportedLocales: context.supportedLocales,
      locale: context.locale,
      theme: context.theme.currentTheme,
      builder: (context, child) {
        return MediaQuery(
          data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.0)),
          child: child ?? SizedBox(),
        );
      },
    ),
  );
}
/// ...code....

Core Concepts

MVVM + Clean Architecture

LayerKit combines the MVVM pattern with Clean Architecture principles:

  • View - Flutter UI components (screens, widgets)
  • ViewModel - Business logic and state management
  • Model - Data models and repository interfaces

With additional Clean Architecture layers:

  • Repositories - Abstract data source interactions and implementations that access data models
  • Data Sources - Concrete implementations of data access including models, requests, and responses

ViewModels

ViewModels in LayerKit extend the BaseViewModel class:

class MovieProvider extends BaseViewModel {
  final MovieRepo _movieRepo;
  final NetworkService _networkService;

  MovieProvider({required MovieRepo movieRepo, required NetworkService networkService})
          : _movieRepo = movieRepo,
            _networkService = networkService;

  Future<bool> getData({
    required String data1,
    required String data2,
    bool listen = true,
  }) async {
    setLoading();
    final isNetwork = await _networkService.isConnected;
    final isSuccess = await apiCallback(
      name: "getData",
      isNetwork: isNetwork,
      doWhenOnline: () async {
        final req = MovieReq(data1: data1, data2: data2);
        final res = await _movieRepo.getData(req);
        showSnackbar(res.message);
        setLoaded();
        return res.status;
      },
      errorListener: (e) {
        setError(e.toString());
      },
    );
    if (listen) notifyListeners();
    return isSuccess;
  }
}

Repositories

Repositories abstract data sources:

abstract interface class MovieRepo {
  Future<MovieResponse> getData(MovieReq req);
}

class MovieRepoImpl implements MovieRepo {
  final DioClient _dioClient;

  MovieRepoImpl({required DioClient dioClient}) : _dioClient = dioClient;

  @override
  Future<MovieResponse> getData(MovieReq req) async {
    return await repoCallback<MovieResponse>(
      name: "getData",
      callback: () async {
        final res = await _dioClient.post(Apis.getData, data: req.toJson());
        return MovieResponse.fromJson(res.data ?? {});
      },
    );
  }
}

Theme Configuration

  • All theme configuration is located under the path: lib/config/theme
  • LayerKit includes AppColors where you can define your app colors and themeColors (of type ThemeColorsModel()) where you can define your dynamic theme colors
  • By default, LayerKit loads the user's device system theme
  • There is also a ThemeSelectorDropdown widget that provides a UI to change theme colors (not dark/light mode; this is used for multiple themes. For dark and light mode, use context.toggleThemeMode(); theme extensions)

Size Configuration

  • Dynamic width, height, and radius values
  • These are all extensions on num that return double values
  • .h -> dynamic height
    • 10.h = 10% of screen height
  • .w -> dynamic width
    • 10.w = 10% of screen width
  • .t -> dynamic value for text sizing
  • .r -> dynamic radius used for border radius, equals height and width (like for square images)

Environment Types (EnvType)

  • This enum helps you change variables by environment
  • Set EnvType in the Defaults `D` class and add conditions based on envType
  • For example:
enum EnvType {
  development, // normal debug and development
  developmentWithPrint, // useful to print logs in running release app
  production, // set this for play store deploy bundle
  ;

  bool get isDevelopment => this == EnvType.development;
  bool get isDevelopmentWithPrint => this == EnvType.developmentWithPrint;
  bool get isProduction => this == EnvType.production;
}
  • Now you can use the envType variable like:
  static const stagingBaseUrl = "https://staging-example.com";
  static const liveBaseUrl = "https://example.com";

  static String get baseUrl => D.envType.isProduction ? liveBaseUrl : stagingBaseUrl;

Theme Atoms

See folder: lib/config/theme/atoms
Useful for maintaining consistent layouts
Customize as per your needs

  • Text: lib/config/theme/atoms/text.dart
/// How to use text instead of Text widget
"This is text".regular16.build(
          textAlign: TextAlign.start,
          maxLines: 20,
          fontSize: 1.4.t,
          color: textColor ?? Colors.white,
          fontWeight: FontWeight.w500,
        )

/// Available types:
// titleX
// title32
// title24
// title18
// regular16
// regular14
// small13
// tiny12
// appbar
// button
  • Gap: lib/config/theme/atoms/padding.dart
  • Also includes padding extensions for ease of coding
Gap.size16.paddingHorizontal

(4.w, 1.h).paddingHV
  • VGap/HGap Spacing widgets: lib/config/theme/atoms/spacing.dart
  • Use these instead of SizedBox for spacing in columns and rows (UI)
VGap.default1,

VGap(5), // This equals SizedBox(height: 5.h); here .h is an extension used for dynamic height (5% of screen height)
  • Theme Extensions - Some theme extensions:
// Basically used to toggle theme mode (light to dark and vice versa)
context.toggleThemeMode();

// Reset theme mode to system mode
context.resetThemeMode();

// This theme colors index is used for multiple themes in the app
// There is a list of themes, users can select their favorite theme
// We store the index of this theme and save it
final index = themeColorsOptions.indexOf(value);
final i = index >= 0 ? index : null;
context.setThemeColorsIndex(i);

Localizations (Multiple Languages in App)

  • Wrap the MainApp widget with AppLocalization
  • Localization configuration is located under the path: lib/config/lang
  • There is also a LangSelectorDropdown widget that provides a UI to change language

Local JSON Files

  • Requires <lang-code>.json files
  • Folder path: lib/config/lang/jsons (if you change this path, also change the path in pubspec.yaml file)
  • Add a list of LanguageModel in the languages.dart file
  • If you don't have an API for localization and the app depends on local JSON files, then remove the lang_from_api folder and related errors (as we set up for API now, they might throw errors)
// languages.dart 
List<LanguageModel> languages = [
  LanguageModel(code: "en", language: "English", displayName: "English", image: "🇬🇧"),
  LanguageModel(code: "de", language: "German", displayName: "Deutsch", image: "🇩🇪"),
];

Localizations from API (for Dynamic Changes)

  • Requires TranslationApiModel type of response (see the toJson method of this class; however, you can change it as per your response)
  • Folder path: lib/config/lang/src/lang_from_api (this is a dummy API explanation; modify as per your requirements)
  • For temporary checking, enable/disable the fetchLanguagesFromApi variable in the DummyLangApi class
  • If you don't have an API for localization and the app depends on local JSON files, then remove the lang_from_api folder and related errors (as we set up for API now, they might throw errors)

Routing

Callbacks

  • LayerKit contains callbacks that make lengthy functions shorter and improve logging

  • Examples of callbacks:

widgetBinding((_) {
  //code
})
onDebugOnly((){
      // code
    });
onTap: () async => await safeRun(
   name: "Feature Button",
   isEnabled: true,
   logEnabled: true,
   tryBlock: () {
     /// TODO: implement onClick logic
     showSnackbar("Feature Button Clicked");
     devlog("Feature Button Clicked");
   },
   errorHandler: (e) {
     /// TODO: handle error
     devlogError("Error occurred on Feature Button click - $e");
   },
 )),
  • Other callbacks used in API calling in repo repoCallback and provider apiCallback (main purpose is logging and identifying which API has errors)

Extensions

LayerKit includes several useful extensions:

// Border radius extension
D.defaultRadius.borderRadiusCircular
20.borderRadiusTop
20.borderRadiusBottom
20.borderRadiusRight

// String casing
"hello world".toTitleCase      // "Hello World"
"example".firstUpperCased      // "Example"

// Time formatting
120.toMMSS                     // "02 : 00"
3725.toHHMMSS                  // "01 : 02 : 05"

See extensions under the core folder for more.

License

LayerKit is available under the MIT license. See the LICENSE file for more info.


Created by Nayan Parmar © 2025