Skip to main content

Google - Flutter contributions

MobileDartAndroid-iOSmacOS-WindowsWeb

Introduction#

Flutter presentation
Flutter presentation

It’s been 5 months since I have launched this Open Source initiative and I learned a lot from it.

Today it’s time for something new…

I found that the format of one contribution every two weeks allowed me to see a wide variety of different projects but does not allow me to fully dive into the project.

I have decided to challenge myself and change the format for the next contributions to focus more in-depth for a few weeks on a specific project (a bit like what Google Summer of Code does) which will allow me to better understand how the project works and to be able to make more significant contributions.

This article is composed of eight contributions to various projects in the Flutter-Dartlang ecosystem:

  • Merged
    Flutter/Cupertino - Date order parameter link
  • Open
    Flutter - Check for Android device battery level link
  • Merged
    Flutter/Cocoon - Config keyHelper reference link
  • Merged
    Dart-lang - `printOnFailure` check current invoker link
  • Merged
    Codemirror.dart - Add SearchCursor wrapper link
  • Merged
    Flutter/Gallery - Refactor [web benchmarks] Move to appropriate folder link
  • Merged
    Flutter/Gallery - Back button overlapping link
  • Open
    Flutter - CPU/GPU/memory metrics for iOS gallery transition tests link

Project#

You can find the Flutter project presentation here.

Flutter/Cupertino - Date order parameter#

Contribution Types

This contribution is a new feature.

Context#

Cupertino (iOS-style) widgets are Beautiful and high-fidelity widgets for current iOS design language.

Flutter Cupertino
Flutter Cupertino

Current behavior#

This contribution adds a new dateOrder parameter to CupertinoDatePicker to define the order of the columns in date mode which overrides the default order value defined by localizations.datePickerDateOrder.

CupertinoDatePicker date order (before)
CupertinoDatePicker default date order

Implement the solution#

Define a new dateOrder parameter which determines the order of the columns.
This parameter can have multiple values defined in DatePickerDateOrder enum (link).

For example, dmy corresponds from left to right to day, month, year.

/// Determines the order of the columns inside [CupertinoDatePicker] in date mode.
/// Defaults to the locale's default date format/order.
final DatePickerDateOrder? dateOrder;

If the dateOrder property is not defined, the order is based on internationalization.

final DatePickerDateOrder datePickerDateOrder =
dateOrder ?? localizations.datePickerDateOrder;
switch (datePickerDateOrder) {
/// ...
}

Add some tests to be sure that everything is working as expected.

We check that with the DatePickerDateOrder.ydm value (Year/Day/Month), the component render the elements in the right order from left to right.

packages/flutter/test/cupertino/date_picker_test.dart
testWidgets('DatePicker displays the date in correct order', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
dateOrder: DatePickerDateOrder.ydm,
mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime newDate) {},
initialDateTime: DateTime(2018, 1, 14, 10, 30),
),
),
),
),
);
expect(
tester.getTopLeft(find.text('2018')).dx,
lessThan(tester.getTopLeft(find.text('14')).dx),
);
expect(
tester.getTopLeft(find.text('14')).dx,
lessThan(tester.getTopLeft(find.text('January')).dx),
);
});

Final result#

Users can now override the default CupertinoDatePicker date order.

CupertinoDatePicker date order
CupertinoDatePicker date order

Flutter - Check for Android device battery level#

Contribution Types

This contribution is a new feature.

Context#

DeviceLab is a physical lab that tests Flutter on real devices.

Current behavior#

This contribution will filter out and warn about Android devices with low battery level (smaller than or equal to 15%).

Implement the solution#

Get the battery level of an Android device with adb shell dumpsys battery.

It should output something like this:

Current Battery Service state:
AC powered: false
USB powered: true
Wireless powered: false
Max charging current: 0
Max charging voltage: 0
Charge counter: 0
status: 2
health: 2
present: true
level: 93
scale: 100
voltage: 4245
temperature: 237
technology: Li-ion

