Repository navigation
Description
Steps to reproduce
- Run the code sample below.
- Check the console logs printed on startup.
- Compare the
LF inputandCRLF inputsections in the app.
Expected results
LicenseEntryWithLineBreaks should treat CRLF line endings the same way as LF line endings inside hard-wrapped paragraphs.
Both LF and CRLF inputs should be reflowed into a single paragraph without leaving \r or \n characters in LicenseParagraph.text.
Actual results
LF input is reflowed as expected, but CRLF input leaves carriage return characters (\r, code unit 13) in LicenseParagraph.text.
Because of this, CRLF license text can render differently from LF license text, with unexpected spacing or line-break-like gaps at the original line endings.
This appears to happen because beforeParagraph already skips \r\n as a paragraph boundary in licenses.dart, but addLine() does not strip the trailing \r when collecting hard-wrapped lines inside a paragraph.
For paragraph boundaries, CRLF is already handled here:
flutter/packages/flutter/lib/src/foundation/licenses.dart
Lines 192 to 202 in 8a9f61c
| case '\r': | |
| case '\n': | |
| case '\f': | |
| if (lines.isNotEmpty) { | |
| result.add(getParagraph()); | |
| } | |
| if (text[currentPosition] == '\r' && | |
| currentPosition < text.length - 1 && | |
| text[currentPosition + 1] == '\n') { | |
| currentPosition += 1; | |
| } |
Code sample
Code sample
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
final String lfParagraph = const LicenseEntryWithLineBreaks(<String>[
'lf_license',
], _lfLicenseText).paragraphs.single.text;
final String crlfParagraph = const LicenseEntryWithLineBreaks(<String>[
'crlf_license',
], _crlfLicenseText).paragraphs.single.text;
debugPrint('LF contains CR: ${lfParagraph.contains('\r')}');
debugPrint('LF contains LF: ${lfParagraph.contains('\n')}');
debugPrint('LF code units: ${lfParagraph.codeUnits}');
debugPrint('CRLF contains CR: ${crlfParagraph.contains('\r')}');
debugPrint('CRLF contains LF: ${crlfParagraph.contains('\n')}');
debugPrint('CRLF code units: ${crlfParagraph.codeUnits}');
LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(<String>[
'lf_license',
], _lfLicenseText);
yield const LicenseEntryWithLineBreaks(<String>[
'crlf_license',
], _crlfLicenseText);
});
runApp(const MyApp());
}
const String _lfLicenseText =
'This paragraph is hard-wrapped with LF line endings.\n'
'These lines should be joined with spaces, not preserved as line breaks.\n'
'The paragraph should appear as one reflowed paragraph.\n';
const String _crlfLicenseText =
'This paragraph is hard-wrapped with CRLF line endings.\r\n'
'These lines should be joined with spaces, not preserved as line breaks.\r\n'
'The paragraph should appear as one reflowed paragraph.\r\n';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: HomePage());
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final String lfParagraph = const LicenseEntryWithLineBreaks(<String>[
'lf_license',
], _lfLicenseText).paragraphs.single.text;
final String crlfParagraph = const LicenseEntryWithLineBreaks(<String>[
'crlf_license',
], _crlfLicenseText).paragraphs.single.text;
return Scaffold(
appBar: AppBar(title: const Text('License CRLF repro')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_ParsedLicenseCard(title: 'LF input', paragraph: lfParagraph),
const SizedBox(height: 16),
_ParsedLicenseCard(title: 'CRLF input', paragraph: crlfParagraph),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => showLicensePage(context: context),
child: const Text('Show licenses'),
),
],
),
);
}
}
class _ParsedLicenseCard extends StatelessWidget {
const _ParsedLicenseCard({required this.title, required this.paragraph});
final String title;
final String paragraph;
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final bool containsCr = paragraph.contains('\r');
final bool containsLf = paragraph.contains('\n');
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(title, style: textTheme.titleMedium),
const SizedBox(height: 8),
Text('contains CR: $containsCr'),
Text('contains LF: $containsLf'),
const SizedBox(height: 8),
Text('code units: ${paragraph.codeUnits}'),
const SizedBox(height: 8),
Text(paragraph),
],
),
),
);
}
}Screenshots or Video
Screenshots / Video demonstration
2026-07-11.14.42.57.mov
Logs
Logs
I/flutter (28156): LF contains CR: false
I/flutter (28156): LF contains LF: false
I/flutter (28156): LF code units: [84, 104, 105, 115, 32, 112, 97, 114, 97, 103, 114, 97, 112, 104, 32, 105, 115, 32, 104, 97, 114, 100, 45, 119, 114, 97, 112, 112, 101, 100, 32, 119, 105, 116, 104, 32, 76, 70, 32, 108, 105, 110, 101, 32, 101, 110, 100, 105, 110, 103, 115, 46, 32, 84, 104, 101, 115, 101, 32, 108, 105, 110, 101, 115, 32, 115, 104, 111, 117, 108, 100, 32, 98, 101, 32, 106, 111, 105, 110, 101, 100, 32, 119, 105, 116, 104, 32, 115, 112, 97, 99, 101, 115, 44, 32, 110, 111, 116, 32, 112, 114, 101, 115, 101, 114, 118, 101, 100, 32, 97, 115, 32, 108, 105, 110, 101, 32, 98, 114, 101, 97, 107, 115, 46, 32, 84, 104, 101, 32, 112, 97, 114, 97, 103, 114, 97, 112, 104, 32, 115, 104, 111, 117, 108, 100, 32, 97, 112, 112, 101, 97, 114, 32, 97, 115, 32, 111, 110, 101, 32, 114, 101, 102, 108, 111, 119, 101, 100, 32, 112, 97, 114, 97, 103, 114, 97, 112, 104, 46]
I/flutter (28156): CRLF contains CR: true
I/flutter (28156): CRLF contains LF: false
I/flutter (28156): CRLF code units: [84, 104, 105, 115, 32, 112, 97, 114, 97, 103, 114, 97, 112, 104, 32, 105, 115, 32, 104, 97, 114, 100, 45, 119, 114, 97, 112, 112, 101, 100, 32, 119, 105, 116, 104, 32, 67, 82, 76, 70, 32, 108, 105, 110, 101, 32, 101, 110, 100, 105, 110, 103, 115, 46, 13, 32, 84, 104, 101, 115, 101, 32, 108, 105, 110, 101, 115, 32, 115, 104, 111, 117, 108, 100, 32, 98, 101, 32, 106, 111, 105, 110, 101, 100, 32, 119, 105, 116, 104, 32, 115, 112, 97, 99, 101, 115, 44, 32, 110, 111, 116, 32, 112, 114, 101, 115, 101, 114, 118, 101, 100, 32, 97, 115, 32, 108, 105, 110, 101, 32, 98, 114, 101, 97, 107, 115, 46, 13, 32, 84, 104, 101, 32, 112, 97, 114, 97, 103, 114, 97, 112, 104, 32, 115, 104, 111, 117, 108, 100, 32, 97, 112, 112, 101, 97, 114, 32, 97, 115, 32, 111, 110, 101, 32, 114, 101, 102, 108, 111, 119, 101, 100, 32, 112, 97, 114, 97, 103, 114, 97, 112, 104, 46, 13]Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.44.6, on macOS 26.5.2 25F84 darwin-arm64, locale ja-JP) [1,026ms]
• Flutter version 3.44.6 on channel stable at /Users/osaki/fvm/versions/3.44.6
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision ee80f08bbf (2 days ago), 2026-07-08 15:02:06 -0700
• Engine revision 83675ed276
• Dart version 3.12.2
• DevTools version 2.57.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets,
enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [2.3s]
• Android SDK at /Users/osaki/Library/Android/sdk
• Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
• Platform android-36, build-tools 36.1.0
• Java binary at: /Users/osaki/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.10+-117844308-b1163.108)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 26.5) [1,786ms]
• Xcode at /Applications/Xcode_26.5.app/Contents/Developer
• Build 17F42
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [5ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (4 available) [6.5s]
• Pixel 10 (mobile) • 55161FDCR002T4 • android-arm64 • Android 17 (API 37)
• motucraft (wireless) (mobile) • 00008150-001C305A3440401C • ios • iOS 26.5.2 23F84
• macOS (desktop) • macos • darwin-arm64 • macOS 26.5.2 25F84 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 150.0.7871.115
[✓] Network resources [672ms]
• All expected network resources are available.
• No issues found!
Activity
saurabh-mirajkar commentedon Jul 13, 2026
@motucraft Thanks for reporting this. I was able to reproduced this behaviour on latest Flutter stable
3.44.6. Routing this for further investigation.justinmc commentedon Jul 14, 2026
@motucraft @saurabh-mirajkar The video looks like Android, is this only reproducible on Android or on all platforms?
Edit: After reading this it seems like it would repro on any platform where you give it carriage returns.
motucraft commentedon Jul 15, 2026
@justinmc
Yes, I confirmed this on iOS as well. This is not Android-specific; as you noted, it can reproduce on any platform when the license text contains CRLF line endings.
Thanks for reviewing the PR!
Treat CRLF like LF in LicenseEntryWithLineBreaks
Treat CRLF like LF in LicenseEntryWithLineBreaks
Treat CRLF like LF in LicenseEntryWithLineBreaks
Treat CRLF like LF in LicenseEntryWithLineBreaks (flutter#189336)
github-actions commentedon Aug 29, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.