Skip to content

flutter-service/service

Repository files navigation

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

About

A Flutter-native service layer that embraces Flutter dynamic widget lifecycle instead of static provider graphs.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages