Skip to content

Latest commit

 

History

History
581 lines (458 loc) · 13.9 KB

File metadata and controls

581 lines (458 loc) · 13.9 KB

Architecture Documentation

Overview

NextStep follows a Feature-First Clean Architecture pattern combined with GetX for state management. This approach ensures scalability, maintainability, and clear separation of concerns.

Architecture Layers

1. Presentation Layer (UI)

  • Screens: Full-screen views that users interact with
  • Widgets: Reusable UI components
  • Controllers: State management and UI logic using GetX

2. Domain Layer (Business Logic)

  • Controllers: Business rules and use cases
  • Models: Business entities
  • Validation: Input validation logic

3. Data Layer

  • Services: Local data storage (SharedPreferences)
  • Network: API clients and HTTP requests
  • Repositories: (Future implementation) Data source abstraction

Directory Structure

lib/
│
├── main.dart                          # App entry point
│
├── core/                              # Shared application-wide code
│   ├── config.dart                    # App configuration
│   ├── constants/
│   │   ├── app_color.dart            # Color palette
│   │   ├── app_image.dart            # Image assets (auto-generated)
│   │   └── app_strings.dart          # String constants
│   ├── functions/                     # Utility functions
│   ├── middlewares/
│   │   ├── auth_middleware.dart      # Authentication guard
│   │   └── onboarding_middleware.dart # First-launch guard
│   ├── network/
│   │   └── api_client.dart           # HTTP client wrapper
│   ├── services/
│   │   └── cache_service.dart        # Local storage service
│   ├── theme/
│   │   └── app_theme.dart            # Material theme config
│   └── utils/
│       ├── validators.dart           # Form validation
│       └── helpers.dart              # Helper functions
│
├── features/                          # Feature modules (organized by feature)
│   │
│   ├── splash/
│   │   └── splash_screen.dart        # Animated splash screen
│   │
│   ├── onboarding/
│   │   ├── controller/
│   │   │   └── onboarding_controller.dart
│   │   ├── onboarding_screen.dart
│   │   └── widgets/
│   │       └── onboarding_page.dart
│   │
│   ├── auth/
│   │   ├── controllers/
│   │   │   ├── login_controller.dart
│   │   │   └── register_controller.dart
│   │   ├── screens/
│   │   │   ├── login_screen.dart
│   │   │   └── register_screen.dart
│   │   └── widgets/
│   │       ├── login_form.dart
│   │       └── register_form.dart
│   │
│   ├── profile_setup/
│   │   ├── controllers/
│   │   │   └── complete_profile_controller.dart
│   │   ├── screens/
│   │   │   └── complete_profile_screen.dart
│   │   └── widgets/
│   │       ├── personal_info_step.dart
│   │       ├── work_locations_step.dart
│   │       └── job_preferences_step.dart
│   │
│   ├── home/                          # Home dashboard (future)
│   ├── jobs/                          # Job listings (future)
│   ├── favorites/                     # Saved jobs (future)
│   ├── apply/                         # Application flow (future)
│   ├── layout/                        # App navigation layout (future)
│   └── settings/                      # App settings (future)
│
├── routes/
│   └── app_routes.dart               # Centralized route definitions
│
└── shared/                            # Shared widgets across features
    ├── buttons/
    │   ├── primary_button.dart
    │   └── secondary_button.dart
    ├── inputs/
    │   ├── custom_text_field.dart
    │   └── dropdown_field.dart
    ├── indicator/
    │   └── loading_indicator.dart
    └── spacer.dart

Design Patterns

1. Feature-First Organization

Each feature is organized as an independent module:

feature_name/
├── controllers/       # Business logic
├── screens/           # UI layouts
├── widgets/           # Feature-specific components
└── models/            # Data models (optional)

Benefits:

  • Easy to locate feature-related code
  • Better code organization for teams
  • Simpler feature addition/removal
  • Clear module boundaries

2. GetX State Management

Controller Pattern:

class LoginController extends GetxController {
  // Observable state
  var email = ''.obs;
  var password = ''.obs;
  var isLoading = false.obs;

  // Services
  final AuthService _authService = Get.find();

