QoraKey
Overview
Every Qora query is identified by a key. Qora accepts two equivalent formats:
// Raw list: frictionless, no import needed
key: ['users', userId]
// QoraKey wrapper: explicit, typed
key: QoraKey(['users', userId])
key: QoraKey.withId('users', userId)
Both are normalised to the same internal representation and compared with deep structural equality. They are fully interchangeable: you can mix them across calls to the same logical query.
Constructors
QoraKey(List<dynamic> parts)
Direct constructor from a list of parts.
QoraKey(['users', userId])
QoraKey(['posts', {'status': 'published', 'page': 1}])
QoraKey.single(String key)
Shorthand for a single-segment key.
QoraKey.single('users') // equivalent to QoraKey(['users'])
QoraKey.withId(String entity, dynamic id)
Shorthand for an entity + ID key.
QoraKey.withId('users', userId) // ['users', userId]
QoraKey.withId('posts', postId) // ['posts', postId]
QoraKey.withFilter(String entity, Map<String, dynamic> filter)
Shorthand for an entity + filter map key.
QoraKey.withFilter('posts', {'status': 'published', 'page': page})
// ['posts', {'status': 'published', 'page': page}]
QoraKey.from(List<dynamic> parts)
Factory constructor that converts a raw list to a QoraKey.
final key = QoraKey.from(['users', userId]);
Properties
parts
final List<dynamic> parts
The raw key segments as an unmodifiable list.
final key = QoraKey.withId('users', 42);
print(key.parts); // [users, 42]
Equality
QoraKey uses deep structural equality: two keys are equal if their parts are recursively equal:
QoraKey(['users', 1]) == QoraKey(['users', 1]) // true
QoraKey(['users', 1]) == QoraKey(['users', 2]) // false
// Raw list and QoraKey are normalised identically
['users', 1] == QoraKey(['users', 1]) // normalised to same cache entry
Supported types in key parts:
| Type | Support |
|---|---|
int, double, bool | Full structural equality |
String | Full structural equality |
null | Supported as a key part "../5.api-reference" |
List | Recursively compared |
Map | Order-independent recursive comparison |
| Custom objects | Must override operator == and hashCode |
UserFilter) in key parts without overriding operator == and hashCode causes a new cache entry on every call. Use primitive-serialisable values or implement equality explicitly.normalizeKey
List<dynamic> normalizeKey(Object key)
Top-level function that normalises any valid key input to an unmodifiable List<dynamic>:
- Accepts
QoraKeyorList<dynamic> - Validates that parts are serialisable
- Returns a deep-copied, unmodifiable list
This is called internally by every QoraClient method. You rarely need to call it directly.
final parts = normalizeKey(['users', 1]); // [users, 1] (unmodifiable)
final parts = normalizeKey(QoraKey.withId('users', 1)); // same result
Throws ArgumentError if the key is not a List or QoraKey.
Key Factories Pattern
Centralise your key definitions to avoid typos and simplify invalidation:
abstract class Keys {
// Users
static List<dynamic> users() => ['users'];
static List<dynamic> user(int id) => ['users', id];
static List<dynamic> userPosts(int userId) => ['users', userId, 'posts'];
// Posts
static List<dynamic> posts(Map<String, dynamic> filters) => ['posts', filters];
static List<dynamic> post(int id) => ['posts', id];
}
// Usage
await client.fetchQuery<User>(
key: Keys.user(userId),
fetcher: () => api.getUser(userId),
);
client.invalidateWhere((key) => equalsKey(key, Keys.users()));
Best Practices
Do
// Consistent structure: entity first, then ID/filters
['users', userId]
['users', userId, 'posts']
['posts', {'status': 'published'}]
// Use primitives: always comparable
['users', userId.toString()]
['posts', {'page': page, 'limit': 20}]
Don't
// Inconsistent ordering
['users', userId]
[userId, 'users'] // Bad: Different cache entry
// Custom objects without equality
['users', UserFilter(status: 'active')] // Bad: Identity equality: always a miss
// Computed values that vary on each call
['posts', DateTime.now().toIso8601String()] // Bad: Never hits cache