zenify 2.2.0 copy "zenify: ^2.2.0" to clipboard
zenify: ^2.2.0 copied to clipboard

Complete state management for Flutter — hierarchical dependency injection, intelligent async caching, and offline-first resilience. Zero boilerplate, automatic cleanup.

Zenify

The state management framework that works the way Flutter works.

Automatic Query Caching • Scoped DI with Auto-Disposal • Offline-First • No Code Generation

pub package likes pub points codecov license: MIT

Zenify is a complete Flutter state management framework with a built-in TanStack Query engine, offline-first resilience, and hierarchical dependency injection — with zero boilerplate and no code generation.

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

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

// Infinite scroll — automatic page management with memory bounds
final feed = ZenInfiniteQuery<PostPage>(
  queryKey: 'feed',
  maxPages: 5,                    // 🚀 NEW: cap pages in RAM, evicts oldest
  initialPageParam: 1,
  infiniteFetcher: (page, _) => api.getPosts(page: page),
  getNextPageParam: (lastPage, all) => lastPage.hasMore ? all.length + 1 : null,
);

feed.fetchNextPage();             // append next page
feed.hasNextPage.value            // know when to stop
feed.isFetchingNextPage.value     // drive your loading footer
feed.data.value                   // all pages, reactive

// Global reactive status — anywhere in the app tree
Zen.isFetching                    // true if ANY query is in-flight
Zen.isMutating                    // true if ANY mutation is in-flight

🎯 The Problem #

Every Flutter developer has written this. Probably many times:

// The async state boilerplate tax — paid on every API call, in every app
bool _isLoading = false;
String? _error;
User? _data;

Future<void> loadUser() async {
  setState(() => _isLoading = true);
  try {
    _data = await api.getUser(id);
    _error = null;
  } catch (e) {
    _error = e.toString();
  } finally {
    setState(() => _isLoading = false);
  }
}

// And if two widgets need the same data? Two separate API calls.
// And if the user goes offline? Blank screen.
// And if the data is stale? Manual cache invalidation logic.
// And if you navigate away and back? Fetch it all over again.

Now here's the same thing with Zenify:

ZenQueryConsumer<User>(
  queryKey: 'user:$id',
  fetcher: (_) => api.getUser(id),
  data: (user) => UserProfile(user),
  loading: () => const CircularProgressIndicator(),
  error: (e, retry) => ErrorView(e, onRetry: retry),
);

Automatic caching. Deduplication. Background refetch. Stale-while-revalidate. Offline resilience. All built in.

This is what TanStack Query brought to the web. Zenify brings it to Flutter — natively, without code generation, integrated directly into the framework's DI and reactivity system.


🏛️ Built on Flutter's Foundations — No Hidden Global Magic #

This isn't just a design goal — it's enforced throughout the entire framework.

Scoped DI resolves dependencies by walking Flutter's native InheritedWidget tree. There is no mutable currentScope global pointer that changes on every navigation event.

Controller lifecycle is tied directly to State.dispose(). When the widget leaves the tree, the controller is disposed — no framework-level singleton registry involved.

Query cache is scoped per ZenRoute. Navigating away disposes the cache with the scope. There is no shared global cache that leaks state across different user sessions or routes.

Reactivity (.obs(), ZenObserver, ZenUpdater) is built on plain Dart objects and InheritedWidget notifications. There is no global event bus and no hidden dependency tracking.

The result: what you see in your widget tree is exactly what resolves your dependencies. Zenify is predictable, deeply linkable, and fully BuildContext-safe from the ground up — not as a documented limitation, but as an architectural guarantee.


Coming from GetX? The .obs() reactive syntax and DI verbs (put, find, delete) will feel familiar. Most migration is mechanical. GetX Migration Guide →

Upgrading from V1? The only mechanical change is adding a controller parameter to every ZenView.build() override. V2 Migration →


🏗️ Understanding Scopes (The Foundation) #

Every Flutter developer has written this — the manual lifecycle tax:

// Without scoped DI — every feature carries this burden
class _ProfilePageState extends State<ProfilePage> {
  late final ProfileController _controller;
  late final UserRepository _repo;
  late final ProfileImageService _imageService;

  @override
  void initState() {
    super.initState();
    _controller = ProfileController();
    _repo = UserRepository(apiClient);
    _imageService = ProfileImageService(apiClient);
  }

