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.
CodePudding user response:
I had similar issues like you and I hope this comment would work for you.
In my case, I listened the whole provider model under the build.
For example,
Class _HomeScreenStatus extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
// context.select work the same as Provider.of<T>(context)
final _theme = context.select<ThemeNotifier>();
// return your widget
return Widget;
In this case whenever the ThemeNotifier changed, the Widget also reloaded. And if there are multiple notifyListeners() within the ChangeNotifier, the Widget would reloaded multiple time.
I solved the issue by using provider select method instead watch method.
context.select<ThemeNotifier, bool>((value) => value.darkTheme);
The select only listened the darkTheme change and only reload when the value is changed.
In your case, I assume the Provider.of part would be fired multiple time.
theme: Provider.of(ctx).darkTheme! ? dark : light,
Maybe using ctx.select<ThemeNotifier, bool>((theme) => theme.darkTheme) could work for you.