agentsclimarketplace

Flutter isar clean arch setup

Skill jahfaliabdulrahman-dev/hermes-skills/skills/flutter-isar-clean-arch-setup

Production-grade agent skills for Flutter, DevOps & AI Governance — forged in real projects, governed by constitution, verified on device.

Install
npx -y skills add jahfaliabdulrahman-dev/hermes-skills --skill flutter-isar-clean-arch-setup

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 19 days oldThe repository was created 19 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Set up a Flutter + Isar + Riverpod Clean Architecture project. Covers manual file creation, schema generation, avoiding dependency hell, and structuring domain/data/presentation layers.

SKILL.md

22.4 KB, ~5.6k tokens by cl100k_base, as published. Nobody here has run it

Flutter + Isar + Clean Architecture Setup

Use this skill when scaffolding or repairing a Flutter project that uses Isar DB with Standard Riverpod and Clean Architecture layers.

Step-by-Step Setup

1. Platform Folders (Critical)

If files are created manually (no flutter create), the project lacks .dart_tool/package_config.json and platform folders. Always run:

flutter create . --project-name <name> --platforms=ios,macos
flutter pub get

Without this, every import shows uri_does_not_exist and build_runner fails.

2. Minimal pubspec.yaml

Avoid dependency hell. Only include what's needed:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.6.1
  isar: ^3.1.0+1
  isar_flutter_libs: ^3.1.0+1
  path_provider: ^2.1.4
  path: ^1.9.0
  intl: ^0.20.2  # Always match flutter_localizations pin
  go_router: ^14.8.1  # For feature-first routing variant

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0
  build_runner: ^2.4.12
  isar_generator: ^3.1.0+1

Pitfalls:

  • Do NOT add riverpod_generator, custom_lint, build_verify, or freezed — they create analyzer version conflicts.
  • intl version is pinned by flutter_localizations — always check with flutter pub outdated.

3. Isar Model Rules

  • Use ONLY part 'model.g.dart' (generated by isar_generator).
  • Do NOT include part '*.freezed.dart' unless Freezed is actually used.
  • Enum fields MUST have @enumerated annotation:
    @enumerated
    late MyEnum myField;
    
  • Id field: Id id = Isar.autoIncrement;
  • Index: @Index() above the field.
  • Constructor must use this.id = Isar.autoIncrement with a default value.

4. Generate Schemas

dart run build_runner build --delete-conflicting-outputs

If stale errors appear (cached), run:

flutter clean
flutter pub get
dart run build_runner build --delete-conflicting-outputs

5. Clean Architecture Structure

Variant A — Flat presentation (simpler):

lib/
├── domain/
│   └── repositories/         # Abstract interfaces only
│       ├── maintenance_repository.dart
│       └── service_task_repository.dart
├── data/
│   ├── models/               # Isar @collection classes
│   ├── datasources/local/    # isar_provider.dart
│   └── repositories/         # Implementation (Isar queries)
├── presentation/
│   └── providers/            # AsyncNotifier (not StateNotifier)
└── main.dart                 # init DB → ProviderScope override → runApp

Variant B — Feature-first + layers (CarSah File 07):

lib/
├── app/                      # MaterialApp, router, theme, locale, bootstrap
│   ├── carsah_app.dart
│   ├── app_bootstrap.dart    # Isar init, provider overrides
│   ├── app_router.dart       # go_router definitions
│   ├── app_theme.dart        # MD3 tokens
│   └── app_locale.dart       # AR/EN delegates
├── core/
│   ├── constants/
│   ├── errors/               # Typed failure classes (sealed AppFailure)
│   ├── result/               # sealed Result<T> (Success | Failure)
│   ├── localization/         # Translation maps
│   ├── validation/           # Field validators
│   └── widgets/              # Shared widgets
├── data/
│   ├── local/
│   │   ├── isar_database.dart
│   │   ├── isar_collections/ # Isar @collection models
│   │   └── mappers/          # Isar model ↔ domain entity
│   └── repositories/         # Repository implementations
├── domain/
│   ├── entities/             # Domain entities
│   ├── enums/                # planLevel, taskStatus, fluidType, etc.
│   ├── value_objects/        # Odometer, Cost, SourceLabel
│   └── repositories/         # Repository contracts (interfaces)
├── features/
│   ├── vehicle_setup/
│   │   ├── presentation/     # Screens, widgets
│   │   └── application/      # Use cases
│   ├── dashboard/
│   │   ├── presentation/
│   │   └── application/
│   ├── history/
│   │   ├── presentation/
│   │   └── application/
│   ├── mechanic_card/
│   ├── audit/
│   └── settings/
├── shared/
│   ├── design_system/        # Colors, spacing, typography tokens
│   ├── source_labels/        # Source label enum + AR/EN display
│   ├── smart_standard_plan/  # Interval definitions, task codes
│   └── terminology/          # Workshop terms
└── main.dart                 # ProviderScope → CarSahApp