We can grab the level property as this is what we are interested in.

Future<int> _getBatteryLevel() async {
final String batteryInfo = await shellEval('dumpsys', <String>['battery']);
final String batteryLevel = grep('level:', from: batteryInfo).single.split(':')[1].trim();
return int.parse(batteryLevel);
}

Define a function to check if the battery level is less than 15%.

/// Whether the device has a battery level smaller than or equal to 15 percent.
@override
Future<bool> hasLowBatteryLevel() async {
return await _getBatteryLevel() <= 15;
}

Check all Android devices battery level and warn about those with low battery level.

final AndroidDevice device = allDevices[math.Random().nextInt(allDevices.length)];
final bool hasLowBatteryLevel = await device.hasLowBatteryLevel();
if (!hasLowBatteryLevel) {
_workingDevice = device;
}
for (final AndroidDevice device in allDevices) {
final String deviceId = device.deviceId;
if (await device.hasLowBatteryLevel()) {
print('Device with ID $deviceId has low battery level');
}
}

Add tests to check that our new functions hasLowBatteryLevel and _getBatteryLevel are working as expected.

group('batteryLevel', () {
test('has enough battery', () async {
FakeDevice.pretendHasEnoughBattery();
expect(await device.hasLowBatteryLevel(), isFalse);
});
test('has not enough battery', () async {
FakeDevice.pretendHasNotEnoughBattery();
expect(await device.hasLowBatteryLevel(), isTrue);
});
});
static void pretendHasNotEnoughBattery() {
output = '''
level: 15
''';
}
static void pretendHasEnoughBattery() {
output = '''
level: 20
''';
}

Final result#

This contribution added a way to warn about Android devices with low battery level in DeviceLab.


Flutter/Cocoon - Config keyHelper reference#

Contribution Types

This contribution is about refactoring.

Context#

Cocoon is a Dart App Engine custom runtime (backend) with a frontend of Flutter apps (build and repository dashboard). Cocoon coordinates and aggregates the results of flutter/flutter builds.

Current behavior#

A keyHelper reference has been added in cocoon config.

KeyHelper get keyHelper =>
KeyHelper(applicationContext: context.applicationContext);

Existing APIs have separate keyHelper definitions in their own scope. To make it consistent, we should update all APIs to use the keyHelper defined in the cocoon config.

Implement the solution#

Before

app_dart/lib/src/request_handlers/reset_prod_task.dart
final ClientContext clientContext = authContext.clientContext;
final KeyHelper keyHelper = KeyHelper(applicationContext: clientContext.applicationContext);

After

app_dart/lib/src/request_handlers/reset_prod_task.dart
final KeyHelper keyHelper = config.keyHelper;

Add some tests and mock the key helper.

setUp(() {
/// Other
keyHelper = FakeKeyHelper(applicationContext: clientContext.applicationContext);
config = FakeConfig(dbValue: datastoreDB, keyHelperValue: keyHelper);
});

Final result#

Made the codebase more sustainable and maintainable by refactoring a part of the codebase that was not relevant anymore.


Dart-lang - printOnFailure check current invoker#

Contribution Types

This contribution is a bug-fix.

Context#

Dart-lang tests provides a standard way of writing and running tests in Dart.

Current behavior#

The printOnFailure function is used to print errors in the console when an error occurs.
If you call it outside the test zone, the error is pretty confusing.

The method 'printOnFailure' was called on null.
Receiver: null
Tried calling: printOnFailure(<your-message>)

We should explicitly check and throw if there is no current invoker.

Implement the solution#

This contribution adds a check within printOnFailure and throw an exception if there is no current invoker.

Before

void printOnFailure(String message) => Invoker.current!.printOnFailure(message);

After

void printOnFailure(String message) {
var invoker = Invoker.current;
if (invoker == null) {
throw StateError(
'There is no current invoker. Please make sure that you are making the '
'call inside a test zone.');
}
return invoker.printOnFailure(message);
}

