Skip to content

Instantly share code, notes, and snippets.

@ScottS2017
Created April 30, 2020 19:01
Show Gist options
  • Save ScottS2017/dea48cb72fabed042e3e4972914bb1d7 to your computer and use it in GitHub Desktop.
Save ScottS2017/dea48cb72fabed042e3e4972914bb1d7 to your computer and use it in GitHub Desktop.
An example of the ValueNotifierSimplified approach to State Management
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