-
Notifications
You must be signed in to change notification settings - Fork 27.9k
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
Defer image decoding when scrolling fast #49389
Defer image decoding when scrolling fast #49389
Conversation
@@ -227,6 +227,11 @@ class ImageCache { | |||
return result; | |||
} | |||
|
|||
/// Returns whether this [key] has been previously added by [putIfAbsent]. | |||
bool containsKey(Object key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a break.
/// method. | ||
/// method. If they need to manage the actual resolution of the image, they | ||
/// should override [resolveForStream] instead of this method. | ||
@nonVirtual |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why must it be nonVirtual? I mean, I get that it's useful for catching people who are using the API the old way, but is it strictly necessary? What if someone wants to take an entirely different approach?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because if some other provider overrides it, its new logic will never get called by the new kind of provider I'm making that would wrap it.
I imagine other providers would have this problem. Plus the logic in it is very complicated.
Alternatively, we could update docs to say "if you override this, composed providers may not work and you should consider overriding useKey instead" - but I'm inclined to think people are more likely to see an analyzer warning than a doc comment.
return stream; | ||
} | ||
|
||
void _safeObtainKey(ImageConfiguration configuration, ImageStream stream) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"_safeObtainKey" doesn't return a key, which is weird naming
@@ -270,10 +270,19 @@ abstract class ImageProvider<T> { | |||
/// This is the public entry-point of the [ImageProvider] class hierarchy. | |||
/// | |||
/// Subclasses should implement [obtainKey] and [load], which are used by this | |||
/// method. | |||
/// method. If they need to manage the actual resolution of the image, they | |||
/// should override [resolveForStream] instead of this method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs are out of date
@@ -270,10 +270,19 @@ abstract class ImageProvider<T> { | |||
/// This is the public entry-point of the [ImageProvider] class hierarchy. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add a section to the class docs (## foo
) that discusses the lifecycle of this class and the order in which the various methods are called, by whom, when, and so on. The class is complicated enough now that an overview is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
/// | ||
/// Subclasses should avoid calling [obtainKey] directly and instead override | ||
/// this method, which makes sure appropriate error handling is in place to | ||
/// notify callers and the framework. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this paragraph is confusing given the first paragraph
/// this method, which makes sure appropriate error handling is in place to | ||
/// notify callers and the framework. | ||
/// | ||
/// It is safe to call [handleError] multiple times if multiple errors occur. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean "It is safe for the implementation of this method to call..." ?
/// | ||
/// The default implementation uses the key to interact with the [ImageCache], | ||
/// calling [ImageCache.putIfAbsent] and notifying listeners of the [stream]. | ||
/// Implementers that do not call super should |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
incomplete
/// The [State] must not be null, and [State.mounted] must be true. | ||
ScrollAwareContextProvider(this._state) | ||
: assert(_state != null), | ||
assert(_state.mounted, 'A ScrollAwareContextProvider was given a BuildContext for an Element that was never mounted.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was never -> is not
return true; | ||
} | ||
|
||
/// Do not touch this. Use [_context]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not clear who this doc line is for. It's a private, so presumably it's for us. Bet we touch it 7 lines lower.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's meant to say don't touch it from the provider. But it's probably not necessary, I'll just remove it.
assert(key != null); | ||
assert(handleError != null); | ||
|
||
void deferredResolve([bool firstCall = false]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you need this to be a nested function? looks like you could get the same results with a regular method, which would be less confusing to follow
572a727
to
7501ec3
Compare
I've updated the docs and moved the build context enclosing class to a new class called Still waiting for the dependent PR to land so I can rebase it into this one and then mark this ready for review. |
Ready for review now! |
6df2db7
to
cc06576
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM after comments are resolved.
@@ -227,6 +227,11 @@ class ImageCache { | |||
return result; | |||
} | |||
|
|||
/// Returns whether this [key] has been previously added by [putIfAbsent]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: key should be surrounded in `` instead of [] (it's referring a method parameter that does not exist as a field on the sounding context).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -182,6 +182,42 @@ typedef DecoderCallback = Future<ui.Codec> Function(Uint8List bytes, {int cacheW | |||
/// | |||
/// The following image formats are supported: {@macro flutter.dart:ui.imageFormats} | |||
/// | |||
/// ## Image resolving |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Maybe just make this heading "Lifecycle"? Or "Lifecycle of resolving an image" or some such?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// This stream will be used to communicate back to the caller when the | ||
/// image is decoded and ready to display, or when an error occurs. | ||
/// 2. Obtain the key for the image using [obtainKey]. | ||
/// Since this method is asynchronous, but also can return synchronously, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sentence is strange. I assume you mean it's an asychnronous method, but it may throw exceptions synchronously? There is no way it can actually return anything useful synchronous?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use SynchronousFuture
s for these, which look async but are actually return synchronously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded a bit.
/// method. | ||
/// method. If they need to change the implementation of [ImageStream] used, | ||
/// they should override [createStream]. If they need to manage the actual | ||
/// resolution of the image, they should override [resolveStreamForKey]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a link to the lifecycle explanation of this method in the class comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done - I think.
return stream; | ||
} | ||
|
||
/// Called when [resolve] has finished calling [obtainKey]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe: Called by [resolve] with the key returned by [obtainKey].
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
}) : assert(context != null), | ||
assert(imageProvider != null); | ||
|
||
/// The context that may or may not be enclosed by a scrollable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: by a [Scrollable]?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
return Scrollable.of(find.byType(TestWidget).evaluate().first).position; | ||
} | ||
|
||
testWidgets('DisposableBuildContext asserts on disposed state', (WidgetTester tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe move the DisposableBuildContext tests into their own file since the impl has its own home as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
expect(state.mounted, true); | ||
|
||
final DisposableBuildContext context = DisposableBuildContext(state); | ||
expect(context.debugValidate(), true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of marking debugValidate visible for tests, could we just test the same by actually accessing the context property and checking that we do or don't get an assert?
(That would also prevent us from ever removing that assert from the context getter)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
velocities.add(velocity); | ||
return super.recommendDeferredLoading(velocity, metrics, context); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add a newline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// Always returns true, but will assert if [dispose] has not been called | ||
/// but the state this is tracking is unmounted. | ||
@visibleForTesting | ||
bool debugValidate() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as pointed out in a comment on the test: Can we keep this private and instead test if accessing context
throws?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review!
We should not land this until cl/289271973 is landed internally so it's a soft break. I'll work on a migration guide.
@@ -182,6 +182,42 @@ typedef DecoderCallback = Future<ui.Codec> Function(Uint8List bytes, {int cacheW | |||
/// | |||
/// The following image formats are supported: {@macro flutter.dart:ui.imageFormats} | |||
/// | |||
/// ## Image resolving |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -227,6 +227,11 @@ class ImageCache { | |||
return result; | |||
} | |||
|
|||
/// Returns whether this [key] has been previously added by [putIfAbsent]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// Called by [resolve] to create the [ImageStream] it returns. | ||
/// | ||
/// Subclasses should override this instead of [resolve] if they need to | ||
/// return some subclass of [ImageStream]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
return stream; | ||
} | ||
|
||
/// Called when [resolve] has finished calling [obtainKey]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// Always returns true, but will assert if [dispose] has not been called | ||
/// but the state this is tracking is unmounted. | ||
@visibleForTesting | ||
bool debugValidate() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
}) : assert(context != null), | ||
assert(imageProvider != null); | ||
|
||
/// The context that may or may not be enclosed by a scrollable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
expect(state.mounted, true); | ||
|
||
final DisposableBuildContext context = DisposableBuildContext(state); | ||
expect(context.debugValidate(), true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
return Scrollable.of(find.byType(TestWidget).evaluate().first).position; | ||
} | ||
|
||
testWidgets('DisposableBuildContext asserts on disposed state', (WidgetTester tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
velocities.add(velocity); | ||
return super.recommendDeferredLoading(velocity, metrics, context); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// method. | ||
/// method. If they need to change the implementation of [ImageStream] used, | ||
/// they should override [createStream]. If they need to manage the actual | ||
/// resolution of the image, they should override [resolveStreamForKey]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done - I think.
Internal CL has landed. |
* Migration guide for flutter/flutter#49389 * .. * typo * Update image-cache-and-provider.md * Update index.md
when they move this to stable branch? |
* Improve: Merge from [Defer image decoding when scrolling fast](flutter/flutter#49389). * Flutter sdk minimum version limit to 1.17.0.
Description
Continuation of #48536
Dependds on #49319
Basically an API cleanup of #48536
Related Issues
#32143
fixes #44510
b/144232910
#48305
Fixes #48775
Tests
I added the following tests:
Tests for new provider, new method on image cache (containsKey), image behavior in a scrolling list.
Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]
). This will ensure a smooth and quick review process.///
).flutter analyze --flutter-repo
) does not report any problems on my PR.Breaking Change
Did any tests fail when you ran them? Please read Handling breaking changes.