Rules for Variant B:

  • Feature folders must NOT directly query Isar from UI.
  • UI talks to providers → providers call use cases → use cases call repository interfaces.
  • Repository implementations call Isar. Domain entities must not depend on Flutter widgets.
  • No backend/api/auth/cloud/QR/VIN folders in MVP.
  • Dependency direction: Presentation → Application → Domain ← Data

Full File 07 reference: references/carsah-file07-architecture.md

6. Isar Provider Pattern

// lib/data/datasources/local/isar_provider.dart
final isarProvider = Provider<Isar>((ref) {
  final existing = Isar.getInstance('db_name');
  if (existing != null) return existing;
  throw StateError('Database not initialized before runApp');
});

Future<Isar> initIsarDatabase() async {
  final existing = Isar.getInstance('db_name');
  if (existing != null) return existing;
  
  final isar = await Isar.open(
    [VehicleSchema, MaintenanceRecordSchema, ...],
    directory: (await getApplicationDocumentsDirectory()).path,
    name: 'db_name',
  );
  return isar;
}

In main.dart:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final isar = await initIsarDatabase();
  runApp(ProviderScope(
    overrides: [isarProvider.overrideWithValue(isar)],
    child: const MyApp(),
  ));
}

7. Repository Implementation Rules

  • Inject Isar via constructor: const MyRepoImpl(this.isar);
  • ALL writes must use isar.writeTxn(() async { ... }).
  • Telemetry extraction MUST run inside the same writeTxn as the primary save for atomicity.
  • deleteAll requires List<Id>, NOT Iterable<Id> → call .toList().
  • Return false on catch blocks — never throw to UI.

8. Arabic Localization (Mandatory)

If you add Locale('ar', '') to supportedLocales, you MUST also add localizationsDelegates to MaterialApp or the app crashes at runtime with No MaterialLocalizations found:

localizationsDelegates: const [
  GlobalMaterialLocalizations.delegate,
  GlobalWidgetsLocalizations.delegate,
  GlobalCupertinoLocalizations.delegate,
],

9. Assets Must Exist AND Be Registered

rootBundle.loadString('assets/oem/file.json') will crash at runtime if:

  • The file does not physically exist on disk.
  • The path is not listed under flutter.assets in pubspec.yaml. Always create the asset file FIRST, then register it:
flutter:
  assets:
    - assets/oem/

Then run flutter pub get — the asset is bundled at build time.

10. Navigation Shell Pattern (Phase 5)

class HomeRootPage extends StatefulWidget {
  @override
  State<HomeRootPage> createState() => _HomeRootPageState();
}
class _HomeRootPageState extends State<HomeRootPage> {
  int _currentIndex = 0;
  late final List<Widget> _pages = [
    const DashboardPage(),
    const TasksPage(),
    const HistoryPage(),
  ];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(index: _currentIndex, children: _pages),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (i) => setState(() => _currentIndex = i),
        destinations: const [
          NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'),
          NavigationDestination(icon: Icon(Icons.checklist_outlined), selectedIcon: Icon(Icons.checklist), label: 'Tasks'),
          NavigationDestination(icon: Icon(Icons.history_outlined), selectedIcon: Icon(Icons.history), label: 'History'),
        ],
      ),
    );
  }
}
  • IndexedStack preserves state across tabs (Riverpod state is preserved).
  • No Navigator routes needed for tab-based navigation.

11. AsyncNotifierProvider Gotcha

ref.watch(maintenanceProvider) returns AsyncValue<MaintenanceState>. ref.watch(maintenanceProvider.notifier) returns MaintenanceNotifier (no .when()). Never mix them — use .provider for state consumption, .provider.notifier for calling methods.

12. Stale Test File

flutter create . generates test/widget_test.dart referencing MyApp. Delete it immediately:

rm test/widget_test.dart

Pitfalls Summary

SymptomCauseFix
uri_does_not_exist on all packagesNo .dart_tool/package_config.jsonflutter create . then flutter pub get
Expected an identifier in build_runnerOrphaned part directivesRemove unused part 'x.freezed.dart'
Enum property must be annotated with @enumeratedIsar 3.x requires itAdd @enumerated above enum field
analyzer version conflictcustom_lint + isar_generatorRemove custom_lint, build_verify, freezed
deleteAll type errorIterable<Id> passedCall .toList() on the map
Stale build_runner errors with wrong line numbersCached asset graphflutter clean + flutter pub get
CardTheme type error in ThemeDatacardTheme expects CardThemeData? not CardThemeUse CardThemeData and AppBarThemeData
withOpacity deprecatedMaterial 3 API changeUse color.withValues(alpha: 0.12) instead
Writing files to wrong project directoryUser has multiple project rootsAlways verify cwd with pwd before file writes
No MaterialLocalizations found crash at runtimeArabic locale without delegatesAdd localizationsDelegates with 3 delegates
rootBundle.loadString crashAsset file missing or not registeredCreate file + add to flutter.assets in pubspec
.when() on notifierUsed .provider.notifier instead of .providerUse .provider for AsyncValue, .notifier for methods
MyApp not found in testStale widget_test.dart from flutter createrm test/widget_test.dart
RTL visual corruption on Arabic devicesLTR text rendered on RTL layoutPin locale in MaterialApp: locale: const Locale('en', '')
RenderViewport expected RenderSliver crashCard widget inside CustomScrollViewUse ListView for Card children, or wrap each Card in SliverToBoxAdapter
state.overdueTasks.contains(task) always falseIsar model identity — different object instancesCompare by string field: .any((ot) => ot.taskKey == task.taskKey)
DropdownButtonFormField deprecation warningvalue parameter deprecated in Flutter 3.33+Use initialValue instead
RenderViewport expected RenderSliver crashCard inside CustomScrollViewUse ListView for cards, or SliverToBoxAdapter
RTL visual corruption on Arabic devicesLTR text rendered on RTL layoutPin locale: locale: const Locale('en', '')
Save silently fails in dialog_formKey.currentState?.validate() returns nullUse ! with try/catch, show SnackBar on failure
Telemetry extraction crashes app_extractPartPricesSync called inside writeTxn but uses putSyncIsar putSync works inside transactions, but guard null/empty parts and zero cost

13. Isar writeTxn Consistency Rule (Critical)

ALL operations inside isar.writeTxn(() async { ... }) MUST use await — never call putSync() or any synchronous mutation method. Mixing sync and async operations inside an async transaction causes Isar to throw silently, and if the exception is caught, the save appears to succeed but data is incomplete.

// WRONG — putSync inside async writeTxn causes a runtime throw
await isar.writeTxn(() async {
  await isar.records.put(record);
  _extractPricesSync(isar, record); // calls putSync -> CRASH
});

// CORRECT — all operations are async with await
await isar.writeTxn(() async {
  await isar.records.put(record);
  await _extractPricesAsync(isar, record); // calls await put() -> atomic
});

This bug is especially dangerous because:

  • The exception is caught by a surrounding try/catch
  • The UI gets a generic "save failed" error
  • No data is partially saved — it's all-or-nothing
  • dart analyze catches nothing — it's a runtime-only failure

Hard rule: If you're inside writeTxn(() async { }), EVERY Isar write must be await isar.xyz.put(...).

14. Batch Save Pattern (Multi-Select Checklist)

For forms where users log multiple items at once (e.g. maintenance batch), use a dynamic checklist instead of one-at-a-time entry:

// State: track which tasks are selected, one cost controller per task
final Set<String> _selectedTasks = {};
final Map<String, TextEditingController> _costControllers = {};

// When task is checked, show inline cost TextField
CheckboxListTile(
  title: Text(task.displayNameEn), // Use human-readable name
  value: _selectedTasks.contains(task.taskKey),
  onChanged: (v) => setState(() => 
    v == true ? _selectedTasks.add(task.taskKey) : _selectedTasks.remove(task.taskKey)
  ),
)
// If checked, show cost input below
if (_selectedTasks.contains(task.taskKey))
  TextFormField(controller: _costControllers[task.taskKey], ...)

