Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AsyncSnapshot.data to throw if error or no data #34626

Merged
merged 7 commits into from
Jul 18, 2019
Merged

Conversation

tvolkert
Copy link
Contributor

@tvolkert tvolkert commented Jun 18, 2019

Description

Currently, if you attempt to access the AsyncSnapshot.data property when the error property is non-null, it will return null. This API often confuses users, who expect the property to throw an error if the error property is set.

This change updates the API to throw an error if error is set -- and to correspondingly remove the requireData property.

After receiving this update, callers should update their code accordingly:

  • If callers were accessing the data property without first checking hasData or hasError, they should update their code to check hasData or hasError, or to wrap their data access in a try/catch.
  • If callers were using the requireData property, they should update their code to simply access the data property instead.

Related Issues

#34545

Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I signed the [CLA].
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

Copy link
Contributor

@a14n a14n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Piinks Piinks added framework flutter/packages/flutter repository. See also f: labels. work in progress; do not review labels Jun 18, 2019
@tvolkert
Copy link
Contributor Author

@goderbauer I made the changes we discussed:

  1. Add a new value getter that throws if there's no value (as requireData currently does)
  2. Add a corresponding new hasValue getter
  3. Deprecate both data and hasData
  4. Remove requireData

I don't love it, because value is not as good a name as data. data matches Stream.listen's onData callback -- and it also exists in StreamBuilder.initialData.

Then again, I think the existing API is broken, so.... wdyt?

@tvolkert tvolkert changed the title WIP: Make AsyncSnapshot.data throw if caller didn't check for error Deprecate AsyncSnapshot.data & friends, add AsyncSnapshot.value & friends Jun 23, 2019
Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is sad that we lose the similarity to the Stream/StreamBuilder API. In the interest of a clean API, maybe we should just do the full-on breaking change under the existing data name?

/// Creates an [AsyncSnapshot] in the specified [state] and with the specified [data].
const AsyncSnapshot.withData(ConnectionState state, T data) : this._(state, data, null);
/// This is deprecated in favor of [AsyncSnapshot.withValue].
@Deprecated('Use `withValue` instead. `withData` will be removed in approximately December 2019')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: end with a .?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(also everywhere else below)

@tvolkert tvolkert changed the title Deprecate AsyncSnapshot.data & friends, add AsyncSnapshot.value & friends AsyncSnapshot.data to throw if error or no data Jul 3, 2019
@tvolkert
Copy link
Contributor Author

tvolkert commented Jul 3, 2019

Ok, @goderbauer I made the changes as discussed and sent out the breaking change announcement. PTAL.

@goderbauer
Copy link
Member

(PR triage): This will need some changes to address the concerns raised in #34545.

@tvolkert
Copy link
Contributor Author

Ok, updates per the conversation in #34545 have all been made. This is ready for another review.

/cc @rrousselGit

/// and optionally either [data] or [error] (but not both).
const AsyncSnapshot._(this.connectionState, this.data, this.error)
/// Creates an [AsyncSnapshot] with the specified [connectionState] and
/// [hasData], and optionally either [data] or [error] (but not both).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be [_data] as it refers to parameter here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// Creates an [AsyncSnapshot] with the specified [connectionState] and
/// [hasData], and optionally either [data] or [error] (but not both).
///
/// It is legal for both [hasData] to be true and [data] to be null.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

@a14n a14n self-requested a review July 16, 2019 07:51

/// Current state of connection to the asynchronous computation.
/// The current state of the connection to the asynchronous computation.
final ConnectionState connectionState;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs more docs

if (hasData)
return data;
return _data;
if (hasError)
throw error;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pity we can't preserve the stack trace

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add a TODO pointing at dart-lang/sdk#30741.

bool get hasError => error != null;

@override
String toString() => '$runtimeType($connectionState, $data, $error)';
String toString() => '$runtimeType($connectionState, $hasData, $_data, $error)';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasData is a bool, so that will be confusing. maybe ${hasData ? "has data" : "no data"} ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, probably omit $_data if there's no data, and omit error if there's no error? in general this could be clearer in various ways.

@@ -272,12 +289,13 @@ class AsyncSnapshot<T> {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use runtimeType not is

/// strategy is given by [builder].
///
/// The [initialData] is used to create the initial snapshot.
/// The [initialData] argument is used to create the initial snapshot.
///
/// The [builder] must not be null.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs should talk about what to do if you don't have the initial data (the common case)

/// to be processed.
/// to be processed. In cases where callers wish to have no initial data, the
/// [new StreamBuilder.withoutInitialData] constructor may be used. Doing so
/// may cause the first frame to have a snapshot that contains no data.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may? definitely will, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible the stream will have produced data by the first frame. The docs above specifically call out that since the builder is called at the discretion of the Flutter pipeline, callers are not guaranteed to see every snapshot in rendered frames.

/// access the [initialData] property will throw a [StateError].
T get initialData {
if (!hasInitialData)
throw StateError('No initial data');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a more useful error message?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more appropriate to use FlutterError here, or is StateError better? The docs on FlutterError say "Error class used to report Flutter-specific assertion failures and contract violations," but it's not clear to me from that description if widget implementations are encouraged to throw FlutterError or not.


@override
AsyncSnapshot<T> afterConnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.waiting);

@override
AsyncSnapshot<T> afterData(AsyncSnapshot<T> current, T data) {
return AsyncSnapshot<T>.withData(ConnectionState.active, data);
return _TypeLiteral.isVoidType(T)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's weird that we special-case void, though I guess it's understandable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should definitely document this though

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(in the class docs, e.g.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already do - see the docs lines starting with "The StreamBuilder<void> and StreamBuilder<Null> types will produce"

@tvolkert
Copy link
Contributor Author

@Hixie all comments addressed - PTAL

const FutureBuilder({
Key key,
this.future,
this.initialData,
@Deprecated(
"Don't provide initialData to FutureBuilder. Instead, check for "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we generally would phrase this more softly as "Instead of providing initialData to a FutureBuilder, consider checking for..."

/// [AsyncSnapshot.hasError] will be true.)
@Deprecated(
"Don't use FutureBuilder.initialData. Instead, check for "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar here

"Don't use FutureBuilder.initialData. Instead, check for "
'ConnectionState.none or ConnectionState.waiting in your build() '
'method to know whether the future has completed or not.',
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deprecation explanation should be in /// dartdocs too

@Hixie
Copy link
Contributor

Hixie commented Jul 18, 2019

Mentions of initialData elsewhere in the docs should also discourage it.
Constructor docs should briefly reiterate the recommendation to avoid creating a new future during build.

LGTM.

@tvolkert tvolkert merged commit b61fcfd into flutter:master Jul 18, 2019
@tvolkert tvolkert deleted the async branch July 18, 2019 23:26
@creativecreatorormaybenot
Copy link
Contributor

creativecreatorormaybenot commented Jul 19, 2019

Is there a documentation for the StreamBuilder changes? I am expecting a lot of users to be confused.

I guess I will be answering some questions on StackOverflow soon 🤷

Edit: Instead, I prepared some questions and referred to the announcement.

@tvolkert
Copy link
Contributor Author

@creativecreatorormaybenot I also added a section to the changelog that covers the changes as well, with examples. https://github.com/flutter/flutter/wiki/Changelog#v183

@creativecreatorormaybenot
Copy link
Contributor

@tvolkert Awesome, that's what I was missing previously.

@amirh
Copy link
Contributor

amirh commented Jul 19, 2019

What's the migration plan for a package that needs to work both on Flutter master and stable, and is broken by this change?

@tvolkert
Copy link
Contributor Author

I actually just sent out #36577, which would swap the semantics of the two StreamBuilder constructors, allowing us a deprecation path rather than a hard API break.

@amirh
Copy link
Contributor

amirh commented Jul 19, 2019

Won't that shift the problem(of not being able to work on both master and stable) to packages that provide initialData?

@amirh
Copy link
Contributor

amirh commented Jul 19, 2019

take that back - I thought that PR removed initialData which isn't the case.

@tvolkert
Copy link
Contributor Author

What's the migration plan for a package that needs to work both on Flutter master and stable, and is broken by this change?

@amirh there's still discussion in #34545 (comment) that could mean we don't merge #36577. If that ends up being the best course of action, then the migration plan for packages that need to work on both master and stable is to have them explicitly pass initialData: null to the StreamBuilder constructor, which will yield the same behavior that they used to get.

tvolkert added a commit that referenced this pull request Jul 20, 2019
tvolkert added a commit that referenced this pull request Jul 21, 2019
johnsonmh pushed a commit to johnsonmh/flutter that referenced this pull request Jul 30, 2019
This updates `AsyncSnapshot.data` to act as `AsyncSnapshot.requireData`
used to -- and it removes `AsyncSnapshot.requireData`. Correspondingly,
this adds a `StreamBuilder.withoutInitialData()` constructor, makes the
`initialData` argument to the default `StreamBuilder()` constructor required,
and deprecates the `initialData` argument to the `FutureBuilder()` constructor.

See the  breaking change announcement for more info.

flutter#34545
https://groups.google.com/forum/#!topic/flutter-announce/H6Od0QdsdrI
johnsonmh pushed a commit to johnsonmh/flutter that referenced this pull request Jul 30, 2019
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants