flutter_platform_widgets
flutter_platform_widgets copied to clipboard
Feature request: PlatformListView (ListView / CupertinoListSection)
Thank you for the useful library.
Currently, PlatformListTile exists, but PlatformListView does not seem to exist. We believe that using PlatformListView will allow for smoother UI implementation without worrying about platform differences. I am currently customizing the code below and implementing it in my app. I plan to create a pull request based on this code.
It's probably difficult to make Cupertino and ListView completely compatible (with/without Header, etc.), so we're considering the following options.
- PlatformListView is an adapter for ListView/CupertinoListSection, so pass it as is.
- On top of that, I plan to write a ListView pattern with Header in the Readme.md and wiki samples.
- Other parts will be implemented based on existing widgets.
If there is a better way, I would appreciate it if you could let me know.
import 'package:flutter/cupertino.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
class PlatformListView extends Stateless {
final List<Widget> children;
final Widget header;
final Color? backgroundColor;
final bool hasLeading;
final EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry? padding;
const PlatformListView({
super.key,
this.children = const [],
this.header = const SizedBox.shrink(),
this.backgroundColor,
this.hasLeading = true,
this.margin,
this.padding,
});
@override
Widget build(BuildContext context) {
final platformTarget = platform(context);
return switch (platformTarget) {
PlatformTarget.iOS => _buildIos(context),
PlatformTarget.android => _buildAndroid(context),
(_) => _buildAndroid(context),
};
}
Widget _buildIos(BuildContext context) {
return CupertinoListSection.insetGrouped(
margin: margin,
hasLeading: hasLeading,
dividerMargin: 14.0,
additionalDividerMargin: hasLeading ? null : 0.0,
backgroundColor:
backgroundColor ?? CupertinoColors.systemGroupedBackground,
header: header,
children: [
...children,
],
);
}
Widget _buildAndroid(BuildContext context) {
return ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: margin,
children: [
Container(
color: backgroundColor,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: header,
),
...children,
],
);
}
}
First look of your implementation:
- These are just three Text widgets without any arguments. Yes you say this is wrong, because I should use
PlatformListTilein it, but I need more granular control about the contents. The MaterialListViewalso offers no header and footer. The Cupertino variant on the other hand offers a nice boxed design. Let's fix that. - We want
cupertinoandmaterialprops similar to the other widgets that allow to do some more fine control, but for now I keep it simple (just paddings and margins). - You also don't want to go for
PlatformTarget, because the user can change thePlatformStylefor eachPlatformTarget. What you want to use is inherit fromPlatformWidgetBase. CupertinoListSectionexpands vertically,ListViewdoes not. Thus, setting the background color will cause Material only to partially dye the background whereas Cupertino does the full screen. We can add our ownColumnandExpandedto do on Material to replicate Cupertino behavior. We also should allow to changebackgroundColoronly for one platform, in case you want to follow a good standard.CupertinoListSectionloadstextTheme.textStyleand adds text widgets viaDefaultTextStylewrapper, also setting font size and weight for the header. We should do the same for Material.- Material has no default boxes, but Cupertino has. So we should make a default box that reminds me of the Android settings app (incl. header and footer)
- Since the
PlatformListViewallows to setpaddingand thus aligns its contents in a way, there is virtually no way to set the background color of the entire item container unless using a restrictivePlatformListTile. I need an onTap event handler similar to whatPlatformListTileoffers, but I want full control about the contents andPlatformListTiledoesn't offer me a free placement with custom paddings, so I introduced aPlatformListRawTile. It's a simpler implementation ofListTileandCupertinoListTile. Please usePlatformListTile, unless you need more control about the contents (i.e. multi line text, icons in a different spot, multiple items, etc.)
This is what I came up with:
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
class CupertinoListRawTileData {
CupertinoListRawTileData({
this.backgroundColor,
this.backgroundColorActivated,
this.minHeight,
this.onTap,
this.padding,
});
final Color? backgroundColor;
final Color? backgroundColorActivated;
final double? minHeight;
final FutureOr<void> Function()? onTap;
final EdgeInsetsGeometry? padding;
}
class MaterialListRawTileData {
MaterialListRawTileData({
this.focusColor,
this.highlightColor,
this.hoverColor,
this.minHeight,
this.onTap,
this.overlayColor,
this.padding,
this.splashColor,
});
final Color? focusColor;
final Color? highlightColor;
final Color? hoverColor;
final double? minHeight;
final FutureOr<void> Function()? onTap;
final WidgetStateProperty<Color?>? overlayColor;
final EdgeInsetsGeometry? padding;
final Color? splashColor;
}
class PlatformListRawTile extends StatefulWidget {
const PlatformListRawTile({
super.key,
this.backgroundColor,
this.backgroundColorActivated,
required this.child,
this.cupertino,
this.material,
this.minHeight,
this.onTap,
this.padding,
this.style,
});
final Color? backgroundColor;
final Color? backgroundColorActivated;
final Widget child;
final PlatformBuilder<CupertinoListRawTileData>? cupertino;
final PlatformBuilder<MaterialListRawTileData>? material;
final double? minHeight;
final FutureOr<void> Function()? onTap;
final EdgeInsetsGeometry? padding;
final TextStyle? style;
@override
State<PlatformListRawTile> createState() => _PlatformListRawTileState();
}
class _PlatformListRawTileState extends State<PlatformListRawTile> {
bool _tappedCupertino = false;
static const double _kCupertinoMinHeight = 44.0;
static const EdgeInsetsGeometry _kCupertinoPadding =
EdgeInsetsDirectional.only(start: 20.0, top: 6.0, end: 14.0, bottom: 6.0);
static const double _kMaterialMinHeight = 56.0;
static const EdgeInsetsGeometry _kMaterialPadding = EdgeInsets.all(16.0);
@override
Widget build(BuildContext context) {
final bool onMaterial = isMaterial(context);
final MaterialListRawTileData? materialData =
onMaterial ? widget.material?.call(context, platform(context)) : null;
final CupertinoListRawTileData? cupertinoData =
!onMaterial ? widget.cupertino?.call(context, platform(context)) : null;
final TextStyle? style = widget.style ??
(onMaterial
? Theme.of(context).textTheme.bodyLarge
: CupertinoTheme.of(context).textTheme.textStyle);
final Padding innerChild = Padding(
padding:
(onMaterial ? materialData?.padding : cupertinoData?.padding) ??
widget.padding ??
(onMaterial ? _kMaterialPadding : _kCupertinoPadding),
child: Row(children: <Widget>[
Expanded(
child: style != null
? DefaultTextStyle(style: style, child: widget.child)
: widget.child,
)
]));
final Container child = Container(
constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: (onMaterial
? materialData?.minHeight
: cupertinoData?.minHeight) ??
widget.minHeight ??
(onMaterial ? _kMaterialMinHeight : _kCupertinoMinHeight)),
child: onMaterial
? Ink(color: widget.backgroundColor, child: innerChild)
: Container(
color: _tappedCupertino
? cupertinoData?.backgroundColorActivated ??
widget.backgroundColorActivated ??
CupertinoColors.systemGrey4.resolveFrom(context)
: cupertinoData?.backgroundColor ?? widget.backgroundColor,
child: innerChild));
if ((onMaterial && materialData?.onTap == null) &&
(!onMaterial && cupertinoData?.onTap == null) &&
widget.onTap == null) {
return child;
}
return onMaterial
? InkWell(
onTap: materialData?.onTap ?? widget.onTap,
focusColor:
materialData?.focusColor ?? widget.backgroundColorActivated,
highlightColor:
materialData?.highlightColor ?? widget.backgroundColorActivated,
hoverColor: materialData?.hoverColor,
overlayColor: materialData?.overlayColor,
splashColor: materialData?.splashColor,
child: child,
)
: GestureDetector(
onTapDown: (_) => setState(() {
_tappedCupertino = true;
}),
onTapCancel: () => setState(() {
_tappedCupertino = false;
}),
onTap: () async {
if (cupertinoData?.onTap != null) {
await cupertinoData!.onTap!();
} else {
await widget.onTap!();
}
if (mounted) {
setState(() {
_tappedCupertino = false;
});
}
},
behavior: HitTestBehavior.opaque,
child: child,
);
}
}
class CupertinoListViewData {
const CupertinoListViewData({
this.backgroundColor,
this.dividerMargin,
this.hasLeading,
this.margin,
});
final Color? backgroundColor;
final double? dividerMargin;
final bool? hasLeading;
final EdgeInsetsGeometry? margin;
}
class MaterialListViewData {
const MaterialListViewData({
this.footerPadding,
this.headerPadding,
this.margin,
});
final EdgeInsetsGeometry? footerPadding;
final EdgeInsetsGeometry? headerPadding;
final EdgeInsetsGeometry? margin;
}
class PlatformListView
extends PlatformWidgetBase<CupertinoListSection, Column> {
const PlatformListView({
super.key,
required this.children,
this.cupertino,
this.footer,
this.header,
this.margin,
this.material,
});
final List<Widget> children;
final PlatformBuilder<CupertinoListViewData>? cupertino;
final Widget? footer;
final Widget? header;
final PlatformBuilder<MaterialListViewData>? material;
final EdgeInsetsGeometry? margin;
static const EdgeInsetsGeometry _kMaterialFooterPadding =
EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
);
static const EdgeInsetsGeometry _kMaterialHeaderPadding =
EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
);
@override
CupertinoListSection createCupertinoWidget(BuildContext context) {
final data = this.cupertino?.call(context, platform(context));
return CupertinoListSection.insetGrouped(
dividerMargin: data?.dividerMargin ?? 14.0,
hasLeading: data?.hasLeading ?? false,
margin: data?.margin ?? this.margin,
backgroundColor: data?.backgroundColor ?? Colors.transparent,
header: header,
footer: footer,
children: [...children],
);
}
@override
Column createMaterialWidget(BuildContext context) {
final data = this.material?.call(context, platform(context));
final ThemeData theme = Theme.of(context);
final TextStyle? decorationStyle =
theme.textTheme.bodyMedium?.apply(fontWeightDelta: 2);
return Column(children: <Widget>[
ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: data?.margin ?? this.margin,
children: [
if (header != null)
Container(
padding: data?.headerPadding ?? _kMaterialHeaderPadding,
child: decorationStyle != null
? DefaultTextStyle(style: decorationStyle, child: header!)
: header,
),
...children,
if (footer != null)
Container(
padding: data?.footerPadding ?? _kMaterialFooterPadding,
child: decorationStyle != null
? DefaultTextStyle(style: decorationStyle, child: footer!)
: footer,
),
],
)
]);
}
}
I went a bit crazy with this, but I kinda like the result. Most importantly it's compatible with PlatformListTile as well. Maybe it's worth making a PR, not sure if the code quality is sufficient enough. The data types under cupertino and material props definitely need all props of the underlying widgets to give accurate flexibility.