// Save: loop through selected tasks independently
for (final taskKey in _selectedTasks) {
  final cost = double.tryParse(_sanitizeDigits(_costControllers[taskKey]!.text.trim())) ?? 0.0;
  
  final record = MaintenanceRecord(
    vehicleId: vehicleId,
    serviceType: taskMap[taskKey], // displayNameEn, NOT taskKey!
    totalCostSar: cost,
    partsReplaced: [taskMap[taskKey]],
    ...
  );
  
  await ref.read(maintenanceProvider.notifier).addRecord(record);
  await ref.read(serviceTaskProvider.notifier).markTaskCompleted(taskKey: taskKey, doneAtKm: odometer);
}

Key rules:

  • Service type = task.displayNameEn (readable), NOT task.taskKey (programmatic).
  • Never hardcode service lists — read from the live provider.
  • Track savedCount vs failedCount — report partial failures to user.
  • Each task gets its own TextEditingController disposed in dispose().

15. Arabic Numeral Sanitization

On Arabic-locale devices, TextField input may contain Arabic-Indic digits (٠-٩, Unicode \u0660-\u0669) instead of ASCII (0-9). int.tryParse("١٢٣") returns null silently.

static final _arabicDigits = {
  '\u0660': '0', '\u0661': '1', '\u0662': '2',
  '\u0663': '3', '\u0664': '4', '\u0665': '5',
  '\u0666': '6', '\u0667': '7', '\u0668': '8',
  '\u0669': '9',
};

String _sanitizeDigits(String input) {
  for (final entry in _arabicDigits.entries) {
    input = input.replaceAll(entry.key, entry.value);
  }
  return input;
}

// Use before any parse:
final cost = double.tryParse(_sanitizeDigits(rawText.trim())) ?? 0.0;
final odometer = int.tryParse(_sanitizeDigits(rawText.trim().replaceAll(',', ''))) ?? 0;

Pitfalls Summary

SymptomCauseFix
uri_does_not_exist on all packagesNo .dart_tool/package_config.jsonflutter create . then flutter pub get
Expected an identifier in build_runnerOrphaned part directivesRemove unused part 'x.freezed.dart'
Enum property must be annotated with @enumeratedIsar 3.x requires itAdd @enumerated above enum field
analyzer version conflictcustom_lint + isar_generatorRemove custom_lint, build_verify, freezed
deleteAll type errorIterable<Id> passedCall .toList() on the map
Stale build_runner errors with wrong line numbersCached asset graphflutter clean + flutter pub get
CardTheme type error in ThemeDatacardTheme expects CardThemeData? not CardThemeUse CardThemeData and AppBarThemeData
withOpacity deprecatedMaterial 3 API changeUse color.withValues(alpha: 0.12) instead
Writing files to wrong project directoryUser has multiple project rootsAlways verify cwd with pwd before file writes
No MaterialLocalizations found crash at runtimeArabic locale without delegatesAdd localizationsDelegates with 3 delegates
rootBundle.loadString crashAsset file missing or not registeredCreate file + add to flutter.assets in pubspec
.when() on notifierUsed .provider.notifier instead of .providerUse .provider for AsyncValue, .notifier for methods
MyApp not found in testStale widget_test.dart from flutter createrm test/widget_test.dart
RTL visual corruption on Arabic devicesLTR text rendered on RTL layoutPin locale: locale: const Locale('en', '')
RenderViewport expected RenderSliver crashCard widget inside CustomScrollViewUse ListView for Card children, or wrap each Card in SliverToBoxAdapter
state.overdueTasks.contains(task) always falseIsar model identity — different object instancesCompare by string field: .any((ot) => ot.taskKey == task.taskKey)
DropdownButtonFormField deprecation warningvalue parameter deprecated in Flutter 3.33+Use initialValue instead
Save silently fails (writeTxn)putSync() called inside async writeTxnUse await put(), never putSync() inside async txn
Service type shows oil_change to userUsed task.taskKey instead of displayNameEnUse task.displayNameEn as serviceType value
int.tryParse returns null on valid inputArabic-Indic numerals (٠-٩) in TextFieldSanitize with _sanitizeDigits() before parsing
Partial save not trackedOnly checked success booleanTrack savedCount and failedCount independently
App crashes immediately on Android launch (no splash)namespaceMainActivity.kt package → ClassNotFoundExceptionUnify all to same package (see §16c)
App crashes after showing splashProGuard stripped Isar adapter classesisMinifyEnabled = false in release buildType (see §16a)
CI build passes, APK installs but crashesisar_flutter_libs manifest has package attrAdd gradle.projectsEvaluated hook (see §16b)
AAPT: error: resource android:attr/lStar not found during verifyReleaseResourcesIsar 3.1.0 targets AGP 4.2 resources; lStar attribute unresolved under AGP 8.8+Defer Isar until a maintained AGP 8+ fork is available or use an alternative (see §16d)

16. Android Release Build Rules (Isar-Specific)

These three rules are MANDATORY. Skipping any one causes either CI failure, crash-on-launch, or both.

16a. ProGuard — isMinifyEnabled = false

Isar generates adapter classes. ProGuard strips them because nothing in Java/Kotlin code directly references them. Isar.open() crashes at runtime.

// android/app/build.gradle.kts — release block
release {
    isMinifyEnabled = false  // REQUIRED for Isar
    isShrinkResources = false
}

16b. AGP 8.8+ — isar_flutter_libs Manifest Fix

isar_flutter_libs ships an AndroidManifest with package="..." — rejected by AGP 8.8+ on GitHub Actions ubuntu-latest. Add to root android/build.gradle.kts:

gradle.projectsEvaluated {
    subprojects {
        if (name == "isar_flutter_libs") {
            tasks.matching { it.name.contains("verifyReleaseResources") }.configureEach {
                enabled = false
            }
            tasks.matching { it.name.startsWith("process") && it.name.contains("Manifest") }.configureEach {
                doFirst {
                    val manifestFile = file("${project.projectDir}/src/main/AndroidManifest.xml")
                    if (manifestFile.exists()) {
                        val content = manifestFile.readText()
                        if (content.contains("package=")) {
                            manifestFile.writeText(content.replace(Regex("""package="[^"]*"\s*"""), ""))
                        }
                    }
                }
            }
        }
    }
}

16c. Namespace-Verification (Post-Swarm / Pre-Release)

AndroidManifest's android:name=".MainActivity" resolves relative to Gradle namespace. Mismatch → ClassNotFoundException → instant crash.

# These MUST match:
grep 'namespace' android/app/build.gradle.kts
grep '^package' android/app/src/main/kotlin/**/MainActivity.kt

Also verify these references are identical:

  • android/app/build.gradle.kts: namespace + applicationId
  • android/build.gradle.kts: subproject namespace injection (line ~30)
  • android/app/proguard-rules.pro: -keep class line

16d. AGP Resource Incompatibility — lStar AAPT Error (Azal DEC-015)

Even with the manifest fix (§16b) applied, Isar 3.1.0 may still fail release builds with:

AAPT: error: resource android:attr/lStar not found

Root cause: Isar 3.1.0 was compiled against AGP 4.2. Its resources reference attributes (lStar was introduced in API 29) that AGP 8.8+ resolves differently. Disabling verifyReleaseResources (which §16b already does) masks this in some cases but the underlying resource incompatibility persists.

⚠️ The pub-cache anti-pattern: Patching ~/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/android/build.gradle to add a namespace line makes the build work on ONE machine. This is NOT reproducible on any other machine, CI runner, or teammate's environment. Never claim a pub-cache patch as a "fix" — it's a local hack.

When to use Isar: Isar 3.1.0 is fundamentally incompatible with AGP 8.8+. Defer Isar until:

  1. The project actually needs local caching (offline storage, draft persistence).
  2. A maintained AGP 8+ compatible Isar fork exists, OR an alternative (drift, hive_ce) has been evaluated.

Verification gate: If Isar is in pubspec.yaml, flutter build apk --release MUST succeed without any pub-cache patches. If it fails, the correct fix is removing Isar — not patching the cache.

Also update the AGP compat hook (§16b) description: even with verifyReleaseResources disabled and the package attribute stripped, the lStar resource error proves that the Isar 3.1.0 binary is incompatible with AGP 8.8+ at the resource level. The hooks in §16a/16b are necessary but NOT sufficient for a working release build with Isar 3.1.0.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.