Files
Krow-workspace/mobile-apps/client-app/architecture.md

12 KiB

Krow Mobile Client App Architecture Document

A. Introduction

This document provides a comprehensive overview of the Krow mobile client application's architecture. The Krow app is a Flutter-based mobile application designed to connect staff with work opportunities. It includes features for event management, invoicing, staff rating, and profile management.

The core purpose of the app is to provide a seamless experience for staff to find, manage, and get paid for work, while allowing clients to manage their events and staff effectively.

B. Full Architecture Overview

The Krow app is built using a layered architecture that separates concerns and promotes modularity. The main layers are the Presentation Layer, the Domain Layer, and the Data Layer, organized into feature-based modules.

Key Modules and Layers

  • Features: The lib/features directory contains the main features of the app, such as sign_in, events, profile, etc. Each feature directory is further divided into presentation and domain layers.
  • Presentation Layer: This layer is responsible for the UI and user interaction. It contains the screens (widgets) and the BLoCs (Business Logic Components) that manage the state of the UI.
  • Domain Layer: This layer contains the core business logic of the application. It includes the BLoCs, which are responsible for orchestrating the flow of data between the UI and the data layer, and the business objects (entities).
  • Data Layer: This layer is responsible for all data-related operations. It includes the repositories that fetch data from the backend and the data sources themselves (e.g., GraphQL API, local cache).
  • Core: The lib/core directory contains shared code that is used across multiple features, such as the API client, dependency injection setup, routing, and common widgets.

Integration Points

  • UI to Domain: The UI (widgets) dispatches events to the BLoCs in the domain layer based on user interactions.
  • Domain to Data: The BLoCs in the domain layer call methods on the repositories in the data layer to fetch or update data.
  • Data to Backend: The repositories in the data layer use the ApiClient to make GraphQL calls to the backend.

C. Backend Architecture

The backend of the Krow app is a hybrid system that leverages both a GraphQL server and Firebase services.

flowchart TD
    subgraph "Client"
        A[Flutter App]
    end

    subgraph "Backend"
        B[GraphQL Server (e.g., Node.js)]
        C[Firebase]
    end

    subgraph "Firebase Services"
        C1[Firebase Auth]
        C2[Firebase Firestore]
        C3[Firebase Storage]
    end

    A -- "GraphQL Queries/Mutations" --> B
    A -- "Authentication" --> C1

    B -- "Data Operations" --> C2
    B -- "File Operations" --> C3

    C1 -- "User Tokens" --> A
    C2 -- "Data" --> B
    C3 -- "Files" --> B

    B -- "Data/Files" --> A

GraphQL

The GraphQL server acts as an intermediary between the Flutter app and the Firebase services. It exposes a set of queries and mutations that the app can use to interact with the backend. This provides a single, unified API for the app to consume, simplifying data fetching and manipulation.

Firebase Integration

  • Firebase Auth: Firebase Auth is used for user authentication. The Flutter app interacts directly with Firebase Auth to handle user sign-in, sign-up, and password reset flows. Once authenticated, the app retrieves a Firebase ID token, which is then used to authenticate with the GraphQL server.
  • Firebase Firestore: Firestore is the primary database for the application. The GraphQL server is responsible for all interactions with Firestore, including fetching, creating, updating, and deleting data.
  • Firebase Storage: Firebase Storage is used for storing user-generated content, such as profile pictures. The GraphQL server handles file uploads and retrieves file URLs that are then sent to the app.

End-to-End Communication Flow

  1. The Flutter app authenticates the user with Firebase Auth.
  2. The app receives a Firebase ID token.
  3. For all subsequent API requests, the app sends the Firebase ID token in the authorization header of the GraphQL request.
  4. The GraphQL server verifies the token and then executes the requested query or mutation.
  5. The GraphQL server interacts with Firestore or Firebase Storage to fulfill the request.
  6. The GraphQL server returns the requested data to the app.

D. API Layer

The API layer is responsible for all communication with the backend. It is built around the graphql_flutter package and a custom ApiClient.

flowchart TD
    subgraph "GraphQL API"
        direction LR
        subgraph "Queries"
            Q1[getEvents]
            Q2[getEventDetails]
            Q3[getInvoices]
            Q4[getInvoiceDetails]
            Q5[getNotifications]
            Q6[getNotificationDetails]
            Q7[getProfile]
            Q8[getAssignedStaff]
        end

        subgraph "Mutations"
            M1[createEvent]
            M2[updateProfile]
            M3[rateStaff]
            M4[clockIn]
            M5[clockOut]
            M6[uploadProfilePicture]
        end
    end

    subgraph "Firebase"
        direction LR
        subgraph "Firestore Collections"
            FS1[events]
            FS2[invoices]
            FS3[notifications]
            FS4[users]
        end

        subgraph "Firebase Storage"
            FB1[Profile Pictures]
        end
    end

    Q1 --> FS1
    Q2 --> FS1
    Q3 --> FS2
    Q4 --> FS2
    Q5 --> FS3
    Q6 --> FS3
    Q7 --> FS4
    Q8 --> FS1
    Q8 --> FS4

    M1 --> FS1
    M2 --> FS4
    M3 --> FS1
    M3 --> FS4
    M4 --> FS1
    M5 --> FS1
    M6 --> FB1

API Handling

  • Error Handling: The ApiClient uses the ErrorPolicy.all policy to catch all GraphQL errors. The BLoCs are responsible for catching these errors and updating the UI state accordingly.
  • Caching: The GraphQLCache with HiveStore is used to cache GraphQL query results. The fetchPolicy is set to cacheAndNetwork to provide a fast user experience while keeping the data up-to-date.
  • Parsing: The app uses the json_serializable package to parse the JSON responses from the GraphQL server into Dart objects.

E. State Management

The Krow app uses the BLoC (Business Logic Component) pattern for state management, powered by the flutter_bloc package.

Why BLoC?

  • Separation of Concerns: BLoC separates the business logic from the UI, making the code more organized, testable, and maintainable.
  • Testability: BLoCs are easy to test in isolation from the UI.
  • Reactivity: BLoC uses streams to manage state, which makes it easy to update the UI in response to state changes.

State Flow

  1. The UI dispatches an event to the BLoC.
  2. The BLoC receives the event and interacts with the data layer (repositories) to fetch or update data.
  3. The data layer returns data or a success/failure status to the BLoC.
  4. The BLoC updates its state based on the result from the data layer.
  5. The UI rebuilds itself in response to the new state.

Integration with the API Layer

The BLoCs do not interact directly with the ApiClient. Instead, they go through a repository layer, which abstracts the data source. This makes it possible to switch out the backend without having to change the BLoCs.

F. Use-Case Flows

The following diagrams illustrate the flow for some of the major use cases in the app.

flowchart TD
    subgraph "Sign-In Flow"
        A1[User enters credentials] --> B1{SignInBloc};
        B1 --> C1[Firebase Auth: signInWithEmailAndPassword];
        C1 -- Success --> D1[Navigate to Home];
        C1 -- Failure --> E1[Show error message];
    end

    subgraph "Password Reset Flow"
        A2[User requests password reset] --> B2{SignInBloc};
        B2 --> C2[Firebase Auth: sendPasswordResetEmail];
        C2 -- Email Sent --> D2[User clicks deep link];
        D2 --> E2[UI with new password fields];
        E2 --> F2{SignInBloc};
        F2 --> G2[Firebase Auth: confirmPasswordReset];
        G2 -- Success --> H2[Show success message];
        G2 -- Failure --> I2[Show error message];
    end

    subgraph "Event Listing Flow"
        A3[User navigates to Events screen] --> B3{EventsBloc};
        B3 --> C3[GraphQL Query: getEvents];
        C3 --> D3[Firestore: events collection];
        D3 -- Returns event data --> C3;
        C3 -- Returns data --> B3;
        B3 --> E3[Display list of events];
    end

    subgraph "Create Event Flow"
        A4[User submits new event form] --> B4{CreateEventBloc};
        B4 --> C4[GraphQL Mutation: createEvent];
        C4 --> D4[Firestore: events collection];
        D4 -- Success --> C4;
        C4 -- Returns success --> B4;
        B4 --> E4[Navigate to event details];
    end

    subgraph "Profile Viewing Flow"
        A5[User navigates to Profile screen] --> B5{ProfileBloc};
        B5 --> C5[GraphQL Query: getProfile];
        C5 --> D5[Firestore: users collection];
        D5 -- Returns profile data --> C5;
        C5 -- Returns data --> B5;
        B5 --> E5[Display profile information];
    end

G. Replacing or Plugging in a New Backend: Considerations & Recommendations

This section provides guidance on how to replace the current GraphQL + Firebase backend with a different backend solution.

Tightly Coupled Components

  • ApiClient: This class is tightly coupled to graphql_flutter.
  • Firebase Auth: The authentication logic is directly tied to the firebase_auth package.
  • BLoCs: Some BLoCs might have direct dependencies on Firebase or GraphQL-specific models.

Abstraction Recommendations

To make the architecture more backend-agnostic, the following abstractions should be implemented:

  • Repositories: Create an abstract Repository class for each feature in the domain layer. The implementation of this repository will be in the data layer. The BLoCs should only depend on the abstract repository.
  • Authentication Service: Create an abstract AuthService class that defines the methods for authentication (e.g., signIn, signOut, getToken). The implementation of this service will be in the data layer and will use the specific authentication provider (e.g., Firebase Auth, OAuth).
  • Data Transfer Objects (DTOs): Use DTOs to transfer data between the data layer and the domain layer. This will prevent the domain layer from having dependencies on backend-specific models.

Suggested Design Improvements

  • Formalize Clean Architecture: While the current architecture has elements of Clean Architecture, it could be more formally implemented by creating a clear separation between the domain, data, and presentation layers for all features.
  • Introduce Use Cases: Introduce UseCase classes in the domain layer to encapsulate specific business operations. This will make the BLoCs simpler and more focused on state management.

Migration Strategies

To replace the current backend with a new one (e.g., REST API, Supabase), follow these steps:

  1. Implement New Repositories: Create new implementations of the repository interfaces for the new backend.
  2. Implement New Auth Service: Create a new implementation of the AuthService interface for the new authentication provider.
  3. Update Dependency Injection: Use dependency injection (e.g., get_it and injectable) to provide the new repository and auth service implementations to the BLoCs.
  4. Gradual Migration: If possible, migrate one feature at a time to the new backend. This will reduce the risk of breaking the entire application at once.