quantity_stepper 0.1.0
quantity_stepper: ^0.1.0 copied to clipboard
Accessible quantity stepper for Flutter: screen-reader adjust actions, 48dp targets, long-press repeat, keyboard support, a real FormField. No dependencies.
import 'package:flutter/material.dart';
import 'package:quantity_stepper/quantity_stepper.dart';
void main() => runApp(const ExampleApp());
/// Shows the main ways of using [QuantityStepper].
class ExampleApp extends StatelessWidget {
/// Creates the example app.
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'quantity_stepper',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1B7F5C)),
useMaterial3: true,
),
home: const ExamplePage(),
);
}
}
/// The single page of the example.
class ExamplePage extends StatefulWidget {
/// Creates the page.
const ExamplePage({super.key});
@override
State<ExamplePage> createState() => _ExamplePageState();
}
class _ExamplePageState extends State<ExamplePage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
bool _rtl = false;
bool _wheel = false;
bool _announce = false;
int _basic = 2;
int _guests = 1;
int _editable = 12;
String _formResult = 'Not submitted yet.';
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: _rtl ? TextDirection.rtl : TextDirection.ltr,
child: Scaffold(
appBar: AppBar(title: const Text('quantity_stepper')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
SwitchListTile(
title: const Text('Right-to-left layout'),
value: _rtl,
onChanged: (bool v) => setState(() => _rtl = v),
),
SwitchListTile(
title: const Text('Mouse wheel (needs focus)'),
value: _wheel,
onChanged: (bool v) => setState(() => _wheel = v),
),
SwitchListTile(
title: const Text('Announce changes'),
value: _announce,
onChanged: (bool v) => setState(() => _announce = v),
),
const Divider(height: 32),
_Section(
title: 'Basic',
caption:
'Between 1 and 10. Tap, hold to repeat, or focus and use '
'ArrowUp / ArrowDown / PageUp / PageDown.',
child: Row(
children: <Widget>[
QuantityStepper(
label: 'Quantity',
min: 1,
max: 10,
initialValue: _basic,
enableMouseWheel: _wheel,
announceChanges: _announce,
onChanged: (int v) => setState(() => _basic = v),
),
const SizedBox(width: 16),
Text('Selected: $_basic'),
],
),
),
_Section(
title: 'In a form',
caption:
'A real FormField<int>: validator, onSaved, autovalidate '
'and reset.',
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
const Text('Guests'),
const SizedBox(width: 16),
QuantityStepperFormField(
label: 'Guests',
initialValue: 1,
max: 8,
autovalidateMode: AutovalidateMode.onUserInteraction,
enableMouseWheel: _wheel,
announceChanges: _announce,
semanticFormatter: (int n) =>
n == 1 ? '1 guest' : '$n guests',
validator: (int? value) => (value ?? 0) < 2
? 'Add at least two guests.'
: null,
onSaved: (int? value) => _guests = value ?? 1,
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
FilledButton(
onPressed: () {
final FormState form = _formKey.currentState!;
if (form.validate()) {
form.save();
setState(() => _formResult = 'Saved: $_guests');
} else {
setState(
() => _formResult = 'Fix the errors first.',
);
}
},
child: const Text('Save'),
),
OutlinedButton(
onPressed: () {
_formKey.currentState!.reset();
setState(() => _formResult = 'Reset.');
},
child: const Text('Reset'),
),
Text(_formResult),
],
),
],
),
),
),
_Section(
title: 'Vertical',
caption: 'axis: Axis.vertical, with a step of 5.',
child: QuantityStepper(
label: 'Boxes',
axis: Axis.vertical,
step: 5,
max: 50,
enableMouseWheel: _wheel,
announceChanges: _announce,
),
),
_Section(
title: 'Editable',
caption:
'Type a number. A value in range applies as you type; out '
'of range is clamped when you press Done or leave the field.',
child: Row(
children: <Widget>[
QuantityStepper(
label: 'Pages',
editable: true,
min: 1,
max: 500,
step: 1,
largeStep: 25,
initialValue: _editable,
enableMouseWheel: _wheel,
announceChanges: _announce,
onChanged: (int v) => setState(() => _editable = v),
),
const SizedBox(width: 16),
Text('Pages: $_editable'),
],
),
),
_Section(
title: 'Custom style, compact',
caption:
'QuantityStepperTheme changes the colours and the painted '
'size. Every button is still a 48x48 target.',
child: Wrap(
spacing: 24,
runSpacing: 16,
children: const <Widget>[
QuantityStepper(
label: 'Seats',
compact: true,
max: 12,
incrementIcon: Icons.add_circle_outline,
decrementIcon: Icons.remove_circle_outline,
style: QuantityStepperTheme(
buttonColor: Color(0xFF1B7F5C),
iconColor: Colors.white,
borderColor: Color(0xFF1B7F5C),
),
),
QuantityStepper(
label: 'Disabled example',
initialValue: 3,
enabled: false,
),
],
),
),
],
),
),
);
}
}
class _Section extends StatelessWidget {
const _Section({
required this.title,
required this.caption,
required this.child,
});
final String title;
final String caption;
final Widget child;
@override
Widget build(BuildContext context) {
final TextTheme text = Theme.of(context).textTheme;
return Padding(
padding: const EdgeInsets.only(bottom: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Semantics(header: true, child: Text(title, style: text.titleMedium)),
const SizedBox(height: 4),
Text(caption, style: text.bodySmall),
const SizedBox(height: 12),
child,
],
),
);
}
}