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

Feature request: PlatformListView (ListView / CupertinoListSection) #453

Open
Pregum opened this issue Feb 17, 2024 · 1 comment
Open

Feature request: PlatformListView (ListView / CupertinoListSection) #453

Pregum opened this issue Feb 17, 2024 · 1 comment

Comments

@Pregum
Copy link

Pregum commented Feb 17, 2024

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,
      ],
    );
  }
}
@martin-braun
Copy link

martin-braun commented May 23, 2024

First look of your implementation:

image
  • These are just three Text widgets without any arguments. Yes you say this is wrong, because I should use PlatformListTile in it, but I need more granular control about the contents. The Material ListView also offers no header and footer. The Cupertino variant on the other hand offers a nice boxed design. Let's fix that.
  • We want cupertino and material props 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 the PlatformStyle for each PlatformTarget. What you want to use is inherit from PlatformWidgetBase.
  • CupertinoListSection expands vertically, ListView does 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 own Column and Expanded to do on Material to replicate Cupertino behavior. We also should allow to change backgroundColor only for one platform, in case you want to follow a good standard.
  • CupertinoListSection loads textTheme.textStyle and adds text widgets via DefaultTextStyle wrapper, 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 PlatformListView allows to set padding and 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 restrictive PlatformListTile. I need an onTap event handler similar to what PlatformListTile offers, but I want full control about the contents and PlatformListTile doesn't offer me a free placement with custom paddings, so I introduced a PlatformListRawTile. It's a simpler implementation of ListTile and CupertinoListTile. Please use PlatformListTile, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants