Skip to content

Template Service

Service adalah layer yang menangani operasi I/O: API call, database, file system, download, notifikasi, dll. Service dipanggil oleh provider melalui dependency injection (lihat Pola D - Provider Template).

Konvensi proyek:

  • 1 file per service di lib/services/
  • Service class tidak extend Riverpod apapun — pure Dart class
  • Service di-inject ke provider lewat Provider<ServiceClass> (DI)

Prasyarat: model sudah jadi. Kalau belum, baca Template Model dulu.


Pola A: Service Sederhana (Stateless / Config-based)

Untuk service yang tidak punya state internal, hanya menerima config lalu memproses: API client, HTTP wrapper, dll.

Contoh di proyek: CloudAiService

Kode — copy paste, lalu ganti yang ditandai ⬅️ GANTI:

dart
// lib/services/namamu_service.dart           // ⬅️ GANTI: nama file
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import '../models/namamu_config.dart';         // ⬅️ GANTI: import model config (jika perlu)

// ⬇️ Custom exception class (opsional — hapus jika tidak perlu)
class NamaException implements Exception {     // ⬅️ GANTI: nama exception class
  final String message;
  final int? statusCode;

  const NamaException(this.message, {this.statusCode});
  //    ⬅️ GANTI: nama constructor

  @override
  String toString() => 'NamaException: $message';
  //                      ⬅️ GANTI: nama exception
}

class NamaService {                            // ⬅️ GANTI: nama service class
  NamaConfig _config;                          // ⬅️ GANTI: config model (atau hapus)

  NamaService([NamaConfig? config])
  // ⬅️ GANTI: constructor + param
      : _config = config ?? const NamaConfig();
      //              ⬅️ GANTI: config default

  NamaConfig get config => _config;
  // ⬅️ GANTI: getter config

  void updateConfig(NamaConfig config) {       // ⬅️ GANTI: method update config
    _config = config;
  }

  void resetConfig() {                         // ⬅️ GANTI: method reset config
    _config = const NamaConfig();
    //            ⬅️ GANTI: config default
  }

  /// ⬇️ Method utama: async operasi
  Future<ReturnType> operasiUtama(             // ⬅️ GANTI: nama method + return type
    ParameterType param, {                     // ⬅️ GANTI: parameter
    NamaConfig? config,                        // ⬅️ GANTI: config override (opsional)
  }) async {
    final cfg = config ?? _config;

    try {
      // ... panggil API / proses data ...
      return hasil;                            // ⬅️ GANTI: return value
    } on SocketException catch (e) {
      throw NamaException('Network error: ${e.message}');
      //       ⬅️ GANTI: exception class
    } on NamaException {
      rethrow;                                 // ⬅️ GANTI: exception class
    } catch (e) {
      throw NamaException('Unexpected error: $e');
      //       ⬅️ GANTI: exception class
    }
  }

  /// ⬇️ Method stream (opsional — hapus jika tidak perlu)
  Stream<String> operasiStream(                // ⬅️ GANTI: nama method
    ParameterType param, {                     // ⬅️ GANTI: parameter
    NamaConfig? config,                        // ⬅️ GANTI: config override
  }) {
    // ... return stream ...
  }

  /// ⬇️ Method private helper
  Map<String, dynamic> _buildBody(             // ⬅️ GANTI: nama helper method
    ParameterType param,
    NamaConfig cfg,
  ) {
    return {
      // ... build request body ...
    };
  }
}
Bagian yang digantiContoh sebelumContoh sesudah
Nama filenamamu_service.dartcloud_ai_service.dart
Exception classNamaExceptionCloudAiException
Service classNamaServiceCloudAiService
Config modelNamaConfigCloudAiConfig
Method utamaoperasiUtama()generateCompletion()
Method streamoperasiStream()generateCompletionStream()

Pola B: Service dengan State Internal

Untuk service yang punya state internal (status, error, initialized flag) dan lifecycle management: download model, database connection, file manager, dll.

Contoh di proyek: AiModelService

Kode — copy paste, lalu ganti yang ditandai ⬅️ GANTI:

dart
// lib/services/namamu_service.dart           // ⬅️ GANTI: nama file
import 'dart:async';
import 'dart:io';

import '../models/namamu_status.dart';         // ⬅️ GANTI: import status enum (jika perlu)

class NamaService {                            // ⬅️ GANTI: nama service class
  // ⬇️ State internal
  TipeStatus _status = StatusDefault;          // ⬅️ GANTI: status enum + default
  String? _errorMessage;                       // ⬅️ GANTI: error tracking

  TipeStatus get status => _status;            // ⬅️ GANTI: getter status
  String? get errorMessage => _errorMessage;   // ⬅️ GANTI: getter error

