Repository navigation
Add useLogicalPixels to ResizeImage for logical pixel cache sizing - #184549
auto-submit[bot] merged 22 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request adds useLogicalPixels to ResizeImage and useLogicalCacheSize to the Image widget, enabling image decoding at logical pixel sizes scaled by the device pixel ratio. Changes include updates to ResizeImageKey, ResizeImage, and Image constructors, along with new regression tests. Feedback identifies a null safety compilation error in image_provider.dart regarding the assignment of effectiveWidth and notes that the new public parameter in resizeIfNeeded requires documentation to comply with the style guide.
victorsanni
left a comment
There was a problem hiding this comment.
I think in the ideal world users should not have to turn on this flag to get the correct behavior. It is scary though as developers are probably already implementing workarounds (most notably multiplying by the device pixel ratio) for this issue.
I wonder if it might be better here to break (by always multiplying with the DPR) even though it will be disruptive for users already adopting the workaround.
| /// These values also set the image's width & height in logical pixels | ||
| /// if it is unconstrained. | ||
| /// | ||
| /// If [useLogicalPixels] is true, [width] and [height] are multiplied by |
There was a problem hiding this comment.
I think the docs should recognize the bug. When/why should useLogicalPixels be set?
There was a problem hiding this comment.
This is an option addition (default off), not a bug fix — wouldn't bug framing be excessive?
| this.filterQuality = FilterQuality.medium, | ||
| int? cacheWidth, | ||
| int? cacheHeight, | ||
| bool useLogicalCacheSize = false, |
There was a problem hiding this comment.
These (and other constructors) will need tests as well.
There was a problem hiding this comment.
I think this is covered through image_provider_resize_image_test.dart.
The Image.* constructors forward useLogicalCacheSize to ResizeImage.resizeIfNeeded via a named parameter; image_test.dart doesn't carry similar forwarding tests for cacheWidth/cacheHeight either. Happy to add explicit forwarding tests if you'd still prefer.
There was a problem hiding this comment.
I think we should still test, even if it is only the forwarding that is tested (i.e the underlying ResizeImage has the expected value). For every public API we are adding here.
| /// These parameters are primarily intended to reduce the memory usage of | ||
| /// [ImageCache]. | ||
| /// | ||
| /// If `useLogicalCacheSize` is true, `cacheWidth` and `cacheHeight` are |
There was a problem hiding this comment.
But why even set this to true? The documentation should explain this.
There was a problem hiding this comment.
Added a use-case hint to the field/param doc. 33d73e2
| /// These parameters are primarily intended to reduce the memory usage of | ||
| /// [ImageCache]. | ||
| /// | ||
| /// If `useLogicalCacheSize` is true, `cacheWidth` and `cacheHeight` are |
| /// These parameters are primarily intended to reduce the memory usage of | ||
| /// [ImageCache]. | ||
| /// | ||
| /// If `useLogicalCacheSize` is true, `cacheWidth` and `cacheHeight` are |
| // so we can directly create the synchronous result to return. | ||
| result = SynchronousFuture<ResizeImageKey>( | ||
| ResizeImageKey._(key, policy, width, height, allowUpscaling), | ||
| ResizeImageKey._(key, policy, width, height, allowUpscaling, devicePixelRatio), |
There was a problem hiding this comment.
Instead of storing only the devicepixelratio in the key, can we store the effective size? That way if the devicepixelratio is different but the effective size (which is the downstream quantity we need) is the same we can just cache that. What do you think?
There was a problem hiding this comment.
Adopted, thanks.
ResizeImageKey now stores effective decode dimensions; two ResizeImages whose post-DPR size matches share a cache entry.
|
Thank you for the review, @victorsanni. I agree — users should get the correct behavior without needing to opt in. I originally proposed the flag out of caution for backward compatibility, but I'd prefer to always multiply I looked into cached_network_image, which is the most widely used library wrapping https://github.com/Baseflow/flutter_cached_network_image/blob/v3.4.1/cached_network_image/lib/src/cached_image_widget.dart#L272-L295 The impact is limited to app-level code that manually multiplies by DPR as a workaround. Does this direction work for you? |
9bdc90e to
b25a17f
Compare
b25a17f to
d5a26bb
Compare
victorsanni
left a comment
There was a problem hiding this comment.
I think changing the behavior of width and height under the hood is probably not good. If we want to solve this problem this way, we should deprecate width and height, and introduce new similar properties that internally multiply by the devicePixelRatio.
That way users (who may have been using these workarounds) don't get the rug pulled from under them. By migrating to these new properties users are basically signing up for this change in behavior.
| /// These values also set the image's width & height in logical pixels | ||
| /// if it is unconstrained. | ||
| /// | ||
| /// [width] and [height] are specified in logical pixels and multiplied by |
There was a problem hiding this comment.
Nit:
| /// [width] and [height] are specified in logical pixels and multiplied by | |
| /// The [width] and [height] are specified in logical pixels and multiplied by |
There was a problem hiding this comment.
Thanks — no longer applies after the shift to opt-in default.
|
Thanks for the follow-up, @victorsanni. Coming back to the flag idea I had originally - let me re-propose it with Concretely: ResizeImage(provider, width: 300, height: 200);
// Opt-out
ResizeImage(provider, width: 300, height: 200, useLogicalPixels: false);Comparing the two routes from a maintenance standpoint, deprecating
Eventually removing the deprecated public API is also a larger break than retiring an internal flag. By contrast, the flag approach:
Thanks for taking the time to review - please let me know if there are concerns I haven't addressed here. |
justinmc
left a comment
There was a problem hiding this comment.
I'm going to throw an approval on here so we can see if the change as-is breaks Google tests or not. That will add some more information to this decision.
Also FYI I think the before and after images in the top PR description are reversed if I'm not mistaken.
|
Hi @koji-1009, thanks for the detailed explanation. I understand better now. While it sucks that the intended behavior will be opt-in, it allows us to nudge users towards it allowing for a gentler migration instead of suddenly breaking two APIs. Feels like we've come full circle :)) |
This reverts commit 683426a.
|
Google testing failure is legitimate - FlutterLogo getting scaled by devicePixelRatio. |
| /// | ||
| /// At least one of `width` and `height` must be non-null. | ||
| /// | ||
| /// Set [useLogicalSize] to specify `width` and `height` in logical pixels. |
There was a problem hiding this comment.
I don't think this is enough discoverability-wise. We need to explain what the default should be, what it is, why this might cause issues, and how to correct it. This line only explains the "how", and introduces no new information that isn't immediately obvious from the name of the flag.
There was a problem hiding this comment.
I want to make sure I'm reading your comment correctly. Are you suggesting the doc should state what the default should be? For example, "logical pixels would be the appropriate default; physical is the current default."
There was a problem hiding this comment.
More than that - a developer doing their due diligence before adopting this widget for use in their project should be able to know from the documentation that specifying cacheWidth/Height may result in lower quality images, why that happens, and how this flag helps resolve this mismatch.
While we don't conventionally link to issues in public-facing API docs, the docs should contain enough information for new adopters to foresee and solve the problem, and the flag should specify the correct default is logical, but is set to physical for backwards compatibility.
There was a problem hiding this comment.
Would one of these match what you have in mind?
A — full inline explanation
/// Whether [width] and [height] are interpreted as logical pixels.
///
/// When `false` (the default), [width] and [height] are treated as physical
/// pixels. On high-density displays, this can result in images being decoded
/// at fewer pixels than the screen can render, producing lower-quality output.
/// When `true`, [width] and [height] are multiplied by the device pixel ratio
/// from the current [ImageConfiguration], so the image is decoded at the
/// resolution that matches the rendered display.
///
/// The correct default is logical, but it is set to physical for backwards
/// compatibility.B — link to the originating issue
/// Whether [width] and [height] are interpreted as logical pixels.
///
/// When `false` (the default), [width] and [height] are treated as physical
/// pixels. When `true`, they are multiplied by the device pixel ratio from
/// the current [ImageConfiguration] before decoding.
///
/// See https://github.com/flutter/flutter/issues/56239 for background on why
/// physical pixels can produce lower-quality output on high-density displays.
///
/// The correct default is logical, but it is set to physical for backwards
/// compatibility.Personally, when I encounter a new flag in the Flutter SDK, I search GitHub for the PR that introduced it — so for me, issue or PR links inside the field doc haven't been necessary.
Could you let me know which of these is appropriate as Flutter documentation? If neither matches your intent, please share the concrete wording you have in mind.
There was a problem hiding this comment.
A is better, we try not to link to issues etc in widget API docs.
Slight tweaks:
- Replace the last paragraph with "Typically in the framework, pixel dimensions are interpreted as logical rather than physical. However, this defaults to physical for backwards compatibility". A behavior being correct or incorrect is a matter of perspective.
|
|
||
| /// Whether [width] and [height] are interpreted as logical pixels rather | ||
| /// than physical pixels. | ||
| /// |
| Map<String, String>? headers, | ||
| int? cacheWidth, | ||
| int? cacheHeight, | ||
| bool useLogicalCacheSize = false, |
There was a problem hiding this comment.
What happens if width is unset but cacheWidth is set and useLogicalCacheSize is true?
There was a problem hiding this comment.
Can we add a test for this as well?
There was a problem hiding this comment.
width (layout) and cacheWidth / useLogicalCacheSize (decode) are independent paths — an unset width doesn't feed into the resize path, so cacheWidth is still DPR-multiplied. That multiplication is already asserted in image_provider_resize_image_test.dart ("useLogicalSize accounts for devicePixelRatio when decoding"), and the constructors just forward the flag. width being unset doesn't reach a distinct path, so there's no separate case to add here.
|
Thank you for the review. I've merged the master branch. |
fix #56239
Add
useLogicalPixelstoResizeImageanduseLogicalCacheSizetoImage.{network, file, asset, memory}andFadeInImage.{memoryNetwork, assetNetwork}constructors.When
true, the cache dimensions are interpreted as logical pixels rather than physical pixels, so the cached resolution follows the surrounding device pixel ratio without requiringBuildContextat the call site. Default isfalse; existing callers are unaffected.app(lib/main.dart)
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.