  @override
  void dispose() {
    _controller.dispose(); // ← easy to forget
    _repo.dispose();       // ← easy to forget
    _imageService.dispose(); // ← easy to forget
    super.dispose();
    // Add a fourth dependency tomorrow? Add another dispose() call.
    // Forget one? Silent memory leak. Good luck finding it in production.
  }
}

Now here's the same thing with Zenify:

class ProfileModule extends ZenModule {
  @override
  void register(ZenScope scope) {
    final api = scope.find<ApiClient>()!;          // resolved from parent scope automatically
    scope.putLazy<UserRepository>(() => UserRepository(api));
    scope.putLazy<ProfileImageService>(() => ProfileImageService(api));
    scope.put<ProfileController>(ProfileController());
  }
}

ZenRoute(moduleBuilder: () => ProfileModule(), page: const ProfilePage())
// Navigate away → scope disposed → all three objects cleaned up. Automatically.
// Navigate back → fresh scope, fresh controllers, fresh state. Always.

One ZenRoute. Zero dispose() calls. Zero memory leaks. Zero stale state.

This is what hierarchical scoped DI means in practice. Zenify organizes dependencies into three levels with automatic lifecycle management. When a scope is destroyed, all its children are automatically cleaned up — the cascade is built in.

flowchart TD
    classDef root fill:#1A237E,stroke:#3949AB,stroke-width:2px,color:#fff
    classDef module fill:#004D40,stroke:#00897B,stroke-width:2px,color:#fff
    classDef page fill:#3E2723,stroke:#6D4C41,stroke-width:2px,color:#fff

    A["🌐 RootScope<br>(App Lifetime)"]:::root

    subgraph Feature_A [Module Scope]
        B["📦 AuthModule<br>(Feature Lifetime)"]:::module
        C["📄 LoginPage<br>(Page Lifetime)"]:::page
        D["📄 RegisterPage<br>(Page Lifetime)"]:::page
    end

    subgraph Feature_B [Module Scope]
        E["📦 CartModule<br>(Feature Lifetime)"]:::module
        F["📄 CheckoutPage<br>(Page Lifetime)"]:::page
    end

    A -->|"Zen.find()"| B
    A -->|"Zen.find()"| E
    B --> C
    B --> D
    E --> F

    note["♻️ Auto-disposed when<br>navigating away"]
    B -.- note
    E -.- note

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 CompanyControllerDepartmentControllerEmployeeController

Page Scope (Page — Page Lifetime)

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

When to Use What #

Scope Use For Lifetime
RootScope Needed across entire app App session
Module Scope Needed across a feature Feature navigation
Page Scope Needed on one page Single page

Learn more about hierarchical scopes →


🚀 Quick Start (30 seconds) #

1. Install #

dependencies:
  zenify: ^2.2.0

2. Initialize #

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

3. Create a Controller #

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

4. Provide & Consume #

// 1. Register — in your module, put the controller into its scope
class CartModule extends ZenModule {
  @override
  void register(ZenScope scope) {
    scope.put<CartController>(CartController());
  }
}

// In your router — ZenRoute creates the scope and runs the module:
ZenRoute(
  moduleBuilder: () => CartModule(),
  page: const CartPage(),
)

// 2. Consume — controller is injected directly into build(), zero lookup code
class CartPage extends ZenView<CartController> {
  const CartPage({super.key});

  @override
  Widget build(BuildContext context, CartController controller) {
    return Column(
      children: [
        // 3. React — auto-rebuilds when itemCount changes
        ZenObserver(() => Text('${controller.itemCount.value} items')),
        ElevatedButton(
          onPressed: controller.checkout,
          child: const Text('Checkout'),
        ),
      ],
    );
  }
}

That's it. The controller is scoped to the route, auto-disposed on pop, multi-instance safe.

See complete example →


🔥 Core Features #

1. Smart Async State (ZenQuery) #

React Query patterns built on the reactive system. Say goodbye to manual isLoading flags.

sequenceDiagram
    participant UI as ZenQueryBuilder
    participant Cache as ZenQuery Cache
    participant API as Network / DB

    UI->>Cache: 1. Request 'user:123'
    alt Cache is Fresh
        Cache-->>UI: 2. Return cached data immediately
    else Cache is Stale
        Cache-->>UI: 2. Return stale data instantly
        Cache->>API: 3. Fetch fresh data in background
        API-->>Cache: 4. New data arrives
        Cache-->>UI: 5. UI reactively updates
    else Cache is Empty
        Cache-->>UI: 2. Yield 'loading' state
        Cache->>API: 3. Fetch data
        API-->>Cache: 4. Data arrives
        Cache-->>UI: 5. Yield 'data' state
    end

Path A — Inline (no controller needed):

ZenQueryConsumer<User>(
  queryKey: 'user:123',
  fetcher: (_) => api.getUser(123),
  data: (user) => UserProfile(user),
  loading: () => const CircularProgressIndicator(),
  error: (error, retry) => ErrorView(error, onRetry: retry),
);

Path B — Shared (query in controller, multiple widgets read it):

class UserController extends ZenController {
  late final query = ZenQuery<User>(
    queryKey: 'user:123',
    fetcher: (_) => api.getUser(123),
    config: ZenQueryConfig(staleTime: Duration(minutes: 5)),
  );
}

ZenQueryBuilder<User>(
  query: controller.query,
  builder: (context, user) => UserProfile(user),
  loading: () => const 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
  • ✅ Optimistic updates with rollback
  • ✅ Infinite scroll pagination with maxPages memory bounds
  • ✅ Real-time streams support
  • ✅ Tag & wildcard group invalidation
  • Zen.isFetching & Zen.isMutating global reactive status hooks

See ZenQuery Guide →

2. Offline-First Resilience #

Offline support isn't a plugin you add. It's how Zenify's query engine is architected. Queries return stale data from disk instantly while fetching fresh data in the background. Mutations that fail when offline are queued, persisted, and automatically replayed when the connection returns.

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

// Queue mutations when offline — auto-replay when back online
final createPost = ZenMutation<Post, Post>(
  mutationKey: 'create_post',
  mutationFn: (post) => api.createPost(post),
);

Key capabilities:

  • Storage agnostic — Hive, SharedPreferences, SQLite, or any ZenStorage implementation
  • Mutation queue — Actions queued and auto-replayed on reconnect
  • Optimistic updates — Update UI immediately, sync later
  • Network modes — Control how queries behave offline (offlineFirst, online, always)

See Offline Guide →

3. ZenView — Pages, Screens & Widgets #

By extending ZenView<T>, the controller is injected directly into build(). No context.read<T>(), no BlocBuilder, no ref.watch(). The controller arrives as a typed parameter — see the Quick Start above for the full Register → Consume → React pattern.

React — pick your reactivity tool:

// Rx<T> values — auto-rebuilds on change:
ZenObserver(() => Text('${controller.count.value}'))

// Manual update() calls — selective rebuilds:
ZenUpdater<CartController>(
  builder: (ctx, ctrl) => CartBadge(count: ctrl.itemCount),
)

// Async/API state — declarative:
controller.productsQuery.when(
  data: (products) => ProductList(products: products),
  loading: () => const CircularProgressIndicator(),
  error: (e) => Text('Error: $e'),
)

Which pattern for which situation?

Situation Pattern
App-level services (auth, analytics, database) Zen.registerModules([AppModule()]) at startup
Route with multiple dependencies ZenRoute(moduleBuilder: () => FeatureModule())
Simple route, single controller ZenProvider.create<T>(create: ...)
App-level singleton (prototyping) Zen.put<T>(...) at startup

4. Hierarchical DI with Auto-Cleanup #

Organize dependencies naturally with feature-based modules and parent-child scopes.

// 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 on navigation)
class UserModule extends ZenModule {
  @override
  void register(ZenScope scope) {
    final db = scope.find<DatabaseService>()!;
    scope.putLazy<UserRepository>(() => UserRepository(db));
    scope.putLazy<UserController>(() => UserController());
  }
}

// Use with any router — it's just a widget
ZenRoute(
  moduleBuilder: () => UserModule(),
  page: const UserPage(),
)

Core API:

  • Zen.put<T>() — Register in root scope
  • Zen.find<T>() — Retrieve (throws if missing)
  • Zen.findOrNull<T>() — Retrieve (returns null if missing)
  • Zen.exists<T>() — Check existence
  • Zen.delete<T>() — Remove from scope
  • Zen.putLazy<T>() — Register a lazy factory

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

See Hierarchical Scopes Guide →

5. Zero-Boilerplate Reactivity #

Reactive system built on Flutter's ValueNotifier. Simple, fast, no magic.

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

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

  void addTodo(String title) => todos.add(Todo(title));
}

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

