createResult method
- @protected
- Query<
TQueryData> query, - DefaultedQueryObserverOptions<
TQueryData, TData> options, { - bool optimistic = false,
Turns query's state into the result this observer reports under
options: applies placeholder data while the query has none, runs
select (memoised on its input and the selector), turns a throwing
select into a QueryError that keeps the last selected value as its
stale data, and fills in the derived flags such as isStale.
With optimistic, the state is first adjusted for the fetch that a
subscribe or setOptions is about to start — see
getOptimisticResult.
Only for subclasses: InfiniteQueryObserver extends this observer.
Callers read currentResult instead.
Implementation
@protected
QueryResult<TData> createResult(
Query<TQueryData> query,
DefaultedQueryObserverOptions<TQueryData, TData> options, {
bool optimistic = false,
}) {
final prevResult = _previousResult;
final prevResultOptions = _currentResultOptions;
final queryChanged = !identical(query, _currentQuery);
final queryInitialState =
queryChanged ? query.state : _currentQueryInitialState;
var state = query.state;
if (optimistic) {
final mounted = hasListeners;
final fetchOnMount = !mounted && options.shouldFetchOnMount(query);
// The same "was it enabled?" [setOptions] is about to ask, so the
// preview shows the fetch the commit starts.
final fetchOptionally = mounted &&
options.shouldFetchOptionally(query, _currentQuery, _options,
wasEnabled: _lastSeenEnabled);
if (fetchOnMount || fetchOptionally) {
final fetchable = canFetch(options.networkMode, _client.onlineManager);
state = state.copyWith(
fetchStatus: fetchable ? FetchStatus.fetching : FetchStatus.paused,
fetchFailureCount: 0,
clearFetchFailure: true,
status: state.hasData ? null : QueryStatus.pending,
clearError: !state.hasData,
);
}
}
var status = state.status;
var error = state.error;
var errorStackTrace = state.errorStackTrace;
var errorUpdatedAt = state.errorUpdatedAt;
var isPlaceholderData = false;
var skipSelect = false;
// What `select` will be given: the query's data, or the placeholder that
// stands in for it. Upstream runs placeholder data through `select` too,
// by leaving it in the same variable rather than selecting it early.
TQueryData? candidate = state.hasData ? state.data : null;
var hasCandidate = state.hasData;
TData? outData;
var hasOutData = false;
// Placeholder data, only while nothing real has resolved.
final placeholderData = options.placeholderData;
if (placeholderData != null &&
!hasCandidate &&
status == QueryStatus.pending) {
if (prevResult != null &&
prevResult.isPlaceholderData &&
identical(placeholderData, prevResultOptions?.placeholderData) &&
options.select == prevResultOptions?.select) {
// Already selected on the previous pass, so `select` must not run
// again over an already-selected value. Only while `select` is the
// same one, though — upstream memoises on the placeholder alone and
// keeps showing the old selection after the selector changed.
outData = prevResult.dataOrNull;
hasOutData = true;
skipSelect = true;
status = QueryStatus.success;
isPlaceholderData = true;
} else {
final placeholder = placeholderData.provide(
_lastQueryWithData?.state.data,
_lastQueryWithData,
);
if (placeholder.hasData) {
status = QueryStatus.success;
isPlaceholderData = true;
candidate = placeholder.data;
hasCandidate = true;
}
}
}
if (!skipSelect) {
final select = options.select;
if (select != null && hasCandidate) {
// The selector is compared with `==`, as the options compare it: an
// instance-method tear-off is `==` to the next tear-off of the same
// method but never `identical`, so an `identical` memo re-ran such a
// `select` on every `setOptions` that changed nothing. A closure is only ever `==` to itself.
if (_hasSelectInput &&
candidate == _selectInput &&
select == _selectFn) {
outData = _selectResult;
hasOutData = _hasSelectResult;
// Equal input through the same selector is this query's selection
// too, whichever query first produced it — unless the input is a
// placeholder, which is no query's data: a selection of it stands
// behind nobody's select error.
_selectQuery = isPlaceholderData ? null : query;
} else {
if (!identical(query, _selectQuery)) {
// The last selection belongs to another query. It must not stand
// behind this query's select error as its stale data.
_selectResult = null;
_hasSelectResult = false;
_selectQuery = query;
}
try {
_selectFn = select;
_selectInput = candidate;
_hasSelectInput = true;
// Shared against the last *reported* data, as upstream's
// `replaceData(prevResult?.data, …)`: a selector that builds a
// fresh but equal list must not count as a change.
//
// `replaceData` routes the `structuralSharing` option, so an
// opt-out governs the selected value too. Upstream calls the hook
// itself on the selected values; here it is typed for the query's
// data (`StructuralSharing<TQueryData>`) and cannot be handed a
// `TData`. So only the recognised opt-out steps back from the
// selection — a hook that shares in its own way leaves the
// selection at the default walk, rather than paying for an
// opt-out it never asked for.
final selected = select(candidate as TQueryData);
outData = isNoStructuralSharing(options.structuralSharing)
? selected
: replaceEqualDeep<TData>(prevResult?.dataOrNull, selected);
_selectResult = outData;
_hasSelectResult = true;
hasOutData = true;
// A selection of a placeholder belongs to no query: the
// next pass over this query's real data starts without stale
// data, as a failed first fetch does.
if (isPlaceholderData) _selectQuery = null;
_clearSelectError();
} catch (selectError, selectStackTrace) {
_selectError = selectError;
_selectErrorStackTrace = selectStackTrace;
_selectErrorUpdatedAt = clock.now();
}
}
} else if (select == null && hasCandidate) {
_hasSelectInput = false;
assert(
candidate is TData,
'A query observer with no select must have the same data type on '
'both sides: $TQueryData cannot be reported as $TData.',
);
if (isPlaceholderData) {
// A placeholder was never written to the cache, so it is shared
// here — through the query's own hook, as upstream's
// `replaceData(prevResult?.data, placeholderData, options)`.
final sharing = options.structuralSharing;
final previousOutput = prevResult?.dataOrNull;
final previous =
prevResultOptions?.select == null && previousOutput is TQueryData
? previousOutput
: null;
outData = (sharing == null
? replaceEqualDeep<TQueryData>(previous, candidate as TQueryData)
: sharing(previous, candidate as TQueryData)) as TData;
} else {
// Cached data goes through as it is, as upstream's `data =
// state.data`: the cache write already applied `structuralSharing`,
// and re-sharing it against the last result here hid an opt-out
// (`(_, next) => next`) from every reader.
outData = candidate as TData;
}
hasOutData = true;
// The error belonged to a selector that is no longer there. Upstream
// keeps reporting it until a *new* selection succeeds, which never
// happens once `select` is gone.
_clearSelectError();
} else if (!hasCandidate) {
_hasSelectInput = false;
// A select error belongs to data that is now gone.
_clearSelectError();
}
}
final selectError = _selectError;
if (selectError != null) {
error = selectError;
errorStackTrace = _selectErrorStackTrace;
errorUpdatedAt = _selectErrorUpdatedAt;
status = QueryStatus.error;
isPlaceholderData = false;
// The last value `select` produced stays on screen behind the error,
// which is what makes a failing selector as survivable as a failing
// fetch.
outData = _selectResult;
hasOutData = _hasSelectResult;
}
final isFetchedAfterMount =
state.dataUpdateCount > queryInitialState.dataUpdateCount ||
state.errorUpdateCount > queryInitialState.errorUpdateCount;
QueryResult<TData> build() {
switch (status) {
case QueryStatus.error:
return QueryError<TData>(
error: error ?? StateError('query is in an error state'),
stackTrace: errorStackTrace ?? StackTrace.empty,
staleData: outData,
hasStaleData: hasOutData,
fetchStatus: state.fetchStatus,
dataUpdatedAt: state.dataUpdatedAt,
errorUpdatedAt: errorUpdatedAt,
failureCount: state.fetchFailureCount,
failureReason: state.fetchFailureReason,
failureStackTrace: state.fetchFailureStackTrace,
errorUpdateCount: state.errorUpdateCount,
consecutiveErrorCount: state.consecutiveErrorCount,
isStale: options.isStaleFor(query),
isEnabled: options.enabled.resolve(query),
isFetched: query.isFetched(),
isFetchedAfterMount: isFetchedAfterMount,
isPlaceholderData: isPlaceholderData,
refetch: refetch,
);
case QueryStatus.success:
return QuerySuccess<TData>(
data: outData as TData,
fetchStatus: state.fetchStatus,
dataUpdatedAt: state.dataUpdatedAt,
errorUpdatedAt: errorUpdatedAt,
failureCount: state.fetchFailureCount,
failureReason: state.fetchFailureReason,
failureStackTrace: state.fetchFailureStackTrace,
errorUpdateCount: state.errorUpdateCount,
consecutiveErrorCount: state.consecutiveErrorCount,
isStale: options.isStaleFor(query),
isEnabled: options.enabled.resolve(query),
isFetched: query.isFetched(),
isFetchedAfterMount: isFetchedAfterMount,
isPlaceholderData: isPlaceholderData,
refetch: refetch,
);
case QueryStatus.pending:
return QueryPending<TData>(
fetchStatus: state.fetchStatus,
dataUpdatedAt: state.dataUpdatedAt,
errorUpdatedAt: errorUpdatedAt,
failureCount: state.fetchFailureCount,
failureReason: state.fetchFailureReason,
failureStackTrace: state.fetchFailureStackTrace,
errorUpdateCount: state.errorUpdateCount,
consecutiveErrorCount: state.consecutiveErrorCount,
isStale: options.isStaleFor(query),
isEnabled: options.enabled.resolve(query),
isFetched: query.isFetched(),
isFetchedAfterMount: isFetchedAfterMount,
isPlaceholderData: isPlaceholderData,
refetch: refetch,
);
}
}
return build();
}