Recently, the project integrated many functional modules of Flutter, and there was no article produced for a long time. Therefore, I plan to write this article to summarize and record some problems in Flutter development. Demo address: github.com/weibindev/f…

Ps: The data in demo is read from JSON file under assets\data\ folder, so it does not involve network request encapsulation, project architecture and other related knowledge. This demo focuses on the implementation of point-to-point structure.

The overall effect is as follows:

Overall structure analysis

There is nothing to say about the entrance of the store on the home page, which is mainly the entrance of our order function and the display of the number of goods in the shopping cart.

Here we mainly analyze the structure of the point single interface.

According to the above picture, the analysis is as follows:

  • 1: the top search box, equivalent toAndroidIn thestatusBar+toolbar
  • 2: on the left side of the first class commodity classification column, some columns will have the situation of the second class classification
  • 3: The column of secondary commodity classification is to further classify a large category of commodities
  • 4: List of commodities classified as level 1 or level 2. Click a single commodity entry to enter the commodity details page
  • 5: Bottom shopping cart, it is located at the top of the entire ordering interface, all functions of this interface will not block the shopping cart (withoverlaysProperty.)

1,2,3,4 can be viewed as a whole, and 5 can be viewed as a whole.

Bottom shopping cart implementation

As for the bottom shopping cart, MY initial implementation idea was to use Overlay, which is described as follows in the source code

/// A [Stack] of entries that can be managed independently.
///
/// Overlays let independent child widgets "float" visual elements on top of
/// other widgets by inserting them into the overlay's [Stack]. The overlay lets
/// each of these widgets manage their participation in the overlay using
/// [OverlayEntry] objects.
///
/// Although you can create an [Overlay] directly, it's most common to use the
/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
/// navigator uses its overlay to manage the visual appearance of its routes.
///
/// See also:
///
/// * [OverlayEntry].
/// * [OverlayState].
/// * [WidgetsApp].
/// * [MaterialApp].
class Overlay extends StatefulWidget {
Copy the code

Overlay is a Stack component. You can insert an OverlayEntry into the Overlay so that its separate child window is suspended on top of other components. Using this feature, you can Overlay the bottom shopping cart component on top of other components.

However, there are many problems in the actual use process. We need to accurately control the display and hiding of the floating control for Overlay package, otherwise people will quit the interface and our shopping cart will still be displayed at the bottom. I think it’s better for things like Popupindow and global custom Dialog.

Is there a component in Flutter that can easily manage a bunch of subcomponents?

When writing the Flutter application, the entry to our application is performed through the runApp(MyApp()) of the main() function. MyApp usually builds a MaterialApp component

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'I need something', home: HomePage(), ); }}Copy the code

The routing between different interfaces will be managed by Navigator, such as navigator.push and navigator.pop. Why is the MaterialApp sensitive to the actions of the Navigator?

The MaterialApp constructor has a field called navigatorKey

class MaterialApp extends StatefulWidget {

  final GlobalKey<NavigatorState> navigatorKey;
    
  /// omit some code
}

class _MaterialAppState extends State<MaterialApp> {
    
  @override
  Widget build(BuildContext context) {
    Widget result = WidgetsApp(
      key: GlobalObjectKey(this),
      navigatorKey: widget.navigatorKey,
      navigatorObservers: _navigatorObservers,
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
        return MaterialPageRoute<T>(settings: settings, builder: builder);
      },
  /// omit some code}}Copy the code

Looking deeper, it is passed to the navigatorKey in the WidgetsApp constructor, which creates a global NavigatorState by default when the component is initialized. Then manage the state of the Navigator created in build(BuildContext Context).

class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
    @override
    void initState() {
        super.initState();
        _updateNavigator();
        _locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales);
        WidgetsBinding.instance.addObserver(this);
    }
  
    // NAVIGATOR
    GlobalKey<NavigatorState> _navigator;

    void _updateNavigator() {// Not specifying navigatorKey in the MaterialApp initializes a global NavigatorState _navigator = widget. NavigatorKey?? GlobalObjectKey<NavigatorState>(this); } override Widget build(BuildContext context) {// Build a Navigator component and put the navigatorKey in it. This allows the Navigator stack to manipulate Widget Navigator;if(_navigator ! = null) { navigator = Navigator( key: _navigator, // If window.defaultRouteName isn't '/', we should assume it was set // intentionally via `setInitialRoute`, and should override whatever // is in [widget.initialRoute]. initialRoute: WidgetsBinding.instance.window.defaultRouteName ! = Navigator.defaultRouteName ? WidgetsBinding.instance.window.defaultRouteName : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName, onGenerateRoute: _onGenerateRoute, onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null ? Navigator.defaultGenerateInitialRoutes : (NavigatorState navigator, String initialRouteName) { return widget.onGenerateInitialRoutes(initialRouteName); }, onUnknownRoute: _onUnknownRoute, observers: widget.navigatorObservers, ); }}}Copy the code

At this point you can basically figure out how to implement the bottom shopping cart function.

Yes, we can customize a Navigator on the ordering interface to manage the jump of routes searching for goods, details of goods, shopping cart list of goods, etc., and other things are controlled by Navigator of our MaterialApp.

The following is the approximate implementation of the functional code:

class OrderPage extends StatefulWidget {
  @override
  _OrderPageState createState() => _OrderPageState();
}

class _OrderPageState extends State<OrderPage> {

  /// Manage the keys of the single-function Navigator
  GlobalKey<NavigatorState> navigatorKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
        onWillPop: () {
          // The listener returns the key, performs stack processing on the route in the custom Navigator, and closes the OrderPage
          navigatorKey.currentState.maybePop().then((value) {
            if (!value) {
              NavigatorUtils.goBack(context);
            }
          });
          return Future.value(false);
        },
        child: Stack(
          children: <Widget>[
            Navigator(
              key: navigatorKey,
              onGenerateRoute: (settings) {
                if (settings.name == '/') {
                  return PageRouteBuilder(
                    opaque: false,
                    pageBuilder:
                        (childContext, animation, secondaryAnimation) =>
                            // Build the content layer
                            _buildContent(childContext),
                    transitionsBuilder:
                        (context, animation, secondaryAnimation, child) =>
                            FadeTransition(opacity: animation, child: child),
                    transitionDuration: Duration(milliseconds: 300)); }return null;
              },
            ),
            Positioned(
              bottom: 0,
              right: 0,
              left: 0.// Shopping cart component, located at the bottom
              child: ShopCart(),
            ),
            // Add a small ball animation to the shopping cartThrowBallAnim(), ], ), ); }}Copy the code

Use of page transition animation Hero

The effect can be seen in the original GIF.

Using Hero is very simple, you need to associate the two components with the Hero component wrapped, and specify the same tag parameters, code as follows:

/ / / the list item
InkWell(
      child: ClipRRect(
        borderRadius: BorderRadius.circular(4),
        child: Hero(
          tag: widget.data,
          child: LoadImage(
            '${widget.data.img}',
            width: 81.0,
            height: 81.0, fit: BoxFit.fitHeight, ), ), ), onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => GoodsDetailsPage(data: widget.data))); });Copy the code

/ / / for details
 Hero(
    tag: tag,
    child: LoadImage(
        imageUrl,
        width: double.infinity,
        height: 300,
        fit: BoxFit.cover,
        ),
    )

Copy the code

Do you feel like you’re done and the Hero effect will come out? Under normal circumstances, there will be an effect, but here we have no effect, with the ordinary route jump a everything, this is why?

We have an effect in the MaterialApp, but the custom Navigator has no effect, so it must be the Navigator of the MaterialApp that does some configuration.

Once again, as you can see from the Source of the MaterialApp, when it is initialized, new a HeroController is added in the constructor parameter, navigatorObservers

class _MaterialAppState extends State<MaterialApp> {
  HeroController _heroController;

  @override
  void initState() {
    super.initState();
    _heroController = HeroController(createRectTween: _createRectTween);
    _updateNavigator();
  }

  @override
  void didUpdateWidget(MaterialApp oldWidget) {
    super.didUpdateWidget(oldWidget);
    if(widget.navigatorKey ! = oldWidget.navigatorKey) {// If the Navigator changes, we have to create a new observer, because the
      // old Navigator won't be disposed (and thus won't unregister with its
      // observers) until after the new one has been created (because the
      // Navigator has a GlobalKey).
      _heroController = HeroController(createRectTween: _createRectTween);
    }
    _updateNavigator();
  }

  List<NavigatorObserver> _navigatorObservers;

  void _updateNavigator() {
    if(widget.home ! =null|| widget.routes.isNotEmpty || widget.onGenerateRoute ! =null|| widget.onUnknownRoute ! =null) {
      _navigatorObservers = List<NavigatorObserver>.from(widget.navigatorObservers) .. add(_heroController); }else {
      _navigatorObservers = const<NavigatorObserver>[]; }}/ / /...
}
Copy the code

This is finally added to Observers, the Navigator construct parameter of the WidgetsApp build

navigator = Navigator(
        key: _navigator,
        // If window.defaultRouteName isn't '/', we should assume it was set
        // intentionally via `setInitialRoute`, and should override whatever
        // is in [widget.initialRoute].
        initialRoute: WidgetsBinding.instance.window.defaultRouteName ! = Navigator.defaultRouteName ? WidgetsBinding.instance.window.defaultRouteName
            : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
        onGenerateRoute: _onGenerateRoute,
        onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
          ? Navigator.defaultGenerateInitialRoutes
          : (NavigatorState navigator, String initialRouteName) {
            return widget.onGenerateInitialRoutes(initialRouteName);
          },
        onUnknownRoute: _onUnknownRoute,
        // The HeroController of the MaterialApp will be added
        observers: widget.navigatorObservers,
      );
Copy the code

So we just need to do the same thing in the Navigator we define:

Observers (children: <Widget>[Navigator(key: navigatorKey, // [HeroController()], onGenerateRoute: (settings) {if (settings.name == '/') {
                  return PageRouteBuilder(
                    opaque: false,
                    pageBuilder:
                        (childContext, animation, secondaryAnimation) =>
                            _buildContent(childContext),
                    transitionsBuilder:
                        (context, animation, secondaryAnimation, child) =>
                            FadeTransition(opacity: animation, child: child),
                    transitionDuration: Duration(milliseconds: 300),
                  );
                }
                returnnull; },), toy (bottom: 0, right: 0, left: 0, child: ShopCart(),), // add goods into the shopping cart animation ThrowBallAnim(),],Copy the code

Implementation of Gaussian blur

The gray area of the bottom shopping cart has a Gaussian blur effect

The control in a Flutter is BackdropFilter, which can be used as follows:

BackdropFilter(
    filter: ImageFilter.blur(sigmaX, sigmaY),
    child: ...)
Copy the code

If you don’t edit it, the Gaussian blur will spread out to the full screen. It should look like this:

ClipRect(
    BackdropFilter(
        filter: ImageFilter.blur(sigmaX, sigmaY),
        child: ...)
)
Copy the code

Ps: In fact, BackdropFilter source code has a more detailed description, I suggest you go to see

The realization of commodity column classification

Commodity column classification of the general point is one, two level menu page switch processing of PageView.

You can view the part of the box on the right of the figure as a PageView, and click the TAB on the left is a vertical page switch operation on PageView. If there is no secondary TAB under the corresponding TAB, then the current page is a ListView.

If there are two tabs, the current page is TabBar+PageView linkage, the direction of the PageView is horizontal

If the above description is not very clear, it doesn’t matter, I have prepared a general structure diagram, which clearly describes the relationship between them:

One more thing to note is that we don’t want the Widgets to reload every time we switch tabs, which would be very bad for the user’s experience, and we want to keep the page status as a page for pages that have already been loaded. This use AutomaticKeepAliveClientMixin can do it.

class SortRightPage extends StatefulWidget {
  final int parentId;
  final List<Sort> data;

  SortRightPage(
      {Key key,
      this.parentId,
      this.data})
      : super(key: key);

  @override
  _SortRightPageState createState() => _SortRightPageState();
}

class _SortRightPageState extends State<SortRightPage>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    if (widget.data == null || widget.data.isEmpty) {
      if(Widget.parentid == -1) {// Package Pagereturn DiscountPage();
      } else{// List of itemsreturn SubItemPage(
          key: Key('subItem${widget.parentId}'), id: widget.parentId ); }}else{// Secondary classificationreturn SubListPage(
        key: Key('subList${widget.parentId}'),
        data: widget.data
      );
    }
  }

  @override
  bool get wantKeepAlive => true;
}

Copy the code

The end of the

Well, the article is almost there. For more details, you can go to Github to see the demo I wrote. The user interaction processing is quite appropriate in the demo.