Skip to content

sdegenaar/zenify

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

125 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Zenify

pub package likes pub points codecov license: MIT

Complete state management for Flutterβ€”hierarchical dependency injection, reactive programming, and intelligent async state. Zero boilerplate, automatic cleanup.

// Hierarchical DI with automatic cleanup
scope.put<UserService>(UserService());
final service = scope.find<UserService>()!;  // Access from child scopes

// Reactive state that just works
final count = 0.obs();
ZenObserver(() => Text('$count'))  // Auto-rebuilds

// Smart async with caching
final userQuery = ZenQuery<User>(
  queryKey: 'user:123',
  fetcher: (_) => api.getUser(123),
);  // Caching, deduplication, refetchingβ€”all handled

🎯 Why Zenify?

Building async-heavy Flutter apps? You're probably fighting:

  • Manual cache management β€” Writing the same cache logic over and over
  • Duplicate API calls β€” Multiple widgets fetching the same data
  • Memory leaks β€” Forgetting to dispose controllers and subscriptions
  • Boilerplate overload β€” Hundreds of lines for simple async state

Zenify solves all of this.


⚑ What Makes Zenify Different

Hierarchical Scoped Architecture

Riverpod-inspired scoping with automatic cleanup. Dependencies flow naturally from parent to child, and scopes dispose themselves automatically when no longer needed. Simple API: Zen.put(), Zen.find(), Zen.delete().

Zero Boilerplate Reactivity

Reactive system with .obs() and ZenObserver() (or Obx() for GetX users). Write less, accomplish more, keep your code clean. Built on Flutter's ValueNotifier for optimal performance.

React Query Style Async State

A native-inspired implementation of TanStack Query patterns: automatic caching, smart refetching, request deduplication, and stale-while-revalidateβ€”built on top of the reactive system.

Offline-First Resilience

Don't let network issues break your app. Zenify includes robust persistence, an offline mutation queue, and optimistic updates out of the box with minimal configuration.


Coming from GetX? The reactive system (.obs(), Obx()), controller lifecycle, and DI verbs are intentionally familiar. Most migration is mechanical. GetX Migration Guide β†’


πŸ—οΈ Understanding Scopes (The Foundation)

Zenify organizes dependencies into three hierarchical levels with automatic lifecycle management:

The Three Scope Levels

RootScope (Global β€” App Lifetime)

  • Services like AuthService, CartService, ThemeService
  • Lives for entire app session
  • Access anywhere via Zen.find<CartService>() or the .to pattern: CartService.to.addItem()

Module Scope (Feature β€” Feature Lifetime)

  • Controllers shared across feature pages
  • Auto-dispose when leaving feature
  • Example: HR feature with CompanyController β†’ DepartmentController β†’ EmployeeController

Page Scope (Page β€” Page Lifetime)

  • Page-specific controllers
  • Auto-dispose when page pops
  • Example: LoginController, ProfileFormController

When to Use What

Scope Use For Example Lifetime
RootScope Needed across entire app Zen.find<T>() App session
Module Scope Needed across a feature Module registration Feature navigation
Page Scope Needed on one page createController Single page

The scope hierarchy automatically manages lifecycle β€” when you exit a feature, all its controllers clean up automatically. No memory leaks, no manual disposal.

Learn more about hierarchical scopes β†’


πŸš€ Quick Start (30 seconds)

1. Install

dependencies:
  zenify: ^1.10.0

2. Initialize

void main() async {
  await Zen.init();
  runApp(MyApp());
}

3. Create a Controller

class CounterController extends ZenController {
  final count = 0.obs();
  void increment() => count.value++;
}

4. Build UI

