mvvm_service 1.2.0 copy "mvvm_service: ^1.2.0" to clipboard
mvvm_service: ^1.2.0 copied to clipboard

A Flutter-native service layer inspired by the MVVM pattern, managing services according to the widget lifecycle, without relying on Provider or Riverpod.

Flutter Service

A Flutter-native service layer inspired by the MVVM pattern,
managing services according to the widget lifecycle,
without relying on Provider or Riverpod.

Why Use This Library? #

  • Widget Lifecycle Native: Designed to work naturally with the Flutter widget lifecycle, avoiding heavy third-party state management overhead.

  • Clean Architecture: Strongly encourages a strict separation of UI and business logic.

  • Asynchronous & Reactive: Supports async data fetching with automatic UI rebuilds out of the box.

  • Highly Testable: Predictable and isolated state management, ideal for MVVM-inspired architectures.

Usage #

Defining a Service #

import 'package:service/service.dart';

/// A simple example service that extends [Service] with integer data.
/// Each call to [fetchData] increments a static counter.
class ExampleService extends Service<int> {
  static int count = 0;

  /// Simulates fetching data asynchronously with a 1-second delay.
  @override
  Future<int> fetchData() async {
    await Future.delayed(Duration(seconds: 1));
    return count += 1; // Increment and return the counter
  }
}

Injecting a Single Service via ServiceWidget #

ServiceWidget is a convenient widget that ties a single service directly to the widget lifecycle. It automatically creates and disposes of the service, and rebuilds the UI whenever the service notifies listeners.

import 'package:flutter/material.dart';
import 'package:service/service.dart';

/// A widget that uses [ExampleService] via [ServiceWidget].
class ExampleWidget extends ServiceWidget<ExampleService> {
  const ExampleWidget({super.key});

  /// Provides the initial instance of [ExampleService].
  @override
  ExampleService get initialService => ExampleService();

  /// Builds the UI based on the current state of the service.
  @override
  Widget build(BuildContext context, ExampleService service) {
    if (service.isLoading) {
      return const CircularProgressIndicator();
    }

    if (service.isError) {
      return Text("Service failed: ${service.error}");
    }

    return RefreshIndicator(
      onRefresh: service.refresh,
      child: Opacity(
        opacity: service.isRefreshing ? 0.5 : 1,
        child: Text(service.data.toString()),
      ),
    );
  }
}

Injecting Multiple Services #

When a screen or widget tree requires multiple services, you can inject them all at once without nested widget trees by using MultiServiceWidget or MultiServiceContainer.

Subclassing MultiServiceWidget

import 'package:flutter/material.dart';
import 'package:service/service.dart';

class MyComplexWidget extends MultiServiceWidget {
  const MyComplexWidget({super.key});

  @override
  List<Service> get initialServices => [
    AuthService(),
    ThemeService(),
  ];

  @override
  Widget build(BuildContext context) {
    // Access the injected services anywhere in the subtree
    final auth = Service.of<AuthService>(context);
    final theme = Service.of<ThemeService>(context);

    return Container(
      color: theme.backgroundColor,
      child: Text('Hello, ${auth.userName}'),
    );
  }
}

Using MultiServiceContainer Directly

If you prefer an inline approach without subclassing, use MultiServiceContainer:

MultiServiceContainer(
  entries: [
    ServiceEntry((context) => AuthService()),
    ServiceEntry((context) => ThemeService()),
  ],
  builder: (context) {
    final auth = Service.of<AuthService>(context);
    return Text('User: ${auth.userName}');
  },
)

Using ServiceContainer Directly #

If you prefer not to subclass ServiceWidget for a single service, you can use ServiceContainer inline:

ServiceContainer<ExampleService>(
  // Create the initial service instance.
  factory: (_) => ExampleService(),
  builder: (context, service) {
    if (service.isLoading) return const CircularProgressIndicator();

    // Show the service data once loaded.
    return Text(service.data.toString());
  },
)

Accessing Services from the Context #

You can easily access an active service instance from any descendant widget in the subtree using the following syntax:

final service = Service.of<MyService>(context);

Consuming Services via ServiceWidgetOf #

If a sub-widget only needs to consume an existing service from the tree without managing its lifecycle, use ServiceWidgetOf to keep your code concise:

/// A subtree widget that depends on [ExampleService] using [ServiceWidgetOf].
class ExampleSubtreeWidget extends ServiceWidgetOf<ExampleService> {
  const ExampleSubtreeWidget({super.key});

  @override
  Widget build(BuildContext context, ExampleService service) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(service.data.toString()),
        TextButton(
          onPressed: service.refresh,
          child: const Text("Refresh"),
        ),
      ],
    );
  }
}

Declarative State Handling with when #

You can use the when extension on any service to declaratively build widgets based on its current state. This eliminates messy conditional statements and maps each state directly to a corresponding widget:

service.when(
  none: () => const Text("Service is idle"), // optional fallback when 'loading'
  loading: () => const CircularProgressIndicator(),
  refresh: () => const CircularProgressIndicator(), // optional fallback when 'loaded'
  failed: (error) => Text("Service failed: $error"),
  loaded: (data) => Text("Data: $data"),
);

Architecture Tips #

Using the Singleton Pattern #

Singletons are highly effective when you want only one instance of a service to exist across your entire application. This ensures shared state remains consistent and avoids unnecessary re-allocations.

Important

Declaring a single instance as static and forcefully providing it manually goes against Flutter's widget-tree-driven philosophy. Wrap your singletons cleanly using the framework's native entry points.

class ExampleService extends Service<int> {
  ExampleService._();

  /// The singleton instance of [ExampleService].
  /// Use this instead of creating a new instance to ensure a single shared service.
  static final ExampleService instance = ExampleService._();

  static int count = 0;

  @override
  Future<int> fetchData() async {
    await Future.delayed(const Duration(seconds: 1));
    return count += 1;
  }
}
2
likes
155
points
147
downloads

Documentation

API reference

Publisher

verified publisherttangkong.dev

Weekly Downloads

A Flutter-native service layer inspired by the MVVM pattern, managing services according to the widget lifecycle, without relying on Provider or Riverpod.

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on mvvm_service