Loupe Flutter

A Flutter widget integration for the loupe reactive state management library.

Loupe Flutter provides a Loupe widget that automatically rebuilds when watched objects change, making it easy to integrate loupe's reactive system with Flutter's declarative UI.

Features

  • Automatic rebuilds - Widget rebuilds when any watched object changes
  • Simple API - Just provide a build function and a list of objects to watch
  • Memory safe - Automatically unsubscribes when widget is disposed
  • Lightweight - Minimal overhead, built on top of the zero-dependency loupe package

Installation

Add to your pubspec.yaml:

dependencies:
  loupe_flutter:
    git:
      url: https://github.com/your-repo/loupe_flutter.git
      ref: main
  loupe:
    git:
      url: https://github.com/your-repo/loupe.git
      ref: main

Or if published to pub.dev:

dependencies:
  loupe_flutter: ^1.0.0
  loupe: ^1.0.0

Usage

Basic Usage

import 'package:flutter/material.dart';
import 'package:loupe/loupe.dart';
import 'package:loupe_flutter/loupe_flutter.dart';

class Counter {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyChanges(this);
  }
}

class MyApp extends StatelessWidget {
  final counter = Counter();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Loupe(
            watchList: [counter],
            build: (context) => Text('Count: ${counter.count}'),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: counter.increment,
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

Watching Multiple Objects

class UserProfile {
  String name;
  int age;
  UserProfile(this.name, this.age);

  void updateName(String newName) {
    name = newName;
    notifyChanges(this);
  }

  void updateAge(int newAge) {
    age = newAge;
    notifyChanges(this);
  }
}

class Settings {
  String theme = 'light';
  
  void toggleTheme() {
    theme = theme == 'light' ? 'dark' : 'light';
    notifyChanges(this);
  }
}

Loupe(
  watchList: [userProfile, settings],
  build: (context) => Column(
    children: [
      Text('Name: ${userProfile.name}'),
      Text('Age: ${userProfile.age}'),
      Text('Theme: ${settings.theme}'),
    ],
  ),
)

Nested Loupe Widgets

Loupe(
  watchList: [user],
  build: (context) => Column(
    children: [
      Text('User: ${user.name}'),
      Loupe(
        watchList: [user.settings],
        build: (context) => Text('Settings: ${user.settings.theme}'),
      ),
    ],
  ),
)

API Reference

Loupe Widget

A StatefulWidget that rebuilds when watched objects change.

Constructor:

Loupe({
  Key? key,
  required Widget Function(BuildContext) build,
  required List<Object> watchList,
})

Parameters:

  • key - Optional widget key
  • build - A function that builds the widget tree. Called on every rebuild.
  • watchList - A list of objects to watch for changes. When notifyChanges() is called on any of these objects, the widget rebuilds.

Important Notes

  • Object Identity: Like the underlying loupe package, this widget uses object identity to track changes. Primitive types (int, String, bool, etc.) will not work correctly. Always use reference types (class instances).
  • Manual Notification: You must call notifyChanges(obj) explicitly when an object changes. The widget does not automatically detect changes.
  • Automatic Cleanup: The widget automatically unsubscribes from all watched objects when it is disposed, preventing memory leaks.
  • BuildContext: The build callback receives the current BuildContext, just like any other Flutter widget builder.

Comparison with Other State Management Solutions

Feature Loupe Flutter Provider Riverpod Bloc
Dependencies Minimal Flutter Many Many
Learning Curve Low Low Medium High
Boilerplate Minimal Minimal Minimal High
Testability Easy Easy Easy Medium
Reactivity Manual Automatic Automatic Manual

Loupe Flutter is ideal when you want:

  • A simple, explicit reactive system
  • Minimal dependencies
  • Full control over when notifications happen
  • Easy integration with existing code

Example: Complete Counter App

import 'package:flutter/material.dart';
import 'package:loupe/loupe.dart';
import 'package:loupe_flutter/loupe_flutter.dart';

class Counter {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyChanges(this);
  }

  void decrement() {
    _count--;
    notifyChanges(this);
  }

  void reset() {
    _count = 0;
    notifyChanges(this);
  }
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Loupe Counter',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const CounterPage(),
    );
  }
}

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    final counter = Counter();

    return Scaffold(
      appBar: AppBar(title: const Text('Loupe Counter')),
      body: Center(
        child: Loupe(
          watchList: [counter],
          build: (context) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text('You have pushed the button this many times:'),
              Text(
                '${counter.count}',
                style: Theme.of(context).textTheme.headlineMedium,
              ),
              const SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  FloatingActionButton(
                    onPressed: counter.decrement,
                    tooltip: 'Decrement',
                    child: const Icon(Icons.remove),
                  ),
                  const SizedBox(width: 20),
                  FloatingActionButton(
                    onPressed: counter.reset,
                    tooltip: 'Reset',
                    child: const Icon(Icons.exposure_zero),
                  ),
                  const SizedBox(width: 20),
                  FloatingActionButton(
                    onPressed: counter.increment,
                    tooltip: 'Increment',
                    child: const Icon(Icons.add),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

License

MIT License.

Libraries

loupe_flutter