class CounterPage extends ZenView<CounterController> {
  @override
  CounterController Function()? get createController => () => CounterController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ZenObserver(() => Text('Count: ${controller.count.value}')),
            ElevatedButton(
              onPressed: controller.increment,
              child: Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

That's it! Fully reactive with automatic cleanup. No manual disposal, no memory leaks.

Note: createController is optional! If your controller is already registered in a module or globally, you can omit it and ZenView will find the controller automatically.

See complete example β†’


πŸ”₯ Core Features

1. Hierarchical DI with Auto-Cleanup

Organize dependencies naturally with feature-based modules and parent-child scopes. When you navigate away, everything cleans up automatically.

// App-level services (persistent)
class AppModule extends ZenModule {
  @override
  void register(ZenScope scope) {
    scope.put<AuthService>(AuthService(), isPermanent: true);
    scope.put<DatabaseService>(DatabaseService(), isPermanent: true);
  }
}

// Feature-level controllers (auto-disposed)
class UserModule extends ZenModule {
  @override
  void register(ZenScope scope) {
    // Access parent services via Zen.find()
    final db = scope.find<DatabaseService>()!;

    // Register feature-specific dependencies
    scope.putLazy<UserRepository>(() => UserRepository(db));
    scope.putLazy<UserController>(() => UserController());
  }
}

// Use with any router - it's just a widget!
ZenRoute(
  moduleBuilder: () => UserModule(),
  page: UserPage(),
  scopeName: 'UserScope',
)

Core API:

  • Zen.put<T>() β€” Register dependencies
  • Zen.find<T>() β€” Retrieve dependencies
  • Zen.delete<T>() β€” Remove dependencies

What you get:

  • Natural dependency flow (parent β†’ child)
  • Automatic disposal (no memory leaks)
  • Clean module organization
  • Easy testing (swap modules)

Works with: GoRouter, AutoRoute, Navigator 2.0, any router you like.

See Hierarchical Scopes Guide β†’

2. Zero-Boilerplate Reactivity

GetX-inspired reactive system built on Flutter's ValueNotifier. Simple, fast, no magic.

class TodoController extends ZenController {
  // Reactive primitives
  final todos = <Todo>[].obs();
  final filter = Filter.all.obs();

  // Computed values (auto-update)
  List<Todo> get filteredTodos {
    switch (filter.value) {
      case Filter.active: return todos.where((t) => !t.done).toList();
      case Filter.completed: return todos.where((t) => t.done).toList();
      default: return todos.toList();
    }
  }

  // Actions
  void addTodo(String title) => todos.add(Todo(title));
  void toggleTodo(Todo todo) => todo.done = !todo.done;
}

// In UI - automatic rebuilds
ZenObserver(() => Text('${controller.todos.length} todos'))
ZenObserver(() => ListView.builder(
  itemCount: controller.filteredTodos.length,
  itemBuilder: (context, i) => TodoItem(controller.filteredTodos[i]),
))

What you get:

  • Minimal rebuilds (only affected widgets)
  • Simple API (.obs(), ZenObserver(), done)
  • Type-safe (compile-time checks)
  • Zero overhead (built on ValueNotifier)

See Reactive Core Guide β†’

3. Smart Async State (ZenQuery)

React Query patterns built on the reactive system.

// Define once
final userQuery = ZenQuery<User>(
  queryKey: 'user:123',
  fetcher: (_) => api.getUser(123),
  config: ZenQueryConfig(
    staleTime: Duration(minutes: 5),
    cacheTime: Duration(hours: 1),
  ),
);

// Use anywhere - automatic caching, deduplication, refetching
ZenQueryBuilder<User>(
  query: userQuery,
  builder: (context, user) => UserProfile(user),
  loading: () => CircularProgressIndicator(),
  error: (error, retry) => ErrorView(error, onRetry: retry),
);

What you get for free:

  • βœ… Automatic caching with configurable staleness
  • βœ… Smart deduplication (same key = one request)
  • βœ… Background refetch on focus/reconnect
  • βœ… Stale-while-revalidate (show cached, fetch fresh)
  • βœ… Request cancellation (no wasted bandwidth)
  • βœ… Optimistic updates with rollback
  • βœ… Infinite scroll pagination
  • βœ… Real-time streams support
  • βœ… Tag & wildcard group invalidation (tags: ['user'], invalidateQueriesByTag, invalidateQueriesByPattern)

Perfect for: REST APIs, GraphQL, Firebase, any async data source.

See ZenQuery Guide β†’

4. Offline Synchronization Engine

Turn your app into an offline-capable powerhouse with minimal configuration.

// Auto-persist data to disk
final postsQuery = ZenQuery<List<Post>>(
  queryKey: 'posts',
  fetcher: (_) => api.getPosts(),
  config: ZenQueryConfig(
    persist: true,
    networkMode: NetworkMode.offlineFirst,
  ),
);

// Queue mutations when offline
final createPost = ZenMutation<Post, Post>(
  mutationKey: 'create_post', // Enables queuing
  mutationFn: (post) => api.createPost(post),
);

Key capabilities:

  • Storage agnostic β€” Works with Hive, SharedPreferences, SQLite, or any backend via ZenStorage
  • Mutation queue β€” Actions are queued and auto-replayed when online
  • Optimistic updates β€” Update UI immediately, sync later
  • Network modes β€” Control exactly how queries behave offline

See Offline Guide β†’


πŸ’‘ Common Patterns

Global Services with .to Pattern

Access services from anywhere without context or injection:

class CartService extends ZenService {
  static CartService get to => Zen.find<CartService>();

  final items = <CartItem>[].obs();

  void addToCart(Product product) {
    items.add(CartItem.fromProduct(product));
  }

  @override
  void onClose() {
    // Cleanup happens automatically
    super.onClose();
  }
}

// Register once
void main() {
  Zen.init();
  Zen.put<CartService>(CartService(), isPermanent: true);
  runApp(MyApp());
}

// Use anywhere - widgets, controllers, helpers
CartService.to.addToCart(product);

Infinite Scroll Pagination

final postsQuery = ZenInfiniteQuery<PostPage>(
  queryKey: ['posts'],
  infiniteFetcher: (cursor, token) => api.getPosts(cursor: cursor),
);

// Auto-load next page when reaching end
if (index == postsQuery.data.length - 1) postsQuery.fetchNextPage();

Optimistic Updates

Mutations provide automatic loading/error states, optimistic UI updates, offline queueing, and cache synchronization. Learn why mutations are better than direct API calls β†’

// Easy way: Use helpers (recommended)
final createPost = ZenMutation.listPut<Post>(
  queryKey: 'posts',
  mutationFn: (post) => api.createPost(post),
  onError: (err, post) => logger.error('Create failed', err), // Rollback automatic!
);

// Advanced: Manual control for complex scenarios
final mutation = ZenMutation<User, UpdateArgs>(
  onMutate: (args) => userQuery.data.value = args.toUser(),
  onError: (err, args, old) => userQuery.data.value = old, // Manual rollback
);

Real-Time Streams

final chatQuery = ZenStreamQuery<List<Message>>(
  queryKey: 'chat',
  streamFn: () => chatService.messagesStream,
);

See complete patterns with detailed examples β†’


πŸ› οΈ Advanced Features

  • Effects β€” Automatic loading/error/success state management (guide)
  • Computed values β€” Auto-updating derived state with dependency tracking
  • Global modules β€” Register app-wide dependencies at startup
  • Performance control β€” Choose between reactive (.obs() + ZenObserver) or manual (update() + ZenBuilder)
  • Workers β€” Debounce, throttle, and interval-based reactive handlers
  • DevTools β€” Built-in inspector for debugging scopes and queries

See detailed examples β†’


πŸŽ“ Learning Path

New to Zenify? Start here:

  1. 5 minutes: Counter Example β€” Basic reactivity
  2. 10 minutes: Todo Example β€” CRUD with effects
  3. 15 minutes: ZenQuery Guide β€” Async state management
  4. 20 minutes: E-commerce Example β€” Real-world patterns
  5. 30 minutes: Offline Demo β€” Full offline-first app with persistence, mutation queue & SharedPreferences

Building something complex?


πŸ“± Widget Quick Reference

Choose the right widget for your use case:

Widget Use When Rebuilds On
ZenView Building pages with controllers Automatic lifecycle
ZenRoute Need module/scope per route Route navigation
ZenObserver Need reactive updates Reactive value changes
ZenBuilder Need manual control controller.update() call
ZenQueryBuilder Fetching API data Query state changes
ZenStreamQueryBuilder Real-time data streams Stream events
ZenEffectBuilder Async operations Effect state changes
ZenConsumer Accessing dependencies Manual (no auto-rebuild)

90% of the time, you'll use:

  • ZenView for pages
  • ZenObserver for reactive UI
  • ZenQueryBuilder for API calls

πŸ”§ Configuration

void main() {
  Zen.init();

  // Optional: Configure logging and performance tracking
  ZenConfig.configure(level: ZenLogLevel.info, performanceTracking: true);

  // Optional: Set global query defaults
  Zen.queryCache.setDefaultConfig(ZenQueryConfig(
    staleTime: Duration(minutes: 5),
    cacheTime: Duration(hours: 1),
  ));

  runApp(MyApp());
}

πŸ§ͺ Testing

Built for testing from the ground up:

void main() {
  setUp(() {
    Zen.testMode().clearQueryCache();
  });
  tearDown(() => Zen.reset());

  test('counter increments', () {
    final controller = CounterController();
    controller.increment();
    expect(controller.count.value, 1);
  });

  test('mock dependencies', () {
    Zen.testMode().mock<ApiClient>(FakeApiClient());
    // Test code uses mock automatically
  });

  test('query with in-memory storage', () async {
    // InMemoryStorage is built-in β€” no external dependencies needed
    Zen.queryCache.setStorage(InMemoryStorage());
    final q = ZenQuery<String>(
      queryKey: 'test',
      fetcher: (_) async => 'hello',
      config: ZenQueryConfig(persist: true, toJson: (s) => {'v': s}, fromJson: (j) => j['v']),
    );
    await q.fetch();
    expect(q.data.value, 'hello');
  });
}

See complete testing guide β†’


πŸ” Flutter DevTools Extension

Zenify has a separate DevTools extension package for real-time inspection and debugging.

Quick Setup

  1. Add the DevTools extension (as a dev dependency):
dev_dependencies:
  zenify_devtools_extension: ^1.0.0
  1. Register service extensions in your app:
import 'package:zenify/devtools/devtools.dart';

void main() {
  ZenServiceExtensions.registerExtensions();
  runApp(MyApp());
}
  1. Enable the extension in devtools_options.yaml:
extensions:
  - zenify_devtools_extension: true

Features

3-Tab Inspector:

  1. Scope Inspector β€” Visualize your entire DI hierarchy with hierarchical tree view, dependency breakdown, and parent-child relationships
  2. Query Cache Viewer β€” Monitor all cached queries; search, filter, refetch, and invalidate with visual status indicators
  3. Metrics Dashboard β€” Live scope and query metrics to identify bottlenecks and detect memory leaks

Learn more β†’


πŸ“š Complete Documentation

Core Guides

Examples


πŸ™ Inspired By

Zenify stands on the shoulders of giants:

  • GetX by Jonny Borges β€” For intuitive reactive patterns
  • Riverpod by Remi Rousselet β€” For hierarchical scoping
  • React Query by Tanner Linsley β€” For smart async state

πŸ’¬ Community & Support


πŸ“„ License

MIT License β€” see LICENSE file


πŸš€ Ready to Get Started?

flutter pub add zenify

Choose your path:

Experience the zen of Flutter development.

About

Powerful asynchronous state management, automatic caching, hierarchical DI, and zero-boilerplate reactivity. A holistic architecture for modern Flutter apps.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors