Services

Services are a fundamental building block in AngularDart applications. They handle tasks like data fetching, business logic, and shared state management.

What is a Service?

A service is a class with a specific, well-defined purpose. Services are typically:

  • Singletons that provide shared data or functionality
  • Data access layers that communicate with APIs
  • Business logic containers
  • State management solutions

Creating a Service

import 'package:angulardart/angular.dart';

@Injectable()
class UserService {
  List<User> _users = [
    User(id: 1, name: 'Alice'),
    User(id: 2, name: 'Bob'),
  ];

  List<User> getUsers() => List.unmodifiable(_users);

  User? getUserById(int id) {
    return _users.where((u) => u.id == id).firstOrNull;
  }

  void addUser(User user) {
    _users.add(user);
  }

  void updateUser(int id, User updatedUser) {
    final index = _users.indexWhere((u) => u.id == id);
    if (index != -1) {
      _users[index] = updatedUser;
    }
  }

  void deleteUser(int id) {
    _users.removeWhere((u) => u.id == id);
  }
}

class User {
  final int id;
  final String name;

  User({required this.id, required this.name});
}

Providing a Service

Root-level (Singleton)

@Component(
  selector: 'app-component',
  template: '<router-outlet></router-outlet>',
  providers: [ClassProvider(UserService)],
)
class AppComponent {}

Component-level

@Component(
  selector: 'user-list',
  template: '<ul>...</ul>',
  providers: [ClassProvider(UserService)],
)
class UserListComponent {
  final UserService _userService;
  UserListComponent(this._userService);
}

Using a Service

@Component(
  selector: 'user-list',
  template: '<h2>Users</h2>'
      '<ul>'
      '<li *ngFor="let user of users">'
      '{{ user.name }}'
      '<button (click)="deleteUser(user.id)">Delete</button>'
      '</li>'
      '</ul>'
      '<button (click)="addUser()">Add User</button>',
  directives: [NgFor],
)
class UserListComponent implements OnInit {
  final UserService _userService;
  List<User> users = [];

  UserListComponent(this._userService);

  @override
  void ngOnInit() {
    users = _userService.getUsers();
  }

  void addUser() {
    final newUser = User(id: users.length + 1, name: 'New User');
    _userService.addUser(newUser);
    users = _userService.getUsers();
  }

  void deleteUser(int id) {
    _userService.deleteUser(id);
    users = _userService.getUsers();
  }
}

Async Services

Services often handle asynchronous operations:

@Injectable()
class ApiService {
  final String _baseUrl = 'https://api.example.com';

  Future<List<User>> getUsers() async {
    final response = await HttpRequest.request('$_baseUrl/users');
    final data = jsonDecode(response.responseText!) as List;
    return data.map((item) => User.fromJson(item)).toList();
  }

  Future<User> getUser(int id) async {
    final response = await HttpRequest.request('$_baseUrl/users/$id');
    final data = jsonDecode(response.responseText!) as Map;
    return User.fromJson(data);
  }

  Future<void> createUser(User user) async {
    await HttpRequest.request(
      '$_baseUrl/users',
      method: 'POST',
      sendData: jsonEncode(user.toJson()),
    );
  }
}

Using async services in components

@Component(
  selector: 'user-list',
  template: '<div *ngIf="isLoading">Loading...</div>'
      '<div *ngIf="error" class="error">{{ error }}</div>'
      '<ul *ngIf="!isLoading">'
      '<li *ngFor="let user of users">{{ user.name }}</li>'
      '</ul>',
  directives: [NgIf, NgFor],
)
class UserListComponent implements OnInit {
  final ApiService _apiService;
  List<User> users = [];
  bool isLoading = true;
  String? error;

  UserListComponent(this._apiService);

  @override
  void ngOnInit() {
    _loadUsers();
  }

  Future<void> _loadUsers() async {
    isLoading = true;
    error = null;
    try {
      users = await _apiService.getUsers();
    } catch (e) {
      error = 'Failed to load users: $e';
    } finally {
      isLoading = false;
    }
  }
}

Services with Streams

Services can expose reactive streams:

@Injectable()
class CartService {
  final _items = StreamController<List<CartItem>>.broadcast();
  final List<CartItem> _cart = [];

  Stream<List<CartItem>> get items => _items.stream;

  List<CartItem> get currentItems => List.unmodifiable(_cart);

  int get itemCount => _cart.length;

  void addItem(CartItem item) {
    _cart.add(item);
    _items.add(List.unmodifiable(_cart));
  }

  void removeItem(int index) {
    _cart.removeAt(index);
    _items.add(List.unmodifiable(_cart));
  }

  void clear() {
    _cart.clear();
    _items.add([]);
  }

  void dispose() {
    _items.close();
  }
}

Consuming streams in components

@Component(
  selector: 'cart',
  template: '<h2>Cart ({{ items.length }} items)</h2>'
      '<ul>'
      '<li *ngFor="let item of items; let i = index">'
      '{{ item.name }} - {{ item.price }}'
      '<button (click)="removeItem(i)">Remove</button>'
      '</li>'
      '</ul>',
  directives: [NgIf, NgFor],
)
class CartComponent implements OnInit, OnDestroy {
  final CartService _cartService;
  List<CartItem> items = [];
  StreamSubscription? _sub;

  CartComponent(this._cartService);

  @override
  void ngOnInit() {
    items = _cartService.currentItems;
    _sub = _cartService.items.listen((newItems) {
      items = newItems;
    });
  }

  @override
  void ngOnDestroy() {
    _sub?.cancel();
  }

  void removeItem(int index) {
    _cartService.removeItem(index);
  }
}

Service with Dependencies

Services can depend on other services:

@Injectable()
class AuthService {
  final StorageService _storage;
  final ApiService _api;

  User? _currentUser;

  AuthService(this._storage, this._api);

  User? get currentUser => _currentUser;

  bool get isLoggedIn => _currentUser != null;

  Future<bool> login(String email, String password) async {
    try {
      final user = await _api.authenticate(email, password);
      _currentUser = user;
      _storage.save('auth_token', user.token);
      return true;
    } catch (e) {
      return false;
    }
  }

  void logout() {
    _currentUser = null;
    _storage.remove('auth_token');
  }
}

@Component(
  selector: 'app',
  providers: [
    ClassProvider(StorageService),
    ClassProvider(ApiService),
    ClassProvider(AuthService),
  ],
)
class AppComponent {}

Best Practices

  • Keep services focused on a single responsibility
  • Use @Injectable() annotation for all services
  • Provide services at the appropriate level (root vs component)
  • Use streams for reactive data
  • Handle errors gracefully
  • Clean up resources in a dispose() method
  • Use abstract classes for service interfaces