Current state
The following methods all return Uint8List
, yet they are only declared to return List<int>
:
Utf8Codec.encode()
BytesBuilder.takeBytes()
BytesBuilder.toBytes()
File.readAsBytes()
File.readAsBytesSync()
RandomAccessFile.read()
RandomAccessFile.readSync()
Uint8List.sublist()
Relatedly, the following sublist()
methods return sublists of the same type as
the source list, yet they only declare that they return the more generic type (e.g. List<int>
, List<float>
, or List<double>
):
Int8List
Uint8ClampedList
Int16List
Uint16List
Int32List
Uint32List
Int64List
Uint64List
Float32List
Float64List
Float32x4List
Int32x4List
Float64x2List
Intended change
This issues proposes to update the API of the aforementioned methods to declare the return value of the more specific return type (e.g. Uint8List
rather than List<int>
).
Rationale
-
Better type safety
Callers would like to statically prove that you can obtain a
ByteBuffer
from the result of these API calls. -
The current API/implementation combo encourages bad practices.
There are two dangers with an API saying it returns
List<int>
and always returningUint8List
:- People do roundabout things to guarantee that the result is a
Uint8List
(such as defensively wrapping the result in aUint8List
), which causes unnecessary overhead. - People start to depend on the result being a
Uint8List
and automatically performing a contravariant cast (which is already happening in Flutter and elsewhere) — and if anyone new implements the interface and returns something other than aUint8List
, code breaks.
- People do roundabout things to guarantee that the result is a
-
To match the documentation
Utf8Encoder
andBase64Decoder.convert
, for instance, already document that they returnUint8List
.
See also:
Expected impact
On callers
Callers of these APIs will not be impacted at all since the new return types are subtypes of the existing return types (and moreover, the return values will be the exact same values).
On implementations of the interfaces
Libraries that are implementing the following interfaces will be broken because they will no longer be implementing the interface:
Utf8Encoder
BytesBuilder
File
RandomAccessFile
Int8List
Uint8List
Uint8ClampedList
Int16List
Uint16List
Int32List
Uint32List
Int64List
Uint64List
Float32List
Float64List
Float32x4List
Int32x4List
Float64x2List
This includes (but is not limited to) some well-known packages, such as package:typed_data
and package:file
.
Steps for mitigation
For known affected packages, the plan will be to issue patch releases to those packages that tighten the SDK constraints to declare that the current version of the package is not compatible with an SDK version greater than the current dev version. Then, once the change lands, we’ll update the affected packages to implement the new API and loosen their SDK constraints once again.
I just hit a break on a private project, where I had
response.transform(utf8.decoder).join("")
for an HTTP request. It runs without issues on 2.4.0, but fails on a locally built SDK (2.5.0-edge). It doesn’t use any packages.I agree that the change is an improvement for some use-cases, but it’s also a hard breaking change for other naturally occurring use-cases.
I think we should consider very hard whether this breaking change is really worth the pain for our users, because I don’t think I’ll be the only one affected.
Our breaking change policy says that we make such breaking changes:
I’m not sure we did a proper re-consideration of this tradeoff when it became clear that the breakage was more widespread than anticipated. That could very well tip the balance towards not doing the change.
The feature hasn’t launched yet, so we can still decide to pull the plug on it.
I know that it would be annoying to roll the change back, especially after doing all this work, but the only thing that matters is end-user impact.
So, I humbly suggest that we do one more evaluation of the cost/benefit of this change before deciding to lauch it.