Files
Krow-workspace/docs/MOBILE/03-data-connect-connectors-pattern.md

8.2 KiB

Data Connect Connectors Pattern

Overview

This document describes the Data Connect Connectors pattern implemented in the KROW mobile app. This pattern centralizes all backend query logic by mirroring backend connector structure in the mobile data layer.

Problem Statement

Without Connectors Pattern:

  • Each feature creates its own repository implementation
  • Multiple features query the same backend connector → duplication
  • When backend queries change, updates needed in multiple places
  • No reusability across features

Example Problem:

staff_main/
  └── data/repositories/profile_completion_repository_impl.dart ← queries staff connector
profile/
  └── data/repositories/profile_repository_impl.dart ← also queries staff connector
onboarding/
  └── data/repositories/personal_info_repository_impl.dart ← also queries staff connector

Solution: Connectors in Data Connect Package

All backend connector queries are implemented once in a centralized location, following the backend structure.

Structure

apps/mobile/packages/data_connect/lib/src/connectors/
├── staff/
│   ├── domain/
│   │   ├── repositories/
│   │   │   └── staff_connector_repository.dart (interface)
│   │   └── usecases/
│   │       └── get_profile_completion_usecase.dart
│   └── data/
│       └── repositories/
│           └── staff_connector_repository_impl.dart (implementation)
├── order/
├── user/
├── emergency_contact/
└── ...

Maps to backend structure:

backend/dataconnect/connector/
├── staff/
├── order/
├── user/
├── emergency_contact/
└── ...

Clean Architecture Layers

Each connector follows Clean Architecture with three layers:

Domain Layer (connectors/{name}/domain/)

Repository Interface:

// staff_connector_repository.dart
abstract interface class StaffConnectorRepository {
  Future<bool> getProfileCompletion();
  Future<Staff> getStaffById(String id);
  // ... more queries
}

Use Cases:

// get_profile_completion_usecase.dart
class GetProfileCompletionUseCase {
  GetProfileCompletionUseCase({required StaffConnectorRepository repository});
  Future<bool> call() => _repository.getProfileCompletion();
}

Characteristics:

  • Pure Dart, no framework dependencies
  • Stable, business-focused contracts
  • One interface per connector
  • One use case per query or related query group

Data Layer (connectors/{name}/data/)

Repository Implementation:

// staff_connector_repository_impl.dart
class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
  final DataConnectService _service;

  @override
  Future<bool> getProfileCompletion() async {
    return _service.run(() async {
      final staffId = await _service.getStaffId();
      final response = await _service.connector
          .getStaffProfileCompletion(id: staffId)
          .execute();
      
      return _isProfileComplete(response);
    });
  }
}

Characteristics:

  • Implements domain repository interface
  • Uses DataConnectService to execute queries
  • Maps backend response types to domain models
  • Contains mapping/transformation logic only
  • Handles type safety with generated Data Connect types

Integration Pattern

Step 1: Feature Needs Data

Feature (e.g., staff_main) needs profile completion status.

Step 2: Use Connector Repository

Instead of creating a local repository, feature uses the connector:

// staff_main_module.dart
class StaffMainModule extends Module {
  @override
  void binds(Injector i) {
    // Register connector repository from data_connect
    i.addSingleton<StaffConnectorRepository>(
      StaffConnectorRepositoryImpl.new,
    );
    
    // Feature creates its own use case wrapper if needed
    i.addSingleton(
      () => GetProfileCompletionUseCase(
        repository: i.get<StaffConnectorRepository>(),
      ),
    );
    
    // BLoC uses the use case
    i.addSingleton(
      () => StaffMainCubit(
        getProfileCompletionUsecase: i.get<GetProfileCompletionUseCase>(),
      ),
    );
  }
}

Step 3: BLoC Uses It

class StaffMainCubit extends Cubit<StaffMainState> {
  StaffMainCubit({required GetProfileCompletionUseCase usecase}) {
    _loadProfileCompletion();
  }
  
  Future<void> _loadProfileCompletion() async {
    final isComplete = await _getProfileCompletionUsecase();
    emit(state.copyWith(isProfileComplete: isComplete));
  }
}

Export Pattern

Connectors are exported from krow_data_connect for easy access:

// lib/krow_data_connect.dart
export 'src/connectors/staff/domain/repositories/staff_connector_repository.dart';
export 'src/connectors/staff/domain/usecases/get_profile_completion_usecase.dart';
export 'src/connectors/staff/data/repositories/staff_connector_repository_impl.dart';

Features import:

import 'package:krow_data_connect/krow_data_connect.dart';

Adding New Queries to Existing Connector

When backend adds getStaffById() query to staff connector:

  1. Add to interface:

    abstract interface class StaffConnectorRepository {
      Future<Staff> getStaffById(String id);
    }
    
  2. Implement in repository:

    @override
    Future<Staff> getStaffById(String id) async {
      return _service.run(() async {
        final response = await _service.connector
            .getStaffById(id: id)
            .execute();
        return _mapToStaff(response.data.staff);
      });
    }
    
  3. Use in features:

    // Any feature can now use it
    final staff = await i.get<StaffConnectorRepository>().getStaffById(id);
    

Adding New Connector

When backend adds new connector (e.g., order):

  1. Create directory: apps/mobile/packages/data_connect/lib/src/connectors/order/

  2. Create domain layer with repository interface and use cases

  3. Create data layer with repository implementation

  4. Export from krow_data_connect.dart

  5. Features can immediately start using it

Benefits

No Duplication - Query implemented once, used by many features
Single Source of Truth - Backend change → update one place
Clean Separation - Connector logic separate from feature logic
Reusability - Any feature can request any connector data
Testability - Mock the connector repo to test features
Scalability - Easy to add new connectors as backend grows
Mirrors Backend - Mobile structure mirrors backend structure

Anti-Patterns

DON'T: Implement query logic in feature repository DON'T: Duplicate queries across multiple repositories
DON'T: Put mapping logic in features
DON'T: Call DataConnectService directly from BLoCs

DO: Use connector repositories through use cases in features.

Current Implementation

Staff Connector

Location: apps/mobile/packages/data_connect/lib/src/connectors/staff/

Available Queries:

  • getProfileCompletion() - Returns bool indicating if profile is complete
    • Checks: personal info, emergency contacts, tax forms, experience (skills/industries)

Used By:

  • staff_main - Guards bottom nav items requiring profile completion

Backend Queries Used:

  • backend/dataconnect/connector/staff/queries/profile_completion.gql

Shifts Connector

Location: apps/mobile/packages/data_connect/lib/src/connectors/shifts/

Available Queries:

  • listShiftRolesByVendorId() - Fetches shifts for a specific vendor with status mapping
  • applyForShifts() - Handles shift application with error tracking

Backend Queries Used:

  • backend/dataconnect/connector/shifts/queries/list_shift_roles_by_vendor.gql
  • backend/dataconnect/connector/shifts/mutations/apply_for_shifts.gql

Future Expansion

As the app grows, additional connectors will be added:

  • order_connector_repository (queries from backend/dataconnect/connector/order/)
  • user_connector_repository (queries from backend/dataconnect/connector/user/)
  • emergency_contact_connector_repository (queries from backend/dataconnect/connector/emergencyContact/)
  • etc.

Each following the same Clean Architecture pattern implemented for Staff Connector.