  // Lifecycle
  @override
  void onInit() {
    super.onInit();
    // Initialize
  }

  @override
  void onClose() {
    // Cleanup
    super.onClose();
  }

  // Methods
  Future<void> login() async {
    try {
      isLoading.value = true;
      await _authService.login(email.value, password.value);
      Get.offAllNamed(AppRoutes.home);
    } catch (e) {
      Get.snackbar('Error', e.toString());
    } finally {
      isLoading.value = false;
    }
  }
}

Screen Usage:

class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.put(LoginController());

    return Scaffold(
      body: Column(
        children: [
          Obx(() => TextField(
            onChanged: (val) => controller.email.value = val,
          )),
          Obx(() => controller.isLoading.value
            ? CircularProgressIndicator()
            : ElevatedButton(
                onPressed: controller.login,
                child: Text('Login'),
              )
          ),
        ],
      ),
    );
  }
}

3. Navigation & Routing

Route Definitions:

abstract class AppRoutes {
  // Route names
  static const String splash = '/';
  static const String login = '/login';

  // Route configurations
  static List<GetPage> routes = [
    GetPage(
      name: splash,
      page: () => SplashScreen(),
    ),
    GetPage(
      name: login,
      page: () => LoginScreen(),
      middlewares: [AuthMiddleware()],
    ),
  ];
}

Navigation Methods:

// Push new screen
Get.toNamed(AppRoutes.login);

// Replace current screen
Get.offNamed(AppRoutes.home);

// Clear stack and navigate
Get.offAllNamed(AppRoutes.splash);

// Pass arguments
Get.toNamed(AppRoutes.jobDetail, arguments: {'jobId': 123});

// Get arguments
final args = Get.arguments;

4. Middleware Pattern

Route guards for authentication and flow control:

class AuthMiddleware extends GetMiddleware {
  @override
  RouteSettings? redirect(String? route) {
    // Check if user is logged in
    final isLoggedIn = CacheService.to.isLoggedIn;

    if (!isLoggedIn) {
      return null; // Allow navigation
    }

    // Redirect to home if already logged in
    return RouteSettings(name: AppRoutes.home);
  }
}

5. Service Pattern

Singleton services for app-wide functionality:

class CacheService extends GetxService {
  static CacheService get to => Get.find();

  late SharedPreferences _prefs;

  Future<CacheService> init() async {
    _prefs = await SharedPreferences.getInstance();
    return this;
  }

  // Getters
  bool get isLoggedIn => _prefs.getBool('isLoggedIn') ?? false;

  // Setters
  Future<void> setLoggedIn(bool value) async {
    await _prefs.setBool('isLoggedIn', value);
  }
}

Service Initialization:

Future<void> initializeServices() async {
  await Get.putAsync(() => CacheService().init());
}

Data Flow

User Interaction Flow

┌─────────────┐
│   Screen    │ ← User interacts
└──────┬──────┘
       │ calls method
       ↓
┌─────────────┐
│ Controller  │ ← Business logic
└──────┬──────┘
       │ updates
       ↓
┌─────────────┐
│ Observable  │ ← State changes
│   State     │
└──────┬──────┘
       │ notifies
       ↓
┌─────────────┐
│  Screen     │ ← UI rebuilds
│  (Obx)      │
└─────────────┘

API Request Flow (Future Implementation)

┌─────────────┐
│ Controller  │
└──────┬──────┘
       │
       ↓
┌─────────────┐
│ Repository  │ ← Data source abstraction
└──────┬──────┘
       │
       ↓
┌─────────────┐
│ API Client  │ ← HTTP requests
└──────┬──────┘
       │
       ↓
┌─────────────┐
│   Server    │
└─────────────┘

Dependency Injection

GetX provides simple dependency injection:

// Lazy initialization (created when first accessed)
Get.lazyPut(() => HomeController());

// Immediate initialization
Get.put(LoginController());

// Singleton service
Get.putAsync(() => CacheService().init());

// Access dependency
final controller = Get.find<HomeController>();

State Management Strategies

1. Simple State (Rx Variables)

var counter = 0.obs;
counter.value++;

// In UI
Obx(() => Text('Count: ${controller.counter.value}'))

2. Complex State (GetxController)

class ProfileController extends GetxController {
  var user = UserModel().obs;

  void updateUser(UserModel newUser) {
    user.value = newUser;
  }
}

3. Workers (Reactive Programming)

@override
void onInit() {
  super.onInit();

  // Called every time the value changes
  ever(count, (_) => print('Count changed'));

  // Called only once when value changes
  once(count, (_) => print('First change'));

  // Called after 1 second of inactivity
  debounce(searchQuery, (_) => fetchResults());

  // Wait 1 second, then call
  interval(refreshToken, (_) => refreshToken());
}

Theme System

Color Management

abstract class AppColor {
  static const Color primary = Color(0xFF130160);
  static const Color secondary = Color(0xFFFF6B2C);
  static const Color background = Color(0xFFF9F9F9);
  static const Color textPrimary = Color(0xFF000000);
  static const Color textSecondary = Color(0xFF524B6B);
}

Theme Configuration

class AppTheme {
  static ThemeData get lightTheme {
    return ThemeData(
      fontFamily: Config.fontFamily,
      colorScheme: ColorScheme.light(
        primary: AppColor.primary,
        secondary: AppColor.secondary,
      ),
      scaffoldBackgroundColor: AppColor.background,
    );
  }
}

Best Practices

1. Controller Best Practices

Do:

  • Keep controllers focused on single features
  • Use descriptive names for observables
  • Dispose resources in onClose()
  • Use workers for reactive side effects

Don't:

  • Put UI widgets in controllers
  • Share controllers across unrelated features
  • Keep large business logic in controllers (extract to services)

2. Widget Best Practices

Do:

  • Extract reusable widgets to shared/
  • Use const constructors when possible
  • Keep widget build methods small
  • Use Obx() for reactive updates

Don't:

  • Create widgets with too many responsibilities
  • Duplicate widgets across features
  • Rebuild entire screens unnecessarily

3. Navigation Best Practices

Do:

  • Use named routes for navigation
  • Define all routes in app_routes.dart
  • Use middlewares for auth checks
  • Pass data via route arguments

Don't:

  • Navigate using direct widget instances
  • Skip middleware checks
  • Pass large objects through routes

4. State Management Best Practices

Do:

  • Use .obs for simple reactive values
  • Use Obx() for UI updates
  • Dispose controllers when not needed
  • Use workers for side effects

Don't:

  • Overuse global state
  • Forget to mark values as observable
  • Create circular dependencies

Testing Strategy

Unit Tests

Test controllers and business logic:

void main() {
  group('LoginController', () {
    late LoginController controller;

    setUp(() {
      controller = LoginController();
    });

    test('email validation', () {
      controller.email.value = 'invalid';
      expect(controller.isEmailValid, false);

      controller.email.value = 'test@example.com';
      expect(controller.isEmailValid, true);
    });
  });
}

Widget Tests

Test UI components:

void main() {
  testWidgets('LoginScreen shows error on invalid input', (tester) async {
    await tester.pumpWidget(MyApp());

    await tester.enterText(find.byType(TextField).first, 'invalid');
    await tester.tap(find.byType(ElevatedButton));
    await tester.pump();

    expect(find.text('Invalid email'), findsOneWidget);
  });
}

Future Enhancements

Planned Architectural Improvements

  1. Repository Pattern

    • Abstract data sources
    • Easier testing and mocking
    • Support multiple data sources
  2. Use Case Layer

    • Separate business logic from controllers
    • Reusable business operations
    • Better testability
  3. Dependency Injection Container

    • Centralized DI management
    • Better dependency tracking
    • Easier testing
  4. API Response Models

    • Typed API responses
    • JSON serialization
    • Error handling
  5. Error Handling Strategy

    • Centralized error management
    • User-friendly error messages
    • Logging and monitoring

Conclusion

This architecture provides a solid foundation for building a scalable Flutter application. The feature-first approach combined with GetX state management ensures code maintainability, testability, and developer productivity.

For questions or suggestions about the architecture, please open an issue or contact the development team.