  NamaService({Dependency? dep}) : _dep = dep; // ⬅️ GANTI: constructor + DI param

  /// ⬇️ Inisialisasi / koneksi
  Future<void> inisialisasi() async {          // ⬅️ GANTI: nama method
    _status = StatusLoading;                   // ⬅️ GANTI: status loading
    _errorMessage = null;

    try {
      // ... inisialisasi / connect ...
      _status = StatusSiap;                    // ⬅️ GANTI: status sukses
    } catch (e) {
      _status = StatusError;                   // ⬅️ GANTI: status error
      _errorMessage = 'Init failed: $e';
    }
  }

  /// ⬇️ Operasi utama
  Future<ReturnType> operasiUtama(             // ⬅️ GANTI: nama method + return type
    ParameterType param,
  ) async {
    if (_status != StatusSiap) {               // ⬅️ GANTI: guard clause
      throw StateError('Service not ready');
    }

    try {
      // ... operasi ...
      return hasil;
    } catch (e) {
      _status = StatusError;                   // ⬅️ GANTI: set error state
      _errorMessage = 'Operasi gagal: $e';
      rethrow;
    }
  }

  /// ⬇️ Reset / cleanup
  void reset() {                               // ⬅️ GANTI: nama method reset
    _status = StatusDefault;                   // ⬅️ GANTI: reset ke default
    _errorMessage = null;
  }

  /// ⬇️ Dispose untuk Riverpod onDispose
  void dispose() {                             // ⬅️ GANTI: nama method dispose
    reset();
    // ... cleanup resources ...
  }
}
Bagian yang digantiContoh sebelumContoh sesudah
Nama filenamamu_service.dartai_model_service.dart
Service classNamaServiceAiModelService
Status enumTipeStatusAiModelStatus
Status defaultStatusDefaultAiModelStatus.notDownloaded
Status siapStatusSiapAiModelStatus.ready
Method initinisialisasi()downloadModel() / initializeModel()
Method utamaoperasiUtama()generateCompletion()
Method resetreset()unload()
Method disposedispose()dispose()

Pola C: Service Singleton

Untuk service yang hanya butuh 1 instance global: notification, analytics, logging, shared preferences, dll.

Contoh di proyek: NotificationService

Kode — copy paste, lalu ganti yang ditandai ⬅️ GANTI:

dart
// lib/services/namamu_service.dart           // ⬅️ GANTI: nama file
import 'package:some_package/some_package.dart'; // ⬅️ GANTI: import dependency

class NamaService {                            // ⬅️ GANTI: nama service class
  // ⬇️ Singleton instance
  static final NamaService instance = NamaService._internal();
  //               ⬅️ GANTI: nama class

  NamaService._internal();                    // ⬅️ GANTI: private constructor

  // ⬇️ Dependencies internal
  final SomePlugin _plugin = SomePlugin();     // ⬅️ GANTI: plugin/dependency

  bool _initialized = false;

  /// ⬇️ Inisialisasi (guard agar tidak double-init)
  Future<void> initialize() async {           // ⬅️ GANTI: nama method init
    if (_initialized) return;

    // ... setup plugin, permission, config ...
    _initialized = true;
  }

  /// ⬇️ Operasi (dengan guard init)
  Future<void> operasi() async {              // ⬅️ GANTI: nama method
    if (!_initialized) return;

    // ... eksekusi operasi ...
  }
}
Bagian yang digantiContoh sebelumContoh sesudah
Nama filenamamu_service.dartnotification_service.dart
Service classNamaServiceNotificationService
DependencySomePluginFlutterLocalNotificationsPlugin
Method initinitialize()initialize()
Method operasioperasi()showModelReadyNotification()

Pola D: Service dengan Exception Kustom + Model Data

Untuk service yang perlu exception class sendiri dan model data internal: API dengan response model, parsing JSON, dll.

Contoh di proyek: CloudAiService (punya CloudMessage + CloudAiException)

Kode — copy paste, lalu ganti yang ditandai ⬅️ GANTI:

dart
// lib/services/namamu_service.dart           // ⬅️ GANTI: nama file
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import '../models/namamu_config.dart';         // ⬅️ GANTI: import config (jika perlu)

// ⬇️ Model data internal service (opsional — jika perlu type khusus)
class NamaMessage {                           // ⬅️ GANTI: nama model class
  final String role;
  final String content;

  const NamaMessage({required this.role, required this.content});
  //    ⬅️ GANTI: constructor

  Map<String, dynamic> toJson() => {           // ⬅️ GANTI: serialisasi (jika perlu)
    'role': role,
    'content': content,
  };
}

