Created
April 30, 2020 19:01
-
-
Save ScottS2017/dea48cb72fabed042e3e4972914bb1d7 to your computer and use it in GitHub Desktop.
An example of the ValueNotifierSimplified approach to State Management
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 'package:flutter/material.dart'; | |
class VnsObject{ | |
/// First, declare / instantiate any private class members | |
static final List<Color> _colors = [ | |
Colors.white, | |
Colors.yellow, | |
Colors.orange, | |
Colors.purple, | |
]; | |
static final List<String> _colorNames = [ | |
'White', | |
'Yellow', | |
'Orange', | |
'Purple', | |
]; | |
/// Second, put any publicly accessible member variables we want to | |
/// use in our UI, however each of these goes in its own ValueNotifier | |
/// | |
/// So, instead of just using a "Color" here like we normally would, | |
/// we use "ValueNotifier<Color>" instead | |
/// | |
/// Technical Tip: The reason we aren't calling the VNS Object a | |
/// Model is that the job a model normally does is being done by | |
/// *each* ValueNotifier here. So, what we actually have is a VNS | |
/// Object that's full of Models | |
/// | |
/// Notice that , unlike when using a ChangeNotifier and Consumer, | |
/// we're not limited to only one notification of any single type. Also, | |
/// *this can be used effectively with primitives*. Those two things | |
/// together mean we can use this approach with every publicly | |
/// accessible member in the object, including primitives, no | |
/// matter how many we might have of the same type | |
/// | |
/// To demonstrate, we'll use two colors and two strings | |
/// within one VNS Object | |
/// | |
/// When you read the constructor, the first one would read as: | |
/// "The ValueNotifier 'objectColor1' is an instance of a | |
/// ValueNotifier that handles a Color, and its value | |
/// property is being instantiated as 'Colors.white'." | |
ValueNotifier<Color> objectColor1Notifier = ValueNotifier<Color>(Colors.white); | |
ValueNotifier<Color> objectColor2Notifier = ValueNotifier<Color>(Colors.white); | |
ValueNotifier<String> colorName1Notifier = ValueNotifier<String>(_colorNames[0]); | |
ValueNotifier<String> colorName2Notifier = ValueNotifier<String>(_colorNames[0]); | |
/// ********************** IMPORTANT | |
/// When you need to use the color, you have to remember that | |
/// objectColor1Notifier is NOT a Color. The actual Color is contained | |
/// in the 'value' property, which you access like this: | |
/// objectColor1Notifier.value | |
/// | |
/// You will make that mistake... count on it. | |
/// Last, we put any logic that affects our member variables. These are what | |
/// some call "Controller" as in MVC, or "Presenter" as in MVP. | |
/// | |
/// There is no need to use "notifyListeners", as changing a | |
/// ValueNotifier causes it to trigger a rebuild in its listeners | |
/// automatically | |
void changeTheColor1() { | |
objectColor1Notifier.value = _colors[(_colors.indexOf(objectColor1Notifier.value) + 1) % _colors.length]; | |
changeTheText1(); | |
} | |
void changeTheColor2() { | |
objectColor2Notifier.value = _colors[(_colors.indexOf(objectColor2Notifier.value) + 1) % _colors.length]; | |
changeTheText2(); | |
} | |
void changeTheText1() { | |
colorName1Notifier.value = _colorNames[(_colorNames.indexOf(colorName1Notifier.value) + 1) % _colorNames.length]; | |
} | |
void changeTheText2() { | |
colorName2Notifier.value = _colorNames[(_colorNames.indexOf(colorName2Notifier.value) + 1) % _colorNames.length]; | |
} | |
} | |
void main() { | |
runApp(VNSExample()); | |
} | |
class VNSExample extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'ReactiveModel-View Example', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
home: const UserInterface(), | |
); | |
} | |
} | |
class UserInterface extends StatefulWidget { | |
const UserInterface({ | |
Key key, | |
}) : super(key: key); | |
@override | |
_UserInterfaceState createState() => _UserInterfaceState(); | |
} | |
class _UserInterfaceState extends State<UserInterface> { | |
/// Provide instances of our VNS Object to use. | |
final VnsObject vnsInstanceOne = VnsObject(); | |
final VnsObject vnsInstanceTwo = VnsObject(); | |
final VnsObject vnsInstanceThree = VnsObject(); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('VNS Example (All Icons Clickable)'), | |
), | |
body: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: <Widget>[ | |
SizedBox( | |
/// This is the lazy way to divide the screen into thirds and | |
/// leave enough room for the vertical lines | |
width: MediaQuery.of(context).size.width * .3, | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: <Widget>[ | |
const Center( | |
/// | |
/// | |
/// This is the top section of the left column | |
/// | |
/// | |
child: Text( | |
'Instance 1\nobjectColor1\ncolorName1\n\n(Toggled by\nboth icons)', | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 16, | |
), | |
), | |
), | |
/// This works the same way for all of the rest, so we'll | |
/// only go over it once | |
/// | |
/// The docs refer to a ValueListenableBuilder as: | |
/// "A widget whose content stays synced with a | |
/// [ValueListenable]." | |
ValueListenableBuilder<Color>( | |
/// | |
/// The docs refer to the valueListenable as: | |
/// "The [ValueListenable] whose value you depend | |
/// on in order to build." | |
/// | |
/// To use it, you first tell it which instance to use | |
/// (vnsInstanceOne) and then tell it which ValueNotifier | |
/// of that instance we need to be listening to | |
/// (objectColor1Notifier). | |
valueListenable: vnsInstanceOne.objectColor1Notifier, | |
/// We're going to use some weird names here just once | |
/// as part of trying to help you understand what's going | |
/// on. | |
/// | |
/// When you create the builder, it first needs the current | |
/// context. | |
/// | |
/// The next thing you need to do is declare a variable | |
/// that will be used to hold the value passed in by | |
/// the ValueNotifier each time we get a notification. | |
/// Normally, this variable is just called "value", but for | |
/// teaching purposes we're going to call this first one | |
/// "theValuePassedInFromTheValueNotifierOnAnUpdate" | |
/// | |
/// After this first one, we'll just use the normal name, | |
/// "value". | |
/// | |
/// The child is an interesting and important thing to | |
/// understand. What if the Widget being rebuilt | |
/// on every update has a big, complicated tree | |
/// of Widgets for its child, and that child tree | |
/// doesn't need to rebuild on update? We would | |
/// be wasting a lot of time rebuilding all those | |
/// other Widgets unnecessarily. | |
/// | |
/// This can get confusing, especially since the linter | |
/// insists a child parameter be placed last. | |
/// | |
/// What we're doing here is declaring a builder | |
/// function, and the last parameter is going to be the | |
/// child of the Widget we're returning (here, the FAB). | |
/// | |
/// It's where we get this child Widget from that can | |
/// get confusing to some people. It's going to be | |
/// whatever you specify as the child of the | |
/// ValueListenableBuilder, which is going to be located | |
/// **below** the end of this function, since the linter | |
/// is going to force us to put the child parameter last. | |
/// | |
/// This parameter is normally called "child", but for | |
/// teaching purposes we'll call this first one | |
/// "theChildOfTheValueListenableBuilder" but, after | |
/// this one, we'll go back to calling it "child". | |
builder: ( | |
BuildContext context, | |
Color theValuePassedInFromTheValueNotifierOnAnUpdate, | |
Widget theChildOfTheValueListenableBuilder, | |
) { | |
return FloatingActionButton( | |
backgroundColor: theValuePassedInFromTheValueNotifierOnAnUpdate, | |
onPressed: () => vnsInstanceOne.changeTheColor1(), | |
child: theChildOfTheValueListenableBuilder, | |
); | |
}, | |
/// This is the child of the ValueListenableBuilder | |
child: const Icon( | |
Icons.lightbulb_outline, | |
color: Colors.black54, | |
), | |
), | |
/// This FAB's color doesn't change. The only | |
/// thing it has to do with our instance is that it | |
/// triggers the function. | |
FloatingActionButton( | |
onPressed: () => vnsInstanceOne.changeTheColor1(), | |
child: const Icon( | |
Icons.settings_power, | |
color: Colors.black54, | |
), | |
), | |
/// The ValueListenableBuilder for the String is almost | |
/// the same as the one for the Color | |
ValueListenableBuilder<String>( | |
valueListenable: vnsInstanceOne.colorName1Notifier, | |
/// The Text doesn't have a child, so no child was | |
/// specified for the ValueListenableBuilder. Since | |
/// there is no child, we just put _ in the third parameter | |
/// as a meaningless placeholder. | |
/// | |
/// The value we're using here is a String | |
builder: (BuildContext context, String value, _) { | |
return Text( | |
/// This is where it gets used | |
value, | |
style: const TextStyle( | |
fontSize: 24, | |
), | |
); | |
}, | |
), | |
/// That's it! Take a look at the rest of these and see if | |
/// you can pick out the different parts that make each | |
/// one work. | |
const HorizontalLine(), | |
/// | |
/// | |
/// This is the bottom section of the left column | |
/// | |
/// | |
const Center( | |
child: Text( | |
'Instance 1\nobjectColor2\ncolorName2', | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 16, | |
), | |
), | |
), | |
ValueListenableBuilder<Color>( | |
valueListenable: vnsInstanceOne.objectColor2Notifier, | |
builder: (BuildContext context, Color value, Widget child) { | |
return FloatingActionButton( | |
backgroundColor: value, | |
onPressed: () => vnsInstanceOne.changeTheColor2(), | |
child: child, | |
); | |
}, | |
child: const Icon( | |
Icons.lightbulb_outline, | |
color: Colors.black54, | |
), | |
), | |
ValueListenableBuilder<String>( | |
valueListenable: vnsInstanceOne.colorName2Notifier, | |
builder: (BuildContext context, String value, _) { | |
return Text( | |
value, | |
style: const TextStyle( | |
fontSize: 24, | |
), | |
); | |
}, | |
), | |
], | |
), | |
), | |
const VerticalLine(), | |
SizedBox( | |
width: MediaQuery.of(context).size.width * .3, | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: <Widget>[ | |
/// | |
/// | |
/// This is the top section of the middle column | |
/// | |
/// | |
const Center( | |
child: Text( | |
'Instance 2\nobjectColor1\ncolorName1\n\n(Toggled by\nboth icons)', | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 16, | |
), | |
), | |
), | |
ValueListenableBuilder<Color>( | |
valueListenable: vnsInstanceTwo.objectColor1Notifier, | |
builder: (BuildContext context, Color value, Widget child) { | |
return FloatingActionButton( | |
backgroundColor: value, | |
onPressed: () => vnsInstanceTwo.changeTheColor1(), | |
child: child, | |
); | |
}, | |
child: const Icon( | |
Icons.lightbulb_outline, | |
color: Colors.black54, | |
), | |
), | |
FloatingActionButton( | |
onPressed: () => vnsInstanceTwo.changeTheColor1(), | |
backgroundColor: Colors.blue, | |
child: const Icon( | |
Icons.settings_power, | |
color: Colors.black54, | |
), | |
), | |
ValueListenableBuilder<String>( | |
valueListenable: vnsInstanceTwo.colorName1Notifier, | |
builder: (BuildContext context, String value, _) { | |
return Text( | |
value, | |
style: const TextStyle( | |
fontSize: 24, | |
), | |
); | |
}, | |
), | |
const HorizontalLine(), | |
/// | |
/// | |
/// This is the bottom section of the middle column | |
/// | |
/// | |
const Center( | |
child: Text( | |
'Instance 2\nobjectColor2\ncolorName2', | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 16, | |
), | |
), | |
), | |
ValueListenableBuilder<Color>( | |
valueListenable: vnsInstanceTwo.objectColor2Notifier, | |
builder: (BuildContext context, Color value, Widget child) { | |
return FloatingActionButton( | |
backgroundColor: value, | |
onPressed: () => vnsInstanceTwo.changeTheColor2(), | |
child: child, | |
); | |
}, | |
child: const Icon( | |
Icons.lightbulb_outline, | |
color: Colors.black54, | |
), | |
), | |
ValueListenableBuilder<String>( | |
valueListenable: vnsInstanceTwo.colorName2Notifier, | |
builder: (BuildContext context, String value, _) { | |
return Text( | |
value, | |
style: const TextStyle( | |
fontSize: 24, | |
), | |
); | |
}, | |
), | |
], | |
), | |
), | |
const VerticalLine(), | |
SizedBox( | |
width: MediaQuery.of(context).size.width * .3, | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: <Widget>[ | |
/// | |
/// | |
/// This is the top section of the right column | |
/// | |
/// | |
const Center( | |
child: Text( | |
'Instance 3\nobjectColor1\ncolorName1\n\n(Toggled by\nboth icons)', | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 16, | |
), | |
), | |
), | |
ValueListenableBuilder<Color>( | |
valueListenable: vnsInstanceThree.objectColor1Notifier, | |
builder: (BuildContext context, Color value, Widget child) { | |
return FloatingActionButton( | |
backgroundColor: value, | |
onPressed: () => vnsInstanceThree.changeTheColor1(), | |
child: child, | |
); | |
}, | |
child: const Icon( | |
Icons.settings_power, | |
color: Colors.black54, | |
), | |
), | |
FloatingActionButton( | |
onPressed: () => vnsInstanceThree.changeTheColor1(), | |
child: const Icon( | |
Icons.settings_power, | |
color: Colors.black54, | |
), | |
), | |
ValueListenableBuilder<String>( | |
valueListenable: vnsInstanceThree.colorName1Notifier, | |
builder: (BuildContext context, String value, _) { | |
return Text( | |
value, | |
style: const TextStyle( | |
fontSize: 24, | |
), | |
); | |
}, | |
), | |
const HorizontalLine(), | |
/// | |
/// | |
/// This is the bottom section of the right column | |
/// | |
/// | |
const Center( | |
child: Text( | |
'Instance 3\nobjectColor2\ncolorName2', | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 16, | |
), | |
), | |
), | |
ValueListenableBuilder<Color>( | |
valueListenable: vnsInstanceThree.objectColor2Notifier, | |
builder: (BuildContext context, Color value, Widget child) { | |
return FloatingActionButton( | |
backgroundColor: value, | |
onPressed: () => vnsInstanceThree.changeTheColor2(), | |
child: child, | |
); | |
}, | |
child: const Icon( | |
Icons.lightbulb_outline, | |
color: Colors.black54, | |
), | |
), | |
ValueListenableBuilder<String>( | |
valueListenable: vnsInstanceThree.colorName2Notifier, | |
builder: (BuildContext context, String value, _) { | |
return Text( | |
value, | |
style: const TextStyle( | |
fontSize: 24, | |
), | |
); | |
}, | |
), | |
], | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class VerticalLine extends StatelessWidget { | |
const VerticalLine({ | |
Key key, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Center( | |
child: Container( | |
height: MediaQuery.of(context).size.height * 0.8, | |
width: 3, | |
color: Colors.black45, | |
), | |
); | |
} | |
} | |
class HorizontalLine extends StatelessWidget { | |
const HorizontalLine({ | |
Key key, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Center( | |
child: Container( | |
width: MediaQuery.of(context).size.width * 0.2, | |
height: 3, | |
color: Colors.black45, | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment