Files
Krow-workspace/.claude/skills/krow-mobile-development-rules/SKILL.md

18 KiB

name, description
name description
krow-mobile-development-rules Enforce KROW mobile app development standards including file structure, naming conventions, logic placement boundaries, localization, Data Connect integration, and prototype migration rules. Use this skill whenever working on KROW Flutter mobile features, creating new packages, implementing BLoCs, integrating with backend, or migrating from prototypes. Critical for maintaining clean architecture and preventing architectural degradation.

KROW Mobile Development Rules

These rules are NON-NEGOTIABLE enforcement guidelines for the KROW mobile application. They prevent architectural degradation and ensure consistency across the codebase.

When to Use This Skill

  • Creating new mobile features or packages
  • Implementing BLoCs, Use Cases, or Repositories
  • Integrating with Firebase Data Connect backend
  • Migrating code from prototypes
  • Reviewing mobile code for compliance
  • Setting up new feature modules
  • Handling user sessions and authentication
  • Implementing navigation flows

1. File Creation & Package Structure

Feature-First Packaging

DO:

  • Create new features as independent packages:
    apps/mobile/packages/features/<app_name>/<feature_name>/
    ├── lib/
    │   ├── src/
    │   │   ├── domain/
    │   │   │   ├── repositories/
    │   │   │   └── usecases/
    │   │   ├── data/
    │   │   │   └── repositories_impl/
    │   │   └── presentation/
    │   │       ├── blocs/
    │   │       ├── pages/
    │   │       └── widgets/
    │   └── <feature_name>.dart  # Barrel file
    └── pubspec.yaml
    

DON'T:

  • Add features to apps/mobile/packages/core directly
  • Create files in app directories (apps/mobile/apps/client/ or apps/mobile/apps/staff/)
  • Create cross-feature or cross-app dependencies (features must not import other features)

Path Conventions (Strict)

Follow these exact paths:

Layer Path Pattern Example
Entities apps/mobile/packages/domain/lib/src/entities/<entity>.dart user.dart, shift.dart
Repository Interface .../features/<app>/<feature>/lib/src/domain/repositories/<name>_repository_interface.dart auth_repository_interface.dart
Repository Impl .../features/<app>/<feature>/lib/src/data/repositories_impl/<name>_repository_impl.dart auth_repository_impl.dart
Use Cases .../features/<app>/<feature>/lib/src/application/<name>_usecase.dart login_usecase.dart
BLoCs .../features/<app>/<feature>/lib/src/presentation/blocs/<name>_bloc.dart auth_bloc.dart
Pages .../features/<app>/<feature>/lib/src/presentation/pages/<name>_page.dart login_page.dart
Widgets .../features/<app>/<feature>/lib/src/presentation/widgets/<name>_widget.dart password_field.dart

Barrel Files

DO:

// lib/auth_feature.dart
export 'src/presentation/pages/login_page.dart';
export 'src/domain/repositories/auth_repository_interface.dart';
// Only export PUBLIC API

DON'T:

// Don't export internal implementation details
export 'src/data/repositories_impl/auth_repository_impl.dart';
export 'src/presentation/blocs/auth_bloc.dart';

2. Naming Conventions (Dart Standard)

Type Convention Example File Name
Files snake_case user_profile_page.dart -
Classes PascalCase UserProfilePage -
Variables camelCase userProfile -
Interfaces End with Interface AuthRepositoryInterface auth_repository_interface.dart
Implementations End with Impl AuthRepositoryImpl auth_repository_impl.dart
BLoCs End with Bloc or Cubit AuthBloc, ProfileCubit auth_bloc.dart
Use Cases End with UseCase LoginUseCase login_usecase.dart

3. Logic Placement (Zero Tolerance Boundaries)

Business Rules → Use Cases ONLY

CORRECT:

// login_usecase.dart
class LoginUseCase extends UseCase<User, LoginParams> {
  @override
  Future<Either<Failure, User>> call(LoginParams params) async {
    // Business logic here: validation, transformation, orchestration
    if (params.email.isEmpty) {
      return Left(ValidationFailure('Email required'));
    }
    return await repository.login(params);
  }
}

FORBIDDEN:

// ❌ Business logic in BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  on<LoginRequested>((event, emit) {
    if (event.email.isEmpty) {  // ← NO! This is business logic
      emit(AuthError('Email required'));
    }
  });
}

// ❌ Business logic in Widget
class LoginPage extends StatelessWidget {
  void _login() {
    if (_emailController.text.isEmpty) {  // ← NO! This is business logic
      showSnackbar('Email required');
    }
  }
}

State Logic → BLoCs ONLY

CORRECT:

// auth_bloc.dart
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  on<LoginRequested>((event, emit) async {
    emit(AuthLoading());
    final result = await loginUseCase(LoginParams(email: event.email));
    result.fold(
      (failure) => emit(AuthError(failure)),
      (user) => emit(AuthAuthenticated(user)),
    );
  });
}

// login_page.dart (StatelessWidget)
class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<AuthBloc, AuthState>(
      builder: (context, state) {
        if (state is AuthLoading) return LoadingIndicator();
        if (state is AuthError) return ErrorWidget(state.message);
        return LoginForm();
      },
    );
  }
}

FORBIDDEN:

// ❌ setState in Pages for complex state
class LoginPage extends StatefulWidget {
  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  bool _isLoading = false;  // ← NO! Use BLoC
  String? _error;          // ← NO! Use BLoC
  
  void _login() {
    setState(() => _isLoading = true);  // ← NO! Use BLoC
  }
}

RECOMMENDATION: Pages should be StatelessWidget with state delegated to BLoCs.

Data Transformation → Repositories

CORRECT:

// profile_repository_impl.dart
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
  @override
  Future<Staff> getProfile(String id) async {
    final response = await dataConnect.getStaffById(id: id).execute();
    // Data transformation happens here
    return Staff(
      id: response.data.staff.id,
      name: response.data.staff.name,
      // Map Data Connect model to Domain entity
    );
  }
}

FORBIDDEN:

// ❌ JSON parsing in UI
class ProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final json = jsonDecode(response.body);  // ← NO!
    final name = json['name'];
  }
}

// ❌ JSON parsing in Domain Use Case
class GetProfileUseCase extends UseCase<Staff, String> {
  @override
  Future<Either<Failure, Staff>> call(String id) async {
    final response = await http.get('/staff/$id');
    final json = jsonDecode(response.body);  // ← NO!
  }
}

Navigation → Flutter Modular + Safe Extensions

CORRECT:

// Use Safe Navigation Extensions
import 'package:krow_core/krow_core.dart';

// In widget/BLoC:
Modular.to.safePush('/profile');
Modular.to.safeNavigate('/home');
Modular.to.popSafe();

// Even better: Use Typed Navigators
Modular.to.toStaffHome();  // Defined in StaffNavigator
Modular.to.toShiftDetails(shiftId: '123');

FORBIDDEN:

// ❌ Direct Navigator.push
Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => ProfilePage()),
);

// ❌ Direct Modular navigation without safety
Modular.to.navigate('/profile');  // ← Can cause blank screens
Modular.to.pop();  // ← Can crash if stack is empty

PATTERN: All navigation MUST have fallback to Home page. Safe extensions automatically handle this.

Session Management → DataConnectService + SessionHandlerMixin

CORRECT:

// In main.dart:
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize session listener (pick allowed roles for app)
  DataConnectService.instance.initializeAuthListener(
    allowedRoles: ['STAFF', 'BOTH'],  // for staff app
  );
  
  runApp(
    SessionListener(  // Wraps entire app
      child: ModularApp(module: AppModule(), child: AppWidget()),
    ),
  );
}

// In repository:
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
  final DataConnectService _service = DataConnectService.instance;
  
  @override
  Future<Staff> getProfile(String id) async {
    // _service.run() handles:
    // - Auth validation
    // - Token refresh (if <5 min to expiry)
    // - Error handling with 3 retries
    return await _service.run(() async {
      final response = await _service.connector
          .getStaffById(id: id)
          .execute();
      return _mapToStaff(response.data.staff);
    });
  }
}

PATTERN:

  • SessionListener widget wraps app and shows dialogs for session errors
  • SessionHandlerMixin in DataConnectService provides automatic token refresh
  • 3-attempt retry logic with exponential backoff (1s → 2s → 4s)
  • Role validation configurable per app

4. Localization Integration (core_localization)

All user-facing text MUST be localized.

String Management

CORRECT:

// In presentation layer:
import 'package:core_localization/core_localization.dart';

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(context.strings.loginButton);  // ← From localization
    return ElevatedButton(
      onPressed: _login,
      child: Text(context.strings.submit),
    );
  }
}

FORBIDDEN:

// ❌ Hardcoded English strings
Text('Login')
Text('Submit')
ElevatedButton(child: Text('Click here'))

BLoC Integration

CORRECT:

// BLoCs emit domain failures (not localized strings)
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  on<LoginRequested>((event, emit) async {
    final result = await loginUseCase(params);
    result.fold(
      (failure) => emit(AuthError(failure)),  // ← Domain failure
      (user) => emit(AuthAuthenticated(user)),
    );
  });
}

// UI translates failures to user-friendly messages
class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<AuthBloc, AuthState>(
      builder: (context, state) {
        if (state is AuthError) {
          final message = ErrorTranslator.translate(
            state.failure,
            context.strings,
          );
          return ErrorWidget(message);  // ← Localized
        }
      },
    );
  }
}

App Setup

Apps must import LocalizationModule():

// app_module.dart
class AppModule extends Module {
  @override
  List<Module> get imports => [
    LocalizationModule(),  // ← Required
    DataConnectModule(),
  ];
}

// main.dart
runApp(
  BlocProvider<LocaleBloc>(  // ← Expose locale state
    create: (_) => Modular.get<LocaleBloc>(),
    child: TranslationProvider(  // ← Enable context.strings
      child: MaterialApp.router(...),
    ),
  ),
);

5. Data Connect Integration

All backend access goes through DataConnectService.

Repository Pattern

Step 1: Define interface in feature domain:

// domain/repositories/profile_repository_interface.dart
abstract interface class ProfileRepositoryInterface {
  Future<Staff> getProfile(String id);
  Future<bool> updateProfile(Staff profile);
}

Step 2: Implement using DataConnectService.run():

// data/repositories_impl/profile_repository_impl.dart
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
  final DataConnectService _service = DataConnectService.instance;
  
  @override
  Future<Staff> getProfile(String id) async {
    return await _service.run(() async {
      final response = await _service.connector
          .getStaffById(id: id)
          .execute();
      return _mapToStaff(response.data.staff);
    });
  }
}

Benefits of _service.run():

  • Automatic auth validation
  • Token refresh if needed
  • 3-attempt retry with exponential backoff
  • Consistent error handling

Session Store Pattern

After successful auth, populate session stores:

// For Staff App:
StaffSessionStore.instance.setSession(
  StaffSession(
    user: user,
    staff: staff,
    ownerId: ownerId,
  ),
);

// For Client App:
ClientSessionStore.instance.setSession(
  ClientSession(
    user: user,
    business: business,
  ),
);

Lazy Loading: If session is null, fetch via getStaffById() or getBusinessById() and update store.

6. Prototype Migration Rules

When migrating from prototypes/:

MAY Copy

  • Icons, images, assets (but match to design system)
  • build methods for UI layout structure
  • Screen flow and navigation patterns

MUST REJECT & REFACTOR

  • GetX, Provider, or MVC patterns
  • Any state management not using BLoC
  • Direct HTTP calls (must use Data Connect)
  • Hardcoded colors/typography (must use design system)
  • Global state variables
  • Navigation without Modular

Colors & Typography Migration

When matching POC to production:

  1. Find closest color in UiColors (don't add new colors without approval)
  2. Find closest text style in UiTypography
  3. Use design system constants, NOT POC hardcoded values

DO NOT change the design system itself. Colors and typography are FINAL. Match your feature to the system, not the other way around.

7. Handling Ambiguity

If requirements are unclear:

  1. STOP - Don't guess domain fields or workflows
  2. ANALYZE - Refer to:
    • Architecture: apps/mobile/docs/01-architecture-principles.md
    • Design System: apps/mobile/docs/02-design-system-usage.md
    • Existing features for patterns
  3. DOCUMENT - Add // ASSUMPTION: <explanation> if you must proceed
  4. ASK - Prefer asking user for clarification on business rules

8. Dependencies

DO NOT

  • Add 3rd party packages without checking apps/mobile/packages/core first
  • Add firebase_auth or firebase_data_connect to Feature packages (they belong in data_connect only)
  • Use addSingleton for BLoCs (always use add method in Modular)

DO

  • Use DataConnectService.instance for backend operations
  • Use Flutter Modular for dependency injection
  • Register BLoCs with i.addSingleton<CubitType>(() => CubitType(...))
  • Register Use Cases as factories or singletons as needed

9. Error Handling Pattern

Domain Failures

// domain/failures/auth_failure.dart
abstract class AuthFailure extends Failure {
  const AuthFailure(String message) : super(message);
}

class InvalidCredentialsFailure extends AuthFailure {
  const InvalidCredentialsFailure() : super('Invalid credentials');
}

Repository Error Mapping

// Map Data Connect exceptions to Domain failures
try {
  final response = await dataConnect.query();
  return Right(response);
} on DataConnectException catch (e) {
  if (e.message.contains('unauthorized')) {
    return Left(InvalidCredentialsFailure());
  }
  return Left(ServerFailure(e.message));
}

UI Feedback

// BLoC emits error state
emit(AuthError(failure));

// UI shows user-friendly message
if (state is AuthError) {
  final message = ErrorTranslator.translate(state.failure, context.strings);
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(message)),
  );
}

Session Errors

SessionListener automatically shows dialogs for:

  • Session expiration
  • Token refresh failures
  • Network errors during auth

10. Testing Requirements

Unit Tests

// Test use cases with real repository implementations
test('login with valid credentials returns user', () async {
  final useCase = LoginUseCase(repository: mockRepository);
  final result = await useCase(LoginParams(email: 'test@test.com'));
  expect(result.isRight(), true);
});

Widget Tests

// Test UI widgets and BLoC interactions
testWidgets('shows loading indicator when logging in', (tester) async {
  await tester.pumpWidget(
    BlocProvider<AuthBloc>(
      create: (_) => authBloc,
      child: LoginPage(),
    ),
  );
  
  authBloc.add(LoginRequested(email: 'test@test.com'));
  await tester.pump();
  
  expect(find.byType(LoadingIndicator), findsOneWidget);
});

Integration Tests

  • Test full feature flows end-to-end with Data Connect
  • Use dependency injection to swap implementations if needed

11. Clean Code Principles

Documentation

  • Add human readable doc comments for dartdoc for all classes and methods.
/// Authenticates user with email and password.
/// 
/// Returns [User] on success or [AuthFailure] on failure.
/// Throws [NetworkException] if connection fails.
class LoginUseCase extends UseCase<User, LoginParams> {
  // ...
}

Single Responsibility

  • Keep methods focused on one task
  • Extract complex logic to separate methods
  • Keep widget build methods concise
  • Extract complex widgets to separate files

Meaningful Names

// ✅ GOOD
final isProfileComplete = await checkProfileCompletion();
final userShifts = await fetchUserShifts();

// ❌ BAD
final flag = await check();
final data = await fetch();

Enforcement Checklist

Before merging any mobile feature code:

Architecture Compliance

  • Feature follows package structure (domain/data/presentation)
  • No business logic in BLoCs or Widgets
  • All state management via BLoCs
  • All backend access via repositories
  • Session accessed via SessionStore, not global state
  • Navigation uses Flutter Modular safe extensions
  • No feature-to-feature imports

Code Quality

  • No hardcoded strings (use localization)
  • No hardcoded colors/typography (use design system)
  • All spacing uses UiConstants
  • Doc comments on public APIs
  • Meaningful variable names
  • Zero analyzer warnings

Integration

  • Data Connect queries via _service.run()
  • Error handling with domain failures
  • Proper dependency injection in modules

Summary

The key principle: Clean Architecture with zero tolerance for violations. Business logic in Use Cases, state in BLoCs, data access in Repositories, UI in Widgets. Features are isolated, backend is centralized, localization is mandatory, and design system is immutable.

When in doubt, refer to existing features following these patterns or ask for clarification. It's better to ask than to introduce architectural debt.