Services topic

What are services?

Services in this codebase are responsible for handling various functionalities and interactions with external systems or APIs. They encapsulate the logic for tasks such as authentication, database operations, cloud functions, remote configuration, and more. By organizing these functionalities into services, the codebase maintains a clean separation of concerns, making it easier to manage and scale.

Key Services and Their Roles

  • Authentication Service: Manages user authentication, including login, logout, and user state changes.
  • Firestore Service: Handles interactions with Firebase Firestore, including CRUD operations for various collections. Firestore is used as the primary database for the application and stores data such as:
    • Event details
    • Posts
    • Comments
    • Event participants
  • Cloud Function Service: Facilitates calling Firebase Cloud Functions for server-side logic execution. This function has it's own documentation topic with further information. The functions are very diverse, but some examples include:
    • Sending push notifications to users when documents are created or updated in Firestore.
    • Actions that only administrators can perform, such approving users or giving them admin privileges.
    • Some niche service connections, such as adding new events to the Google Calendar.
  • Storage Service: Manages file uploads and deletions in Firebase Storage. This includes:
    • Uploading banner images for events.
    • Uploading profile pictures for users.
  • Remote Config Service: Fetches and activates remote configuration values from Firebase Remote Config. This is limited to:
    • The current version of the app.
    • Thel links found in the 'quick access' section of the ALV page.
  • Cloud Messaging Service: Manages Firebase Cloud Messaging for push notifications. This includes:
    • Handling receiving and displaying push notifications.
    • Managing topic subscriptions for push notifications (i.e. which users are subscribed to which topics).
  • Calendar Service: Interacts with the device's calendar to add events. Currently this only involves adding birthdays to the device's calendar when a birthday is clicked on a user's profile.
  • Drive API Service: Handles interactions with Google Drive for file management. This includes:
    • Fetching files from Google Drive for photos, Eurokeys, booklets and ALV resources.
    • Uploading files to Google Drive, although this is not currently used.
  • Local Notification Service: Manages local notifications for event reminders for events the users has subscribed to (by clicking 'participate').
  • Local Storage Service: Handles storing and retrieving data locally on the device. This is used for, among other things:
    • Storing user preferences, such as the user's theme preference.
    • Stroring some user's name and profile picture for offline use.

Each service is designed to be modular and reusable, ensuring that the application remains maintainable and extensible as it grows.

How to Use Services

To use services within the codebase, you typically interact with them through providers. Providers are a way to manage state and dependencies in a structured manner. The example below demonstrates how to use the FirestoreService from the provided firestoreService.dart file.

Example: Using FirestoreService

  1. Import the necessary packages and providers:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'path/to/firestoreService.dart';
  1. Access the service within a widget:
class DeletePostButton extends ConsumerWidget {
  final String postId;

  DeletePostButton({required this.postId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () async {
        await ref.read(firestoreServiceProvider).deletePost(postId);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Post deleted successfully')),
        );
      },
      child: Text('Delete Post'),
    );
  }
}

Why This Structure?

The current structure is chosen for several reasons:

  • Separation of Concerns: By organizing functionalities into services, each service is responsible for a specific part of the application logic. This makes the codebase easier to understand and maintain.
  • Modularity: Services are designed to be modular and reusable. This allows for easy testing and the ability to replace or update services without affecting other parts of the application.
  • State Management: Using providers for services ensures that state and dependencies are managed efficiently. Providers help in maintaining a clean and reactive state management system.
  • Scalability: As the application grows, the modular structure allows for easy scaling. New services can be added without disrupting the existing architecture.
  • Testing: Services can be easily tested in isolation, ensuring that each service functions correctly and meets the required specifications. Additionally, the instances of services can be mocked for testing other parts of the application.

By following this structure, the codebase remains clean, maintainable, and scalable, ensuring a robust and efficient application development process.

How to maintain services

When working with services in this codebase, it's important to follow these guidelines to ensure consistency, maintainability, and scalability:

  1. Do Not Pass Ref or BuildContext into Services:
  • Services should be designed to operate independently of the Flutter framework's context and state management. This ensures that they remain modular and reusable across different parts of the application.
  • Instead, pass only the necessary data and dependencies directly to the service methods.
  1. Avoid Calling Another Service from Within a Service:
  • Each service should be self-contained and should not directly depend on other services. This prevents tight coupling and makes it easier to test and maintain each service independently.
  • If a service needs to use functionality from another service, this should be handled at a higher level, such as within a provider or a controller.
  1. Keep Services Focused on a Single Responsibility:
  • Each service should have a clear and specific responsibility. This aligns with the Single Responsibility Principle (SRP) and helps in maintaining a clean separation of concerns.
  • Avoid adding unrelated functionalities to a single service.
  1. Ensure Services are Stateless:
  • Services should generally be stateless, meaning they do not hold any state between method calls. This ensures that they are thread-safe and can be reused across different parts of the application without side effects.
  1. Document Service Methods Clearly:
  • Provide clear documentation for each method within a service, including its purpose, parameters, and return values. This helps other developers understand how to use the service correctly.

By adhering to these guidelines, you can ensure that the services in your codebase remain clean, maintainable, and scalable, contributing to the overall robustness and efficiency of the application.

How do I know if a method should be included in a service?

Typically, the functionality of a service revolves around some kind of 'instance', which is typically provided by a provider with a name as 'InstanceProvider'. Within the service, methods are then created to interact with this instance. For example, the FirestoreService interacts with the Firestore instance provided by firebaseFirestoreInstanceProvider. As an exapmle, see the following method from the FirestoreService:

  /// Creates a post with the given [data].
  Future<CreatePostResult> createPost(PostModel data) async {
    return instance
        .collection('posts')
        .doc(data.postID)
        .set(data.toJson())
        .then((_) => CreatePostResult.successful);
  }

In this case, the createPost method interacts with the Firestore instance to create a post. This method is a good candidate for inclusion in the FirestoreService because it directly interacts with the Firestore instance provided by the firebaseFirestoreInstanceProvider.

If you find that a method does not directly interact with an instance provided by a provider, it may not be suitable for inclusion in a service. Instead, consider whether the method belongs in a different service or a separate utility class.

Classes

AuthService Services
A service for interacting with Firebase Auth.
CalendarService Services
A service for adding events to the user's calendar.
CloudFunctionService Services Cloud Functions
A service for interacting with Firebase Functions.
CloudMessagingService Services
A service for interacting with Firebase Cloud Messaging.
DriveApiService Services
A service for interacting with Google Drive.
FirestoreService Services
A service for interacting with Firebase Firestore.
LocalNotificationService Services
The service used for handling local notifications.
RemoteConfigService Services
A service for interacting with Firebase Remote Config.
StorageService Services
A service for interacting with Firebase Storage.

Functions

isConnected(dynamic ref) bool Services
A provider for the device's connectivity status.
isConnectedStream(dynamic ref) Stream<bool> Services
A provider for a stream of booleans that represent the device's connectivity.
sharedPreferences(dynamic ref) SharedPreferences Services
A provider that returns an instance of SharedPreferences.