Skip to content
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

[animations] Introduce layoutBuilder parameter to PageTransitionSwitcher #170

Merged
merged 17 commits into from
Sep 3, 2020

Conversation

federunco
Copy link
Contributor

@federunco federunco commented Jun 15, 2020

Fixes flutter/flutter#54585 by introducing layoutBuilder, which allows PageTransitionSwitcher to lay out its children in a custom manner.

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here with @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@federunco
Copy link
Contributor Author

@googlebot I signed it!

@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

@shihaohong shihaohong changed the title Make Stack alignment a property [animations] Allow Stack alignment to be adjusted in PageTransitionSwitcher Jun 16, 2020
@shihaohong
Copy link

@goderbauer This would solve the problem in flutter/flutter#54585 if Alignment.topLeft was passed into Stack.

I'm wondering if this should just be Alignment.topLeft by default, but also warn developers about having the passed child widgets be the same size or provide guidance on how to do so through documentation.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

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

This PR is also missing tests. Can you please add some?

Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
);
Widget child,
Copy link
Member

Choose a reason for hiding this comment

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

revert these formatting changes.

@@ -149,6 +148,7 @@ class PageTransitionSwitcher extends StatefulWidget {
this.duration = const Duration(milliseconds: 300),
this.reverse = false,
@required this.transitionBuilder,
this.alignment = Alignment.center,
Copy link
Member

Choose a reason for hiding this comment

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

assert that alignment is not null and mention that in the doc above.

@@ -203,6 +203,14 @@ class PageTransitionSwitcher extends StatefulWidget {
/// The child provided to the transitionBuilder may be null.
final PageTransitionSwitcherTransitionBuilder transitionBuilder;

/// The alignment of [child] when its animated
Copy link
Member

Choose a reason for hiding this comment

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

nit: its -> it's

/// The given alignment is applied during the animation of the child, when
/// the animation is finished, the original alignment is applied to the child.
///
/// Defaults to Alignment.center
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// Defaults to Alignment.center
/// Defaults to [Alignment.center].

/// The alignment of [child] when its animated
///
/// The given alignment is applied during the animation of the child, when
/// the animation is finished, the original alignment is applied to the child.
Copy link
Member

Choose a reason for hiding this comment

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

It's not clear what "the original alignment" is.

@@ -310,8 +318,8 @@ class _PageTransitionSwitcherState extends State<PageTransitionSwitcher>
secondaryController,
);
assert(
transition != null,
'PageTransitionSwitcher.builder must not return null.',
transition != null,
Copy link
Member

Choose a reason for hiding this comment

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

revert formatting change

@@ -352,8 +360,8 @@ class _PageTransitionSwitcherState extends State<PageTransitionSwitcher>
entry.secondaryController,
);
assert(
transition != null,
'PageTransitionSwitcher.builder must not return null.',
transition != null,
Copy link
Member

Choose a reason for hiding this comment

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

revert formatting change

@goderbauer
Copy link
Member

I'm wondering if this should just be Alignment.topLeft by default, but also warn developers about having the passed child widgets be the same size or provide guidance on how to do so through documentation.

AlignmentDirectional.topStart may have been an alternative choice for the default.

This widget was heavily inspired by the AnimatedSwitcher (https://master-api.flutter.dev/flutter/widgets/AnimatedSwitcher-class.html), which also uses center as the default. That widget solves this problem by allowing the user to completely configure the layout via a provided layoutBuilder (https://master-api.flutter.dev/flutter/widgets/AnimatedSwitcher/layoutBuilder.html). We should probably do the same for this widget as well to give users maximum flexibility in case they want to change other aspects of the Stack.

@federunco
Copy link
Contributor Author

Should I implement a layoutBuilder?

@shihaohong
Copy link

@federunco that seems like a more flexible way to approach this problem, since it allows devs to have a more fine-tuned control of the layout of the outgoing/incoming widgets.

@goderbauer
Copy link
Member

fly-by comment: looks like the analyzer is unhappy. It may require the submission of #173 to fix that.

Copy link

@shihaohong shihaohong left a comment

Choose a reason for hiding this comment

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

Some comments about the documentation. It also needs a test or two to ensure that the default layout is preserved, as well as another test to ensure that a modified layoutBuilder is properly used.

@@ -142,17 +153,19 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function(
class PageTransitionSwitcher extends StatefulWidget {
/// Creates a [PageTransitionSwitcher].
///
/// The [duration], [reverse], and [transitionBuilder] parameters
/// The [duration], [reverse], [transitionBuilder] and [alignment] parameters
Copy link

@shihaohong shihaohong Jun 18, 2020

Choose a reason for hiding this comment

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

Since alignment had been removed:

Suggested change
/// The [duration], [reverse], [transitionBuilder] and [alignment] parameters
/// The [duration], [reverse], and [transitionBuilder] parameters

/// the [child] that's transitioning in, with a widget that lays all of them
/// out. This is called every time this widget is built. The function must not
/// return null.
///

Choose a reason for hiding this comment

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

We should add some sample code here for one or two use cases for modifying the layoutBuilder here. Otherwise, developers may not know how to go to this parameter to resolve their problem.

Also, since this property's purpose might be less intuitive from just reading its name, it might be helpful to put a pointer to here from the constructor or class API docs as well. Something like: "If the layout of the transitioning child widgets are not what you are looking for, see layoutBuilder for suggestions on how to configure the layout of the incoming and outgoing child widgets."

@shihaohong shihaohong changed the title [animations] Allow Stack alignment to be adjusted in PageTransitionSwitcher [animations] Introduce layoutBuilder parameter to PageTransitionSwitcher Jun 18, 2020
@shihaohong
Copy link

Hey @federunco, any progress on this PR?

@federunco
Copy link
Contributor Author

I should update the PR next wednesday, i'm quite busy at the moment

@shihaohong
Copy link

Sounds good, thanks for the update.

@shihaohong
Copy link

Seems like the formatter is still unhappy with one of the dart files.

@shihaohong shihaohong self-assigned this Jul 8, 2020
Copy link

@shihaohong shihaohong left a comment

Choose a reason for hiding this comment

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

The implementation itself looks good, there are two main types of comments:

  1. Some clarifications for the API just so that our users aren't confused by how to use the new layoutBuilder parameter and the terminology surrounding it.
  2. Seeing if there's some way to keep the instantiating ChildEntry private to the library, since we do not want our users getting the wrong idea on how to use it and trying to instantiate ChildEntry in their applications.

Comment on lines 9 to 11
/// Internal representation of a child that, now or in the past, was set on the
/// PageTransitionSwitcher.child field, but is now in the process of
/// transitioning.

Choose a reason for hiding this comment

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

Since ChildEntry should not really be used as a "public API", but is necessary because it needs to be exposed as an argument for layoutBuilder, I think at the very least we should document how this is used for and that users of the animations package shouldn't be using this class unless they were using it together with PageTransitionSwitcher.layoutBuilder. This is my attempt to clarify this a little bit:

Suggested change
/// Internal representation of a child that, now or in the past, was set on the
/// PageTransitionSwitcher.child field, but is now in the process of
/// transitioning.
/// An internal representation of a child widget subtree that, now or in the past,
/// was set on the PageTransitionSwitcher.child field and is now in the process of
/// transitioning.
///
/// This class should not be used outside of the context of
/// [PageTransitionSwitcher.layoutBuilder], which uses this class in a callback to
/// customize the layout of the transitioning widgets.
///
/// For more information, please see the documentation of
/// [PageTransitionSwitcher.layoutBuilder].

///
/// The [primaryController], [secondaryController], [transition] and
/// [widgetChild] parameters must not be null.
ChildEntry({

Choose a reason for hiding this comment

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

cc/ @goderbauer I've never tried this before, but would it be possible to make this constructor private while keeping the class itself public? That way we don't get users that attempt to create a ChildEntry outside of the context of the PageTransitionSwitcher widget, but they can still use it in the callback for layoutBuilder:

Suggested change
ChildEntry({
ChildEntry._({

Copy link
Member

Choose a reason for hiding this comment

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

Yes, you can do that :)

Widget widgetChild;

/// Animation disposal method

Choose a reason for hiding this comment

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

Use verbiage similar to other api documentation for dispose

Suggested change
/// Animation disposal method
/// Release the resources used by this object.
///
/// The object is no longer usable after this method is called.

packages/animations/lib/src/page_transition_switcher.dart Outdated Show resolved Hide resolved
Comment on lines 160 to 164
/// PageTransitionSwitcher uses the property [layoutBuilder] to lay out all
/// active entries.
/// If the layout of the transitioning child widgets are not what you are looking
/// for, see [PageTransitionSwitcher.layoutBuilder] for suggestions on how to
/// configure the layout of the incoming and outgoing child widgets

Choose a reason for hiding this comment

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

Here are some grammatical suggestions. I also removed the usage of "active entries" since users will be more familiar with referring to the new and old child widgets based on prior documentation.

Suggested change
/// PageTransitionSwitcher uses the property [layoutBuilder] to lay out all
/// active entries.
/// If the layout of the transitioning child widgets are not what you are looking
/// for, see [PageTransitionSwitcher.layoutBuilder] for suggestions on how to
/// configure the layout of the incoming and outgoing child widgets
/// PageTransitionSwitcher uses the [layoutBuilder] property to lay out the
/// old and new child widgets. By default, [defaultLayoutBuilder] is used.
/// See the documentation for [layoutBuilder] for suggestions on how to
/// configure the layout of the incoming and outgoing child widgets if
/// [defaultLayoutBuilder] is not your desired layout.

/// how a layout builder should function.
///
/// The following example shows a [layoutBuilder] that places the new child inside
/// a [Column], that sizes its height depending on the heights of active entries.

Choose a reason for hiding this comment

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

nit:

Suggested change
/// a [Column], that sizes its height depending on the heights of active entries.
/// a [Column] that sizes its height depending on the heights of active entries.

///
/// The builder should return a widget which contains the given children, laid
/// out as desired. It must not return null. The builder should be able to
/// handle an empty list of `_activeEntries`.

Choose a reason for hiding this comment

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

We should define "active entries" here, since users might not get what these refer to. The only reference they have without peeking into the implementation is the concept of an "old" and "new" child widget, so this concept will need to be clarified.

/// largest of the child or a previous child. The children are centered on
/// each other.
///
/// This is an [PageTransitionSwitcherTransitionBuilder] function.

Choose a reason for hiding this comment

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

Suggested change
/// This is an [PageTransitionSwitcherTransitionBuilder] function.
/// See [PageTransitionSwitcherTransitionBuilder] for more information on the function signature.

///
final PageTransitionSwitcherLayoutBuilder layoutBuilder;

/// The layout builder used as the default value of [layoutBuilder].

Choose a reason for hiding this comment

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

Suggested change
/// The layout builder used as the default value of [layoutBuilder].
/// The default layout builder for [PageTransitionSwitcher].

Comment on lines 260 to 262
/// The new child is placed in a [Stack] that sizes itself to match the
/// largest of the child or a previous child. The children are centered on
/// each other.

Choose a reason for hiding this comment

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

Also, doesn't the [Stack] size itself to match the largest child, not the previous child?

Suggested change
/// The new child is placed in a [Stack] that sizes itself to match the
/// largest of the child or a previous child. The children are centered on
/// each other.
/// This function is the default way for how the new and old child widgets are placed
/// during the transition between the two widgets. The new child is placed in a
/// [Stack] that sizes itself to match the largest of the child or a previous child.
/// The children are centered on each other.

@federunco
Copy link
Contributor Author

I've pushed all the suggested changes, how about making ChildEntry constructor private, any news?

@shihaohong
Copy link

@federunco I think we should make the constructor private. That way, nobody can accidentally abuse the ChildEntry class.

Copy link

@shihaohong shihaohong left a comment

Choose a reason for hiding this comment

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

LGTM modulo a doc comment nit.

@goderbauer could you take a look to make sure this looks good?

// to expose to the public API (like the controllers).
class _ChildEntry {
_ChildEntry({
// An internal representation of a child widget subtree that, now or in the past,

Choose a reason for hiding this comment

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

Suggested change
// An internal representation of a child widget subtree that, now or in the past,
/// An internal representation of a child widget subtree that, now or in the past,

@goderbauer
Copy link
Member

Added to my review queue. Sorry for the delay.

class _ChildEntry {
_ChildEntry({
// An internal representation of a child widget subtree that, now or in the past,
/// was set on the PageTransitionSwitcher.child field and is now in the process of
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// was set on the PageTransitionSwitcher.child field and is now in the process of
/// was set on the [PageTransitionSwitcher.child] field and is now in the process of

///
/// The builder should return a widget which contains the given children, laid
/// out as desired. It must not return null. The builder should be able to
/// handle an empty list of `_activeEntries`.
Copy link
Member

Choose a reason for hiding this comment

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

A comment on a public member should nor reference a private member.

/// out as desired. It must not return null. The builder should be able to
/// handle an empty list of `_activeEntries`.
typedef PageTransitionSwitcherLayoutBuilder = Widget Function(
List<ChildEntry> _activeEntries,
Copy link
Member

Choose a reason for hiding this comment

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

_activeEntries -> activeEntries

Or why not just entries?

/// out. This is called every time this widget is built. The function must not
/// return null.
///
/// The default is [PageTransitionSwitcherLayoutBuilder] used is
Copy link
Member

Choose a reason for hiding this comment

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

remove the first "is".

Comment on lines 247 to 250
/// See also:
///
/// * [PageTransitionSwitcherLayoutBuilder] for more information about
/// how a layout builder should function.
Copy link
Member

Choose a reason for hiding this comment

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

The see also section should always come last.

packages/animations/lib/src/page_transition_switcher.dart Outdated Show resolved Hide resolved
Comment on lines 252 to 247
/// The following example shows a [layoutBuilder] that places the new child inside
/// a [Column] that sizes its height depending on the heights of active entries.
Copy link
Member

Choose a reason for hiding this comment

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

This seems a little confusing. The layoutBuilder puts all children in a column, not just the new one.

/// ```dart
/// layoutBuilder: (
/// List<ChildEntry> activeEntries,
/// ) => Column(
Copy link
Member

Choose a reason for hiding this comment

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

What effect is this example trying to achieve? It seems odd to just place them all in a column?

Comment on lines 270 to 277
/// during the transition between the two widgets. The new child is placed in a
/// [Stack] that sizes itself to match the largest of the child or a previous child.
/// The children are centered on each other.
Copy link
Member

Choose a reason for hiding this comment

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

not just the new child, all children are placed in a stack.

///
/// The [primaryController], [secondaryController], [transition] and
/// [widgetChild] parameters must not be null.
ChildEntry({
Copy link
Member

Choose a reason for hiding this comment

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

Yes, you can do that :)

/// out as desired. It must not return null. The builder should be able to
/// handle an empty list of `_activeEntries`.
typedef PageTransitionSwitcherLayoutBuilder = Widget Function(
List<ChildEntry> _activeEntries,
Copy link
Member

Choose a reason for hiding this comment

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

Why do we hand the full ChildEntry to the layoutBuilder? It seems that handing them just the transition Widgets from the ChildEntries would be enough. The layout builder shouldn't have access to the animation controllers in ChildEntry. Those shouldn't be messed with.

Similarly to AnimatedSwitcher's layoutBuilder it probably also makes sense to hand over the currentEntry and all other entries in separate arguments.

Copy link
Member

Choose a reason for hiding this comment

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

In other words, _ChildEntry should stay private.

/// out as desired. It must not return null. The builder should be able to
/// handle an empty list of `_activeEntries`.
typedef PageTransitionSwitcherLayoutBuilder = Widget Function(
List<ChildEntry> _activeEntries,
Copy link
Member

Choose a reason for hiding this comment

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

In other words, _ChildEntry should stay private.

@federunco
Copy link
Contributor Author

I made all the suggested changes, I used most of the AnimatedSwitcher code (just deleted some of the assertions that didn't make sense when the switcher was set in reverse and tweaked the code a bit) but I didn't test it on a real device, all the unit test passed btw.

@@ -6,11 +6,14 @@ import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

// Internal representation of a child that, now or in the past, was set on the
// PageTransitionSwitcher.child field, but is now in the process of
// transitioning. The internal representation includes fields that we don't want
Copy link
Member

Choose a reason for hiding this comment

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

Leave the comment about how we don't want to expose some of these fields?

class PageTransitionSwitcher extends StatefulWidget {
/// Creates a [PageTransitionSwitcher].
///
/// The [duration], [reverse], and [transitionBuilder] parameters
/// The [duration], [reverse] and [transitionBuilder] parameters
Copy link
Member

Choose a reason for hiding this comment

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

nit: leave this comma as is, https://en.wikipedia.org/wiki/Serial_comma

/// See [PageTransitionSwitcherTransitionBuilder] for more information on the function
/// signature.
static Widget defaultLayoutBuilder(
Widget currentChild, List<Widget> previousChildren, bool reverse) {
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
Widget currentChild, List<Widget> previousChildren, bool reverse) {
Widget currentChild, List<Widget> previousChildren, bool reverse,) {

... and then format with flutter format

Comment on lines 288 to 293
/*
return Column(
children: activeEntries
.map<Widget>((ChildEntry entry) => entry.transition)
.toList(),
);*/
Copy link
Member

Choose a reason for hiding this comment

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

Why is this commented out?

@@ -226,7 +314,9 @@ class _PageTransitionSwitcherState extends State<PageTransitionSwitcher>
// If the transition builder changed, then update all of the old
// transitions.
if (widget.transitionBuilder != oldWidget.transitionBuilder) {
_activeEntries.forEach(_updateTransitionForEntry);
_outgoingEntries.forEach(_updateTransitionForEntry);
if (_currentEntry != null) _updateTransitionForEntry(_currentEntry);
Copy link
Member

Choose a reason for hiding this comment

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

use { } around the body of the if and put it on a separate line.

if (widget.child == null) {
return;
}
if (widget.child == null) return;
Copy link
Member

Choose a reason for hiding this comment

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

leave this as-as

@@ -363,19 +438,25 @@ class _PageTransitionSwitcherState extends State<PageTransitionSwitcher>

@override
void dispose() {
for (_ChildEntry entry in _activeEntries) {
if (_currentEntry != null) _currentEntry.dispose();
Copy link
Member

Choose a reason for hiding this comment

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

use { } around body and put body on new line

entry.dispose();
}
super.dispose();
}

void _markChildWidgetCacheAsDirty() => _outgoingWidgets = null;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
void _markChildWidgetCacheAsDirty() => _outgoingWidgets = null;
void _markChildWidgetCacheAsDirty() {
_outgoingWidgets = null;
}

if (widget.reverse) {
_currentEntry.primaryController.reverse();
} else {
_currentEntry.secondaryController.forward();
}
_outgoingEntries.add(_currentEntry);
Copy link
Member

Choose a reason for hiding this comment

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

This will not work: It will cause the order of widgets to change unexpectedly if _currentEntry was previously shown below all _outgoingEntries instead of on top of it.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe the layout builder for this widget should just get a List of widgets without knowledge of the current entry, to avoid this problem?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the old activeEntries list, right?

@goderbauer
Copy link
Member

Also: the analyzer check is unhappy. You may have to rebase this PR to the latest master.

@shihaohong
Copy link

It seems to still be failing the analyzer check. Make sure to run flutter analyze on the animations package repo and correct any warnings/errors to make sure that everything looks good.

Copy link

@shihaohong shihaohong left a comment

Choose a reason for hiding this comment

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

Still LGTM, with a nit on the sample code

packages/animations/lib/src/page_transition_switcher.dart Outdated Show resolved Hide resolved
Co-authored-by: Shi-Hao Hong <shihaohong@google.com>
Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

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

Generally, this looks good now after the two comments are resolved.

Comment on lines 13 to 18
/// This class should not be used outside of the context of
/// [PageTransitionSwitcher.layoutBuilder], which uses this class in a callback to
/// customize the layout of the transitioning widgets.
///
/// For more information, please see the documentation of
/// [PageTransitionSwitcher.layoutBuilder].
Copy link
Member

Choose a reason for hiding this comment

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

This comment seems outdated. It's not used as part of the layoutBuilder anymore?

/// [defaultLayoutBuilder].
///
/// The following example shows a [layoutBuilder] that places all entries inside
/// a [Column] that sizes its height depending on the heights of active entries.
Copy link
Member

Choose a reason for hiding this comment

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

Ideally, examples should show good practices. I can't think of any good use cases where putting the children in a column is a good idea. Maybe show an example where a Stack with a different alignment is used (e.g. topleft.)

@shihaohong
Copy link

The latest changes look like they address @goderbauer 's concerns and he's on vacation, so I'll merge the PR for now. If he has any issues with it, we can always just revert it or fix forward. Thank you so much for working through this PR!

@shihaohong shihaohong merged commit bbc94e1 into flutter:master Sep 3, 2020
stuartmorgan pushed a commit to stuartmorgan/packages that referenced this pull request Apr 30, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants