Skip to content

Instantly share code, notes, and snippets.

@xsahil03x
Created August 30, 2021 19:23
Show Gist options
  • Save xsahil03x/a7538d393bf2356497bdee07c53dc7cd to your computer and use it in GitHub Desktop.
Save xsahil03x/a7538d393bf2356497bdee07c53dc7cd to your computer and use it in GitHub Desktop.
Simple counter button
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rxdart/rxdart.dart';
typedef CounterEdit = Future<bool> Function(int);
/// Returns true if the [valueBeforeZero] is handled by user.
typedef OnValueZero = Future<bool> Function(int valueBeforeZero);
const double _kButtonHeight = 32.0;
const double _kButtonRadius = 8.0;
const double _kButtonWidth = 32.0;
const double _kCounterWidth = 56.0;
const double _kCounterHeight = _kButtonHeight;
class CounterController {
final int initialValue;
final _subjectCurrentValue = BehaviorSubject<MapEntry<bool, int>>();
ValueStream<MapEntry<bool, int>> get currentValue =>
_subjectCurrentValue.stream;
void setCurrentValue(int value, [bool updateText = true]) =>
_subjectCurrentValue.add(MapEntry(updateText, value));
final _textController = TextEditingController();
TextEditingController get textController => _textController;
FocusNode _node = FocusNode();
FocusNode get node => _node;
void noValueEnteredListener() {
if (!node.hasFocus) {
if (_textController.text.isEmpty) {
_textController.text = "0";
}
}
}
CounterController(this.initialValue) {
_textController.text = initialValue.toString();
_node.addListener(noValueEnteredListener);
}
dispose() {
_node.removeListener(noValueEnteredListener);
_subjectCurrentValue?.close();
_textController?.dispose();
_node?.dispose();
}
}
class CounterWidget extends StatefulWidget {
final int initialValue;
final CounterEdit onChange;
final CounterController counterController;
final Color iconColor;
final Color textColor;
final Color textBackgroundColor;
final Color iconBackgroundColor;
final double buttonWidth;
final double buttonRadius;
final double counterWidth;
final double buttonPadding;
final bool enabled;
final bool showControls;
final int maxLength;
final OnValueZero onValueZero;
const CounterWidget({
Key key,
this.initialValue,
@required this.onChange,
this.iconColor = const Color(0xff005486),
this.textColor = Colors.black,
this.textBackgroundColor = const Color(0xffdaf5ff),
this.iconBackgroundColor = const Color(0xFFF5F5F5),
this.buttonWidth = _kButtonWidth,
this.buttonRadius = _kButtonRadius,
this.counterWidth = _kCounterWidth,
this.buttonPadding,
this.enabled = true,
this.showControls = true,
this.maxLength = 3,
this.onValueZero,
@required this.counterController,
}) : super(key: key);
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
StreamSubscription<MapEntry<bool, int>> _currentValueSubscription;
@override
void initState() {
super.initState();
_subscribeToCurrentValue();
}
@override
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.counterController.currentValue !=
widget.counterController.currentValue) {
if (_currentValueSubscription != null) {
_unsubscribeToCurrentValue();
}
_subscribeToCurrentValue();
}
}
_subscribeToCurrentValue() {
if (_currentValueSubscription == null) {
_currentValueSubscription =
widget?.counterController?.currentValue?.listen((it) {
if (it.key) {
final value = it.value.toString();
widget.counterController.textController.text = value;
}
});
}
}
_unsubscribeToCurrentValue() {
if (_currentValueSubscription != null) {
_currentValueSubscription.cancel();
_currentValueSubscription = null;
}
}
@override
void dispose() {
_unsubscribeToCurrentValue();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: widget.counterController.currentValue.map((it) => it.value),
initialData: widget.initialValue ?? 0,
builder: (context, snapshot) {
var children = [
Container(
height: widget.buttonWidth,
width: widget.counterWidth,
decoration: BoxDecoration(
color: widget.textBackgroundColor,
borderRadius:
BorderRadius.circular(widget.showControls ? 0 : 8),
),
child: TextField(
inputFormatters: [
WhitelistingTextInputFormatter(RegExp(r'[0-9]')),
LengthLimitingTextInputFormatter(widget.maxLength),
],
textAlign: TextAlign.center,
controller: widget.counterController.textController,
keyboardType: TextInputType.number,
onTap: () {
if (widget.counterController.textController.text == "0") {
widget.counterController.textController.text = "";
}
},
enabled: widget.enabled,
onChanged: (v) async {
var value = int.tryParse(v) ?? 0;
var updateText = false;
if (widget.onValueZero != null &&
widget.initialValue != 0 &&
value == 0) {
updateText = true;
final previousValue = snapshot.data;
final isHandled = await widget.onValueZero(previousValue);
if (!isHandled) value = previousValue;
}
widget.counterController.setCurrentValue(value, updateText);
widget.onChange(value);
},
focusNode: widget.counterController.node,
decoration: InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide.none),
contentPadding: EdgeInsets.all(4.0),
),
cursorColor: Colors.black,
style: Theme.of(context).textTheme.bodyText2.copyWith(
color: widget.textColor,
),
),
)
];
var width = widget.counterWidth;
if (widget.showControls) {
width += 2 * widget.buttonWidth;
children = [
Container(
height: widget.buttonWidth,
width: widget.buttonWidth,
child: Center(
child: FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.horizontal(
left: Radius.circular(widget.buttonRadius),
),
),
color: widget.iconBackgroundColor,
padding: EdgeInsets.all(widget.buttonPadding ?? 0),
child: FittedBox(
child: Icon(Icons.remove, color: widget.iconColor),
),
onPressed: () async {
final previousValue = snapshot.data;
var newValue = previousValue <= 0 ? 0 : previousValue - 1;
if (widget.onValueZero != null) {
if (widget.initialValue != 0 && newValue == 0) {
final isHandled =
await widget.onValueZero(previousValue);
if (!isHandled) newValue = previousValue;
}
}
FocusScope.of(context).unfocus();
if (await widget.onChange(newValue)) {
if (newValue.toString().length <= widget.maxLength) {
widget.counterController.setCurrentValue(newValue);
}
}
},
),
),
),
...children,
Container(
height: widget.buttonWidth,
width: widget.buttonWidth,
child: FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.horizontal(
right: Radius.circular(widget.buttonRadius),
),
),
padding: EdgeInsets.all(widget.buttonPadding ?? 0),
child: FittedBox(
child: Icon(
Icons.add,
color: widget.iconColor,
)),
color: widget.iconBackgroundColor,
onPressed: () async {
final previousValue = snapshot.data;
final newValue = previousValue + 1;
FocusScope.of(context).unfocus();
if (await widget.onChange(newValue)) {
if (newValue.toString().length <= widget.maxLength) {
widget.counterController.setCurrentValue(newValue);
}
}
},
),
)
];
}
return Container(
width: width,
child: Row(children: children),
);
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment