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

Defer image decoding when scrolling fast #49389

Merged
merged 14 commits into from
Jan 30, 2020

Conversation

dnfield
Copy link
Contributor

@dnfield dnfield commented Jan 23, 2020

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.

  • 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 read the Tree Hygiene wiki page, which explains my responsibilities.
  • 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

Did any tests fail when you ran them? Please read Handling breaking changes.

@dnfield dnfield added framework flutter/packages/flutter repository. See also f: labels. c: API break Backwards-incompatible API changes f: scrolling Viewports, list views, slivers, etc. a: images Loading, displaying, rendering images labels Jan 23, 2020
@@ -227,6 +227,11 @@ class ImageCache {
return result;
}

/// Returns whether this [key] has been previously added by [putIfAbsent].
bool containsKey(Object key) {
Copy link
Contributor Author

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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a break.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

@dnfield dnfield changed the title Image provider widget Defer image decoding when scrolling fast Jan 23, 2020
return stream;
}

void _safeObtainKey(ImageConfiguration configuration, ImageStream stream) {
Copy link
Contributor

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.
Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Member

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.
Copy link
Contributor

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.
Copy link
Contributor

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
Copy link
Contributor

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.');
Copy link
Contributor

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].
Copy link
Contributor

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.

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 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]) {
Copy link
Contributor

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

@dnfield
Copy link
Contributor Author

dnfield commented Jan 24, 2020

I've updated the docs and moved the build context enclosing class to a new class called DisposableBuildContext.

Still waiting for the dependent PR to land so I can rebase it into this one and then mark this ready for review.

@dnfield dnfield marked this pull request as ready for review January 25, 2020 16:10
@dnfield
Copy link
Contributor Author

dnfield commented Jan 25, 2020

Ready for review now!

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.

LGTM after comments are resolved.

@@ -227,6 +227,11 @@ class ImageCache {
return result;
}

/// Returns whether this [key] has been previously added by [putIfAbsent].
Copy link
Member

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).

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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,
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We use SynchronousFutures for these, which look async but are actually return synchronously.

Copy link
Contributor Author

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].
Copy link
Member

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?

Copy link
Contributor Author

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].
Copy link
Member

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].

Copy link
Contributor Author

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.
Copy link
Member

Choose a reason for hiding this comment

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

nit: by a [Scrollable]?

Copy link
Contributor Author

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 {
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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)

Copy link
Contributor Author

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);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

nit: add a newline

Copy link
Contributor Author

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() {
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor Author

@dnfield dnfield left a 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
Copy link
Contributor Author

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].
Copy link
Contributor Author

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].
Copy link
Contributor Author

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].
Copy link
Contributor Author

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() {
Copy link
Contributor Author

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.
Copy link
Contributor Author

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);
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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);
}
}
Copy link
Contributor Author

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].
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done - I think.

dnfield added a commit to dnfield/website that referenced this pull request Jan 29, 2020
@dnfield
Copy link
Contributor Author

dnfield commented Jan 30, 2020

Internal CL has landed.

@Gperez88
Copy link

when they move this to stable branch?

zmtzawqlp added a commit to fluttercandies/extended_image that referenced this pull request Aug 4, 2020
* Improve:

  Merge from [Defer image decoding when scrolling fast](flutter/flutter#49389).

*  Flutter sdk minimum version limit to 1.17.0.
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 31, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
a: images Loading, displaying, rendering images c: API break Backwards-incompatible API changes f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

How to stop image load when scrolling list ? High images RAM consumption
6 participants