Migrating from 2.x to 3.x
Environment Requirements
Cached Query 3.0 requires Dart 3.0.0 or higher. Make sure to update your pubspec.yaml:
environment:
  sdk: ">=3.0.0 <4.0.0"
Query State
All query, infinite query and mutation states have been changed to sealed classes to take advantage of dart 3 pattern matching.
There is no longer a query status enum. There is now a matching sealed class, for exaple the query sealed class is:
class QueryInitial<T> extends QueryStatus<T> {}
class QueryLoading<T> extends QueryStatus<T> {}
class QuerySuccess<T> extends QueryStatus<T> {}
class QueryError<T> extends QueryStatus<T> {}
This change allows for more specific information to be passed with each state, such as the reason for the loading state or the data key being non-nullable in the success state.
For convenience you can check the current status of a query using:
final allStatuses = state.isLoading || state.isInitial || state.isError || state.isSuccess;
Example
- Before
- After
- Using Switch
class Post extends StatelessWidget {
    final int id;
    const Post({required this.id, super.key});
    
    Widget build(BuildContext context) {
        return QueryBuilder<PostModel>(
        // Can use key if the query already exists.
        queryKey: service.postKey(id),
        builder: (context, state) {
            final data = state.data;
            if (state.error != null) return Text(state.error.toString());
            if (data == null) return const SizedBox();
            return Container(
            margin: const EdgeInsets.all(10),
            child: Column(
                children: [
                    const Text(
                        "Title",
                        textAlign: TextAlign.center,
                        style: TextStyle(fontSize: 20),
                    ),
                    Text(
                        data.title,
                        textAlign: TextAlign.center,
                    ),
                    const Text(
                        "Body",
                        textAlign: TextAlign.center,
                        style: TextStyle(fontSize: 20),
                    ),
                    Text(
                        data.body,
                        textAlign: TextAlign.center,
                    ),
                    ],
                ),
            );
        },
        );
    }
}
class Post extends StatelessWidget {
  final int id;
  final bool enabled;
  const Post({Key? key, required this.id, required this.enabled})
      : super(key: key);
  
  Widget build(BuildContext context) {
    return QueryBuilder<QueryState<PostModel>>(
      enabled: enabled,
      // Can use key if the query already exists.
      queryKey: service.postKey(id),
      builder: (context, state) {
        final data = state.data;
        if (state case QueryError(:final error)) return Text(error.toString());
        if (data == null) return const SizedBox();
        return Container(
          margin: const EdgeInsets.all(10),
          child: Column(
            children: [
              const Text(
                "Title",
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 20),
              ),
              Text(
                data.title,
                textAlign: TextAlign.center,
              ),
              const Text(
                "Body",
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 20),
              ),
              Text(
                data.body,
                textAlign: TextAlign.center,
              ),
            ],
          ),
        );
      },
    );
  }
}
class Post extends StatelessWidget {
    final int id;
    final bool enabled;
    const Post({super.key, required this.id, required this.enabled});
    
    Widget build(BuildContext context) {
        return QueryBuilder<QueryStatus<PostModel>>(
        enabled: enabled,
        // Can use key if the query already exists.
        queryKey: service.postKey(id),
        builder: (context, state) {
            return switch (state) {
                QueryError<PostModel>() =>
                    Text((state as QueryError).error.toString()),
                QueryStatus<PostModel>(:final data) when data != null => Container(
                    margin: const EdgeInsets.all(10),
                    child: Column(
                        children: [
                        const Text("Title",
                            textAlign: TextAlign.center,
                            style: TextStyle(fontSize: 20)),
                        Text(
                            data.title,
                            textAlign: TextAlign.center,
                        ),
                        const Text(
                            "Body",
                            textAlign: TextAlign.center,
                            style: TextStyle(fontSize: 20),
                        ),
                        Text(
                            data.body,
                            textAlign: TextAlign.center,
                        ),
                        ],
                    ),
                    ),
                _ => const SizedBox(),
                };
            },
        );
    }
}
The base class for each cachable item is:
- QueryStatus - for single queries.
- InfiniteQueryStatus - for infinite queries.
- MutationState - for mutations
Configuration Changes
The configuration system has been updated with separate global and query-specific config objects.
Classes are now:
- GlobalQueryConfigFlutter- Global config for all queries, infinite queries and mutations. If using Cached Query Flutter.
- QueryConfigFlutter- Query specific config for queries and infinite queries. If using Cached Query Flutter.
- GlobalQueryConfig- Global config for all queries, infinite queries and mutations. If using Cached Query (non-Flutter).
- QueryConfig- Query specific config for queries and infinite queries. If using Cached Query (non-Flutter).
The local config will still inherit from the global config which is the same as before.
- Before
- After
void main() {
  CachedQuery.instance.configFlutter(
    config: QueryConfigFlutter(
      refetchDuration: Duration(seconds: 4),
    ),
    storage: await CachedStorage.ensureInitialized(),
  );
  runApp(MyApp());
}
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  CachedQuery.instance.configFlutter(
    config: GlobalQueryConfigFlutter(
      refetchOnResume: true,
    ),
    storage: await CachedStorage.ensureInitialized(),
    observers: [
      Observer(),
      QueryLoggingObserver(colors: !Platform.isIOS),
    ],
  );
  runApp(const MyApp());
}
Multiple observers can now be passed to the global config.
Data Type Changes
Query success state data is now the type of the generic, usually this is a non-null type. If you need nullable data, explicitly pass T? as the generic type.
- Before
- After
QueryBuilder<PostModel>(
  queryKey: ['post', id],
  queryFn: () => api.getPost(id),
  builder: (context, state) {
    final data = state.data; // data is PostModel?
    if (data == null) return const SizedBox();
    return Text(data.title);
  },
)
QueryBuilder<PostModel>(
  queryKey: ['post', id],
  queryFn: () => api.getPost(id),
  builder: (context, state) {
    return switch (state) {
      QuerySuccess<PostModel>(:final data) => Text(data.title), // data is PostModel (non-nullable)
      QueryError<PostModel>() => Text('Error: ${state.error}'),
      _ => const SizedBox(),
    };
  },
)
For nullable data, use:
Query<PostModel?>(
    ...
)
Query Interface and Observers
The base class for queries is now Cachable<T>. This has changed from QueryBase<T>.
class Observer extends QueryObserver {
  
  void onChange(
    Cacheable<dynamic> query,
    QueryState<dynamic> nextState,
  ) {
    // Do something when changing
    super.onChange(query, nextState);
  }
}
Mutation
The queryFn parameter has been renamed to mutationFn in the Mutation class.