y_infra

A reusable Flutter infrastructure package that provides the building blocks for clean architecture Flutter applications.

Purpose

y_infra exists to stop duplicating the same plumbing across Flutter projects. Every app ends up re-implementing cache, token storage, a Dio setup, error mapping, permission handlers, form validators, paginated/CRUD cubits, and so on. This package collects those building blocks behind small, stable interfaces so each new project can wire them together instead of rewriting them.

It is deliberately unopinionated about UI and business logic. What it gives you:

  • Interface-first: every subsystem (cache, storage, logger, notifier, repository, firebase, push) is an I… abstract class with a default implementation. Swap in your own without touching the callers.
  • Clean-architecture ready: layering mirrors core / data / domain / platform so features fit into an existing clean-arch project.
  • Composable: base cubits (BaseOperationCubit, BaseListCubit, PaginatedCubit, CrudCubit) compose with FilterableMixin; Dio interceptors compose via InterceptorPipeline; caches compose via ReadOnly / WriteOnly / NoCache variants.
  • Localisation hooks: error messages, validator messages, and locale itself are overridable so the package doesn't lock you to English.

Installation

dependencies:
  y_infra:
    path: ../y_infra  # or from pub.dev / git
import 'package:y_infra/y_infra.dart';

What's Inside

lib/
├── core/           Cache, errors, logging, notifier, storage, theme
├── data/           Database (SQLite), file (CSV/JSON), network (Dio)
├── domain/         Repository with queue/dedup and cache invalidation
├── auth/           Token storage, AuthCubit, auth state management
├── firebase/       Analytics, messaging, realtime database abstractions
├── push/           Push notifications (Awesome Notifications)
├── platform/       Permission handlers, connectivity, location
├── base_features/  CRUD, list, paginated, operation, locale cubits
├── components/     Reusable UI components (bottom sheet selector)
├── mixins/         SnackBarY, FilterableMixin
└── utils/          Formatters, generators, validators, routing, map

Core

  • Cache — in-memory, type-safe, TTL + invalidation. Variants: DefaultCache, ReadOnlyCache, WriteOnlyCache, NoCache.
  • Errors — categorized AppError hierarchy with automatic DioException mapping and ErrorMessages for i18n.
  • Logging — pluggable ILogger + IPrinter (DebugLogger, ConsolePrinter).
  • Notifier — stream-based event bus for cross-layer reactive communication.
  • Storage — unified ILocalStorage with SecureStorage and SharedPreferencesStorage.
  • ThemeYInfraColors ThemeExtension for package-wide colors.

Data

  • NetworkBaseNetworkConfig (Dio factory), InterceptorPipeline, AuthInterceptor, LoggingInterceptor, IRemoteDatasource base class.
  • Database — SQLite abstraction (IDbManager) with command builders and data mappers.
  • File — CSV/JSON file controllers (CsvFileController, JsonFileController) with path providers.

Domain

  • IRepository — base repository with in-flight request deduplication, caching, and cache invalidation by key / prefix / type.

Auth

  • AuthTokenStorage — persists TokenPair via ILocalStorage.
  • AuthCubit — manages AuthState (AuthInitial, AuthLoading, Authenticated, Unauthenticated) and auto-logs-out on auth failures.

Firebase

  • IFirebaseService — aggregates analytics, messaging, realtime DB; call initApp() then initServices().
  • IFirebaseAnalyticsService — log events (FirebaseAnalyticsEvent) and set UserProperty.
  • IFirebaseMessagingService — FCM token + message listeners.
  • IFirebaseRealtimeDatabaseService — subscribe to paths with IFirebaseDatabaseEventHandler.

Push

  • IPushNotificationManager — Awesome Notifications setup, channel config from .env, local + remote push.
  • INotificationListeners — callbacks for action / dismiss / create / display events.

Platform

  • PermissionsCamera, Location, Notification, Storage, ExternalStorage, MotionActivity handlers.
  • ConnectivityConnectivityService with isConnected + status stream.
  • LocationLocationService with cached position + position stream.

Base Features (Cubits)

  • BaseOperationCubit<TResult, Id> — single async operation with duplicate-call guard.
  • BaseListCubit<T> — non-paginated list loading.
  • PaginatedCubit<T, Id> — paginated list with load-more / refresh / selection.
  • CrudCubit<T, Id> — full create/read/update/delete with filtering and selection.
  • LocaleCubit — current locale with supported-language check, syncs with easy_localization.

Components

  • BottomSheetSelectorFeature — searchable bottom-sheet selector with cubit-based selection.