Final result#

Made the codebase more consistent by avoiding some issues and improving the developer experience.


Codemirror.dart - Add SearchCursor wrapper#

Contribution Types

This contribution is a new feature.

Context#

A Dart wrapper around the CodeMirror text editor.

From codemirror.net:

CodeMirror is a versatile text editor implemented in JavaScript for the browser. It is specialized for editing code, and comes with a number of language modes and add-ons that implement more advanced editing functionality.

Current behavior#

The wrapper didn't have a way to use the search cursor add-on which can be used to implement search/replace functionality.
This wrapper will be used for dart-lang/dart-pad#1866.

Implement the solution#

Add the add-on wrapper with all its methods.

The main method is getSearchCursor(query, start, options) → cursor which returns a search cursor with the following methods:

  • findNext() - findPrevious() → boolean
    Search forward or backward from the current position.
  • from() - to() → {line, ch}
    Return {line, ch} objects pointing at the start and end of the match.
  • replace(text: string, ?origin: string)
    Replaces the currently found match with the given text and adjusts the cursor position to reflect the replacement.
// Copyright (c) 2021, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/// A wrapper around the `add-on/search/searchcursor.js` add-on.
library codemirror.searchcursor;
import 'dart:js';
import 'src/js_utils.dart';
import 'codemirror.dart';
class SearchCursor {
/// Retrieve the search cursor from the editor instance.
static SearchCursorContainer getSearchCursor(CodeMirror editor, String value,
{Position? start, Map? options}) {
if (options == null) {
return SearchCursorContainer._(
editor.callArgs('getSearchCursor', [value, start]));
} else {
return SearchCursorContainer._(
editor.callArgs('getSearchCursor', [value, start, jsify(options)]));
}
}
}
class SearchCursorContainer extends ProxyHolder {
SearchCursorContainer._(JsObject? jsProxy) : super(jsProxy);
bool get atOccurrence => jsProxy!['atOccurrence'];
Doc get doc => Doc.fromProxy(jsProxy!['doc']);
Position get pos => Position.fromProxy(jsProxy!['pos']);
/// Search forward from the current position
bool findNext() => call('findNext');
/// Search backward from the current position
bool findPrevious() => call('findPrevious');
Position from() => Position.fromProxy(call('from'));
Position to() => Position.fromProxy(call('to'));
String replace(String text, {dynamic origin}) {
if (origin == null) {
return callArg('replace', text);
} else {
return callArgs('replace', [text, origin]);
}
}
}

Add tests to check that every method of the add-on is working properly.
We are using an html file to attach the Codemirror instance to.

// Copyright (c) 2014, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
@TestOn('browser')
library codemirror.tests;
import 'dart:html';
import 'package:codemirror/codemirror.dart';
import 'package:test/test.dart';
import 'package:codemirror/searchcursor.dart';
void main() {
group('searchCursor', createSearchCursorTests);
}
void createSearchCursorTests() {
late CodeMirror editor;
setUp(() {
editor = CodeMirror.fromTextArea(
querySelector('#textContainer') as TextAreaElement?);
});
tearDown(() {
editor.dispose();
});
test('getSearchCursor', () {
var cursor = SearchCursor.getSearchCursor(editor, 'Lorem');
print(cursor.pos);
expect(cursor, isNotNull);
expect(cursor, isA<SearchCursorContainer>());
});
test('findPrevious / findNext', () {
var cursor = SearchCursor.getSearchCursor(editor, 'ipsum');
var hasNext = cursor.findNext();
expect(hasNext, isTrue);
cursor.findNext();
var hasPrev = cursor.findPrevious();
expect(hasPrev, isTrue);
});
test('from / to', () {
var cursor = SearchCursor.getSearchCursor(editor, 'Pellentesque');
cursor.findNext();
var from = cursor.from();
var to = cursor.to();
expect(from, isNotNull);
expect(to, isNotNull);
});
test('atOccurrence', () {
var cursor = SearchCursor.getSearchCursor(editor, 'dolor');
cursor.findNext();
expect(cursor.atOccurrence, isTrue);
});
test('doc', () {
var cursor = SearchCursor.getSearchCursor(editor, 'sapien');
expect(cursor.doc, isA<Doc>());
});
test('pos', () {
var cursor = SearchCursor.getSearchCursor(editor, 'ipsum');
expect(cursor.pos, isA<Position>());
});
}

