Skip to main content

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>(),
      ],
    );
  });
}

Widget Tests

// 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

  • Code follows project style guide
  • All tests pass
  • No compiler warnings
  • Documentation updated
  • Self-review completed
  • Complex logic has comments
  • No hardcoded values
  • Error handling implemented
  • Logging added where appropriate

For Reviewers

  • Code is readable and maintainable
  • Logic is correct
  • Edge cases handled
  • Performance considerations
  • Security implications checked
  • Tests are adequate
  • Documentation is clear
  • No code duplication

๐Ÿš€ Performance Guidelines

1. Widget Optimization

// โœ… 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

  1. No Hardcoded Secrets
    // โŒ Bad
    const apiKey = 'sk_live_abc123';
    
    // โœ… Good
    final apiKey = dotenv.env['API_KEY'];
    
  2. Validate User Input
    // โœ… Good
    if (email.isEmpty || !email.contains('@')) {
      return 'Invalid email';
    }
    
  3. Secure Storage
    // โœ… Good: Use encrypted storage
    await ConfigLocalService().saveToken(token);
    

๐Ÿ“ฑ Platform-Specific Guidelines

iOS

  • 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