I'm trying to implement a simple ChangeNotifier Provider in Flutter to handle switching my application from dark mode to light mode. I've read some of the cautionary tales of widgets down the widget tree rebuilding multiple times using a provider, and I have seem to fallen into the same trap.
First, here is the expected outcome (which works), switching my theme from light to dark:
But, tipped off by a simple print statement I left in my home screen main widget (a stateful widget), toggling the theme triggers the print statement to render at least 11-12 times each time.
My main widget triggers a FutureBuilder, but whatever I have in that widget does not seem to matter. The print statement will be called the same amount of times each time I call the ChangeNotifierProvider. My code in my main.dart:
void main() async {
final api = WtfdaApi.mainAPICall();
runApp(
MultiProvider(
providers: [
Provider(create: (context) => api),
ChangeNotifierProvider(
create: (_) => ThemeNotifier()),
],
child: Builder(
builder: (ctx) {
return MaterialApp(
theme: Provider.of<ThemeNotifier>(ctx).darkTheme! ? dark : light,
initialRoute: '/',
routes: {
'/': (context) =>
Scaffold(
body: HomeScreen(),
),
'SearchScreen': (context) =>
Scaffold(
body: FacilitySearchStartScreen(activityTitle: 'Search By Facility'),
),
'LocationSearchScreen': (context) =>
Scaffold(
body: LocationSearchStartScreen(activityTitle: 'Search By Location'),
),
}
);}
),
));
}
My ThemeProvider:
hemeData light = ThemeData(
// Define the default brightness and colors.
brightness: Brightness.light,
primaryColor: Colors.lightBlue[800],
// Define the default `TextTheme`. Use this to specify the default
// text styling for headlines, titles, bodies of text, and more.
textTheme: const TextTheme(
headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
headline5: TextStyle(fontSize: 18.0),
headline6: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold),
bodyText2: TextStyle(fontSize: 14.0, fontFamily: 'Hind'),
),
);
ThemeData dark = ThemeData(
// Define the default brightness and colors.
brightness: Brightness.dark,
primaryColor: Colors.lightBlue[800],
// Define the default `TextTheme`. Use this to specify the default
// text styling for headlines, titles, bodies of text, and more.
textTheme: const TextTheme(
headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
headline5: TextStyle(fontSize: 18.0),
headline6: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold),
bodyText2: TextStyle(fontSize: 14.0, fontFamily: 'Hind'),
),
);
class ThemeNotifier extends ChangeNotifier {
final String key = "theme";
SharedPreferences? _pref;
bool? _darkTheme;
bool? get darkTheme => _darkTheme;
ThemeNotifier() {
_darkTheme = true;
_loadFromPrefs();
}
toggleTheme(){
_darkTheme = !_darkTheme!;
_saveToPrefs(); // add this line
notifyListeners();
}
_initPrefs() async {
if(_pref == null)
_pref = await SharedPreferences.getInstance();
}
_loadFromPrefs() async {
await _initPrefs();
_darkTheme = _pref!.getBool(key) ?? true;
notifyListeners();
}
_saveToPrefs() async {
await _initPrefs();
_pref!.setBool(key, _darkTheme!);
}
}
And finally a sample of the home screen where I currently have the toggling available for the theme:
lass HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
Position? _currentPosition;
HelperFunctions helperFunctions = new HelperFunctions();
int? sortingPrefVal = 1;
int? signalPrefVal = 2;
bool locationServicesTimeOut = true;
bool locationServicesEnabled = true;
bool selected = false;
bool connected = true;
int? curSigStrengthVal;
List<RadioStation> fmDial = <RadioStation>[];
var tvStudyServerList;
var getTheCoords;
WtfdaApi wtfdaApi = new WtfdaApi.mainAPICall();
var _preferencesProvider;
final _debouncer = Debouncer(milliseconds: 500);
RadioLandSearchValues radioLandSearchValues = new RadioLandSearchValues();
callback() {
if (curSigStrengthVal != radioLandSearchValues.signalStrength){
tvStudyServerList = wtfdaApi.returnListOfStationsFromAPI(radioLandSearchValues);
}
helperFunctions.sortFMStations(radioLandSearchValues.sorting, fmDial);
helperFunctions.initializeLocationSearch(radioLandSearchValues);
setPrefs();
}
void setPrefs() {
helperFunctions.getSortingPrefFromSettings().then((value) => setState(() {
print('Hanging with the horseshoes');
radioLandSearchValues.sorting = value!;
}));
helperFunctions.getSignalPrefFromSettings().then((value) => setState(() {
radioLandSearchValues.signalStrength = value!;
curSigStrengthVal = value!;
}));
}
@override
void initState() {
_checkInternetConnection();
_getCurrentLocation();
super.initState();
setPrefs();
WidgetsBinding.instance!.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
buildNavigationDrawer(){
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child:ConstrainedBox(
constraints:
BoxConstraints(minWidth: 10, minHeight: 10),
child:
Image.asset(helperFunctions.getRadioLandLogo(),
width: 70,
height: 70,
),
),
),
ListTile(
leading: Icon(Icons.house_sharp),
title: Text(navMenuCurrent),
onTap: () => {Navigator.of(context).pop()},
),
ListTile(
leading: Icon(Icons.location_city),
title: Text(navMenuByLocation),
onTap: () => {Navigator.pushNamed(context, 'LocationSearchScreen')},
),
ListTile(
leading: Icon(Icons.headphones_battery),
title: Text(navMenuByCalls),
onTap: () => {Navigator.pushNamed(context, 'SearchScreen')},
),
ListTile(
leading: Icon(Icons.people_sharp),
title: Text(navMenuAboutTheApp),
onTap: () => {Navigator.pushNamed(context, 'AboutTheTeam')},
),
SwitchListTile(
value: Provider.of<ThemeNotifier>(context, listen: false).darkTheme!,
onChanged: (bool value) {Provider.of<ThemeNotifier>(context, listen: false).toggleTheme(); },
title: Text("Dark Mode"),
),
],
),
);
}
@override
Widget build(BuildContext context) {
Is the problem having a Stateful widget to begin with? That would just seem to cause so many challenges with how I have the rest of the home_screen laid out. Any feedback would be extremely appreciated.
