Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Cristiang1021/ErgoKawsay/llms.txt

Use this file to discover all available pages before exploring further.

ErgoKawsay manages global UI state with three ChangeNotifier controllers wired at the app root in app.dart. Each controller owns one slice of global state, reads its persisted value from StorageService on construction, and notifies listeners when the user changes a preference. State is propagated through the widget tree via custom InheritedWidget scope classes — no external state management library is involved. All three controllers are instantiated in _ErgoKawsayAppState.initState() and disposed in dispose(), so their lifetimes match the widget tree.

Controllers Overview

LocaleController

Manages the active Locale. Switches between Spanish (es) and Kichwa (qu) and persists the choice via StorageService.

ThemeController

Manages ThemeMode (light / dark / system). Persists the user’s preference and rebuilds MaterialApp on change.

AccessibilityController

Manages AccessibilitySettings — text size scale and colour-blind filter. Applied globally via MediaQuery and ColorFiltered.

LocaleController

File: lib/core/localization/locale_controller.dart
class LocaleController extends ChangeNotifier {
  LocaleController(StorageService storage);

  Locale get locale => Locale(_storage.getLanguage() ?? 'es');

  Future<void> updateLanguage(String code) async {
    await _storage.setLanguage(code);
    notifyListeners();
  }
}
MemberDescription
localeReturns the current Locale constructed from the persisted language code ('es' or 'qu')
updateLanguage(String code)Persists code via StorageService and triggers a rebuild of MaterialApp
LocaleController is exposed to the subtree via the LocaleControllerScope InheritedWidget:
class LocaleControllerScope extends InheritedWidget {
  static LocaleController of(BuildContext context) { ... }
}
Call LocaleControllerScope.of(context) anywhere in the tree to obtain the controller.

ThemeController

File: lib/core/localization/theme_controller.dart
class ThemeController extends ChangeNotifier {
  ThemeController(StorageService storage);

  ThemeMode get themeMode => _storage.getThemeMode();

  Future<void> setThemeMode(ThemeMode mode) async {
    await _storage.setThemeMode(mode);
    notifyListeners();
  }
}
MemberDescription
themeModeReturns the current ThemeMode from StorageService (light, dark, or system)
setThemeMode(ThemeMode)Persists the selection via StorageService and notifies listeners
Exposed via ThemeControllerScope:
class ThemeControllerScope extends InheritedWidget {
  static ThemeController of(BuildContext context) { ... }
}
The stored string values map to AppConstants.themeLight, AppConstants.themeDark, and AppConstants.themeSystem.

AccessibilityController

File: lib/core/localization/accessibility_controller.dart
class AccessibilityController extends ChangeNotifier {
  AccessibilityController(StorageService storage);

  AccessibilitySettings get settings => _storage.getAccessibilitySettings();

  Future<void> updateSettings(AccessibilitySettings s) async {
    await _storage.saveAccessibilitySettings(s);
    notifyListeners();
  }
}
MemberDescription
settingsReturns the current AccessibilitySettings from StorageService
updateSettings(AccessibilitySettings)Serialises the settings to JSON, persists them, and notifies
Exposed via AccessibilityControllerScope:
class AccessibilityControllerScope extends InheritedWidget {
  static AccessibilityController of(BuildContext context) { ... }
}

App Builder

_ErgoKawsayAppState.build() wraps the entire widget tree in four nested scope widgets and a single ListenableBuilder that merges all three controllers into one listenable. This means any change to locale, theme, or accessibility causes exactly one MaterialApp rebuild:
return StorageServiceScope(
  storage: widget.storage,
  child: LocaleControllerScope(
    controller: _localeController,
    child: ThemeControllerScope(
      controller: _themeController,
      child: AccessibilityControllerScope(
        controller: _accessibilityController,
        child: ListenableBuilder(
          listenable: Listenable.merge([
            _localeController,
            _themeController,
            _accessibilityController,
          ]),
          builder: (context, _) {
            final a11y = _accessibilityController.settings;
            return MaterialApp(
              theme: AppTheme.lightTheme,
              darkTheme: AppTheme.darkTheme,
              themeMode: _themeController.themeMode,
              locale: _localeController.locale,
              // ...routes, localizationsDelegates, etc.
              builder: (context, child) {
                var content = child ?? const SizedBox.shrink();

                // Deuteranopia colour filter
                if (a11y.colorBlind) {
                  content = ColorFiltered(
                    colorFilter: const ColorFilter.matrix([
                      0.625, 0.375, 0,   0, 0,
                      0.700, 0.300, 0,   0, 0,
                      0,     0.300, 0.7, 0, 0,
                      0,     0,     0,   1, 0,
                    ]),
                    child: content,
                  );
                }

                // Text scale from AccessibilitySettings.textScale
                return MediaQuery(
                  data: MediaQuery.of(context).copyWith(
                    textScaler: TextScaler.linear(a11y.textScale),
                  ),
                  child: content,
                );
              },
            );
          },
        ),
      ),
    ),
  ),
);
The colour-blind filter applies a deuteranopia simulation matrix via ColorFilter.matrix. It is wrapped around the entire child subtree so that every pixel rendered by MaterialApp is filtered uniformly. The text scale overrides the system MediaQuery text scaler so that the user’s in-app size preference always takes precedence over OS-level scaling.

StorageServiceScope

StorageServiceScope is an InheritedWidget that makes the single StorageService instance available anywhere in the widget tree without passing it down manually through constructors:
class StorageServiceScope extends InheritedWidget {
  const StorageServiceScope({
    required StorageService storage,
    required Widget child,
  });

  static StorageService of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<StorageServiceScope>()!
        .storage;
  }
}
It is the outermost scope in _ErgoKawsayAppState.build(), so every screen and controller can call StorageServiceScope.of(context) to read or write persisted data.

Usage Pattern

Access a controller from any widget that is a descendant of the app root:
// Switch the app language to Kichwa
final localeCtrl = LocaleControllerScope.of(context);
await localeCtrl.updateLanguage('qu');

// Switch to dark mode
final themeCtrl = ThemeControllerScope.of(context);
await themeCtrl.setThemeMode(ThemeMode.dark);

// Enable colour-blind filter and increase text size
final a11yCtrl = AccessibilityControllerScope.of(context);
await a11yCtrl.updateSettings(
  a11yCtrl.settings.copyWith(
    colorBlind: true,
    textSize: TextSizeOption.large,
  ),
);
All three controllers are ChangeNotifier subclasses, so you can also wrap a widget with ListenableBuilder(listenable: LocaleControllerScope.of(context), ...) to rebuild only when locale changes, rather than rebuilding the full MaterialApp.

Kichwa Locale Workaround

Kichwa (qu) is not included in flutter_localizations, so Flutter’s GlobalMaterialLocalizations.delegate and GlobalWidgetsLocalizations.delegate skip it. Without a fallback, Material components like dialogs, date pickers, and time pickers would throw a missing-localizations assertion at runtime. ErgoKawsay solves this with two custom delegates declared in app.dart:
// Injects Spanish Material localizations for the 'qu' locale
class _KichwaMaterialDelegate
    extends LocalizationsDelegate<MaterialLocalizations> {
  const _KichwaMaterialDelegate();

  @override
  bool isSupported(Locale locale) => locale.languageCode == 'qu';

  @override
  Future<MaterialLocalizations> load(Locale locale) =>
      SynchronousFuture(const DefaultMaterialLocalizations());

  @override
  bool shouldReload(_KichwaMaterialDelegate old) => false;
}

// Injects Spanish Widgets localizations for the 'qu' locale
class _KichwaWidgetsDelegate
    extends LocalizationsDelegate<WidgetsLocalizations> {
  const _KichwaWidgetsDelegate();

  @override
  bool isSupported(Locale locale) => locale.languageCode == 'qu';

  @override
  Future<WidgetsLocalizations> load(Locale locale) =>
      SynchronousFuture(const DefaultWidgetsLocalizations());

  @override
  bool shouldReload(_KichwaWidgetsDelegate old) => false;
}
These delegates are registered before the Global* delegates in localizationsDelegates so they win the first-match resolution for the qu locale:
localizationsDelegates: const [
  AppLocalizations.delegate,
  _KichwaMaterialDelegate(),   // ← must precede GlobalMaterialLocalizations
  _KichwaWidgetsDelegate(),    // ← must precede GlobalWidgetsLocalizations
  GlobalMaterialLocalizations.delegate,
  GlobalWidgetsLocalizations.delegate,
  GlobalCupertinoLocalizations.delegate,
],
Additionally, localeResolutionCallback is overridden to always return the controller’s locale, bypassing Flutter’s default locale negotiation algorithm entirely:
localeResolutionCallback: (_, __) => _localeController.locale,
This ensures the app never falls back to a device locale — it always displays the language the user explicitly selected.

Build docs developers (and LLMs) love