Instantly share code, notes, and snippets.
Created
August 30, 2021 19:23
-
Star
(1)
1
You must be signed in to star a gist -
Fork
(1)
1
You must be signed in to fork a gist
-
Save xsahil03x/a7538d393bf2356497bdee07c53dc7cd to your computer and use it in GitHub Desktop.
Simple counter button
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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