Final result#

The search cursor add-on lets users customize their Codemirror instance to add search-replace functionalities.


Flutter/Gallery - Refactor [web benchmarks] Move to appropriate folder#

Contribution Types

This contribution is about refactoring.

Context#

Flutter Gallery is a resource to help developers evaluate and use Flutter. It is a collection of Material Design & Cupertino widgets, behaviors, and vignettes implemented with Flutter.

Current behavior#

Moves benchmarks_test.dart and the benchmarks sub-folder from the test folder to test_benchmarks as it is more appropriate.

Implement the solution#

This contribution is mainly about moving files to another directory, nothing special here.

final taskResult = await serveWebBenchmark(
benchmarkAppDirectory: projectRootDirectory(),
entryPoint: 'test_benchmarks/benchmarks/client.dart',
useCanvasKit: false,
);

Final result#

Made the codebase more easily maintainable by moving the benchmark tests inside a more appropriate directory.


Flutter/Gallery - Back button overlapping#

Contribution Types

This contribution is a bug-fix.

Current behavior#

Fixes the overlapping "Back" button with the bottom navigation bar problem.

Implement the solution#

Add a new hasBottomNavBar property which define if we should add some bottom padding to the back button.

lib/routes.dart
(context, match) =>
const StudyWrapper(study: reply.ReplyApp(), hasBottomNavBar: true)),

Add a vertical bottom padding if the bottom nav bar is present.

lib/pages/home.dart
/// ...
padding: EdgeInsets.symmetric(
horizontal: 16.0,
vertical: widget.hasBottomNavBar
? kBottomNavigationBarHeight + 16.0
: 16.0),
/// ...

Final result#

Improved the User Experience as the back button is no longer overlapping with the bottom nav bar and is more easily accessible.

Gallery back button overlapping
Gallery back button overlapping

Flutter - CPU/GPU/memory metrics for iOS gallery transition tests#

Contribution Types

This contribution is a new feature.

Context#

DeviceLab is a physical lab that tests Flutter on real devices.

Current behavior#

This contribution adds missing CPU/GPU/memory metrics for iOS gallery transition tests.

Implement the solution#

Report the CPU/GPU/memory metrics if the measureCpuGpu and measureMemory properties are true and if the device OS is iOS.

dev/devicelab/lib/tasks/gallery.dart
final bool measureCpuGpu;
final bool measureMemory;
final bool isAndroid = deviceOperatingSystem == DeviceOperatingSystem.android;
if (measureCpuGpu && !isAndroid) ...<String>[
// See https://github.com/flutter/flutter/issues/68888
if (summary['average_cpu_usage'] != null) 'average_cpu_usage',
if (summary['average_gpu_usage'] != null) 'average_gpu_usage',
],
if (measureMemory && !isAndroid) ...<String>[
// See https://github.com/flutter/flutter/issues/68888
if (summary['average_memory_usage'] != null) 'average_memory_usage',
if (summary['90th_percentile_memory_usage'] != null) '90th_percentile_memory_usage',
if (summary['99th_percentile_memory_usage'] != null) '99th_percentile_memory_usage',
],

Final result#

Added some missing metrics for GalleryTransition tests to improve maintainers experience.


Takeaway#

Problems encountered#

The code exploration was the part that took me the most time as some projects are pretty big.
I was also busy with work, school and other things which is why I didn't make as many contributions as I would have liked.

What did I learn ?#

These were my first contributions in Dart that taught me a lot!
It was also a good experience contributing to the Flutter ecosystem💙