Skip to content

Added flutter rust bridge impelementation as the dart_icu4x pacage #4

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

farawehassan
Copy link
Contributor

@farawehassan farawehassan commented Aug 5, 2025

🎯 Overview

This PR significantly improves the dart_icu4x project documentation by replacing the generic Flutter FFI template README with a comprehensive, project-specific guide that accurately describes the ICU4X Flutter plugin functionality.

✨ Key Improvements

  • Clear Project Description – Purpose, ICU4X integration, and capabilities explained.
  • Project Structure – Diagram + breakdown of Rust backend, Dart library, and platform configs.
  • Features – Unicode properties, case mapping, search, and usage examples.
  • Installation & Setup – End-user and contributor guides with platform support info.
  • Development Workflow – Build process, Make commands, FRB codegen, and testing.
  • Code Examples – Real API usage and Flutter integration patterns.
  • Dependencies – Updated to use local path for contributor setup.

✅ Benefits

  • Faster onboarding for contributors.
  • Clear, accurate, and maintainable documentation.
  • Professional presentation aligned with ICU4X standards.

This PR transforms the dart_icu4x project from having generic template documentation to having comprehensive, project-specific documentation that accurately reflects its purpose as a Unicode and internationalization Flutter plugin powered by ICU4X.

# installed. You should not increase this version, as doing so will cause
# the plugin to fail to compile for some customers of the plugin.
cmake_minimum_required(VERSION 3.10)

Copy link
Member

Choose a reason for hiding this comment

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

Suggestion, here and elsewhere: seems like this file isn't used right now. I think we should only add files that are being used by the main project.

@sffc
Copy link
Member

sffc commented Aug 13, 2025

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds the initial implementation of the dart_icu4x package, including the Rust backend, Dart/Flutter frontend, and an example application. The implementation provides functionality to query Unicode character properties and perform case mapping. My review focuses on improving correctness, performance, and maintainability. I've identified several areas for improvement, including fixing placeholder values in configuration files, correcting flawed logic in the search functionality, and addressing performance bottlenecks in the Rust implementation. There are also some critical items like adding a license file and updating placeholder metadata before this package can be considered for publishing.

@@ -0,0 +1 @@
TODO: Add your license here.

Choose a reason for hiding this comment

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

high

The project is missing a license. This is critical for others to be able to use your code. The README mentions that the project is licensed under the same license as the parent ICU4X project, so that license should be added here.

Comment on lines 16 to 20
clean: ## Cleans Flutter project.
flutter clean
flutter pub get
cargo build --manifest-path rust/Cargo.toml
flutter_rust_bridge_codegen generate

Choose a reason for hiding this comment

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

high

The clean target is misnamed. It performs a full build cycle (clean, fetch dependencies, build, generate code) rather than just cleaning build artifacts. This can be confusing and lead to unexpected behavior. It's better to separate cleaning from building.

For example:

clean: ## Cleans the project build artifacts.
	flutter clean
	cargo clean --manifest-path rust/Cargo.toml

setup: ## Installs dependencies and generates code.
	flutter pub get
	make cargo-build
	make generate

fresh: ## Runs `clean` and `setup` for a fresh setup.
	make clean
	make setup

Comment on lines 8 to 14
s.summary = 'A new Flutter FFI plugin project.'
s.description = <<-DESC
A new Flutter FFI plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => '[email protected]' }

Choose a reason for hiding this comment

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

high

The podspec contains several placeholder values from the template (summary, description, homepage, author). These should be updated with the actual project information before publishing.

Comment on lines +77 to +97
let general_category_map = CodePointMapData::<GeneralCategory>::new();
let script_map = CodePointMapData::<Script>::new();
let bidi_class_map = CodePointMapData::<BidiClass>::new();
let east_asian_width_map = CodePointMapData::<EastAsianWidth>::new();
let line_break_map = CodePointMapData::<LineBreak>::new();
let word_break_map = CodePointMapData::<WordBreak>::new();
let sentence_break_map = CodePointMapData::<SentenceBreak>::new();
let grapheme_cluster_break_map = CodePointMapData::<GraphemeClusterBreak>::new();
let hangul_syllable_type_map = CodePointMapData::<HangulSyllableType>::new();
let joining_type_map = CodePointMapData::<JoiningType>::new();
let alphabetic = CodePointSetData::new::<Alphabetic>();
let uppercase = CodePointSetData::new::<Uppercase>();
let lowercase = CodePointSetData::new::<Lowercase>();
let white_space = CodePointSetData::new::<WhiteSpace>();
let math = CodePointSetData::new::<Math>();
let dash = CodePointSetData::new::<Dash>();
let diacritic = CodePointSetData::new::<Diacritic>();
let emoji = CodePointSetData::new::<Emoji>();
let emoji_presentation = CodePointSetData::new::<EmojiPresentation>();
let emoji_modifier = CodePointSetData::new::<EmojiModifier>();
let emoji_modifier_base = CodePointSetData::new::<EmojiModifierBase>();

