Documentation Index
Fetch the complete documentation index at: https://docs-mstore.faisalaffan.com/llms.txt
Use this file to discover all available pages before exploring further.
Development Guidelines
Panduan development untuk kontributor MStore Mobile.
๐ฏ Development Workflow
1. Setup Development Environment
# Clone repository
git clone <repository-url>
cd mstore_mobile
# Install dependencies
flutter pub get
# Generate code
flutter pub run build_runner build --delete-conflicting-outputs
# Run development
flutter run --flavor development -t lib/main_development.dart
2. Branch Strategy
main (production)
โ
develop (staging)
โ
feature/feature-name
fix/bug-description
hotfix/critical-fix
Branch Naming:
feature/cashier-barcode-scan - New features
fix/inventory-sync-issue - Bug fixes
hotfix/payment-crash - Critical production fixes
refactor/bloc-architecture - Code refactoring
docs/api-documentation - Documentation updates
3. Commit Convention
Gunakan Conventional Commits:
<type>(<scope>): <subject>
<body>
<footer>
Types:
feat: New feature
fix: Bug fix
docs: Documentation
style: Code style (formatting, etc)
refactor: Code refactoring
perf: Performance improvement
test: Adding tests
chore: Maintenance tasks
Examples:
feat(cashier): add barcode scanning support
- Implement mobile_scanner integration
- Add barcode search in product list
- Update UI for scanner button
Closes #123
---
fix(inventory): resolve stock sync issue
The inventory was not syncing properly when offline.
Now using queue-based sync with retry mechanism.
Fixes #456
---
docs(api): update authentication endpoints
Added documentation for refresh token flow
and device registration endpoint.
4. Pull Request Process
Before Creating PR
# Update from develop
git checkout develop
git pull origin develop
# Rebase your feature branch
git checkout feature/your-feature
git rebase develop
# Run tests
flutter test
# Run code analysis
flutter analyze
# Format code
flutter format .
# Build to ensure no errors
flutter build apk --flavor development -t lib/main_development.dart
PR Template
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] Unit tests added/updated
- [ ] Widget tests added/updated
- [ ] Manual testing completed
## Screenshots (if applicable)
[Add screenshots here]
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Comments added for complex code
- [ ] Documentation updated
- [ ] No new warnings
- [ ] Tests pass locally
## Related Issues
Closes #123
๐๏ธ Project Structure
Feature Module Structure
features/
โโโ feature_name/
โโโ bloc/
โ โโโ feature_bloc.dart
โ โโโ feature_event.dart
โ โโโ feature_state.dart
โโโ pages/
โ โโโ feature_list_page.dart
โ โโโ feature_detail_page.dart
โโโ widgets/
โ โโโ feature_card.dart
โ โโโ feature_form.dart
โโโ models/
โโโ feature_model.dart
Core Module Structure
core/
โโโ domain_name/
โโโ domain_api.dart # Retrofit API
โโโ domain_repository.dart # Repository interface
โโโ domain_service.dart # Business logic
โโโ models/
โโโ domain_model.dart
โโโ domain_dto.dart
๐ Coding Standards
Dart Style Guide
// โ
Good
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback? onTap;
const ProductCard({
super.key,
required this.product,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Card(
child: Column(
children: [
Text(product.name),
Text('Rp ${product.price}'),
],
),
),
);
}
}
// โ Bad
class productcard extends StatelessWidget {
Product product;
Function onTap;
productcard(this.product, this.onTap);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onTap(),
child: Card(
child: Column(
children: [
Text(product.name),
Text('Rp ' + product.price.toString()),
],
),
),
);
}
}
BLoC Pattern
// โ
Good: Clear event and state naming
abstract class ProductEvent extends Equatable {}
class LoadProducts extends ProductEvent {
const LoadProducts();
@override
List<Object?> get props => [];
}
class LoadProductById extends ProductEvent {
final String productId;
const LoadProductById(this.productId);
@override
List<Object?> get props => [productId];
}
// โ Bad: Unclear naming
class ProductEvent {}
class Load extends ProductEvent {}
class Get extends ProductEvent {
String id;
Get(this.id);
}
Repository Pattern
// โ
Good: Use Either for error handling
abstract class ProductRepository {
Future<Either<Failure, List<Product>>> getProducts();
Future<Either<Failure, Product>> getProductById(String id);
}
class ProductRepositoryImpl implements ProductRepository {
final ProductApi _api;
@override
Future<Either<Failure, List<Product>>> getProducts() async {
try {
final response = await _api.getProducts();
return Right(response.data);
} on DioException catch (e) {
return Left(NetworkException.fromDioError(e));
}
}
}
// โ Bad: Throw exceptions
abstract class ProductRepository {
Future<List<Product>> getProducts(); // Can throw
}
๐งช Testing
Unit Tests
// test/features/product/bloc/product_bloc_test.dart
void main() {
group('ProductBloc', () {
late ProductBloc bloc;
late MockProductRepository mockRepository;
setUp(() {
mockRepository = MockProductRepository();
bloc = ProductBloc(productRepository: mockRepository);
});
tearDown(() {
bloc.close();
});
test('initial state is ProductInitial', () {
expect(bloc.state, equals(const ProductInitial()));
});
blocTest<ProductBloc, ProductState>(
'emits [ProductLoading, ProductLoaded] when LoadProducts succeeds',
build: () {
when(() => mockRepository.getProducts()).thenAnswer(
(_) async => Right([Product(id: '1', name: 'Test')]),
);
return bloc;
},
act: (bloc) => bloc.add(const LoadProducts()),
expect: () => [
const ProductLoading(),
isA<ProductLoaded>(),
],
);
});
}
// test/features/product/widgets/product_card_test.dart
void main() {
testWidgets('ProductCard displays product info', (tester) async {
final product = Product(
id: '1',
name: 'Test Product',
price: 50000,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProductCard(product: product),
),
),
);
expect(find.text('Test Product'), findsOneWidget);
expect(find.text('Rp 50.000'), findsOneWidget);
});
testWidgets('ProductCard calls onTap when tapped', (tester) async {
var tapped = false;
final product = Product(id: '1', name: 'Test', price: 50000);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProductCard(
product: product,
onTap: () => tapped = true,
),
),
),
);
await tester.tap(find.byType(ProductCard));
expect(tapped, isTrue);
});
}
Integration Tests
// integration_test/cashier_flow_test.dart
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Complete cashier flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// Navigate to cashier
await tester.tap(find.text('Cashier'));
await tester.pumpAndSettle();
// Add product to cart
await tester.tap(find.byKey(Key('product_1')));
await tester.pumpAndSettle();
// Verify cart
expect(find.text('1 item'), findsOneWidget);
// Checkout
await tester.tap(find.text('Checkout'));
await tester.pumpAndSettle();
// Select payment method
await tester.tap(find.text('Cash'));
await tester.pumpAndSettle();
// Complete payment
await tester.tap(find.text('Process Payment'));
await tester.pumpAndSettle();
// Verify success
expect(find.text('Payment Success'), findsOneWidget);
});
}
๐ Code Review Checklist
For Authors
For Reviewers
// โ
Good: Use const constructors
const Text('Hello World')
const SizedBox(height: 16)
// โ
Good: Extract widgets
class ProductList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) => ProductCard(product: products[index]),
);
}
}
// โ Bad: Inline complex widgets
ListView.builder(
itemBuilder: (context, index) {
return Card(
child: Column(
children: [
// ... many nested widgets
],
),
);
},
)
2. State Management
// โ
Good: Specific BlocBuilder
BlocBuilder<ProductBloc, ProductState>(
buildWhen: (previous, current) => previous.products != current.products,
builder: (context, state) => ProductList(products: state.products),
)
// โ Bad: Rebuild entire tree
BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) => EntireApp(state: state),
)
3. Network Optimization
// โ
Good: Implement caching
@override
Future<Either<Failure, List<Product>>> getProducts() async {
// Try local cache first
final cached = await _localRepository.getProducts();
if (cached.isNotEmpty) {
// Return cached data immediately
_syncInBackground();
return Right(cached);
}
// Fetch from API
final result = await _api.getProducts();
result.fold(
(failure) => null,
(products) => _localRepository.saveProducts(products),
);
return result;
}
๐ Monitoring & Logging
Logging Best Practices
// โ
Good: Structured logging
AppLog.i('cashier', 'transaction_completed', meta: {
'transaction_id': transaction.id,
'total': transaction.total,
'items_count': transaction.items.length,
'duration_ms': duration.inMilliseconds,
});
// โ Bad: Unstructured logging
print('Transaction completed: ${transaction.id}');
Error Tracking
// โ
Good: Report to Crashlytics
try {
await repository.createTransaction(transaction);
} catch (e, stackTrace) {
AppLog.e('transaction', 'create_failed', e, stackTrace);
FirebaseCrashlytics.instance.recordError(e, stackTrace);
rethrow;
}
๐ Security Guidelines
-
No Hardcoded Secrets
// โ Bad
const apiKey = 'sk_live_abc123';
// โ
Good
final apiKey = dotenv.env['API_KEY'];
-
Validate User Input
// โ
Good
if (email.isEmpty || !email.contains('@')) {
return 'Invalid email';
}
-
Secure Storage
// โ
Good: Use encrypted storage
await ConfigLocalService().saveToken(token);
- Follow iOS Human Interface Guidelines
- Use Cupertino widgets when appropriate
- Test on multiple iOS versions
- Handle safe area insets
Android
- Follow Material Design guidelines
- Test on different screen sizes
- Handle back button properly
- Request permissions properly
Next Steps
Remember:
- โ
Write clean, readable code
- โ
Test your changes
- โ
Document complex logic
- โ
Follow conventions
- โ
Ask for help when needed