Created comprehensive skills covering development rules, architecture, design system, release process, and Data Connect patterns. Total 3,935 lines extracted from mobile documentation.
25 KiB
name, description
| name | description |
|---|---|
| krow-mobile-data-connect | KROW Data Connect connectors pattern for centralized backend query management. Use when integrating backend queries, creating connector repositories, adding queries to existing connectors, implementing feature repositories, preventing query duplication, or understanding Clean Architecture data layer. Covers connector structure, repository pattern, feature integration, and benefits over feature-specific repositories. |
KROW Mobile Data Connect Connectors Pattern
This skill describes the Data Connect Connectors pattern used in KROW mobile apps to centralize all backend query logic by mirroring backend connector structure.
When to Use This Skill
- Integrating backend queries into mobile features
- Creating new connector repositories
- Adding queries to existing connectors
- Understanding data layer architecture
- Preventing duplicate backend queries
- Implementing feature repositories that use connectors
- Refactoring feature-specific queries to connectors
- Debugging Data Connect integration issues
- Understanding session management and token refresh
Problem Statement
Without Connectors Pattern
Each feature creates its own repository implementation, leading to:
❌ Query Duplication:
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
Issues:
- Multiple features query the same backend connector
- When backend queries change, updates needed in multiple places
- No reusability across features
- Code duplication and maintenance burden
With Connectors Pattern
All backend connector queries implemented once:
✅ Centralized:
data_connect/
└── connectors/
└── staff/
├── domain/
│ ├── repositories/staff_connector_repository.dart
│ └── usecases/get_profile_completion_usecase.dart
└── data/
└── repositories/staff_connector_repository_impl.dart
# Features use connector repositories
staff_main/ → uses StaffConnectorRepository
profile/ → uses StaffConnectorRepository
onboarding/ → uses StaffConnectorRepository
Benefits:
- ✅ Single implementation per query
- ✅ Reused across all features
- ✅ Backend change → update one place
- ✅ No duplication
1. Connector Structure
Package Organization
Location: apps/mobile/packages/data_connect/lib/src/connectors/
Structure:
data_connect/lib/src/connectors/
├── staff/ # Mirrors backend/dataconnect/connector/staff/
│ ├── domain/
│ │ ├── repositories/
│ │ │ └── staff_connector_repository.dart # Interface
│ │ └── usecases/
│ │ ├── get_profile_completion_usecase.dart
│ │ └── get_staff_by_id_usecase.dart
│ └── data/
│ └── repositories/
│ └── staff_connector_repository_impl.dart # Implementation
├── order/ # Mirrors backend/dataconnect/connector/order/
│ ├── domain/
│ │ ├── repositories/
│ │ │ └── order_connector_repository.dart
│ │ └── usecases/
│ └── data/
│ └── repositories/
│ └── order_connector_repository_impl.dart
├── shifts/ # Mirrors backend/dataconnect/connector/shifts/
│ ├── domain/
│ │ ├── repositories/
│ │ │ └── shifts_connector_repository.dart
│ │ └── usecases/
│ │ ├── list_shifts_usecase.dart
│ │ └── apply_for_shifts_usecase.dart
│ └── data/
│ └── repositories/
│ └── shifts_connector_repository_impl.dart
└── user/ # Mirrors backend/dataconnect/connector/user/
├── domain/
└── data/
Mirroring Backend:
backend/dataconnect/connector/
├── staff/
│ ├── queries/
│ │ └── profile_completion.gql
│ └── mutations/
├── order/
├── shifts/
│ ├── queries/
│ │ └── list_shift_roles_by_vendor.gql
│ └── mutations/
│ └── apply_for_shifts.gql
└── user/
Key Principle: Mobile connector structure mirrors backend connector structure.
2. Clean Architecture in Connectors
Each connector follows Clean Architecture with three layers.
Domain Layer (connectors/{name}/domain/)
Repository Interface:
Define contract (what operations are available):
// staff_connector_repository.dart
abstract interface class StaffConnectorRepository {
/// Returns true if staff profile is complete.
///
/// Checks: personal info, emergency contacts, tax forms, experience.
Future<bool> getProfileCompletion();
/// Fetches staff entity by ID.
///
/// Returns Staff entity or throws exception if not found.
Future<Staff> getStaffById(String id);
/// Updates staff profile.
Future<void> updateStaff(Staff staff);
}
Use Cases:
One use case per query or related query group:
// get_profile_completion_usecase.dart
class GetProfileCompletionUseCase extends UseCase<bool, NoParams> {
final StaffConnectorRepository _repository;
GetProfileCompletionUseCase({
required StaffConnectorRepository repository,
}) : _repository = repository;
@override
Future<Either<Failure, bool>> call(NoParams params) async {
try {
final result = await _repository.getProfileCompletion();
return Right(result);
} on DataConnectException catch (e) {
return Left(ServerFailure(e.message));
}
}
}
// get_staff_by_id_usecase.dart
class GetStaffByIdUseCase extends UseCase<Staff, String> {
final StaffConnectorRepository _repository;
GetStaffByIdUseCase({
required StaffConnectorRepository repository,
}) : _repository = repository;
@override
Future<Either<Failure, Staff>> call(String staffId) async {
try {
final staff = await _repository.getStaffById(staffId);
return Right(staff);
} on DataConnectException catch (e) {
return Left(ServerFailure(e.message));
}
}
}
Characteristics:
- Pure Dart (no Flutter dependencies)
- Stable, business-focused contracts
- One interface per connector domain
- One use case per query or logical query group
Data Layer (connectors/{name}/data/)
Repository Implementation:
Implements domain interface using DataConnectService:
// staff_connector_repository_impl.dart
class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
final DataConnectService _service;
StaffConnectorRepositoryImpl({
DataConnectService? service,
}) : _service = service ?? DataConnectService.instance;
@override
Future<bool> getProfileCompletion() async {
return await _service.run(() async {
// Get current staff ID from session
final staffId = await _service.getStaffId();
// Execute Data Connect query
final response = await _service.connector
.getStaffProfileCompletion(id: staffId)
.execute();
// Check completion criteria
return _isProfileComplete(response);
});
}
@override
Future<Staff> getStaffById(String id) async {
return await _service.run(() async {
final response = await _service.connector
.getStaffById(id: id)
.execute();
// Map Data Connect model to Domain entity
return _mapToStaff(response.data.staff);
});
}
@override
Future<void> updateStaff(Staff staff) async {
return await _service.run(() async {
await _service.connector
.updateStaff(
id: staff.id,
name: staff.name,
email: staff.email,
// ... other fields
)
.execute();
});
}
/// Maps Data Connect staff model to Domain Staff entity
Staff _mapToStaff(dynamic dataConnectStaff) {
return Staff(
id: dataConnectStaff.id,
name: dataConnectStaff.name,
email: dataConnectStaff.email,
status: _mapStatus(dataConnectStaff.status),
);
}
/// Checks if profile is complete based on business rules
bool _isProfileComplete(dynamic response) {
final data = response.data.staff;
return data.personalInfo != null &&
data.emergencyContacts.isNotEmpty &&
data.taxForms != null &&
data.experience != null;
}
}
Key Features of _service.run():
- ✅ Auto validates user is authenticated
- ✅ Refreshes token if <5 minutes to expiry
- ✅ Executes the query
- ✅ 3-attempt retry with exponential backoff (1s → 2s → 4s)
- ✅ Maps exceptions to domain failures
- ✅ Consistent error handling
Characteristics:
- Implements domain repository interface
- Uses
DataConnectServiceto execute queries - Maps backend response types to domain entities
- Contains mapping/transformation logic only
- Handles type safety with generated Data Connect types
3. Feature Integration Pattern
Step 1: Feature Needs Data
Feature (e.g., staff_main) needs profile completion status.
Step 2: Register Connector in Feature Module
Instead of creating a local repository, feature uses connector:
// staff_main_module.dart
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.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
// Or uses connector use case directly
i.addSingleton(
() => GetProfileCompletionUseCase(
repository: i.get<StaffConnectorRepository>(),
),
);
// BLoC uses the use case
i.addSingleton(
() => StaffMainCubit(
getProfileCompletionUsecase: i.get<GetProfileCompletionUseCase>(),
),
);
}
@override
void routes(RouteManager r) {
r.child(
'/',
child: (_) => StaffMainPage(),
);
}
}
Step 3: BLoC Uses Connector via Use Case
// staff_main_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
class StaffMainCubit extends Cubit<StaffMainState> {
final GetProfileCompletionUseCase _getProfileCompletionUsecase;
StaffMainCubit({
required GetProfileCompletionUseCase getProfileCompletionUsecase,
}) : _getProfileCompletionUsecase = getProfileCompletionUsecase,
super(const StaffMainState()) {
_loadProfileCompletion();
}
Future<void> _loadProfileCompletion() async {
emit(state.copyWith(isLoading: true));
final result = await _getProfileCompletionUsecase(NoParams());
result.fold(
(failure) => emit(state.copyWith(
isLoading: false,
error: failure.message,
)),
(isComplete) => emit(state.copyWith(
isLoading: false,
isProfileComplete: isComplete,
)),
);
}
}
Step 4: UI Reacts to State
// staff_main_page.dart
class StaffMainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<StaffMainCubit, StaffMainState>(
builder: (context, state) {
if (state.isLoading) {
return const LoadingIndicator();
}
if (state.isProfileComplete) {
return CompleteProfileView();
}
return IncompleteProfileView();
},
);
}
}
4. Export Pattern
Exporting from Data Connect Package
Connectors are exported from krow_data_connect for easy access:
// lib/krow_data_connect.dart
library krow_data_connect;
// Data Connect Service
export 'src/services/data_connect_service.dart';
// Session Stores
export 'src/session/staff_session_store.dart';
export 'src/session/client_session_store.dart';
// Staff Connector
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/domain/usecases/get_staff_by_id_usecase.dart';
export 'src/connectors/staff/data/repositories/staff_connector_repository_impl.dart';
// Shifts Connector
export 'src/connectors/shifts/domain/repositories/shifts_connector_repository.dart';
export 'src/connectors/shifts/domain/usecases/list_shifts_usecase.dart';
export 'src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart';
// Order Connector
export 'src/connectors/order/domain/repositories/order_connector_repository.dart';
export 'src/connectors/order/data/repositories/order_connector_repository_impl.dart';
Features Import
Features import with single statement:
import 'package:krow_data_connect/krow_data_connect.dart';
// Now have access to:
// - StaffConnectorRepository
// - GetProfileCompletionUseCase
// - DataConnectService
// - StaffSessionStore
// etc.
5. Adding New Queries to Existing Connector
When backend adds getStaffById() query to staff connector:
Step 1: Add to Interface
// staff_connector_repository.dart
abstract interface class StaffConnectorRepository {
Future<bool> getProfileCompletion();
// NEW: Add method signature
Future<Staff> getStaffById(String id);
}
Step 2: Implement in Repository
// staff_connector_repository_impl.dart
class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
// ... existing methods ...
// NEW: Implement method
@override
Future<Staff> getStaffById(String id) async {
return await _service.run(() async {
final response = await _service.connector
.getStaffById(id: id)
.execute();
return _mapToStaff(response.data.staff);
});
}
}
Step 3: Create Use Case (Optional)
// get_staff_by_id_usecase.dart
class GetStaffByIdUseCase extends UseCase<Staff, String> {
final StaffConnectorRepository _repository;
GetStaffByIdUseCase({
required StaffConnectorRepository repository,
}) : _repository = repository;
@override
Future<Either<Failure, Staff>> call(String staffId) async {
try {
final staff = await _repository.getStaffById(staffId);
return Right(staff);
} on DataConnectException catch (e) {
return Left(ServerFailure(e.message));
}
}
}
Step 4: Export
// krow_data_connect.dart
export 'src/connectors/staff/domain/usecases/get_staff_by_id_usecase.dart';
Step 5: Use in Features
// Any feature can now use it
final staff = await i.get<StaffConnectorRepository>().getStaffById(id);
// Or via use case
final result = await i.get<GetStaffByIdUseCase>()(staffId);
6. Creating New Connector
When backend adds new connector (e.g., notifications):
Step 1: Create Directory Structure
mkdir -p apps/mobile/packages/data_connect/lib/src/connectors/notifications
mkdir -p apps/mobile/packages/data_connect/lib/src/connectors/notifications/domain/repositories
mkdir -p apps/mobile/packages/data_connect/lib/src/connectors/notifications/domain/usecases
mkdir -p apps/mobile/packages/data_connect/lib/src/connectors/notifications/data/repositories
Step 2: Define Domain Interface
// notifications_connector_repository.dart
abstract interface class NotificationsConnectorRepository {
Future<List<Notification>> getNotifications(String userId);
Future<void> markAsRead(String notificationId);
Future<int> getUnreadCount(String userId);
}
Step 3: Create Use Cases
// get_notifications_usecase.dart
class GetNotificationsUseCase extends UseCase<List<Notification>, String> {
final NotificationsConnectorRepository _repository;
GetNotificationsUseCase({
required NotificationsConnectorRepository repository,
}) : _repository = repository;
@override
Future<Either<Failure, List<Notification>>> call(String userId) async {
try {
final notifications = await _repository.getNotifications(userId);
return Right(notifications);
} on DataConnectException catch (e) {
return Left(ServerFailure(e.message));
}
}
}
Step 4: Implement Repository
// notifications_connector_repository_impl.dart
class NotificationsConnectorRepositoryImpl
implements NotificationsConnectorRepository {
final DataConnectService _service;
NotificationsConnectorRepositoryImpl({
DataConnectService? service,
}) : _service = service ?? DataConnectService.instance;
@override
Future<List<Notification>> getNotifications(String userId) async {
return await _service.run(() async {
final response = await _service.connector
.getNotifications(userId: userId)
.execute();
return response.data.notifications
.map(_mapToNotification)
.toList();
});
}
@override
Future<void> markAsRead(String notificationId) async {
return await _service.run(() async {
await _service.connector
.markNotificationAsRead(id: notificationId)
.execute();
});
}
@override
Future<int> getUnreadCount(String userId) async {
return await _service.run(() async {
final response = await _service.connector
.getUnreadNotificationCount(userId: userId)
.execute();
return response.data.count;
});
}
Notification _mapToNotification(dynamic data) {
return Notification(
id: data.id,
title: data.title,
message: data.message,
isRead: data.isRead,
createdAt: DateTime.parse(data.createdAt),
);
}
}
Step 5: Export from Package
// krow_data_connect.dart
export 'src/connectors/notifications/domain/repositories/notifications_connector_repository.dart';
export 'src/connectors/notifications/domain/usecases/get_notifications_usecase.dart';
export 'src/connectors/notifications/data/repositories/notifications_connector_repository_impl.dart';
Step 6: Features Use Immediately
// feature_module.dart
i.addSingleton<NotificationsConnectorRepository>(
NotificationsConnectorRepositoryImpl.new,
);
i.addSingleton(
() => GetNotificationsUseCase(
repository: i.get<NotificationsConnectorRepository>(),
),
);
7. Benefits Summary
✅ No Duplication
Before:
staff_main/data/repositories/→ implements profile completion queryprofile/data/repositories/→ duplicates same queryonboarding/data/repositories/→ duplicates same query
After:
data_connect/connectors/staff/→ implements once- All features use same connector repository
✅ Single Source of Truth
Backend Change: getStaffProfileCompletion query updated
Before:
- Update in 3+ feature repositories
- Risk of missing updates
- Inconsistent implementations
After:
- Update once in
StaffConnectorRepositoryImpl - All features automatically use new implementation
✅ Clean Separation
- Connector Logic: Query backend, map responses
- Feature Logic: Use cases, business rules, UI state
✅ Reusability
Any feature can use any connector:
// Feature A
i.get<StaffConnectorRepository>().getProfileCompletion()
// Feature B
i.get<StaffConnectorRepository>().getStaffById(id)
// Feature C
i.get<StaffConnectorRepository>().updateStaff(staff)
✅ Testability
Mock connector repository to test features:
class MockStaffConnectorRepository extends Mock
implements StaffConnectorRepository {}
final mockRepo = MockStaffConnectorRepository();
when(mockRepo.getProfileCompletion()).thenAnswer((_) async => true);
final useCase = GetProfileCompletionUseCase(repository: mockRepo);
✅ Scalability
Easy to add new connectors as backend grows:
- Backend adds
paymentsconnector → Mobile addspayments/folder - Backend adds
messagingconnector → Mobile addsmessaging/folder - Pattern scales indefinitely
✅ Mirrors Backend
Mobile structure mirrors backend structure, making it intuitive:
backend/dataconnect/connector/staff/
↕️
data_connect/connectors/staff/
8. Anti-Patterns to Avoid
❌ DON'T: Implement Queries in Feature Repositories
// ❌ BAD: Feature-specific repository querying backend directly
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
Future<Staff> getProfile() async {
// Directly querying Data Connect
final response = await FirebaseDataConnect.instance
.getStaffById(id: id)
.execute();
return Staff(...);
}
}
Problem: Duplicated across multiple features, no reusability.
❌ DON'T: Duplicate Queries Across Features
// ❌ BAD: Same query in multiple features
// staff_main/data/repositories/profile_repository_impl.dart
Future<bool> checkProfileCompletion() { /*...*/ }
// profile/data/repositories/profile_repository_impl.dart
Future<bool> checkProfileCompletion() { /*...*/ }
// onboarding/data/repositories/onboarding_repository_impl.dart
Future<bool> checkProfileCompletion() { /*...*/ }
Problem: 3x duplication, 3x maintenance, 3x bug risk.
❌ DON'T: Put Mapping Logic in Features
// ❌ BAD: Feature doing data transformation
class ProfileCubit extends Cubit<ProfileState> {
Future<void> loadProfile() async {
final response = await connector.getStaff();
// Mapping logic in BLoC
final staff = Staff(
id: response.data.staff.id,
name: response.data.staff.name,
);
}
}
Problem: Violates Clean Architecture, duplicated mapping logic.
❌ DON'T: Call DataConnectService Directly from BLoCs
// ❌ BAD: BLoC bypassing repository layer
class ProfileCubit extends Cubit<ProfileState> {
Future<void> loadProfile() async {
final response = await DataConnectService.instance.connector
.getStaffById(id: id)
.execute();
}
}
Problem: No abstraction, no testability, tight coupling.
✅ DO: Use Connector Repositories Through Use Cases
// ✅ GOOD: Clean Architecture flow
Feature BLoC → Use Case → Connector Repository → Data Connect Service → Backend
9. Current Implementation
Staff Connector
Location: apps/mobile/packages/data_connect/lib/src/connectors/staff/
Available Queries:
getProfileCompletion()- Returns bool indicating if profile complete- Checks: personal info, emergency contacts, tax forms, experience
Used By:
staff_main- Guards bottom nav items requiring profile completionprofile- Displays completion statusonboarding- Checks if onboarding needed
Backend Queries:
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 with status mappingapplyForShifts()- Handles shift application with error tracking
Used By:
shiftsfeature - Displays available shiftsshift_detailsfeature - Shows shift information
Backend Queries:
backend/dataconnect/connector/shifts/queries/list_shift_roles_by_vendor.gqlbackend/dataconnect/connector/shifts/mutations/apply_for_shifts.gql
10. Future Expansion
As app grows, additional connectors will be added:
Planned Connectors:
order_connector_repository- Frombackend/dataconnect/connector/order/user_connector_repository- Frombackend/dataconnect/connector/user/emergency_contact_connector_repository- Frombackend/dataconnect/connector/emergencyContact/documents_connector_repository- Frombackend/dataconnect/connector/documents/payments_connector_repository- Frombackend/dataconnect/connector/payments/
Each following the same Clean Architecture pattern.
Summary
Core Pattern:
- Mirror Backend: Connector structure mirrors backend connector structure
- Clean Architecture: Domain interfaces → Data implementations
- Centralized: All backend queries in one place per connector
- Reusable: Any feature can use any connector via dependency injection
- Single Source: Backend change → update one repository
- Type Safe: Uses Data Connect generated types
Implementation Flow:
Feature Module registers connector repository
↓
Feature Use Case uses connector repository
↓
Connector Repository uses DataConnectService.run()
↓
DataConnectService executes Data Connect query
↓
Repository maps response to Domain entity
↓
Use Case returns Result to BLoC
↓
BLoC emits state to UI
When implementing features:
- Identify which backend connector you need (staff, order, shifts, etc.)
- Use corresponding connector repository from
data_connect - Register in feature module via dependency injection
- BLoC uses connector repository through use cases
- Don't create feature-specific repositories for backend queries
When backend adds queries:
- Add to appropriate connector repository interface
- Implement in connector repository implementation
- Features automatically have access via dependency injection
The connector pattern eliminates duplication, ensures consistency, and scales as the backend grows. Always use connectors for backend access, never query Data Connect directly from features.