signals_query 1.2.1 copy "signals_query: ^1.2.1" to clipboard
signals_query: ^1.2.1 copied to clipboard

A robust, reactive asynchronous state management and data-fetching library for Flutter leveraging the minimal, hooks-free power of Signals.

signals_query #

A Flutter package that mirrors TanStack Query (React Query) features, powered by Signals.

It provides an asynchronous state management and data-fetching solution for Flutter applications. We bypass complicated widgets, contexts, hooks, and builders, replacing them with a sleek reactive API!

Features #

  • Query & Mutation: Fetch and mutate data seamlessly.
  • Cache Management: Cache data manually or let QueryClient manage it.
  • Signals Powered: Reactive and performant UI rebuilding utilizing pure Signals! No hook-builders!

Initialization #

First, initialize a QueryClient that utilizes a Cache implementation (e.g., MemoryCache):

import 'package:signals_query/signals_query.dart';

final queryClient = QueryClient(cache: MemoryCache());

Basic Usage #

createQuery #

Define your query elegantly and natively Watch it:

import 'package:flutter/material.dart';
import 'package:signals_flutter/signals_flutter.dart';
import 'package:signals_query/signals_query.dart';

// Assuming you have a globally defined queryClient
// final queryClient = QueryClient(cache: MemoryCache());

class QueryExample extends StatefulWidget {
  const QueryExample({super.key});

  @override
  State<QueryExample> createState() => _QueryExampleState();
}

class _QueryExampleState extends State<QueryExample> {
  late final Query<String> userQuery;

  @override
  void initState() {
    super.initState();
    // 1. Create your Query instance
    userQuery = createQuery<String>(
      client: queryClient,
      key: () => ['user', 'u_123'],
      fn: () async {
        await Future.delayed(const Duration(seconds: 2));
        return 'Loaded User: u_123';
      },
    );
  }

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

  @override
  Widget build(BuildContext context) {
    // 2. Watch the state perfectly without boilerplate!
    return Watch((context) {
      if (userQuery.isLoading) return const CircularProgressIndicator();
      if (userQuery.state.value.isError) return Text('Error: ${userQuery.state.value.error}');

      return Column(
         children: [
           Text('Data: ${userQuery.data}'),
           ElevatedButton(
             onPressed: () => userQuery.refetch(),
             child: const Text('Refresh'),
           ),
         ],
      );
    });
  }
}

createMutation #

The createMutation offers a typed functional API for side effects. Mutations do not run immediately, but instead trigger when you call .mutate():

import 'package:flutter/material.dart';
import 'package:signals_flutter/signals_flutter.dart';
import 'package:signals_query/signals_query.dart';

class MutationExample extends StatefulWidget {
  const MutationExample({super.key});

  @override
  State<MutationExample> createState() => _MutationExampleState();
}

class _MutationExampleState extends State<MutationExample> {
  late final Mutation<String, String> saveMutation;

  @override
  void initState() {
    super.initState();
    // 1. Create your mutation
    saveMutation = createMutation<String, String>(
      client: queryClient,
      fn: (variables) async {
        await Future.delayed(const Duration(seconds: 1));
        return "Saved $variables!";
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Watch((context) {
      return Column(
        children: [
          ElevatedButton(
            onPressed: () async {
              final result = await saveMutation.mutate('New Data');
              if (result != null) {
                // You can access the global queryClient or invalidate your keys directly
                queryClient.invalidateQuery(['user', 'u_123']);
              }
            },
            child: saveMutation.isLoading.value
                ? const CircularProgressIndicator()
                : const Text('Save Data'),
          ),
          if (saveMutation.data.value != null)
            Text('Result: ${saveMutation.data.value}'),
        ],
      );
    });
  }
}

Usage with infinite_scroll_pagination #

signals_query seamlessly blends with infinite_scroll_pagination. Additionally, by incorporating a Signal inside your key array, your entire infinite query setup can reactively reset and refetch from the beginning automatically whenever that parameter/signal changes!

import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:signals_flutter/signals_flutter.dart';
import 'package:signals_query/signals_query.dart';

// Assuming you have a global client
// final queryClient = QueryClient(cache: MemoryCache());

class UsersPage {
  final List<String> items;
  final String? nextCursor;
  const UsersPage({required this.items, this.nextCursor});
}

class UsersPagedList extends StatefulWidget {
  const UsersPagedList({super.key});

  @override
  State<UsersPagedList> createState() => _UsersPagedListState();
}

class _UsersPagedListState extends State<UsersPagedList> {
  final _pagingController = PagingController<String?, String>(firstPageKey: null);
  late final InfiniteQuery<UsersPage, String> usersQuery;
  
  // Example of a reactive dependency: a Signal inside your key function
  final filterSignal = signal<String>('active');

  @override
  void initState() {
    super.initState();
    
    // 1. Create the infinite query. Notice the signal in the key array!
    usersQuery = createInfiniteQuery<UsersPage, String>(
      client: queryClient,
      key: () => ['users', filterSignal.value],
      initialPageParam: null,
      fn: (cursor) async {
        await Future.delayed(const Duration(milliseconds: 300));
        
        final start = cursor == null ? 0 : int.parse(cursor);
        final items = List.generate(20, (i) => 'User ${start + i} (${filterSignal.value})');
        final next = (start + items.length) >= 100 ? null : '${start + items.length}';
        
        return UsersPage(items: items, nextCursor: next);
      },
      getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
    );

    // 2. Bind the PagingController
    _pagingController.addPageRequestListener((pageKey) async {
      try {
        if (pageKey != _pagingController.firstPageKey) {
          await usersQuery.fetchNextPage();
        }

        final pages = usersQuery.pages;
        if (pages.isEmpty) return; // Still running initial fetch
        
        final lastPage = pages.last;
        final newItems = lastPage.items;
        final nextKey = lastPage.nextCursor;

        if (nextKey == null) {
          _pagingController.appendLastPage(newItems);
        } else {
          _pagingController.appendPage(newItems, nextKey);
        }
      } catch (e) {
        _pagingController.error = e;
      }
    });
    
    // 3. Reactively reset the pagination visually when the query fundamentally resets due to a dependency changing
    effect(() {
        // Read the state so the effect subscribes
        final state = usersQuery.state.value;
        if (state.isLoading && state.pages.isEmpty) {
           _pagingController.refresh();
        }
    });
  }

  @override
  void dispose() {
    _pagingController.dispose();
    usersQuery.dispose();
    filterSignal.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Watch((context) {
       return Scaffold(
         appBar: AppBar(
           title: const Text('Infinite Users'),
           actions: [
             IconButton(
               icon: const Icon(Icons.change_circle),
               // Changing this signal triggers the QueryClient to automatically
               // invalidate, dispose of the old stream, and re-run initial fetch! 
               onPressed: () => filterSignal.value = filterSignal.value == 'active' ? 'archived' : 'active', 
             )
           ]
         ),
         body: PagedListView<String?, String>(
           pagingController: _pagingController,
           builderDelegate: PagedChildBuilderDelegate<String>(
             itemBuilder: (context, item, index) => ListTile(title: Text(item)),
             firstPageProgressIndicatorBuilder: (_) =>
                 usersQuery.isLoading ? const Center(child: CircularProgressIndicator()) : const SizedBox.shrink(),
             newPageProgressIndicatorBuilder: (_) =>
                 usersQuery.isFetchingNextPage ? const Center(child: CircularProgressIndicator()) : const SizedBox.shrink(),
           ),
         ),
       );
    });
  }
}
1
likes
150
points
240
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A robust, reactive asynchronous state management and data-fetching library for Flutter leveraging the minimal, hooks-free power of Signals.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, signals, signals_flutter

More

Packages that depend on signals_query