// ⬇️ Exception custom
class NamaException implements Exception {     // ⬅️ GANTI: nama exception class
  final String message;
  final int? statusCode;
  final String? body;

  const NamaException(                         // ⬅️ GANTI: constructor
    this.message, {
    this.statusCode,
    this.body,
  });

  @override
  String toString() => 'NamaException: $message';
  //                      ⬅️ GANTI: nama exception
}

class NamaService {                            // ⬅️ GANTI: nama service class
  NamaConfig _config;                          // ⬅️ GANTI: config

  NamaService([NamaConfig? config])
  // ⬅️ GANTI: constructor
      : _config = config ?? const NamaConfig();

  // ⬇️ Method utama (complete response)
  Future<String> operasiUtama(                 // ⬅️ GANTI: nama method + return type
    List<NamaMessage> messages, {             // ⬅️ GANTI: param (model)
    NamaConfig? config,
  }) async {
    final cfg = config ?? _config;
    final body = _buildBody(messages, cfg);

    try {
      final request = await _postRequest(cfg, body);
      final response = await request.close();
      final responseBody = await response.transform(utf8.decoder).join();

      if (response.statusCode != 200) {
        throw NamaException(                   // ⬅️ GANTI: exception class
          _parseError(responseBody, response.statusCode),
          statusCode: response.statusCode,
          body: responseBody,
        );
      }

      final json = jsonDecode(responseBody) as Map<String, dynamic>;
      return _parseResponse(json);             // ⬅️ GANTI: parse response
    } on SocketException catch (e) {
      throw NamaException('Network error: ${e.message}');
      //       ⬅️ GANTI: exception class
    } on NamaException {
      rethrow;                                 // ⬅️ GANTI: exception class
    } catch (e) {
      throw NamaException('Unexpected error: $e');
      //       ⬅️ GANTI: exception class
    }
  }

  // ⬇️ Method stream (opsional)
  Stream<String> operasiStream(                // ⬅️ GANTI: nama method
    List<NamaMessage> messages, {             // ⬅️ GANTI: param
    NamaConfig? config,
  }) {
    // ... return stream ...
  }

  // ⬇️ Private helpers
  Map<String, dynamic> _buildBody(             // ⬅️ GANTI: nama helper
    List<NamaMessage> messages,               // ⬅️ GANTI: tipe model
    NamaConfig cfg, {                         // ⬅️ GANTI: tipe config
    bool stream = false,
  }) {
    return {
      // ... build body ...
    };
  }

  Future<HttpClientRequest> _postRequest(      // ⬅️ GANTI: nama helper
    NamaConfig cfg,                           // ⬅️ GANTI: tipe config
    Map<String, dynamic> body,
  ) async {
    // ... build + return request ...
  }

  String _parseError(String body, int statusCode) {  // ⬅️ GANTI: nama helper
    // ... parse error response ...
  }

  String _parseResponse(Map<String, dynamic> json) { // ⬅️ GANTI: nama helper
    // ... parse success response ...
  }
}
Bagian yang digantiContoh sebelumContoh sesudah
Nama filenamamu_service.dartcloud_ai_service.dart
Model internalNamaMessageCloudMessage
Exception classNamaExceptionCloudAiException
Service classNamaServiceCloudAiService
Config modelNamaConfigCloudAiConfig
Method utamaoperasiUtama()generateCompletion()
Method streamoperasiStream()generateCompletionStream()
Private helper_postRequest()_postRequest()
Parse response_parseResponse()(inline di generateCompletion)
Parse error_parseError()_parseError()

Ringkasan

PolaStateKapan DipakaiContoh di Proyek
AStateless (config-based)API client, HTTP wrapperCloudAiService
BState internal (status, error)Download, DB connection, lifecycle panjangAiModelService
CSingletonNotifikasi, analytics, shared prefsNotificationService
DException + model internalAPI parsing kompleks, banyak error typeCloudAiService

Catatan: Pola bisa dikombinasikan. Misalnya CloudAiService adalah kombinasi A + D (config-based + exception custom + model internal).

Alur Development

Model                          Service                         Provider
─────                          ───────                         ────────
lib/models/namamu_config.dart  lib/services/namamu_service.dart  lib/providers/namamu_provider.dart
    │                                │                                │
    └─── Config di-inject ──────────┘                                │
                                     │                                │
                                     └─── Service di-inject ─────────┘
                                     via Provider<ServiceClass>       via ref.read(serviceProvider)

Setelah service jadi, lanjut ke Template Provider - Pola D untuk menghubungkan service ke provider, lalu ke Template Test Provider - Pola D untuk testing dengan mocktail.