From b37c6b5073b514ab3ae318ab682a841b71d39091 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuchmenko Date: Wed, 25 Oct 2023 12:43:43 +0300 Subject: [PATCH] refactor: misc lib's code refactoring --- .../screens/gallery/ios_developer_screen.dart | 1 + lib/src/list/settings_list.dart | 120 +++--- .../platforms/android_settings_section.dart | 23 +- .../platforms/ios_settings_section.dart | 36 +- .../platforms/web_settings_section.dart | 16 +- lib/src/sections/settings_section.dart | 7 +- .../platforms/android_settings_tile.dart | 29 +- .../tiles/platforms/ios_settings_tile.dart | 403 ++++++++++-------- .../ios_settings_tile_additional_info.dart | 31 ++ .../tiles/platforms/web_settings_tile.dart | 10 + lib/src/tiles/settings_tile.dart | 9 +- lib/src/utils/exceptions.dart | 11 + lib/src/utils/settings_theme.dart | 6 +- .../cupertino_theme_provider.dart | 20 +- .../material_theme_provider.dart | 20 +- .../{ => theme_providers}/theme_provider.dart | 25 +- .../cupertino_theme_settings_list_test.dart | 2 +- ...cupertino_theme_settings_section_test.dart | 2 +- 18 files changed, 479 insertions(+), 292 deletions(-) create mode 100644 lib/src/tiles/platforms/ios_settings_tile_additional_info.dart create mode 100644 lib/src/utils/exceptions.dart rename lib/src/utils/{ => theme_providers}/cupertino_theme_provider.dart (82%) rename lib/src/utils/{ => theme_providers}/material_theme_provider.dart (81%) rename lib/src/utils/{ => theme_providers}/theme_provider.dart (86%) diff --git a/example/lib/screens/gallery/ios_developer_screen.dart b/example/lib/screens/gallery/ios_developer_screen.dart index cf34a47..dddfd8b 100644 --- a/example/lib/screens/gallery/ios_developer_screen.dart +++ b/example/lib/screens/gallery/ios_developer_screen.dart @@ -17,6 +17,7 @@ class _IosDeveloperScreen extends State { Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar(middle: Text('Developer')), + // TODO: usage of SafeArea causes blank left and right spacing in horizontal mode child: SafeArea( bottom: false, child: SettingsList( diff --git a/lib/src/list/settings_list.dart b/lib/src/list/settings_list.dart index 40ba346..2cb1e21 100644 --- a/lib/src/list/settings_list.dart +++ b/lib/src/list/settings_list.dart @@ -1,11 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:settings_ui/src/sections/abstract_settings_section.dart'; +import 'package:settings_ui/src/utils/exceptions.dart'; import 'package:settings_ui/src/utils/platform_utils.dart'; import 'package:settings_ui/src/utils/settings_theme.dart'; -import 'package:settings_ui/src/utils/theme_provider.dart'; +import 'package:settings_ui/src/utils/theme_providers/theme_provider.dart'; -class SettingsList extends StatelessWidget { +class SettingsList extends StatefulWidget { const SettingsList({ required this.sections, this.shrinkWrap = false, @@ -16,6 +17,7 @@ class SettingsList extends StatelessWidget { this.contentPadding, this.scrollController, this.useSystemTheme = false, + this.wideScreenBreakpoint = 810, Key? key, }) : super(key: key); @@ -28,6 +30,10 @@ class SettingsList extends StatelessWidget { final List sections; final ScrollController? scrollController; + /// Width of the screen from [MediaQuery]'s [Size] + /// at which the wide screen padding will be applied + final double wideScreenBreakpoint; + /// If true, some parameters will be applied from the system theme /// instead of default values of package or values from [SettingsThemeData] /// that is: backgroundColor, tileBackgroundColor @@ -35,24 +41,38 @@ class SettingsList extends StatelessWidget { final bool useSystemTheme; @override - Widget build(BuildContext context) { - DevicePlatform platform; - if (this.platform == null || this.platform == DevicePlatform.device) { + State createState() => _SettingsListState(); +} + +class _SettingsListState extends State { + late DevicePlatform platform; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (widget.platform == null || widget.platform == DevicePlatform.device) { platform = PlatformUtils.detectPlatform(context); } else { - platform = this.platform!; + platform = widget.platform!; } + } + + @override + Widget build(BuildContext context) { + final themeBrightness = _getThemeBrightness(context, platform); + final userDefinedTheme = themeBrightness == Brightness.dark + ? widget.darkTheme + : widget.lightTheme; - final brightness = calculateBrightness(context, platform); final themeData = ThemeProvider.getTheme( context: context, platform: platform, - brightness: brightness, - useSystemTheme: useSystemTheme, + brightness: themeBrightness, + useSystemTheme: widget.useSystemTheme, ).merge( - userDefinedSettingsTheme: - brightness == Brightness.dark ? darkTheme : lightTheme, - useSystemTheme: useSystemTheme); + userDefinedSettingsTheme: userDefinedTheme, + useSystemTheme: widget.useSystemTheme, + ); return Container( color: themeData.settingsListBackground, @@ -62,44 +82,29 @@ class SettingsList extends StatelessWidget { themeData: themeData, platform: platform, child: ListView.builder( - controller: scrollController, - physics: physics, - shrinkWrap: shrinkWrap, - itemCount: sections.length, - padding: contentPadding ?? calculateDefaultPadding(platform, context), + controller: widget.scrollController, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + itemCount: widget.sections.length, + padding: + widget.contentPadding ?? _getDefaultPadding(platform, context), itemBuilder: (BuildContext context, int index) { - return sections[index]; + return widget.sections[index]; }, ), ), ); } - EdgeInsets calculateDefaultPadding( - DevicePlatform platform, BuildContext context) { - if (MediaQuery.of(context).size.width > 810) { - double padding = (MediaQuery.of(context).size.width - 810) / 2; - switch (platform) { - case DevicePlatform.android: - case DevicePlatform.fuchsia: - case DevicePlatform.linux: - case DevicePlatform.iOS: - case DevicePlatform.macOS: - case DevicePlatform.windows: - return EdgeInsets.symmetric(horizontal: padding); - case DevicePlatform.web: - return EdgeInsets.symmetric(vertical: 20, horizontal: padding); - case DevicePlatform.device: - throw Exception( - 'You can\'t use the DevicePlatform.device in this context. ' - 'Incorrect platform: SettingsList.calculateDefaultPadding', - ); - default: - return EdgeInsets.symmetric( - horizontal: padding, - ); - } - } + EdgeInsets _getDefaultPadding( + DevicePlatform platform, + BuildContext context, + ) { + final mqSizeWidth = MediaQuery.sizeOf(context).width; + final isWideScreen = mqSizeWidth > widget.wideScreenBreakpoint; + double horizontalPaddingValue = + (mqSizeWidth - widget.wideScreenBreakpoint) / 2; + switch (platform) { case DevicePlatform.android: case DevicePlatform.fuchsia: @@ -107,19 +112,34 @@ class SettingsList extends StatelessWidget { case DevicePlatform.iOS: case DevicePlatform.macOS: case DevicePlatform.windows: - return const EdgeInsets.symmetric(vertical: 0); + return isWideScreen + ? EdgeInsets.symmetric( + horizontal: horizontalPaddingValue, + ) + : EdgeInsets.zero; case DevicePlatform.web: - return const EdgeInsets.symmetric(vertical: 20); + return isWideScreen + ? EdgeInsets.symmetric( + vertical: 20, + horizontal: horizontalPaddingValue, + ) + : const EdgeInsets.symmetric( + vertical: 20, + ); case DevicePlatform.device: - throw Exception( - 'You can\'t use the DevicePlatform.device in this context. ' - 'Incorrect platform: SettingsList.calculateDefaultPadding', + throw InvalidDevicePlatformDeviceUsage( + 'SettingsList._getDefaultPadding', ); } } - Brightness calculateBrightness( - BuildContext context, DevicePlatform platform) { + Brightness _getThemeBrightness( + BuildContext context, + DevicePlatform platform, + ) { + // final platformBrightness = + // View.of(context).platformDispatcher.platformBrightness; + // TODO: remove this deprecated Window usage whenever min dark SDK constraint is 3.0 final Brightness platformBrightness = WidgetsBinding.instance.window.platformBrightness; final materialBrightness = Theme.of(context).brightness; diff --git a/lib/src/sections/platforms/android_settings_section.dart b/lib/src/sections/platforms/android_settings_section.dart index e64adc8..1fb46c4 100644 --- a/lib/src/sections/platforms/android_settings_section.dart +++ b/lib/src/sections/platforms/android_settings_section.dart @@ -18,16 +18,13 @@ class AndroidSettingsSection extends StatelessWidget { @override Widget build(BuildContext context) { - return buildSectionBody(context); - } - - Widget buildSectionBody(BuildContext context) { final theme = SettingsTheme.of(context); + + // TODO: usage of textScaleFactor requires documentation final scaleFactor = MediaQuery.of(context).textScaleFactor; - final tileList = buildTileList(); if (title == null) { - return tileList; + return _IosTileList(tiles: tiles); } return Padding( @@ -37,6 +34,7 @@ class AndroidSettingsSection extends StatelessWidget { children: [ Padding( padding: titlePadding ?? + // TODO: move literals to file with theme constants EdgeInsetsDirectional.only( top: 24 * scaleFactor, bottom: 10 * scaleFactor, @@ -52,14 +50,23 @@ class AndroidSettingsSection extends StatelessWidget { ), Container( color: theme.themeData.settingsSectionBackground, - child: tileList, + child: _IosTileList(tiles: tiles), ), ], ), ); } +} + +class _IosTileList extends StatelessWidget { + final List tiles; + + const _IosTileList({ + required this.tiles, + }); - Widget buildTileList() { + @override + Widget build(BuildContext context) { return ListView.builder( shrinkWrap: true, itemCount: tiles.length, diff --git a/lib/src/sections/platforms/ios_settings_section.dart b/lib/src/sections/platforms/ios_settings_section.dart index 76bf015..0f3f6c8 100644 --- a/lib/src/sections/platforms/ios_settings_section.dart +++ b/lib/src/sections/platforms/ios_settings_section.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:settings_ui/settings_ui.dart'; -import 'package:settings_ui/src/tiles/platforms/ios_settings_tile.dart'; +import 'package:settings_ui/src/tiles/platforms/ios_settings_tile_additional_info.dart'; class IOSSettingsSection extends StatelessWidget { const IOSSettingsSection({ @@ -25,6 +25,7 @@ class IOSSettingsSection extends StatelessWidget { return Padding( padding: margin ?? + // TODO: move literals to file with theme constants EdgeInsets.only( top: 14.0 * scaleFactor, bottom: isLastNonDescriptive ? 27 * scaleFactor : 10 * scaleFactor, @@ -38,11 +39,13 @@ class IOSSettingsSection extends StatelessWidget { Padding( padding: titlePadding ?? EdgeInsetsDirectional.only( + // TODO: move literals to file with theme constants start: 18, bottom: 5 * scaleFactor, ), child: DefaultTextStyle( key: const Key('ios_settings_section_title_style'), + // TODO: move literals to file with theme constants style: TextStyle( color: theme.themeData.sectionTitleColor, // TODO: is this one hardcoded? @@ -51,13 +54,22 @@ class IOSSettingsSection extends StatelessWidget { child: title!, ), ), - buildTileList(), + _IosTileList(tiles: tiles), ], ), ); } +} + +class _IosTileList extends StatelessWidget { + final List tiles; + + const _IosTileList({ + required this.tiles, + }); - Widget buildTileList() { + @override + Widget build(BuildContext context) { return ListView.builder( shrinkWrap: true, itemCount: tiles.length, @@ -68,26 +80,30 @@ class IOSSettingsSection extends StatelessWidget { bool enableTop = false; - if (index == 0 || + // TODO: document this large condition + final isTopBorderRadiusRequired = index == 0 || (index > 0 && tiles[index - 1] is SettingsTile && - (tiles[index - 1] as SettingsTile).description != null)) { + (tiles[index - 1] as SettingsTile).description != null); + if (isTopBorderRadiusRequired) { enableTop = true; } - var enableBottom = false; - + var isBottomBorderRadiusRequired = false; + // TODO: document this large condition if (index == tiles.length - 1 || (index < tiles.length && tile is SettingsTile && (tile).description != null)) { - enableBottom = true; + isBottomBorderRadiusRequired = true; } + final isDividerRequired = index != tiles.length - 1; + return IOSSettingsTileAdditionalInfo( enableTopBorderRadius: enableTop, - enableBottomBorderRadius: enableBottom, - needToShowDivider: index != tiles.length - 1, + enableBottomBorderRadius: isBottomBorderRadiusRequired, + needToShowDivider: isDividerRequired, child: tile, ); }, diff --git a/lib/src/sections/platforms/web_settings_section.dart b/lib/src/sections/platforms/web_settings_section.dart index 6723316..751ac67 100644 --- a/lib/src/sections/platforms/web_settings_section.dart +++ b/lib/src/sections/platforms/web_settings_section.dart @@ -31,6 +31,7 @@ class WebSettingsSection extends StatelessWidget { children: [ if (title != null) Container( + // TODO: move literals to file with theme constants height: 65 * scaleFactor, padding: titlePadding ?? EdgeInsetsDirectional.only( @@ -40,6 +41,7 @@ class WebSettingsSection extends StatelessWidget { ), child: DefaultTextStyle( style: TextStyle( + // TODO: move literals to file with theme constants color: theme.themeData.titleTextColor, fontSize: 15, ), @@ -48,17 +50,27 @@ class WebSettingsSection extends StatelessWidget { ), Card( shape: + // TODO: move literals to file with theme constants RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), elevation: 4, color: theme.themeData.settingsSectionBackground, - child: buildTileList(), + child: _WebTileList(tiles: tiles), ), ], ), ); } +} + +class _WebTileList extends StatelessWidget { + final List tiles; - Widget buildTileList() { + const _WebTileList({ + required this.tiles, + }); + + @override + Widget build(BuildContext context) { return ListView.separated( shrinkWrap: true, itemCount: tiles.length, diff --git a/lib/src/sections/settings_section.dart b/lib/src/sections/settings_section.dart index 01af9d1..d3ec032 100644 --- a/lib/src/sections/settings_section.dart +++ b/lib/src/sections/settings_section.dart @@ -4,6 +4,7 @@ import 'package:settings_ui/src/sections/platforms/android_settings_section.dart import 'package:settings_ui/src/sections/platforms/ios_settings_section.dart'; import 'package:settings_ui/src/sections/platforms/web_settings_section.dart'; import 'package:settings_ui/src/tiles/abstract_settings_tile.dart'; +import 'package:settings_ui/src/utils/exceptions.dart'; import 'package:settings_ui/src/utils/platform_utils.dart'; import 'package:settings_ui/src/utils/settings_theme.dart'; @@ -38,6 +39,7 @@ class SettingsSection extends AbstractSettingsSection { case DevicePlatform.iOS: case DevicePlatform.macOS: case DevicePlatform.windows: + // TODO: decide whether this commented code snipped is still required // return CupertinoListSection.insetGrouped( // header: title, // // margin: margin, @@ -60,10 +62,7 @@ class SettingsSection extends AbstractSettingsSection { titlePadding: titlePadding, ); case DevicePlatform.device: - throw Exception( - 'You can\'t use the DevicePlatform.device in this context. ' - 'Incorrect platform: SettingsSection.build', - ); + throw InvalidDevicePlatformDeviceUsage('SettingsSection.build'); } } } diff --git a/lib/src/tiles/platforms/android_settings_tile.dart b/lib/src/tiles/platforms/android_settings_tile.dart index 4441dce..db2a77b 100644 --- a/lib/src/tiles/platforms/android_settings_tile.dart +++ b/lib/src/tiles/platforms/android_settings_tile.dart @@ -37,37 +37,41 @@ class AndroidSettingsTile extends StatelessWidget { final EdgeInsetsGeometry? trailingPadding; final EdgeInsetsGeometry? descriptionPadding; + void _executeAction(BuildContext context) { + if (tileType == SettingsTileType.switchTile) { + onToggle?.call(!initialValue); + } else { + onPressed?.call(context); + } + } + @override Widget build(BuildContext context) { final theme = SettingsTheme.of(context); final scaleFactor = MediaQuery.of(context).textScaleFactor; - final cantShowAnimation = tileType == SettingsTileType.switchTile + final isActionDisabled = tileType == SettingsTileType.switchTile ? onToggle == null && onPressed == null : onPressed == null; return ClipRRect( + // TODO: move literals to file with theme constants borderRadius: BorderRadius.circular(24), child: IgnorePointer( ignoring: !enabled, child: Material( color: Colors.transparent, child: InkWell( - onTap: cantShowAnimation + onTap: isActionDisabled ? null - : () { - if (tileType == SettingsTileType.switchTile) { - onToggle?.call(!initialValue); - } else { - onPressed?.call(context); - } - }, + : () => _executeAction(context), highlightColor: theme.themeData.tileHighlightColor, child: Row( children: [ if (leading != null) Padding( padding: leadingPadding ?? + // TODO: move literals to file with theme constants const EdgeInsetsDirectional.only(start: 24), child: IconTheme( data: IconTheme.of(context).copyWith( @@ -80,6 +84,7 @@ class AndroidSettingsTile extends StatelessWidget { ), Expanded( child: Padding( + // TODO: move literals to file with theme constants padding: EdgeInsetsDirectional.only( start: 24, end: 24, @@ -90,6 +95,7 @@ class AndroidSettingsTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ DefaultTextStyle( + // TODO: move literals to file with theme constants style: TextStyle( color: enabled ? theme.themeData.tileTitleTextColor @@ -101,6 +107,7 @@ class AndroidSettingsTile extends StatelessWidget { ), if (value != null) Padding( + // TODO: move literals to file with theme constants padding: const EdgeInsets.only(top: 4.0), child: DefaultTextStyle( style: TextStyle( @@ -114,6 +121,7 @@ class AndroidSettingsTile extends StatelessWidget { else if (description != null) Padding( padding: descriptionPadding ?? + // TODO: move literals to file with theme constants const EdgeInsets.only(top: 4.0), child: DefaultTextStyle( style: TextStyle( @@ -133,6 +141,7 @@ class AndroidSettingsTile extends StatelessWidget { children: [ trailing!, Padding( + // TODO: move literals to file with theme constants padding: const EdgeInsetsDirectional.only(end: 8), child: Switch( value: initialValue, @@ -147,6 +156,7 @@ class AndroidSettingsTile extends StatelessWidget { else if (tileType == SettingsTileType.switchTile) Padding( padding: + // TODO: move literals to file with theme constants const EdgeInsetsDirectional.only(start: 16, end: 8), child: Switch( value: initialValue, @@ -159,6 +169,7 @@ class AndroidSettingsTile extends StatelessWidget { else if (trailing != null) Padding( padding: trailingPadding ?? + // TODO: move literals to file with theme constants const EdgeInsets.symmetric(horizontal: 16), child: IconTheme( data: IconTheme.of(context).copyWith( diff --git a/lib/src/tiles/platforms/ios_settings_tile.dart b/lib/src/tiles/platforms/ios_settings_tile.dart index e693900..7c981bf 100644 --- a/lib/src/tiles/platforms/ios_settings_tile.dart +++ b/lib/src/tiles/platforms/ios_settings_tile.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:settings_ui/settings_ui.dart'; +import 'package:settings_ui/src/tiles/platforms/ios_settings_tile_additional_info.dart'; class IOSSettingsTile extends StatefulWidget { const IOSSettingsTile({ @@ -49,34 +50,32 @@ class IOSSettingsTileState extends State { Widget build(BuildContext context) { final additionalInfo = IOSSettingsTileAdditionalInfo.of(context); final theme = SettingsTheme.of(context); + final scaleFactor = MediaQuery.of(context).textScaleFactor; - return IgnorePointer( - ignoring: !widget.enabled, - child: Column( - children: [ - buildTile( - context: context, - theme: theme, - additionalInfo: additionalInfo, - ), - if (widget.description != null) - buildDescription( - context: context, - theme: theme, - additionalInfo: additionalInfo, - ), - ], - ), + Widget content = _IosTileContent( + theme: theme, + additionalInfo: additionalInfo, + tileType: widget.tileType, + leading: widget.leading, + title: widget.title, + subtitle: widget.subtitle, + description: widget.description, + onPressed: widget.onPressed, + onToggle: widget.onToggle, + value: widget.value, + initialValue: widget.initialValue, + enabled: widget.enabled, + activeSwitchColor: widget.activeSwitchColor, + trailing: widget.trailing, + titlePadding: widget.titlePadding, + leadingPadding: widget.leadingPadding, + subtitlePadding: widget.subtitlePadding, + isPressed: isPressed, + executeAction: _executeAction, + changePressState: _changePressState, ); - } - Widget buildTile({ - required BuildContext context, - required SettingsTheme theme, - required IOSSettingsTileAdditionalInfo additionalInfo, - }) { - Widget content = buildTileContent(context, theme, additionalInfo); - DevicePlatform platform = PlatformUtils.detectPlatform(context); + final platform = PlatformUtils.detectPlatform(context); if (platform != DevicePlatform.iOS) { content = Material( color: Colors.transparent, @@ -84,104 +83,51 @@ class IOSSettingsTileState extends State { ); } - return ClipRRect( - borderRadius: BorderRadius.vertical( - top: additionalInfo.enableTopBorderRadius - ? const Radius.circular(12) - : Radius.zero, - bottom: additionalInfo.enableBottomBorderRadius - ? const Radius.circular(12) - : Radius.zero, - ), - child: content, - ); - } - - Widget buildDescription({ - required BuildContext context, - required SettingsTheme theme, - required IOSSettingsTileAdditionalInfo additionalInfo, - }) { - final scaleFactor = MediaQuery.of(context).textScaleFactor; - - return Container( - width: MediaQuery.of(context).size.width, - padding: EdgeInsets.only( - left: 18, - right: 18, - top: 8 * scaleFactor, - bottom: additionalInfo.needToShowDivider ? 24 : 8 * scaleFactor, - ), - decoration: BoxDecoration( - // TODO: check if this is correct - color: theme.themeData.settingsListBackground, - ), - child: DefaultTextStyle( - style: TextStyle( - color: theme.themeData.titleTextColor, - fontSize: 13, - ), - child: widget.description!, - ), - ); - } - - Widget buildTrailing({ - required BuildContext context, - required SettingsTheme theme, - }) { - final scaleFactor = MediaQuery.of(context).textScaleFactor; - - return Row( - children: [ - if (widget.trailing != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: IconTheme( - data: IconTheme.of(context).copyWith( - color: widget.enabled - ? theme.themeData.tileLeadingIconsColor - : theme.themeData.tileDisabledContentColor, - ), - child: widget.trailing!, - ), - ), - if (widget.tileType == SettingsTileType.switchTile) - CupertinoSwitch( - value: widget.initialValue ?? true, - onChanged: widget.onToggle, - activeColor: widget.enabled - ? widget.activeSwitchColor - : theme.themeData.tileDisabledContentColor, - ), - if (widget.tileType == SettingsTileType.navigationTile && - widget.value != null) - DefaultTextStyle( - style: TextStyle( - color: widget.enabled - ? theme.themeData.tileTrailingTextColor - : theme.themeData.tileDisabledContentColor, - fontSize: 17, + return IgnorePointer( + ignoring: !widget.enabled, + child: Column( + children: [ + ClipRRect( + // TODO: move literals to file with theme constants + borderRadius: BorderRadius.vertical( + top: additionalInfo.enableTopBorderRadius + ? const Radius.circular(12) + : Radius.zero, + bottom: additionalInfo.enableBottomBorderRadius + ? const Radius.circular(12) + : Radius.zero, ), - child: widget.value!, + child: content, ), - if (widget.tileType == SettingsTileType.navigationTile) - Padding( - padding: const EdgeInsetsDirectional.only(start: 6, end: 2), - child: IconTheme( - data: IconTheme.of(context) - .copyWith(color: theme.themeData.tileLeadingIconsColor), - child: Icon( - CupertinoIcons.chevron_forward, - size: 18 * scaleFactor, + if (widget.description != null) + Container( + width: MediaQuery.of(context).size.width, + // TODO: move literals to file with theme constants + padding: EdgeInsets.only( + left: 18, + right: 18, + top: 8 * scaleFactor, + bottom: additionalInfo.needToShowDivider ? 24 : 8 * scaleFactor, + ), + decoration: BoxDecoration( + // TODO: check if this is correct + color: theme.themeData.settingsListBackground, + ), + child: DefaultTextStyle( + // TODO: move literals to file with theme constants + style: TextStyle( + color: theme.themeData.titleTextColor, + fontSize: 13, + ), + child: widget.description!, ), ), - ), - ], + ], + ), ); } - void changePressState({bool isPressed = false}) { + void _changePressState({bool isPressed = false}) { if (mounted) { setState(() { this.isPressed = isPressed; @@ -189,52 +135,97 @@ class IOSSettingsTileState extends State { } } - Widget buildTileContent( - BuildContext context, - SettingsTheme theme, - IOSSettingsTileAdditionalInfo additionalInfo, - ) { + void _executeAction(BuildContext context) { + _changePressState(isPressed: true); + + widget.onPressed!.call(context); + Future.delayed( + // TODO: move literals to file with theme constants + const Duration(milliseconds: 100), + () => _changePressState(isPressed: false), + ); + } +} + +class _IosTileContent extends StatelessWidget { + final SettingsTheme theme; + final SettingsTileType tileType; + final Widget? leading; + final Widget? title; + final Widget? subtitle; + final Widget? description; + final Function(BuildContext context)? onPressed; + final Function(bool value)? onToggle; + final Widget? value; + final bool? initialValue; + final bool enabled; + final Color? activeSwitchColor; + final Widget? trailing; + final EdgeInsetsGeometry? titlePadding; + final EdgeInsetsGeometry? leadingPadding; + final EdgeInsetsGeometry? subtitlePadding; + final IOSSettingsTileAdditionalInfo additionalInfo; + final bool isPressed; + final Function(BuildContext context) executeAction; + final Function({bool isPressed}) changePressState; + + const _IosTileContent({ + required this.theme, + required this.additionalInfo, + required this.tileType, + required this.leading, + required this.title, + required this.subtitle, + required this.description, + required this.onPressed, + required this.onToggle, + required this.value, + required this.initialValue, + required this.enabled, + required this.activeSwitchColor, + required this.trailing, + required this.titlePadding, + required this.leadingPadding, + required this.subtitlePadding, + required this.isPressed, + required this.executeAction, + required this.changePressState, + }); + + @override + Widget build(BuildContext context) { final scaleFactor = MediaQuery.of(context).textScaleFactor; return GestureDetector( behavior: HitTestBehavior.translucent, - onTap: widget.onPressed == null - ? null - : () { - changePressState(isPressed: true); - - widget.onPressed!.call(context); - - Future.delayed( - const Duration(milliseconds: 100), - () => changePressState(isPressed: false), - ); - }, + onTap: onPressed == null ? null : () => executeAction(context), onTapDown: (_) => - widget.onPressed == null ? null : changePressState(isPressed: true), + onPressed == null ? null : changePressState(isPressed: true), onTapUp: (_) => - widget.onPressed == null ? null : changePressState(isPressed: false), + onPressed == null ? null : changePressState(isPressed: false), onTapCancel: () => - widget.onPressed == null ? null : changePressState(isPressed: false), + onPressed == null ? null : changePressState(isPressed: false), child: Container( key: const Key('ios_settings_tile_container'), color: isPressed ? theme.themeData.tileHighlightColor : theme.themeData.settingsSectionBackground, + // TODO: move literals to file with theme constants padding: const EdgeInsetsDirectional.only(start: 18), child: Row( children: [ - if (widget.leading != null) + if (leading != null) Padding( - padding: widget.leadingPadding ?? + padding: leadingPadding ?? + // TODO: move literals to file with theme constants const EdgeInsetsDirectional.only(end: 12.0), child: IconTheme.merge( data: IconThemeData( - color: widget.enabled + color: enabled ? theme.themeData.tileLeadingIconsColor : theme.themeData.tileDisabledContentColor, ), - child: widget.leading!, + child: leading!, ), ), Expanded( @@ -251,50 +242,63 @@ class IOSSettingsTileState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: widget.titlePadding ?? + padding: titlePadding ?? + // TODO: move literals to file with theme constants EdgeInsetsDirectional.only( top: 12.5 * scaleFactor, - bottom: widget.subtitle == null + bottom: subtitle == null ? (12.5 * scaleFactor) : (3.5 * scaleFactor), ), child: DefaultTextStyle( + // TODO: move literals to file with theme constants style: TextStyle( - color: widget.enabled + color: enabled ? theme.themeData.tileTitleTextColor : theme .themeData.tileDisabledContentColor, fontSize: 16, ), - child: widget.title!, + child: title!, ), ), - if (widget.subtitle != null) + if (subtitle != null) Padding( - padding: widget.subtitlePadding ?? + padding: subtitlePadding ?? EdgeInsetsDirectional.only( + // TODO: move literals to file with theme constants bottom: 12.5 * scaleFactor, ), child: DefaultTextStyle( style: TextStyle( - color: widget.enabled + color: enabled ? theme.themeData.titleTextColor : theme.themeData .tileDisabledContentColor, fontSize: 15, ), - child: widget.subtitle!, + child: subtitle!, ), ), ], ), ), - buildTrailing(context: context, theme: theme), + _IosTileTrailing( + context: context, + theme: theme, + tileType: tileType, + value: value, + initialValue: initialValue, + enabled: enabled, + activeSwitchColor: activeSwitchColor, + onToggle: onToggle, + trailing: trailing, + ), ], ), ), - if (widget.description == null && - additionalInfo.needToShowDivider) + if (description == null && additionalInfo.needToShowDivider) + // TODO: move literals to file with theme constants Divider( height: 0, thickness: 0.7, @@ -310,32 +314,83 @@ class IOSSettingsTileState extends State { } } -class IOSSettingsTileAdditionalInfo extends InheritedWidget { - final bool needToShowDivider; - final bool enableTopBorderRadius; - final bool enableBottomBorderRadius; +class _IosTileTrailing extends StatelessWidget { + final Widget? trailing; + final SettingsTileType tileType; + final Widget? value; + final bool? initialValue; + final bool enabled; + final Color? activeSwitchColor; + final Function(bool value)? onToggle; - const IOSSettingsTileAdditionalInfo({ - Key? key, - required this.needToShowDivider, - required this.enableTopBorderRadius, - required this.enableBottomBorderRadius, - required Widget child, - }) : super(key: key, child: child); + final BuildContext context; + final SettingsTheme theme; + + const _IosTileTrailing({ + required this.trailing, + required this.tileType, + required this.value, + required this.initialValue, + required this.enabled, + required this.activeSwitchColor, + required this.onToggle, + required this.context, + required this.theme, + }); @override - bool updateShouldNotify(IOSSettingsTileAdditionalInfo oldWidget) => true; + Widget build(BuildContext context) { + final scaleFactor = MediaQuery.of(context).textScaleFactor; - static IOSSettingsTileAdditionalInfo of(BuildContext context) { - final IOSSettingsTileAdditionalInfo? result = context - .dependOnInheritedWidgetOfExactType(); - // assert(result != null, 'No IOSSettingsTileAdditionalInfo found in context'); - return result ?? - const IOSSettingsTileAdditionalInfo( - needToShowDivider: true, - enableBottomBorderRadius: true, - enableTopBorderRadius: true, - child: SizedBox(), - ); + return Row( + children: [ + if (trailing != null) + Padding( + // TODO: move literals to file with theme constants + padding: const EdgeInsets.symmetric(horizontal: 16), + child: IconTheme( + data: IconTheme.of(context).copyWith( + color: enabled + ? theme.themeData.tileLeadingIconsColor + : theme.themeData.tileDisabledContentColor, + ), + child: trailing!, + ), + ), + if (tileType == SettingsTileType.switchTile) + CupertinoSwitch( + value: initialValue ?? true, + onChanged: onToggle, + activeColor: enabled + ? activeSwitchColor + : theme.themeData.tileDisabledContentColor, + ), + if (tileType == SettingsTileType.navigationTile && value != null) + DefaultTextStyle( + // TODO: move literals to file with theme constants + style: TextStyle( + color: enabled + ? theme.themeData.tileTrailingTextColor + : theme.themeData.tileDisabledContentColor, + fontSize: 17, + ), + child: value!, + ), + if (tileType == SettingsTileType.navigationTile) + Padding( + // TODO: move literals to file with theme constants + padding: const EdgeInsetsDirectional.only(start: 6, end: 2), + child: IconTheme( + data: IconTheme.of(context) + .copyWith(color: theme.themeData.tileLeadingIconsColor), + child: Icon( + CupertinoIcons.chevron_forward, + // TODO: move literals to file with theme constants + size: 18 * scaleFactor, + ), + ), + ), + ], + ); } } diff --git a/lib/src/tiles/platforms/ios_settings_tile_additional_info.dart b/lib/src/tiles/platforms/ios_settings_tile_additional_info.dart new file mode 100644 index 0000000..b6cf6e4 --- /dev/null +++ b/lib/src/tiles/platforms/ios_settings_tile_additional_info.dart @@ -0,0 +1,31 @@ +import 'package:flutter/cupertino.dart'; + +class IOSSettingsTileAdditionalInfo extends InheritedWidget { + final bool needToShowDivider; + final bool enableTopBorderRadius; + final bool enableBottomBorderRadius; + + const IOSSettingsTileAdditionalInfo({ + Key? key, + required this.needToShowDivider, + required this.enableTopBorderRadius, + required this.enableBottomBorderRadius, + required Widget child, + }) : super(key: key, child: child); + + @override + bool updateShouldNotify(IOSSettingsTileAdditionalInfo oldWidget) => true; + + static IOSSettingsTileAdditionalInfo of(BuildContext context) { + final IOSSettingsTileAdditionalInfo? result = context + .dependOnInheritedWidgetOfExactType(); + // assert(result != null, 'No IOSSettingsTileAdditionalInfo found in context'); + return result ?? + const IOSSettingsTileAdditionalInfo( + needToShowDivider: true, + enableBottomBorderRadius: true, + enableTopBorderRadius: true, + child: SizedBox(), + ); + } +} diff --git a/lib/src/tiles/platforms/web_settings_tile.dart b/lib/src/tiles/platforms/web_settings_tile.dart index b82a7cb..e3a2a56 100644 --- a/lib/src/tiles/platforms/web_settings_tile.dart +++ b/lib/src/tiles/platforms/web_settings_tile.dart @@ -66,6 +66,7 @@ class WebSettingsTile extends StatelessWidget { if (leading != null) Padding( padding: leadingPadding ?? + // TODO: move literals to file with theme constants const EdgeInsetsDirectional.only( start: 24, ), @@ -80,6 +81,7 @@ class WebSettingsTile extends StatelessWidget { ), Expanded( child: Padding( + // TODO: move literals to file with theme constants padding: EdgeInsetsDirectional.only( start: 24, end: 24, @@ -90,6 +92,7 @@ class WebSettingsTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ DefaultTextStyle( + // TODO: move literals to file with theme constants style: TextStyle( color: enabled ? theme.themeData.tileTitleTextColor @@ -101,6 +104,7 @@ class WebSettingsTile extends StatelessWidget { ), if (value != null) Padding( + // TODO: move literals to file with theme constants padding: const EdgeInsets.only(top: 4.0), child: DefaultTextStyle( style: TextStyle( @@ -114,6 +118,7 @@ class WebSettingsTile extends StatelessWidget { else if (description != null) Padding( padding: descriptionPadding ?? + // TODO: move literals to file with theme constants const EdgeInsets.only(top: 4.0), child: DefaultTextStyle( style: TextStyle( @@ -144,6 +149,8 @@ class WebSettingsTile extends StatelessWidget { child: Switch.adaptive( activeColor: enabled ? (activeSwitchColor ?? + // TODO: move color constant to file with theme constants + const Color.fromRGBO(138, 180, 248, 1.0)) : theme.themeData.tileDisabledContentColor, value: initialValue, @@ -154,11 +161,13 @@ class WebSettingsTile extends StatelessWidget { ) else if (tileType == SettingsTileType.switchTile) Padding( + // TODO: move literals to file with theme constants padding: const EdgeInsetsDirectional.only(start: 16, end: 8), child: Switch( value: initialValue, activeColor: enabled ? (activeSwitchColor ?? + // TODO: move color constant to file with theme constants const Color.fromRGBO(138, 180, 248, 1.0)) : theme.themeData.tileDisabledContentColor, onChanged: onToggle, @@ -167,6 +176,7 @@ class WebSettingsTile extends StatelessWidget { else if (trailing != null) Padding( padding: trailingPadding ?? + // TODO: move literals to file with theme constants const EdgeInsets.symmetric(horizontal: 16), child: IconTheme( data: IconTheme.of(context).copyWith( diff --git a/lib/src/tiles/settings_tile.dart b/lib/src/tiles/settings_tile.dart index be23962..5ab1a11 100644 --- a/lib/src/tiles/settings_tile.dart +++ b/lib/src/tiles/settings_tile.dart @@ -3,6 +3,7 @@ import 'package:settings_ui/src/tiles/abstract_settings_tile.dart'; import 'package:settings_ui/src/tiles/platforms/android_settings_tile.dart'; import 'package:settings_ui/src/tiles/platforms/ios_settings_tile.dart'; import 'package:settings_ui/src/tiles/platforms/web_settings_tile.dart'; +import 'package:settings_ui/src/utils/exceptions.dart'; import 'package:settings_ui/src/utils/platform_utils.dart'; import 'package:settings_ui/src/utils/settings_theme.dart'; @@ -134,7 +135,8 @@ class SettingsTile extends AbstractSettingsTile { case DevicePlatform.iOS: case DevicePlatform.macOS: case DevicePlatform.windows: - // return CupertinoListTile( + // TODO: decide whether this commented code snipped is still required + // return CupertinoListTile( // title: title, // leading: leading, // additionalInfo: value, @@ -180,10 +182,7 @@ class SettingsTile extends AbstractSettingsTile { descriptionPadding: descriptionPadding, ); case DevicePlatform.device: - throw Exception( - 'You can\'t use the DevicePlatform.device in this context. ' - 'Incorrect platform: SettingsTile.build', - ); + throw InvalidDevicePlatformDeviceUsage('SettingsTile.build'); } } } diff --git a/lib/src/utils/exceptions.dart b/lib/src/utils/exceptions.dart new file mode 100644 index 0000000..8eab34f --- /dev/null +++ b/lib/src/utils/exceptions.dart @@ -0,0 +1,11 @@ +class InvalidDevicePlatformDeviceUsage implements Exception { + final String message; + final String usageSrc; + + InvalidDevicePlatformDeviceUsage(this.usageSrc) + : message = "You can't use the DevicePlatform.device in this context. " + "Incorrect platform: $usageSrc"; + + @override + String toString() => message; +} diff --git a/lib/src/utils/settings_theme.dart b/lib/src/utils/settings_theme.dart index 5cdc69c..f095ba2 100644 --- a/lib/src/utils/settings_theme.dart +++ b/lib/src/utils/settings_theme.dart @@ -16,9 +16,7 @@ class SettingsTheme extends InheritedWidget { bool updateShouldNotify(SettingsTheme oldWidget) => true; static SettingsTheme of(BuildContext context) { - final SettingsTheme? result = - context.dependOnInheritedWidgetOfExactType(); - return result!; + return context.dependOnInheritedWidgetOfExactType()!; } } @@ -69,9 +67,11 @@ class SettingsThemeData { final Color? tileDescriptionColor; final Color? tileTitleTextColor; final Color? tileDisabledContentColor; + // TODO: check how it works // Only in android and web. final Color? inactiveSubtitleColor; + // TODO: implement final int? borderRadius; diff --git a/lib/src/utils/cupertino_theme_provider.dart b/lib/src/utils/theme_providers/cupertino_theme_provider.dart similarity index 82% rename from lib/src/utils/cupertino_theme_provider.dart rename to lib/src/utils/theme_providers/cupertino_theme_provider.dart index ae515c8..6aaddd0 100644 --- a/lib/src/utils/cupertino_theme_provider.dart +++ b/lib/src/utils/theme_providers/cupertino_theme_provider.dart @@ -8,8 +8,11 @@ class CupertinoThemeProvider { required bool useSystemTheme, }) { return SettingsThemeData( - settingsListBackground: - getSettingsListBackgroundColor(context, useSystemTheme, brightness), + settingsListBackground: _getSettingsListBackgroundColor( + context, + brightness, + useSystemTheme: useSystemTheme, + ), tileHighlightColor: _tileHighlightColors[brightness], settingsSectionBackground: sectionBackgroundColors[brightness], titleTextColor: _titleColors[brightness], @@ -23,12 +26,17 @@ class CupertinoThemeProvider { ); } - static Color getSettingsListBackgroundColor( - BuildContext context, bool useSystemTheme, Brightness brightness) { + static Color _getSettingsListBackgroundColor( + BuildContext context, + Brightness brightness, { + required bool useSystemTheme, + }) { + final scaffoldBackgroundColor = + CupertinoTheme.of(context).scaffoldBackgroundColor; if (useSystemTheme) { - return CupertinoTheme.of(context).scaffoldBackgroundColor; + return scaffoldBackgroundColor; } else { - return backgroundColors[brightness]!; + return backgroundColors[brightness] ?? scaffoldBackgroundColor; } } diff --git a/lib/src/utils/material_theme_provider.dart b/lib/src/utils/theme_providers/material_theme_provider.dart similarity index 81% rename from lib/src/utils/material_theme_provider.dart rename to lib/src/utils/theme_providers/material_theme_provider.dart index c99fc70..f83ef22 100644 --- a/lib/src/utils/material_theme_provider.dart +++ b/lib/src/utils/theme_providers/material_theme_provider.dart @@ -8,8 +8,11 @@ class MaterialThemeProvider { required bool useSystemTheme, }) { return SettingsThemeData( - settingsListBackground: - getBackgroundColor(context, useSystemTheme, brightness), + settingsListBackground: _getBackgroundColor( + context, + brightness, + useSystemTheme: useSystemTheme, + ), tileHighlightColor: _tileHighlightColors[brightness], titleTextColor: _titleTextColors[brightness], tileTitleTextColor: _tileTextColors[brightness], @@ -21,13 +24,16 @@ class MaterialThemeProvider { } } -Color getBackgroundColor( - BuildContext context, bool useSystemTheme, Brightness brightness) { +Color _getBackgroundColor( + BuildContext context, + Brightness brightness, { + required bool useSystemTheme, +}) { + final scaffoldBackgroundColor = Theme.of(context).scaffoldBackgroundColor; if (useSystemTheme) { - return Theme.of(context).scaffoldBackgroundColor; + return scaffoldBackgroundColor; } else { - return _backgroundColors[brightness] ?? - Theme.of(context).scaffoldBackgroundColor; + return _backgroundColors[brightness] ?? scaffoldBackgroundColor; } } diff --git a/lib/src/utils/theme_provider.dart b/lib/src/utils/theme_providers/theme_provider.dart similarity index 86% rename from lib/src/utils/theme_provider.dart rename to lib/src/utils/theme_providers/theme_provider.dart index f5899b7..e81b6af 100644 --- a/lib/src/utils/theme_provider.dart +++ b/lib/src/utils/theme_providers/theme_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; -import 'package:settings_ui/src/utils/cupertino_theme_provider.dart'; -import 'package:settings_ui/src/utils/material_theme_provider.dart'; +import 'package:settings_ui/src/utils/exceptions.dart'; +import 'package:settings_ui/src/utils/theme_providers/cupertino_theme_provider.dart'; +import 'package:settings_ui/src/utils/theme_providers/material_theme_provider.dart'; import 'package:settings_ui/src/utils/platform_utils.dart'; import 'package:settings_ui/src/utils/settings_theme.dart'; @@ -16,11 +17,13 @@ class ThemeProvider { case DevicePlatform.fuchsia: case DevicePlatform.linux: return MaterialThemeProvider.androidTheme( - context: context, - brightness: brightness, - useSystemTheme: useSystemTheme); + context: context, + brightness: brightness, + useSystemTheme: useSystemTheme, + ); case DevicePlatform.iOS: case DevicePlatform.macOS: + // TODO: Windows probably should have non Cupertino theme case DevicePlatform.windows: return CupertinoThemeProvider.iosTheme( context: context, @@ -29,14 +32,12 @@ class ThemeProvider { ); case DevicePlatform.web: return _webTheme( - context: context, - brightness: brightness, - useSystemTheme: useSystemTheme); - case DevicePlatform.device: - throw Exception( - 'You can\'t use the DevicePlatform.device in this context. ' - 'Incorrect platform: ThemeProvider.getTheme', + context: context, + brightness: brightness, + useSystemTheme: useSystemTheme, ); + case DevicePlatform.device: + throw InvalidDevicePlatformDeviceUsage('ThemeProvider.getTheme'); } } diff --git a/test/theme_tests/cupertino_theme_settings_list_test.dart b/test/theme_tests/cupertino_theme_settings_list_test.dart index d95cd59..23314d0 100644 --- a/test/theme_tests/cupertino_theme_settings_list_test.dart +++ b/test/theme_tests/cupertino_theme_settings_list_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:settings_ui/settings_ui.dart'; -import 'package:settings_ui/src/utils/cupertino_theme_provider.dart'; +import 'package:settings_ui/src/utils/theme_providers/cupertino_theme_provider.dart'; import '../cupertino_app_wrapper.dart'; diff --git a/test/theme_tests/cupertino_theme_settings_section_test.dart b/test/theme_tests/cupertino_theme_settings_section_test.dart index ca9c6be..49c2efb 100644 --- a/test/theme_tests/cupertino_theme_settings_section_test.dart +++ b/test/theme_tests/cupertino_theme_settings_section_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:settings_ui/settings_ui.dart'; -import 'package:settings_ui/src/utils/cupertino_theme_provider.dart'; +import 'package:settings_ui/src/utils/theme_providers/cupertino_theme_provider.dart'; import '../cupertino_app_wrapper.dart';