GardenMobile & interfaces

A small observer pattern in Flutter

A shared subject and provider-scoped access, with subscription lifetime visible in the widget.

The original experiment used a small subject/observer pair, then exposed the subject through Provider rather than a static global. Those choices address notification and access scope separately.

Subject and shared state

class Subject<T> {
  List<Observer<T>> observers = <Observer<T>>[];
  T _state;

  T get state => _state;

  set state(T value) {
    _state = value;
    notifyAllObservers();
  }

  void addObserver(Observer<T> observer) {
    observers.add(observer);
  }

  void removeObserver(Observer<T> observer) {
    observers.remove(observer);
  }

  void notifyAllObservers() {
    for (final Observer<T> observer in observers) {
      observer.update(state);
    }
  }
}

abstract class Observer<T> {
  void update(T state);
}
import 'observable.dart';

class TimerProvider extends Subject<int> {

}

Subscribe from a widget

import 'package:flutter/material.dart';
import 'package:habitapp/services/app/observable.dart';
import 'package:habitapp/services/app/timer_provider.dart';
import 'package:provider/provider.dart';

class WObservable extends StatefulWidget {
  const WObservable({Key key, this.child, this.update}) : super(key: key);

  final Widget child;
  final Function(int state) update;

  @override
  _WObservableState createState() => _WObservableState();
}

class _WObservableState extends State<WObservable>
    with WidgetsBindingObserver, Observer<int> {
  TimerProvider timerProvider;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      timerProvider = Provider.of(context, listen: false);
      _attach();
    });
  }

  @override
  void dispose() {
    _detach();
    super.dispose();
  }

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    print('state: $state');
    switch (state) {
      case AppLifecycleState.paused:
        _detach();
        break;
      case AppLifecycleState.resumed:
        _attach();
        break;
      default:
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    return widget.child ?? Container();
  }

  @override
  void update(int state) {
    if (widget.update != null) {
      widget.update(state);
    }
  }

  // Attach to timer provider
  void _attach() {
    if (timerProvider != null) {
      timerProvider.addObserver(this);
      update(timerProvider.state);
    }
  }

  // Detach from timer provider
  void _detach() {
    timerProvider?.removeObserver(this);
  }
}

Supply the subject through the application tree


Future<void> myMain() async {
  /// Start services later
  WidgetsFlutterBinding.ensureInitialized();


  /// Run Application
  runApp(
    MultiProvider(
      providers: <SingleChildWidget>[
        Provider<TimerProvider>(create: (_) => TimerProvider()),
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatefulWidget {
  const MyApp({Key key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {

  Timer _timer;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);

    /// After first build
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _startTimer();
    });
  }

  @override
  void dispose() {
    _stopTimer();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    switch (state) {
      case AppLifecycleState.paused:
        _stopTimer();
        break;
      case AppLifecycleState.resumed:
        _startTimer();
        break;
      default:
        break;
    }
  }

  @override
  Widget build(BuildContext context) {

    /// Build Material app
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: WObservable(),
    );
  }

  /// Start timer
  Future<void> _startTimer() async {
    _stopTimer();
    await _timerLogic();
    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
      _timerLogic();
    });
  }

  // Timer logic
  Future<void> _timerLogic() async {
    context.read<TimerProvider>().state = DateTime.now().millisecondsSinceEpoch;
  }

  /// Stop timer
  void _stopTimer() {
    if (_timer != null) {
      _timer.cancel();
      _timer = null;
    }
  }
}

A provider makes access convenient, but does not automatically cancel timers or subscriptions. Duplicate observers, mutation during notification, and notifications after disposal require a policy in the complete implementation.

The example uses historical Dart/Flutter syntax and application imports. No timer or widget was executed for this revision. Source: Gist revision.

← Back to the garden