Mixins

  • FilterableMixin — generic search / sort / filter on top of any cubit.
  • SnackBarY — themed success / error snackbars.

Utils

  • Formatters — input masks (date, phone, card, expiry, uppercase) + display (date/time, price).
  • Validators — configurable form validators with overridable messages.
  • GeneratorsIUuidProvider / UuidProvider.
  • RoutingRoutingBase, AppNavObserver.
  • MapMapLauncher (Apple Maps on iOS, Google Maps elsewhere).

Usage

Cache

final cache = DefaultCache();
cache.set('users', userList, type: CacheType.personal, ttl: Duration(minutes: 10));
final users = cache.get<List<User>>('users');
cache.removeByPrefix('user_');
cache.removeAllType(CacheType.personal);

Errors

try {
  await dio.get('/endpoint');
} catch (e) {
  final error = ErrorMapper.map(e);
  print(error.category);         // ErrorCategory.network
  print(error.isRetryable);      // true
  print(error.shouldShowToUser); // true
}

// Localise error messages once at startup
ErrorMessages.instance = ErrorMessages(
  networkError: 'errors.network'.tr(),
  sessionExpired: 'errors.session_expired'.tr(),
);

Storage

final storage = SecureStorage(FlutterSecureStorage());
await storage.save<String>('key', 'value');
await storage.save<Map>('data', {'nested': true});
final value = await storage.get<String>('key');

Logging

final logger = DebugLogger(printer: ConsolePrinter());
logger.info('User logged in', data: LogData(tag: 'AUTH'));
logger.error('Failed to fetch', data: LogData(tag: 'API'));

Notifier

final notifier = NotifierService();
notifier.listen('user_updated').listen((event) => print(event.data));
notifier.notify(key: 'user_updated', data: updatedUser);

Theme

MaterialApp(
  theme: ThemeData(
    extensions: [
      YInfraColors(successColor: Colors.green, warningColor: Colors.orange),
    ],
  ),
)

Network

final config = BaseNetworkConfig(
  baseUrl: 'https://api.example.com',
  connectTimeout: Duration(seconds: 15),
);
final dio = config.createDio();

dio.interceptors.add(
  InterceptorPipeline([
    AuthInterceptor(
      authTokenStorage: tokenStorage,
      onTokenRefresh: (refresh) => api.refreshToken(refresh),
      onAuthFailure: () => authCubit.logout(),
      retryDio: Dio(BaseOptions(baseUrl: 'https://api.example.com')),
      skipAuthPaths: ['/login', '/register'],
    ),
    LoggingInterceptor(),
  ]),
);

Pipeline rules: interceptors run in declaration order. Any one can short-circuit via handler.resolve(response) or handler.reject(error); otherwise call handler.next(...) to continue. Each interceptor also works standalone without InterceptorPipeline.

Remote Datasource

class UserRemoteDatasource extends IRemoteDatasource {
  UserRemoteDatasource(super.authTokenStorage, super.dio);

  Future<User> getUser(int id) => doRequest(
    () async {
      final response = await dio.get('/users/$id');
      return User.fromJson(response.data);
    },
  );
}

Database

class UserTable extends IDatabaseTable {
  const UserTable() : super('users');

  @override
  Map<String, String> get columns => {
    'id': 'INTEGER PRIMARY KEY',
    'name': 'TEXT',
    'email': 'TEXT',
  };
}

File

final csv = CsvFileController();
final data = await csv.load('assets/data.csv');
await csv.save(DownloadPathProvider('export.csv'), converter);

Repository

class UserRepository extends IRepository {
  UserRepository(super.notifierService);

  Future<User> getUser(int id) => queue(
    'user_$id',
    () => api.fetchUser(id),
    cache: defaultCache,
    cacheType: CacheType.personal,
    invalidatePrefix: 'user_list',
  );
}

Auth — Token Storage

final tokenStorage = AuthTokenStorage(secureStorage);
await tokenStorage.saveTokenPair(TokenPair(
  accessToken: 'abc',
  refreshToken: 'xyz',
));
final pair = await tokenStorage.getTokenPair();

Auth — AuthCubit

AuthCubit manages authentication state. It does NOT perform login — that's project-specific. Call onAuthenticated after a successful login.

final authCubit = AuthCubit(
  tokenStorage: tokenStorage,
  notifier: notifierService,       // optional: listens for auth failures
  authFailureKey: 'auth_failure',  // key that AuthInterceptor notifies on
);

await authCubit.checkAuth();               // on app startup
authCubit.onAuthenticated(tokens);          // after successful login
await authCubit.logout();                   // explicit logout

BlocBuilder<AuthCubit, AuthState>(
  builder: (context, state) => switch (state) {
    Authenticated() => HomePage(),
    Unauthenticated(reason: final reason) => LoginPage(reason: reason),
    AuthLoading() => SplashScreen(),
    _ => SplashScreen(),
  },
)

Auth states are base class — extend them in your project:

class AuthenticatedWithUser extends Authenticated {
  final User user;
  const AuthenticatedWithUser(this.user);
}

Firebase

IFirebaseService aggregates the three sub-services. Subclass it and provide whichever ones you need.

class AppFirebaseService extends IFirebaseService {
  AppFirebaseService()
      : super(
          analytics: AppAnalyticsService(),
          messaging: AppMessagingService(MyMessageListener(), MyForegroundConfig()),
          realTimeDb: AppRealtimeDbService(),
        );
}

await IFirebaseService.initApp();   // Firebase.initializeApp()
AppFirebaseService().initServices(); // analytics/messaging/realTimeDb .init()

Extend the sub-services:

class AppAnalyticsService extends IFirebaseAnalyticsService {
  @override
  void init() { /* set collection enabled, default params, etc. */ }

  @override
  void logEvent(FirebaseAnalyticsEvent event) =>
      analyticsInstance.logEvent(name: event.name, parameters: event.params);

  @override
  void setUserProperty(UserProperty property) =>
      analyticsInstance.setUserProperty(name: property.name, value: property.value);
}

Push Notifications

IPushNotificationManager wraps Awesome Notifications. Channels are read from a .env file via CHANNEL_CONFIGURATION_LIST + per-channel keys (e.g. DEFAULT_CHANNEL_KEY, DEFAULT_CHANNEL_NAME, DEFAULT_IMPORTANCE).

class AppPushManager extends IPushNotificationManager {
  AppPushManager()
      : super(
          notificationListeners: AppNotificationListeners(...),
          envFilePath: '.env',
          iconPath: 'resource://drawable/notification_icon',
        );

  @override
  void pushLocal(ICustomRemoteMessage message, {PushNotificationChannelConfig? channelConfig}) {
    // build and show a local notification from `message`
  }

  @override
  void pushRemote(ICustomRemoteMessage message, {PushNotificationChannelConfig? channelConfig}) {
    // handle an incoming remote message
  }
}

await AppPushManager().init();

Example .env:

CHANNEL_CONFIGURATION_LIST=DEFAULT,ALERTS
DEFAULT_CHANNEL_KEY=default
DEFAULT_CHANNEL_NAME=General
DEFAULT_CHANNEL_DESCRIPTION=General notifications
DEFAULT_IMPORTANCE=Max

Permissions

final camera = CameraPermissionHandler();
if (await camera.g2g) { /* granted */ }
await camera.askPermIfNeeded();

Available handlers: Camera, Location, Notification, Storage, ExternalStorage, MotionActivity.

Connectivity

final connectivity = ConnectivityService();
final isOnline = await connectivity.isConnected;
connectivity.onStatusChange.listen((status) => print(status));

Location

final location = LocationService(positionCacheDuration: Duration(minutes: 5));
await location.init();
final position = await location.position;
final stream = await location.positionStream;

Operation Cubit

class DeleteItemCubit extends BaseOperationCubit<void, int> {
  final ItemRepository _repo;
  DeleteItemCubit(this._repo);

  Future<void> delete(int id) => execute(
    targetId: id,
    operation: () => _repo.delete(id),
  );
}

States: OperationInitialOperationInProgressOperationSuccess<T, Id> / OperationFailure.

List Cubit

class StoresCubit extends BaseListCubit<Store> {
  final StoreRepository _repo;
  StoresCubit(this._repo);

  @override
  Future<List<Store>> fetchItems() => _repo.getStores();
}

Paginated Cubit

class ProductsCubit extends PaginatedCubit<Product, int> {
  final ProductRepository _repo;
  ProductsCubit(this._repo);

  @override
  int getId(Product item) => item.id;

  @override
  Future<PaginatedResponse<Product>> fetchPage(int page, int pageSize) =>
      _repo.getProducts(page: page, pageSize: pageSize);
}

CRUD Cubit

class UsersCubit extends CrudCubit<User, int> {
  final UserRepository _repo;
  UsersCubit(this._repo);

  @override
  int getId(User item) => item.id;

  @override
  Future<List<User>> fetchItems() => _repo.getUsers();

  Future<void> createUser(CreateUserDto dto) => performSave(
    operation: () => _repo.create(dto),
    successMessage: 'User created',
    updateList: (user) => addToList(user),
  );

