Flutter. Settings screen in seconds with settings_ui package

Introduction
I was reading comments on some FlutterDev thread and somebody posted the link to this package.
I checked it out and it is definitely worth adding to my project template.
The package allows for creating Settings pages similar to Android/iPhone native settings.
It is pretty popular, has about 1K likes on pub.dev, and is actively downloaded. The number of downloads (245K/month) says that the package is adopted by enterprises. (Most downloads are made by automated build systems.)
Usage
The package is very straightforward. It has three classes: SettingsList
, SettingsSection
, and SettingsTile
.
We have all of them on the above screen.
class ExampleView extends GetView<ExampleController> {
const ExampleView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Settings example')),
body: SettingsList(
sections: [
SettingsSection(
title: Text('Settings section'),
tiles: [
SettingsTile.navigation(
leading: Icon(Icons.settings),
title: Text('Navigation settings tile'),
onPressed: (context) {
Get.toNamed(Routes.ANDROID_SETTINGS);
},
),
SettingsTile.switchTile(
initialValue: false,
onToggle: (_) {},
title: Text('Switch settings tile'),
),
],
),
],
),
);
}
}
First, we created SettingsList
, which took the whole body
of the Scaffold
. Inside SettingsList
we have SettingsSections
and inside sections we put SettingsTiles
.
There are two implementations of SettingsTile
: navigation
and switchTile
. For some reason, they implemented as different constructors of the same class. Obvious violation of the Single Responsibility Principle.
The settings page can be easily styled for Android using the devicePlatform
property of SettingsList
:
return Scaffold(
appBar: AppBar(title: Text('Settings example')),
body: SettingsList(
platform: DevicePlatform.android, //<-
I have an opinionated practical approach to design which can be explained:
Design for iOS first, then use the same design on both platforms.
So, for me, this Android styling option is kinda useless, but the option is an option — always lovely to have.
What I would like to see instead is more SettingsTile
implementations.
Let’s add a missing tile
For almost every settings page we need single and multiple select from the list of choices.
Single select
Since iOS doesn’t have checkboxes, there is an ongoing debate among iOS developers about what to use for multiple select: checkmarks or custom checkboxes.
Though in Flutter we don’t have any problem with checkboxes I vote for checkmarks.
So, let's implement the missing tiles.
Here is the result with CheckmarkListTile
widget:
The CheckmarkListTile
semantically means the tile with a list of checkmarks.
It is a stateless widget so we need to manage its state in a ViewModel (Controller):
import 'package:get/get.dart';
import 'package:getx_miscellanous/app/global_widgets/checkmark_list_tile.dart';
class CheckmarkExampleController extends GetxController {
var _singleSelectOptions = [
CheckmarkListTileOption(title: 'Option 1', checked: true),
CheckmarkListTileOption(title: 'Option 2', checked: false),
CheckmarkListTileOption(title: 'Option 3', checked: false),
];
get singleSelectOptions => _singleSelectOptions;
var _multiSelectOptions = [
CheckmarkListTileOption(title: 'Option 1', checked: true),
CheckmarkListTileOption(title: 'Option 2', checked: true),
CheckmarkListTileOption(title: 'Option 3', checked: false),
CheckmarkListTileOption(title: 'Option 4', checked: false),
];
get multiSelectOptions => _multiSelectOptions;
void selectFromSingle(index) {
if (_singleSelectOptions[index].checked) {
return;
}
for (int i = 0; i < _singleSelectOptions.length; i++) {
_singleSelectOptions[i].checked = false;
}
_singleSelectOptions[index].checked = true;
update();
}
void selectFromMulti(index) {
int checkedCount = 0;
for (int i = 0; i < _multiSelectOptions.length; i++) {
if (_multiSelectOptions[i].checked) {
checkedCount++;
}
}
if (_multiSelectOptions[index].checked) {
if (checkedCount > 1) {
_multiSelectOptions[index].checked = false;
}
update();
return;
}
_multiSelectOptions[index].checked = true;
update();
}
}
Here is our View:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_miscellanous/app/global_widgets/checkmark_list_tile.dart';
import 'package:getx_miscellanous/app/modules/checkmark_example/checkmark_example_controller.dart';
import 'package:settings_ui/settings_ui.dart';
class CheckmarkExampleView extends GetView<CheckmarkExampleController> {
const CheckmarkExampleView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Settings example')),
body: Padding(
padding: const EdgeInsets.all(18.0),
child: GetBuilder<CheckmarkExampleController>(
builder: (controller) {
return SettingsList(
platform: DevicePlatform.android,
sections: [
SettingsSection(
title: Text('Single select section'),
tiles: [
CheckmarkListTile(
options: controller.singleSelectOptions,
onTap: controller.selectFromSingle),
],
),
SettingsSection(
title: Text('Multi select section'),
tiles: [
CheckmarkListTile(
options: controller.multiSelectOptions,
onTap: controller.selectFromMulti),
],
),
],
);
}
),
),
);
}
}
And here is the implementation:
import 'package:flutter/material.dart';
import 'package:settings_ui/settings_ui.dart';
import 'package:animated_checkmark/animated_checkmark.dart';
final class CheckmarkListTile extends AbstractSettingsTile {
final List<CheckmarkListTileOption> options;
final Function(int index) onTap;
final Color? checkmarkColor;
final Color? backgroundColor;
const CheckmarkListTile({
super.key,
required this.options,
required this.onTap,
this.checkmarkColor,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: backgroundColor ?? Theme.of(context).cardColor,
border: Border.all(
color: Colors.white,
width: 0.5,
),
borderRadius: BorderRadius.all(Radius.circular(10))),
clipBehavior: Clip.hardEdge,
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: options.length,
itemBuilder: (context, index) {
return Material(
color: backgroundColor ?? Theme.of(context).cardColor,
child: InkWell(
onTap: () {
onTap(index);
},
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Expanded(
child: Text(
options[index].title,
style: Theme.of(context).textTheme.bodyLarge,
),
),
const SizedBox(width: 16),
SizedBox(
width: 30,
height: 30,
child: Checkmark(
checked: options[index].checked,
color: checkmarkColor ?? Theme.of(context).primaryColor,
size: 30,
weight: 3,
),
),
],
),
),
),
);
},
),
);
}
}
final class CheckmarkListTileOption {
final String title;
bool checked;
CheckmarkListTileOption({required this.title, required this.checked});
}
Note the animated_checkmark
import. We need to add it to the project:
flutter pub add animated_checkmark
The CheckmarkListTile
can be used without the settings_ui package — just remove the extends AbstractSettingsTile.
Summary
With the settings_ui package, we make the settings pages in seconds.
Since it lacks important widgets we implemented the
CheckmarkListTile
.
Thank you for reading!
Subscribe to my newsletter
Read articles from Yuriy Novikov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
