boxy 1.1.1

  • Readme
  • Changelog
  • Example
  • Installing
  • 74

Boxy - Advanced multi-child layouts in Flutter. #

This library provides several widgets and utilities that enable you to create advanced layouts without in-depth knowledge of the framework and minimal boilerplate.

Flex layouts #

A common pattern is when you need one or more widgets in a Row or Column to have the same cross axis size as another child in the list, you can achieve this layout using BoxyRow and Dominant, for example:

BoxyRow(
  mainAxisSize: MainAxisSize.min,
  children: [
    Child1(),
    Dominant(child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Child2(),
        Child3(),
      ],
    )),
  ],
]);

Complex custom layouts #

For more complex layouts this library provides CustomBoxy, a multi-child layout widget that allows you to inflate, constrain, lay out, and paint each child manually similar to a CustomMultiChildLayout.

This is useful if you need layouts that no other widget can provide, for example one where one child is positioned above the border of two others:

class MyLayout extends StatelessWidget {
  final Widget top;
  final Widget middle;
  final Widget bottom;
  
  // The margin between the middle widget and right edge
  final double inset;
  
  MyLayout({
    @required this.top,
    @required this.middle,
    @required this.bottom,
    @required this.inset,
  });

  @override
  Widget build(context) => CustomBoxy(
    delegate: MyDelegate(inset: inset),
    children: [
      // Use LayoutId to give each child an id
      LayoutId(id: #top, child: top),
      LayoutId(id: #bottom, child: bottom),
      // The middle widget should be rendered above the others
      // so we put it at the bottom of the list
      LayoutId(id: #middle, child: middle),
    ],
  );
}

class MyDelegate extends BoxyDelegate {
  final double inset;

  MyDelegate({@required this.inset});
  
  @override
  Size layout() {
    // Get each child handle by a Symbol id
    var top = getChild(#top);
    var middle = getChild(#middle);
    var bottom = getChild(#bottom);
    
    // Children should have unbounded height
    var topConstraints = constraints.widthConstraints();
    
    // Lay out and position top widget
    var topSize = title.layout(topConstraints);
    top.position(Offset.zero);
    
    // Lay out and position middle widget using size of top widget
    var middleSize = middle.layout(BoxConstraints());
    middle.position(Offset(
      topSize.width - (middle.width + inset),
      topSize.height - middle.height / 2,
    ));
    
    // Lay out bottom widget
    var bottomSize = info.layout(topConstraints.tighten(
      // Bottom widget should be same width as top widget
      width: topSize.width,
    ));
    
    // Position bottom widget directly below top widget
    bottom.position(Offset(0, topSize.height));
    
    // Calculate total size
    return Size(
      topSize.width,
      topSize.height + bottomSize.height,
    );
  }
  
  // Check if any properties have changed
  @override
  bool shouldRelayout(MyDelegate old) => old.inset != inset;
}

See the Product Tile example for an implementation of this layout, and the documentation of CustomBoxy for more information.

Sliver containers #

Ever want to give SliverList a box decoration? The sliver library provides SliverContainer which allows you to use a box widget as the foreground or background of a sliver:

This card effect can be achieved with SliverCard:

SliverCard(
  color: Colors.white,
  clipBehavior: Clip.antiAlias,
  sliver: SliverList(...),
)

The following example uses SliverContainer to give SliverList a rounded blue border:

SliverContainer(
  // How far the background will extend off-screen, prevents the border
  // from shrinking as the sliver is scrolled out of view
  bufferExtent: 12.0,
  
  // The background and foreground are layed out to cover the visible
  // space of the sliver
  background: DecoratedBox(
    border: Border.all(
      color: Colors.blue,
      width: 2,
    ),
    borderRadius: BorderRadius.circular(12),
  ),

  margin: EdgeInsets.all(8.0),
  padding: EdgeInsets.all(8.0),
  sliver: SliverList(...),
)

Utilities #

The utils library provides extensions with axis dependant methods and constructors for several data types. These extensions make writing direction agnostic math significantly easier.

Full list of methods:

BoxConstraintsAxisUtil.create
BoxConstraintsAxisUtil.expand
BoxConstraintsAxisUtil.tightFor
BoxConstraintsAxisUtil.tightForFinite
BoxConstraints.hasTightAxis
BoxConstraints.hasTightCrossAxis
BoxConstraints.hasBoundedAxis
BoxConstraints.hasBoundedCrossAxis
BoxConstraints.hasInfiniteAxis
BoxConstraints.hasInfiniteCrossAxis
BoxConstraints.maxAxis
BoxConstraints.minAxis
BoxConstraints.maxCrossAxis
BoxConstraints.minCrossAxis
BoxConstraints.tightenAxis
BoxConstraints.constrainAxisDimensions
BoxConstraints.constrainAxis
BoxConstraints.constrainCrossAxis
BoxConstraints.copyWithAxis
BoxConstraints.axisConstraints
BoxConstraints.crossAxisConstraints
Axis.cross
Axis.direction
Axis.crossDirection
VerticalDirection.reversed
VerticalDirection.direction
AxisDirection.axis
AxisDirection.crossAxis
AxisDirection.isReverse
AxisDirection.isForward
AxisDirection.reversed
AxisDirection.ccw
AxisDirection.cw
AxisDirection.operator+
AxisDirection.operator-
RenderBox.getMinIntrinsicAxis
RenderBox.getMinIntrinsicCrossAxis
RenderBox.getMaxIntrinsicAxis
RenderBox.getMaxIntrinsicCrossAxis
OffsetAxisUtil.create
OffsetAxisUtil.direction
Offset.axisOffset
Offset.crossAxisOffset
Offset.directionExtent
SizeAxisUtil.create
SizeAxisUtil.from
SizeAxisUtil.crossFrom
Size.axisSize
Size.crossAxisSize
EdgeInsetsAxisUtil.create
EdgeInsetsAxisUtil.symmetric
EdgeInsetsAxisUtil.direction
EdgeInsets.directionExtent
AxisSizedBox

[1.1.1] #

  • Bug fixes

[1.1.0] #

  • Added SliverContainer / SliverCard
  • Added more axis/direction utilities
  • Added OverflowPadding
  • Bug fixes

[1.0.1] #

  • Fixed Flutter SDK version constraints

[1.0.0] - Initial release #

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:boxy_gallery/pages/blog_tile.dart';
import 'package:boxy_gallery/pages/boxy_row.dart';
import 'package:boxy_gallery/pages/line_numbers.dart';
import 'package:boxy_gallery/pages/product_tile_simple.dart';
import 'package:boxy_gallery/pages/sliver_container.dart';
import 'package:boxy_gallery/pages/product_tile.dart';
import 'package:boxy_gallery/pages/tree_view.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:url_launcher/url_launcher.dart';

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

class NiceColors {
  static const background = Color(0xff21252b);
  static const primary = Color(0xff282c34);
  static const divider = Color(0xff46494f);
  static const text = Color(0xffcbd3e3);
}

class MyApp extends StatelessWidget {
  build(BuildContext context) => MaterialApp(
    title: 'Boxy gallery',
    theme: ThemeData.dark().copyWith(
      scaffoldBackgroundColor: NiceColors.background,
      primaryColor: NiceColors.primary,
    ),
    home: MyHomePage(),
    routes: {
      "tree-view": (_) => TreeViewPage(),
      "product-tile": (_) => ProductTilePage(),
      "product-tile-simple": (_) => ProductTileSimplePage(),
      "boxy-row": (_) => BoxyRowPage(),
      "line-numbers": (_) => LineNumberPage(),
      "blog-tile": (_) => BlogTilePage(),
      "sliver-container": (_) => SliverContainerPage(),
    },
  );
}

class DemoTile extends StatelessWidget {
  DemoTile({
    @required this.icon,
    @required this.name,
    @required this.route,
  });

  final String name;
  final IconData icon;
  final String route;

  build(context) => Material(child: InkWell(child: Row(children: [
    Container(
      child: Icon(
        icon,
        color: NiceColors.text,
      ),
      padding: EdgeInsets.only(
        left: 20,
        top: 8,
        bottom: 8,
        right: 16
      ),
    ),
    Text(
      name,
      style: TextStyle(
        color: NiceColors.text,
        fontSize: 16,
      ),
    ),
  ]), onTap: () {
    Navigator.pushNamed(context, route);
  }), color: NiceColors.background);
}

class Separator extends StatelessWidget {
  build(context) => Container(
    height: 1,
    color: NiceColors.divider,
  );
}

class GalleryAppBarButton extends StatelessWidget {
  final IconData icon;
  final VoidCallback onTap;
  final String tooltip;

  GalleryAppBarButton(this.icon, this.onTap, {this.tooltip});

  build(context) {
    Widget result = ConstrainedBox(child: Padding(child: Material(child: InkWell(
      child: Icon(
        icon,
        color: NiceColors.text,
        size: 16,
      ),
      onTap: onTap,
    ),
      color: NiceColors.primary,
      borderRadius: BorderRadius.circular(2),
    ), padding: EdgeInsets.only(
      top: 8,
      bottom: 8,
      left: 8,
    )), constraints: BoxConstraints(minWidth: 56));

    if (tooltip != null) {
      result = Tooltip(
        message: tooltip,
        child: result,
      );
    }

    return result;
  }
}

class GalleryAppBar extends StatelessWidget implements PreferredSizeWidget {
  final List<String> title;
  final String source;
  final List<Widget> actions;
  
  GalleryAppBar(this.title, {this.source, this.actions});
  
  build(context) => AppBar(
    leading: title.length == 1 ? null : GalleryAppBarButton(
      Icons.arrow_back_ios, () {
        Navigator.pop(context);
      }
    ),
    title: SizedBox(height: kToolbarHeight, child: OverflowBox(
      child: Row(children: [
        for (var i = 0; i < title.length; i++) ...[
          if (i != 0) Padding(
            child: Icon(Icons.arrow_right, color: NiceColors.text.withOpacity(0.5)),
            padding: EdgeInsets.all(8),
          ),
          Text(
            title[i],
            style: TextStyle(
              color: NiceColors.text,
            ),
          ),
        ]
      ]),
      alignment: Alignment.centerLeft,
      maxWidth: double.infinity,
    )),
    elevation: 0,
    actions: [
      if (actions != null) ...actions,
      if (source != null) GalleryAppBarButton(
        Icons.description, () {
          launch(source);
        }, tooltip: "Source code",
      ),
      Padding(padding: EdgeInsets.only(right: 8)),
    ],
  );

  get preferredSize => Size.fromHeight(kToolbarHeight);
}

class MyHomePage extends StatelessWidget {
  build(BuildContext context) => Scaffold(
    appBar: GalleryAppBar(["Boxy Gallery"]),
    body: Container(child: ListView(children: [
      Separator(),
      DemoTile(
        icon: MdiIcons.fileTree,
        name: "Tree View",
        route: "tree-view",
      ),
      DemoTile(
        icon: MdiIcons.collage,
        name: "BoxyRow",
        route: "boxy-row",
      ),
      DemoTile(
        icon: MdiIcons.dockBottom,
        name: "Product Tile",
        route: "product-tile",
      ),
      DemoTile(
        icon: MdiIcons.dockBottom,
        name: "Simple Product Tile",
        route: "product-tile-simple",
      ),
      DemoTile(
        icon: MdiIcons.formatListNumbered,
        name: "Line Numbers",
        route: "line-numbers",
      ),
      DemoTile(
        icon: MdiIcons.viewSplitVertical,
        name: "Blog Tile",
        route: "blog-tile",
      ),
      DemoTile(
        icon: MdiIcons.pageLayoutBody,
        name: "Sliver Container",
        route: "sliver-container",
      ),
      Separator(),
    ], physics: BouncingScrollPhysics()), color: NiceColors.primary),
  );
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  boxy: ^1.1.1

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:boxy/boxy.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
49
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
74
Learn more about scoring.

We analyzed this package on Jul 9, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.14
  • Flutter: 1.17.5

Analysis suggestions

Package not compatible with SDK dart

Because:

  • boxy that is a package requiring null.

Health suggestions

Fix lib/src/sliver_card.dart. (-0.50 points)

Analysis of lib/src/sliver_card.dart reported 1 hint:

line 1 col 8: Unused import: 'dart:math'.

Format lib/boxy.dart.

Run flutter format to format lib/boxy.dart.

Format lib/flex.dart.

Run flutter format to format lib/flex.dart.

Fix additional 8 files with analysis or formatting issues.

Additional issues in the following files:

  • lib/padding.dart (Run flutter format to format lib/padding.dart.)
  • lib/slivers.dart (Run flutter format to format lib/slivers.dart.)
  • lib/src/axis_utils.dart (Run flutter format to format lib/src/axis_utils.dart.)
  • lib/src/boxy_flex.dart (Run flutter format to format lib/src/boxy_flex.dart.)
  • lib/src/custom_boxy.dart (Run flutter format to format lib/src/custom_boxy.dart.)
  • lib/src/overflow_padding.dart (Run flutter format to format lib/src/overflow_padding.dart.)
  • lib/src/sliver_container.dart (Run flutter format to format lib/src/sliver_container.dart.)
  • lib/utils.dart (Run flutter format to format lib/utils.dart.)

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.12 1.14.13
meta 1.1.8 1.2.1
sky_engine 0.0.99
typed_data 1.1.6 1.2.0
vector_math 2.0.8 2.1.0-nullsafety
Dev dependencies
flutter_test