  Future<void> deleteUser(int id) => performDelete(
    operation: () => _repo.delete(id),
    id: id,
    successMessage: 'User deleted',
  );
}

Locale Cubit

final localeCubit = LocaleCubit(supportedCodes: {'en', 'tr', 'de'});

localeCubit.isSupported('fr');                      // false
localeCubit.changeLocale(context, 'tr');            // sets locale + calls context.setLocale

FilterableMixin

class ProductsCubit extends PaginatedCubit<Product, int> with FilterableMixin {
  @override
  void onFiltersChanged() => refresh();

  @override
  Future<PaginatedResponse<Product>> fetchPage(int page, int pageSize) =>
      repo.getProducts(page: page, search: searchQuery, sortBy: sortBy);
}

// Also works with BaseListCubit
class StoresCubit extends BaseListCubit<Store> with FilterableMixin {
  @override
  void onFiltersChanged() => refresh();
}

SnackBarY

class MyWidget extends StatelessWidget with SnackBarY {
  void onTap(BuildContext context) {
    displaySuccessSnack(context: context, message: 'Saved!');
    displayErrorSnack(context: context, message: 'Something went wrong');
  }
}

Bottom Sheet Selector

BottomSheetSelectorFeature<City>(
  child: BottomSheetSelectorContainer(
    emptyChild: Text('Select a city'),
    childBuilder: (city) => Text(city.name),
    bottomSheet: SearchableBottomSheetList(
      itemsProvider: () => repository.getCities(),
      itemBuilder: (city) => ListTile(title: Text(city.name)),
      searchHint: 'Search...',
      searchFilter: (city, query) => city.name.toLowerCase().contains(query),
    ),
  ),
)

Formatters

Input formatters (for TextFormField):

  • SeparatorInputFormatter — configurable base for masked input
  • DateInputFormatterDD/MM/YYYY
  • UpperCaseInputFormatter
  • PhoneNumberInputFormatter555 555 55 55
  • CreditCardNumberInputFormatter
  • CardExpiryInputFormatterMM/YY

Display formatters:

const DateTimeFormatter().dateFormatter(DateTime.now(), context);   // '17 April 2026'
const DateTimeFormatter().timeFormatter(DateTime.now(), context);   // '14:32'
const DateTimeFormatter().formatSeconds(125);                       // '2:05'

PriceFormatter.configure(symbol: '₺', symbolAfter: true, decimalDigits: 2);
PriceFormatter.format(199.9);                      // '199,90 ₺'
PriceFormatter.formatRange(10, 50, symbol: '€');   // '10,00 € - 50,00 €'
PriceFormatter.formatDiscount(200, 150);           // '-25%'

Validators

final v = Validators(
  messages: ValidatorMessages(
    required: 'Required',
    invalidEmail: 'Invalid email',
  ),
);

TextFormField(validator: v.email());
TextFormField(validator: v.password(minLength: 8));
TextFormField(validator: v.mustMatch(() => passwordController.text));

Map Launcher

final mapLauncher = MapLauncher(errorMessage: 'Could not open maps');
mapLauncher.open(context: context, latitude: 41.0, longitude: 29.0);

Opens Apple Maps on iOS, Google Maps on other platforms. Override appleMapsUrlBuilder / googleMapsUrlBuilder to customise URL schemes.

Routing

class AppRouter extends RoutingBase {
  const AppRouter(super.navigatorKey);

  @override
  Route generateRoute(RouteSettings settings) => switch (settings.name) {
    '/home' => materialRouting(HomePage(), settings),
    '/details' => iosRouting(DetailsPage(), settings),
    '/modal' => opacityRoute(ModalPage(), settings),
    _ => materialRouting(NotFoundPage(), settings),
  };
}

MaterialApp(
  navigatorKey: navigatorKey,
  onGenerateRoute: AppRouter(navigatorKey).generateRoute,
  navigatorObservers: [AppNavObserver()],
)

License

MIT

Libraries