For manual/selective rebuilds, use ZenUpdater:

// Controller:
controller.update(['counter']); // Only notifies 'counter' listeners

// Widget:
ZenUpdater<CounterController>(
  id: 'counter',
  builder: (context, ctrl) => Text('${ctrl.count}'),
)

See Reactive Core Guide →


💡 Common Patterns #

Global Services with .to Pattern #

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

  final items = <CartItem>[].obs();
  void addToCart(Product product) => items.add(CartItem.fromProduct(product));
}

// Register once at startup
Zen.put<CartService>(CartService(), isPermanent: true);

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

Global Reactive State (Theme, Auth, Settings) #

For app-wide reactive state that belongs to no single page, use Zen.put + the .to pattern + ZenObserver. Do not use ZenView for thisZenView is strictly for page-level controllers scoped to a route.

class ThemeController extends ZenController {
  static ThemeController get to => Zen.find<ThemeController>()!;

  final isDark = false.obs();
  final accentColor = Colors.blue.obs();

  void toggleDark() => isDark.value = !isDark.value;
}

// Register once at app startup — isPermanent keeps it alive for the full app session
Zen.put<ThemeController>(ThemeController(), isPermanent: true);

// React to it anywhere in the widget tree — no ZenView, no ZenProvider needed
ZenObserver(() => Icon(
  ThemeController.to.isDark.value ? Icons.dark_mode : Icons.light_mode,
))

// Access imperatively in callbacks
ElevatedButton(
  onPressed: () => ThemeController.to.toggleDark(),
  child: const Text('Toggle Theme'),
)

Rule of thumb: If a controller belongs to one route → ZenProvider.create + ZenView. If a controller is truly app-wide → Zen.put + .to + ZenObserver.

Infinite Scroll Pagination & Memory Limits #

final postsQuery = ZenInfiniteQuery<PostPage>(
  queryKey: ['posts'],
  maxPages: 3, // 🚀 Memory bounds: Keep at most 3 pages in RAM, evicting oldest
  infiniteFetcher: (cursor, token) => api.getPosts(cursor: cursor),
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
);

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

Global Query & Mutation Hooks (Zen.isFetching, Zen.isMutating) #

Show app-wide loading bars, spinners, or save overlays effortlessly without prop drilling:

// Global progress bar at the top of the app
ZenObserver(() {
  if (Zen.isFetching) {
    return const LinearProgressIndicator();
  }
  return const SizedBox.shrink();
})

// Global save/busy indicator for mutations
ZenObserver(() {
  if (Zen.isMutating) {
    return const SavingBadge();
  }
  return const SizedBox.shrink();
})

Optimistic Updates #

// Easy way — helpers handle rollback automatically
final createPost = ZenMutation.listPut<Post>(
  queryKey: 'posts',
  mutationFn: (post) => api.createPost(post),
  onError: (err, post) => logger.error('Create failed', err),
);

// Advanced — full control
final mutation = ZenMutation<User, UpdateArgs>(
  onMutate: (args) => userQuery.data.value = args.toUser(),
  onError: (err, args, old) => userQuery.data.value = old,
);

Real-Time Streams #

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

🛠️ Advanced Features #

  • Global status hooksZen.isFetching & Zen.isMutating for app-wide loading indicators with zero prop-drilling
  • Memory-bounded paginationZenInfiniteQuery.maxPages caps RAM usage during long scroll sessions
  • Filtered fetch statusZen.queryCache.isFetching(tag: ...) checks status for any subset of queries
  • Effects — Automatic loading/error/success state management (guide)
  • Workersever, debounce, throttle, interval, condition reactive handlers
  • Computed values — Auto-updating derived state
  • Performance control — Fine-grained: ZenObserver (reactive) or ZenUpdater (manual)
  • DevTools — Built-in scope/query inspector

📱 Widget Quick Reference #

Widget Use When Rebuilds On
ZenView Building pages with controllers Controller injected into build()
ZenRoute Need module/scope per route Route navigation
ZenObserver Fine-grained reactive updates .obs() value changes
ZenUpdater Manual control over rebuild timing controller.update() call
ZenConsumer Access a controller, no rebuild Never (manual)
ZenQueryConsumer Fetch data inline, no controller needed Query state changes
ZenQueryBuilder Shared query instance across widgets Query state changes
ZenStreamQueryBuilder Real-time data streams Stream events
ZenEffectBuilder Async operations with loading/error states Effect state changes

90% of the time, you'll use:

  • ZenView for pages
  • ZenObserver for reactive UI
  • ZenQueryConsumer for simple API calls
  • ZenQueryBuilder when the query is shared across widgets

🔧 Configuration #

void main() {
  Zen.init();

  // Optional: configure logging
  ZenConfig.applyEnvironment(ZenEnvironment.development);

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

  runApp(const 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.onInit();
    controller.increment();
    expect(controller.count.value, 1);
    controller.dispose();
  });

  test('mock dependencies', () {
    Zen.testMode().mock<ApiClient>(FakeApiClient());
    // All code that calls Zen.find<ApiClient>() gets the mock
  });

  test('query with in-memory storage', () async {
    Zen.queryCache.setStorage(InMemoryStorage()); // built-in, zero deps
    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 →


⬆️ Migrating from V1 #

V2 has several breaking changes. The most impactful is mechanical:

// ❌ V1 — magic getter, global registry
class CartPage extends ZenView<CartController> {
  @override
  Widget build(BuildContext context) {
    return Text('${controller.totalItems}');
  }
}

// ✅ V2 — explicit injection, tree-bound
class CartPage extends ZenView<CartController> {
  const CartPage({super.key});

  @override
  Widget build(BuildContext context, CartController controller) {
    return Text('${controller.totalItems}');
  }
}

Full change summary:

V1 V2 Impact
build(BuildContext context) + magic getter build(BuildContext context, T controller) Breaking — compiler enforces it
ZenScopeWidget / ZenScopeWidget.create ZenProvider / ZenProvider.create Breaking — rename
ZenControllerScope<T>() Removed — use ZenProvider.create<T>() Breaking — must migrate
initController override on ZenView Removed — use ZenProvider.create at callsite Breaking — must migrate
ZenBuilder<T> ZenUpdater<T> — fully renamed Breaking — rename required
Global Zen.put for UI controllers ZenProvider scope — no global fallback Architectural shift

Full V2 Migration Guide →


🔍 Flutter DevTools Extension #

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

Quick Setup #

dev_dependencies:
  zenify_devtools_extension: ^1.0.0
void main() {
  Zen.init(registerDevTools: true); // registers extensions automatically
  runApp(const MyApp());
}

3-Tab Inspector:

  1. Scope Inspector — Visualize your entire DI hierarchy
  2. Query Cache Viewer — Monitor, refetch, and invalidate queries
  3. Metrics Dashboard — Live metrics to identify bottlenecks

Learn more →


🎓 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

Building something complex?


📚 Complete Documentation #

Core Guides #

Examples #

  • Counter — Simple reactive state
  • Todo App — CRUD operations
  • E-commerce — Real-world patterns
  • Hierarchical Scopes (Flat) — Explicit passing of parent scopes for standard Navigator
  • Hierarchical Scopes (Nested) — Canonical Zero-Config DI using go_router ShellRoute
  • ZenQuery Demo — Async state management
  • Offline Demo — Full offline-first app
  • Showcase — All features

🙏 Acknowledgements #

  • TanStack Query by Tanner Linsley — For proving that async state deserves a first-class engine
  • Riverpod by Remi Rousselet — For hierarchical scoping patterns in Flutter
  • GetX by Jonny Borges — For pioneering terse reactive syntax in Flutter

💬 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.

13
likes
0
points
829
downloads

Documentation

Documentation

Publisher

verified publisherpixel-innovations.com

Weekly Downloads

Complete state management for Flutter — hierarchical dependency injection, intelligent async caching, and offline-first resilience. Zero boilerplate, automatic cleanup.

Repository (GitHub)
View/report issues

Topics

#state-management #dependency-injection #reactive-programming #offline-first #caching

License

unknown (license)

Dependencies

flutter

More

Packages that depend on zenify