mvvm_service 1.1.2 copy "mvvm_service: ^1.1.2" to clipboard
mvvm_service: ^1.1.2 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? #

  • Designed to work naturally with Flutter widget lifecycle, rather than relying on heavy third-party state management libraries like Riverpod.

  • Encourages separation of UI and business logic.

  • Supports async data fetching with automatic rebuilds.

  • Testable and predictable, ideal for MVVM-inspired architecture.

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
  }
}

Using ServiceWidget #

ServiceWidget is a convenient widget that ties a service to the widget lifecycle:

  • Automatically creates and disposes the service.
  • Rebuilds the UI whenever the service notifies listeners.
  • Provides build method with the current service instance.
import 'package:flutter/material.dart';
import 'package:service/service.dart';

/// A widget that uses [ExampleService] via [ServiceWidget].
/// Shows a loading indicator while data is being fetched,
/// and displays the fetched integer once available.
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.
  /// Shows a [CircularProgressIndicator] while loading,
  /// and displays the service's integer data once loaded.
  @override
  Widget build(BuildContext context, ExampleService service) {
    if (service.isLoading) {
      return CircularProgressIndicator();
    }

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

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

Using ServiceBuilder Directly #

ServiceBuilder allows you to use services without subclassing ServiceWidget. It provides a factory for the service and a builder for the UI.

ServiceBuilder<ExampleService>(
  // Create the initial service instance.
  factory: (_) => ExampleService(),
  builder: (context, service) {
    // (Exception handling omitted)
    ...

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

Using Provider-like. #

You can easily access a service from an ancestor widget using the following syntax:

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

Using ServiceWidgetOf #

If you don't need to directly reference the service instance in your widget tree, you can simplify your code by using ServiceWidgetOf instead:

/// A subtree widget that depends on [ExampleService] using [ServiceWidgetOf].
/// Displays the loaded integer data with a refresh mechanism.
class ExampleSubtreeWidget extends ServiceWidgetOf<ExampleService> {
  const ExampleSubtreeWidget({super.key});

  @override
  Widget build(BuildContext context, ExampleService service) {
    return ...;
  }
}

Using When #

You can use the when extension on the service to declaratively build widgets based on its current state. This keeps the UI code concise and clearly maps each state to a corresponding widget:

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

Tip #

Using Singleton Pattern

Singletons are useful when you want only one instance of a service to exist across your app.
This ensures shared state is consistent and avoids creating multiple instances unnecessarily.

Important

Also, declaring a single instance as static and providing it via a Provider is inefficient and goes against the Flutter philosophy.

/// A simple example service that extends [Service] with integer data.
/// It increments a static counter each time [fetchData] is called.
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;

  // Simulates fetching data asynchronously with a 1-second delay.
  @override
  Future<int> fetchData() async {
    await Future.delayed(Duration(seconds: 1));
    return count += 1;
  }
}
0
likes
160
points
53
downloads

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

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on mvvm_service