auth/cubit/auth_cubit
auth/cubit/auth_state
auth/enums/unauthenticated_reason
auth/i_auth_token_storage
auth/implementations/auth_token_storage
auth/objects/token_pair
base_features/crud/crud_cubit
base_features/crud/crud_state
base_features/list/base_list_cubit
base_features/list/base_list_state
base_features/locale/locale_cubit
base_features/operation/base_operation_cubit
base_features/operation/base_operation_state
base_features/paginated/paginated_cubit
base_features/paginated/paginated_state
components/bottom_sheet_selector/bottom_sheet_selector_container
components/bottom_sheet_selector/bottom_sheet_selector_feature
components/bottom_sheet_selector/cubit/bottom_sheet_selector_cubit
components/bottom_sheet_selector/widgets/bottom_sheet_selector_empty_container
components/bottom_sheet_selector/widgets/bottom_sheet_selector_selected_container
components/bottom_sheet_selector/widgets/searchable_bottom_sheet_list
core/cache/i_cache
core/cache/implementations/default_cache
core/cache/implementations/no_cache
core/cache/implementations/read_only_cache
core/cache/implementations/write_only_cache
core/cache/objects/cache_data
core/cache/objects/cache_type
core/errors/app_error
core/errors/error_category
core/errors/error_mapper
core/errors/error_messages
core/errors/types/auth_error
core/errors/types/conflict_error
core/errors/types/network_error
core/errors/types/not_found_error
core/errors/types/server_error
core/errors/types/unexpected_error
core/errors/types/validation_error
core/log/i_logger
core/log/implementations/debug_logger
core/log/objects/log_data
core/log/objects/log_level
core/log/printers/console_printer
core/log/printers/i_printer
core/notifier/i_notifier_service
core/notifier/implementations/notifier_service
core/notifier/objects/notifier_data
core/storage/i_local_storage
core/storage/implementations/secure_storage
core/storage/implementations/shared_preferences_storage
core/theme/y_infra_colors
data/database/builder/command/i_db_raw_command_builder
data/database/builder/command/implementations/create_table_command_builder
data/database/builder/command/implementations/insert_command_builder
data/database/builder/command/implementations/query_command_builder
data/database/builder/query/i_query_builder
data/database/builder/query/implementations/query_builder
data/database/i_db_manager
data/database/implementations/db_manager
data/database/mapper/delete/i_db_delete_data_mapper
data/database/mapper/insert/i_db_data_mapper
data/database/mapper/insert/i_db_insert_data_mapper
data/database/mapper/insert/i_db_insert_data_raw_mapper
data/database/mapper/update/i_db_update_data_mapper
data/database/objects/i_database_table
data/file/controller/i_file_controller
data/file/controller/implementations/csv_file_controller
data/file/controller/implementations/json_file_controller
data/file/converter/i_file_converter
data/file/converter/implementations/map_list_file_converter
data/file/path/i_path_provider
data/file/path/implementations/application_support_path_provider
data/file/path/implementations/download_path_provider
data/network/config/base_network_config
data/network/datasource/i_remote_datasource
data/network/interceptors/auth_interceptor
data/network/interceptors/interceptor_pipeline
data/network/interceptors/logging_interceptor
data/network/objects/paginated_response
domain/i_repository
firebase/analytics/objects/analytics_data
firebase/analytics/objects/analytics_event
firebase/analytics/objects/user_property
firebase/analytics/services/i_analytics_service
firebase/i_firebase_service
firebase/messaging/configs/foreground_presentation
firebase/messaging/listeners/i_message_listener
firebase/messaging/services/i_messaging_service
firebase/realtime_database/handlers/i_event_handler
firebase/realtime_database/objects/db_reference_path
firebase/realtime_database/services/i_realtime_database_once_service
firebase/realtime_database/services/i_realtime_database_service
mixins/filterable_mixin
mixins/snackbar_y
platform/connectivity/i_connectivity_service
platform/connectivity/implementations/connectivity_service
platform/location/i_location_service
platform/location/implementations/location_service
platform/permission/i_permission_handler
platform/permission/implementations/camera
platform/permission/implementations/external_storage
platform/permission/implementations/location
platform/permission/implementations/motion_activity
platform/permission/implementations/notification
platform/permission/implementations/storage
push/configs/notification_channel_config
push/handlers/i_remote_message_handler
push/listeners/i_notification_listeners
push/managers/i_push_notification_manager
push/objects/i_custom_notification_content
push/objects/i_custom_remote_message
push/objects/i_custom_remote_notification
push/objects/i_remote_message_handler_result
push/utility/environment_interpreter
utils/formatters/display/date_time_formatter
utils/formatters/display/price_formatter
utils/formatters/input/card_expiry_input_formatter
utils/formatters/input/credit_card_number_input_formatter
utils/formatters/input/date_input_formatter
utils/formatters/input/phone_number_input_formatter
utils/formatters/input/separator_input_formatter
utils/formatters/input/upper_case_input_formatter
utils/generators/uuid/i_uuid_provider
utils/generators/uuid/implementations/uuid_provider
utils/map/map_launcher
utils/routing/app_nav_observer
utils/routing/objects/route_stack_item
utils/routing/routing_base
utils/validators/validator_messages
utils/validators/validators
y_infra