Choose a reason for hiding this comment

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

high

The ICU data providers (CodePointMapData, CodePointSetData, etc.) are initialized on every call to get_unicode_char_properties. This is inefficient and can cause significant performance degradation, especially since this is a synchronous FFI call. These providers should be initialized only once and reused across calls. You can use a crate like once_cell or lazy_static to ensure they are created lazily and stored in a static variable. The same issue exists for CaseMapper in get_character_case_mapping.

Comment on lines 101 to 123
if let Some(ref s) = search {
if !s.is_empty() {
// If search is a single character, search for that specific character
let mut chars = s.chars();
if let Some(c) = chars.next() {
if chars.next().is_none() {
results.push(c);
} else {
// Search is multiple characters, filter across all fields
results = CodePointInversionList::all()
.iter_chars()
.filter(|&c| {
let char_str = c.to_string();
let code_point_str = (c as u32).to_string();
let unicode_value = format!("U+{:04X}", c as u32);
let general_category = format!("{:?}", general_category_map.get(c));

s.to_lowercase().contains(&char_str.to_lowercase()) ||
code_point_str.contains(s) ||
unicode_value.to_lowercase().contains(&s.to_lowercase()) ||
general_category.to_lowercase().contains(&s.to_lowercase())
})
.collect();

Choose a reason for hiding this comment

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

high

The search logic in get_unicode_char_properties has some issues that affect correctness and user experience:

  1. Single-character search: When the search term is a single character, the result set is just that character. The pagination (skip/take) is then applied to this single-element list. This means if offset > 0, the function will incorrectly return an empty list.
  2. Multi-character search: The condition s.to_lowercase().contains(&char_str.to_lowercase()) seems to be reversed. It checks if the search term contains the character, which is probably not what's intended. You likely want to search if the character's name contains the search term.
  3. The search does not include the character's name, which is a primary way users might search for a character.

Comment on lines 114 to 115
dart_icu4x:
path: ../icu4x/dart_icu4x

Choose a reason for hiding this comment

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

medium

The installation instructions use a local path dependency which is great for contributors. For users of the package, it would be helpful to also show how to add it as a versioned dependency from pub.dev, once it's published. Also, the path ../icu4x/dart_icu4x seems specific to a particular monorepo structure and might be confusing. The example app uses path: ../, which is more standard for an example within a package.

Suggested change
dart_icu4x:
path: ../icu4x/dart_icu4x
dart_icu4x: ^0.0.1 # Or the latest version from pub.dev
# For contributors, use a path dependency:
# dependencies:
# dart_icu4x:
# path: ../

@@ -0,0 +1,32 @@
name: dart_icu4x
description: "A new Flutter FFI plugin project."

Choose a reason for hiding this comment

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

medium

The package description is a placeholder from the template. Please update it to accurately describe the dart_icu4x package.

description: A Flutter plugin that provides Unicode and internationalization (i18n) functionality using ICU4X.


android {
if (project.android.hasProperty("namespace")) {
namespace 'com.flutter_rust_bridge.dart_icu4x'

Choose a reason for hiding this comment

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

medium

The group and namespace are using a placeholder value from the flutter_rust_bridge template (com.flutter_rust_bridge.dart_icu4x). These should be updated to a proper value for your project, for example, org.unicode.dart_icu4x.

        namespace 'org.unicode.dart_icu4x'

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.flutter_rust_bridge.dart_icu4x">

Choose a reason for hiding this comment

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

medium

The package attribute is using a placeholder value from the flutter_rust_bridge template. It should be updated to a proper value for your project, for example, org.unicode.dart_icu4x, to match the group and namespace in build.gradle.

        package="org.unicode.dart_icu4x">

Comment on lines 8 to 9
test('Can call rust function', () async {
//expect(greet(name: "Tom"), "Hello, Tom!");

Choose a reason for hiding this comment

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

medium

The integration test contains a commented-out example from a template. Please replace it with a meaningful test for the dart_icu4x plugin's functionality, for example, by testing getUnicodeCharProperties or getCharacterCaseMapping.

    test('getUnicodeCharProperties returns properties', () async {
      final properties = getUnicodeCharProperties(limit: BigInt.from(1));
      expect(properties, isNotEmpty);
      expect(properties[0].character, isNotNull);
    });

@farawehassan farawehassan force-pushed the dart-icu4x-implementation branch from b0cb808 to 7420fd3 Compare August 13, 2025 07:13
@farawehassan
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the dart_icu4x package, a Flutter plugin using flutter_rust_bridge to provide ICU4X functionality. The changes are extensive, including the Rust backend, Dart frontend, an example application, and all necessary configuration files for Android and iOS. The documentation is comprehensive and well-written. My review focuses on improving code quality, consistency, and performance. I've identified several areas for improvement, particularly regarding performance in the Rust API by using once_cell::sync::Lazy for expensive initializations, and ensuring consistency in package naming within the Android build configuration. There are also several minor formatting and style issues that should be addressed.

@@ -0,0 +1,56 @@
// The Android Gradle Plugin builds the native code with the Android NDK.

group 'com.flutter_rust_bridge.dart_icu4x'

Choose a reason for hiding this comment

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

high

The group ID com.flutter_rust_bridge.dart_icu4x is inconsistent with the namespace org.unicode.dart_icu4x defined on line 29. For better consistency and to align with standard package naming conventions, the group ID should match the namespace.

group 'org.unicode.dart_icu4x'

Comment on lines +78 to +98
let general_category_map = CodePointMapData::<GeneralCategory>::new();
let script_map = CodePointMapData::<Script>::new();
let bidi_class_map = CodePointMapData::<BidiClass>::new();
let east_asian_width_map = CodePointMapData::<EastAsianWidth>::new();
let line_break_map = CodePointMapData::<LineBreak>::new();
let word_break_map = CodePointMapData::<WordBreak>::new();
let sentence_break_map = CodePointMapData::<SentenceBreak>::new();
let grapheme_cluster_break_map = CodePointMapData::<GraphemeClusterBreak>::new();
let hangul_syllable_type_map = CodePointMapData::<HangulSyllableType>::new();
let joining_type_map = CodePointMapData::<JoiningType>::new();
let alphabetic = CodePointSetData::new::<Alphabetic>();
let uppercase = CodePointSetData::new::<Uppercase>();
let lowercase = CodePointSetData::new::<Lowercase>();
let white_space = CodePointSetData::new::<WhiteSpace>();
let math = CodePointSetData::new::<Math>();
let dash = CodePointSetData::new::<Dash>();
let diacritic = CodePointSetData::new::<Diacritic>();
let emoji = CodePointSetData::new::<Emoji>();
let emoji_presentation = CodePointSetData::new::<EmojiPresentation>();
let emoji_modifier = CodePointSetData::new::<EmojiModifier>();
let emoji_modifier_base = CodePointSetData::new::<EmojiModifierBase>();

Choose a reason for hiding this comment

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

high

These CodePointMapData and CodePointSetData instances are initialized on every call to get_unicode_char_properties. This is highly inefficient as it involves reading and parsing ICU data repeatedly. To improve performance, these should be initialized only once using once_cell::sync::Lazy and stored in static variables at the module level.

For example:

static GENERAL_CATEGORY_MAP: Lazy<CodePointMapData<GeneralCategory>> = Lazy::new(CodePointMapData::<GeneralCategory>::new);

Then, inside the function, you can use &*GENERAL_CATEGORY_MAP.


#[flutter_rust_bridge::frb(sync)]
pub fn get_character_case_mapping(character: String) -> CaseMappingResult {
let casemapper = CaseMapper::new();

Choose a reason for hiding this comment

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

high

The CaseMapper is created on every call. This is inefficient. It should be initialized once using once_cell::sync::Lazy and stored in a static variable to improve performance.

Define this at the module level:

static CASE_MAPPER: Lazy<CaseMapper> = Lazy::new(CaseMapper::new);

Then use it in the function as suggested.

Suggested change
let casemapper = CaseMapper::new();
let casemapper = &*CASE_MAPPER;

make setup

cargo-build: ## Runs `cargo build --manifest-path rust/Cargo.toml`.
cargo build --manifest-path rust/Cargo.toml

Choose a reason for hiding this comment

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

medium

There is a trailing whitespace at the end of this line which should be removed for cleanliness.

	cargo build --manifest-path rust/Cargo.toml


test: ## Runs unit tests.
flutter test

Choose a reason for hiding this comment

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

medium

This file has an extra empty line with a tab and is missing a final newline. It's good practice for text files to end with a single newline character after the last command.

@@ -0,0 +1,3 @@
rust_input: crate::api
rust_root: rust/
dart_output: lib/src/rust

Choose a reason for hiding this comment

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

medium

It's a good practice for text files to end with a newline character. Please add one at the end of this file.

DESC
s.homepage = 'https://github.com/unicode-org/gsoc-unicode-app'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'https://icu4x.unicode.org/' }

Choose a reason for hiding this comment

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

medium

The author is set to the placeholder 'Your Company'. This should be updated to reflect the actual author or organization, for example, 'The Unicode Consortium'.

  s.author           = { 'The Unicode Consortium' => 'https://icu4x.unicode.org/' }

'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libdart_icu4x.a',
}
end

Choose a reason for hiding this comment

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

medium

It's a good practice for text files to end with a newline character. Please add one at the end of this file.

android:
ffiPlugin: true
ios:
ffiPlugin: true

Choose a reason for hiding this comment

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

medium

It's a good practice for text files to end with a newline character. Please add one at the end of this file.

#[flutter_rust_bridge::frb(sync)]
pub fn get_character_case_mapping(character: String) -> CaseMappingResult {
let casemapper = CaseMapper::new();
let langid: LanguageIdentifier = "und".parse().unwrap();

Choose a reason for hiding this comment

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

medium

Using unwrap() can cause the program to panic if the Result is an Err. While "und" is a valid language identifier, it's better practice in library code to handle potential errors more gracefully. Consider using expect() with a descriptive message to provide more context on panic.

Suggested change
let langid: LanguageIdentifier = "und".parse().unwrap();
let langid: LanguageIdentifier = "und".parse().expect("Failed to parse 'und' as a language identifier");

@farawehassan
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the dart_icu4x package, a Flutter plugin that provides ICU4X functionality through Rust bindings. The overall structure is well-organized, including an example application and integration tests. However, there are several areas that need improvement. The Rust implementation has significant performance issues due to the repeated initialization of heavy ICU4X data structures within API functions. The Android build configurations use non-standard and future SDK versions, which could cause build failures for users. Additionally, there are minor inconsistencies and typos in various configuration files. Addressing these points will greatly improve the package's stability, performance, and usability.

distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

Choose a reason for hiding this comment

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

critical

The Gradle version 8.12 specified in the distributionUrl is invalid and will cause the build to fail. Please update this to a valid Gradle version. The latest stable version is 8.8.

distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip

Comment on lines +11 to +17
icu = "2.0.0"
icu_casemap = "2.0.0"
icu_collections = "2.0.0"
icu_properties = "2.0.1"
icu_provider = "2.0.0"
icu_locale_core = "2.0.0"
icu_locid = "1.5.0"

Choose a reason for hiding this comment

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

high

The versions for the icu crates (e.g., icu = "2.0.0") do not match the latest official releases on crates.io (which is 1.5.0). Using non-standard or unpublished versions can lead to dependency conflicts and build issues for consumers of this package. Please align with the official crate versions.

Comment on lines +234 to +269
pub fn get_character_case_mapping(character: String) -> CaseMappingResult {
let casemapper = CaseMapper::new();
let langid: LanguageIdentifier = "und".parse().expect("Failed to parse 'und' as a language identifier");

let mut chars = character.chars();
let c = match chars.next() {
Some(ch) if chars.next().is_none() => ch,
_ => return CaseMappingResult {
original: character.clone(),
mapped: character,
has_mapping: false,
},
};

let original = c.to_string();
let upper = casemapper.uppercase_to_string(&original, &langid).into_owned();
let lower = casemapper.lowercase_to_string(&original, &langid).into_owned();

// Check if the character has a case mapping (upper or lower is different from original)
let has_mapping = upper != original || lower != original;

// Return the appropriate mapping (prefer uppercase if different, otherwise lowercase)
let mapped = if upper != original {
upper
} else if lower != original {
lower
} else {
original.clone()
};

CaseMappingResult {
original,
mapped,
has_mapping,
}
}

Choose a reason for hiding this comment

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

high

A new CaseMapper instance is created on every call to this function. This is inefficient and can be a performance bottleneck. The CaseMapper should be initialized once and reused across calls. Please use once_cell::sync::Lazy to create a static instance.

Comment on lines +175 to +185
pub fn get_all_scripts() -> Vec<String> {
let script_map = CodePointMapData::<Script>::new();
let mut names: BTreeSet<String> = BTreeSet::new();

for c in CodePointInversionList::all().iter_chars() {
let s = script_map.get(c);
names.insert(s.long_name().to_string());
}

names.into_iter().collect()
}

Choose a reason for hiding this comment

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

high

This function iterates over all Unicode characters to build the list of script names on every call, which is inefficient. Since the list of scripts is static, it should be computed once and cached. Please use once_cell::sync::Lazy to store the result and avoid re-computation.

Comment on lines +88 to +108
let general_category_map = CodePointMapData::<GeneralCategory>::new();
let script_map = CodePointMapData::<Script>::new();
let bidi_class_map = CodePointMapData::<BidiClass>::new();
let east_asian_width_map = CodePointMapData::<EastAsianWidth>::new();
let line_break_map = CodePointMapData::<LineBreak>::new();
let word_break_map = CodePointMapData::<WordBreak>::new();
let sentence_break_map = CodePointMapData::<SentenceBreak>::new();
let grapheme_cluster_break_map = CodePointMapData::<GraphemeClusterBreak>::new();
let hangul_syllable_type_map = CodePointMapData::<HangulSyllableType>::new();
let joining_type_map = CodePointMapData::<JoiningType>::new();
let alphabetic = CodePointSetData::new::<Alphabetic>();
let uppercase = CodePointSetData::new::<Uppercase>();
let lowercase = CodePointSetData::new::<Lowercase>();
let white_space = CodePointSetData::new::<WhiteSpace>();
let math = CodePointSetData::new::<Math>();
let dash = CodePointSetData::new::<Dash>();
let diacritic = CodePointSetData::new::<Diacritic>();
let emoji = CodePointSetData::new::<Emoji>();
let emoji_presentation = CodePointSetData::new::<EmojiPresentation>();
let emoji_modifier = CodePointSetData::new::<EmojiModifier>();
let emoji_modifier_base = CodePointSetData::new::<EmojiModifierBase>();

Choose a reason for hiding this comment

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

high

This function initializes numerous large CodePointMapData and CodePointSetData objects on every call, which is highly inefficient and will negatively impact performance. These objects should be initialized only once and reused. You can achieve this by using once_cell::sync::Lazy.

For example:

use once_cell::sync::Lazy;

static GENERAL_CATEGORY_MAP: Lazy<CodePointMapData<GeneralCategory>> = Lazy::new(CodePointMapData::<GeneralCategory>::new);

// Then, in your function, you can use it like this:
let general_category = format!("{:?}", GENERAL_CATEGORY_MAP.get(c));

This optimization should be applied to all ICU4X data providers within this function.

s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '11.0'

Choose a reason for hiding this comment

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

medium

The iOS platform version is set to 11.0, which is inconsistent with the example app's deployment target of 12.0. To ensure consistency, please update this to 12.0.

  s.platform = :ios, '12.0'

DESC
s.homepage = 'https://github.com/unicode-org/gsoc-unicode-app'
s.license = { :file => '../LICENSE' }
s.author = { 'YThe Unicode Consortium' => 'https://icu4x.unicode.org/' }

Choose a reason for hiding this comment

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

medium

There appears to be a typo in the author's name. The leading 'Y' should be removed.

  s.author           = { 'The Unicode Consortium' => 'https://icu4x.unicode.org/' }

@@ -0,0 +1,3 @@
## 0.0.1

* TODO: Describe initial release.

Choose a reason for hiding this comment

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

medium

Please describe the initial release instead of leaving a TODO. This is important for users to understand the initial set of features and the scope of the version.

Suggested change
* TODO: Describe initial release.
* Initial release of the `dart_icu4x` package.
* Provides access to Unicode character properties from ICU4X.
* Includes case mapping and basic search functionality.


### Platform Support
- **Android**: Native ARM64 and x86_64 support
- **iOS**: Native ARM64 and x86_64 support

Choose a reason for hiding this comment

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

medium

This line has trailing whitespace. Please remove it to maintain consistent code style.

Suggested change
- **iOS**: Native ARM64 and x86_64 support
- **iOS**: Native ARM64 and x86_64 support

dart format .

test: ## Runs unit tests.
flutter test

Choose a reason for hiding this comment

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

medium

This file is missing a final newline character. It's a common convention to end files with a newline to prevent issues with file concatenation and some version control systems.

	flutter test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants