How to theme with Hive and Riverpod | Flutter - flutter

I am trying to implement my very own Theming system that for now consists of only the color palette. This palette is different based on which Theme is selected.
The logic: When the app first starts or the user did not change the AppSettings yet, the App should use the light or dark theme based on the system settings. The indicator for "use system settings" is that no value is provided for the key theme in Hive.
Whenever the user changes the settings from "use system settings" to "light", "dark" or "christmas", the value should be 1, 2 or 3 respectively. When switching back to system settings, the value gets deleted and the system theme should be get.
First I created an abstract class that defines the colors that can be used and will be implemented by the different theme classes:
abstract class CustomTheme {
Color get backgroundColor;
Color get secondaryBackgroundColor;
}
class LightTheme implements CustomTheme {
#override
Color get backgroundColor => Palette.white;
#override
Color get secondaryBackgroundColor => Palette.gray200;
}
class DarkTheme implements CustomTheme {
#override
Color get backgroundColor => Palette.gray900;
#override
Color get secondaryBackgroundColor => Palette.gray800;
}
class ChristmasTheme implements CustomTheme {
#override
Color get backgroundColor => Palette.green700;
#override
Color get secondaryBackgroundColor => Palette.green600;
}
I found out that with the help of a StatefulClass I am able to override didChangePlatformBrightness which is called whenever the system theme changes.
I tried creating a class that later on can be used to wrap around MaterialApp.
class ThemeBuilder extends StatefulWidget {
const ThemeBuilder({
super.key,
required this.child
});
final Widget child;
#override
State<ThemeBuilder> createState() => _ThemeBuilderState();
}
class _ThemeBuilderState extends State<ThemeBuilder> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this); // TODO: Needed?
// TODO: Start listening to changes from Hive.box('themeBox')
// If value changes to NULL, get system theme
// Otherwise set to theme based on value
// 1=Light, 2=Dark, 3=Christmas
}
#override
void didChangePlatformBrightness() {
super.didChangePlatformBrightness();
bool systemIsDarkMode = SchedulerBinding.instance.platformDispatcher.platformBrightness == Brightness.dark;
// TODO: If value in Hive is NULL, set the system theme mode
// Otherwise do nothing
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
This should also listen to changes, because whenever another theme is selected in Hive, the UI should change immediately.
I can not find any way to implement this behavior so that I can simply do something like CustomTheme.backgroundColor for the color parameters.
The Box will be open since the start of the app:
void main() async {
...
await Hive.openBox('themeBox');
...
}
PS: I don't want to make use of the default theme parameters inside the MaterialApp constructor.

You could always create a ThemeProvider class that houses all the code needed to store and save theme data via hive and then listen to the provider in your material app widget and use it as a theme.
Also I would recommend using Enums if your colors are constants
enum CustomTheme {
lightTheme(
backgroundColor: Palette.white,
secondaryBackgroundColor: Palette.gray200,
),
darkTheme(
backgroundColor: Palette.gray900,
secondaryBackgroundColor: Palette.gray800,
)
christmasTheme(
backgroundColor: Palette.green700,
secondaryBackgroundColor: Palette.green600,
);
const CustomTheme({
required this.backgroundColor,
required this.secondaryBackgroundColor,
});
final Color backgroundColor;
final Color secondaryBackgroundColor;
}
final themeProvider = ChangeNotifierProvider((_) => ThemeProvider());
/// Instance of ThemeProvider
class ThemeProvider extends ChangeNotifier {
CustomTheme _currentTheme = CustomTheme.lightTheme;
CustomTheme get currentTheme => _currentTheme;
set currentTheme(Color val) {
_currentTheme = val;
notifyListeners();
}
...
/// Use hive to save theme data as hex strings or something similar
Future<void> saveTheme(){}
/// Use hive to load saved theme data from hex or something similar
Future<void> loadTheme(){}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const ProviderScope(
child: ProviderScopeApp(),
);
}
}
class ProviderScopeApp extends HookConsumerWidget {
const ProviderScopeApp({super.key});
#override
Widget build(BuildContext context, ref) {
final currentTheme = ref.watch(themeVM.select(it) => it.currentTheme);
return MaterialApp(
title: 'Your app',
debugShowCheckedModeBanner: false,
theme: ThemeData(), /// override the default values here with values from `currentTheme`
...
);
}
}

Related

How to create custom class in Flutter by extending ThemeData and an abstract class

I want to setup dark mode in my Flutter app, but not in traditional way.
In my earlier apps, I was not having dark mode, and I was using a class AppColors where every variable were declared as static const and I was accessing them as AppColors.primaryColor. But for the new projects I am using darkmode, but I don't want to go with the traditional way, or conditionally assign colors.
Instead I want to assign colors like Get.theme.primaryColor.
I am trying to create an abstract class AppTheme and two other classes AppLightTheme and AppDarkTheme which will extend ThemeData and implement AppTheme.
abstract class AppTheme {
// I want to create my own theme here
Color get primaryColor;
Color get accentColor;
Color get backgroundColor;
}
class AppDarkTheme extends ThemeData implements AppTheme{
#override
Color get primaryColor => Color(0xFF000000);
#override
Color get accentColor => Color(0xFF000000);
#override
Color get backgroundColor => Color(0xFF000000);
// I want to create my own theme here for dark mode
}
class AppLightTheme extends ThemeData implements AppTheme{
#override
Color get primaryColor => Color(0xFFFFFFFF);
#override
Color get accentColor => Color(0xFFFFFFFF);
#override
Color get backgroundColor => Color(0xFFFFFFFF);
// I want to create my own theme here for light mode
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetMaterialApp(
title: AppStrings.appTitle,
initialRoute: AppPages.INITIAL,
initialBinding: BaseBindings(),
theme: AppLightTheme(),
darkTheme: AppDarkTheme(),
getPages: AppPages.routes,
);
}
}

How to set ThemeMode in splash screen using value stored in sqflite FLUTTER

I have a Flutter Application where an sqflite database stored the user preference of ThemeMode (viz Dark, Light and System). I have created a splash screen using flutter_native_splash which supports dark mode too.
The Problem is this that I want the splash screen to follow the users stored value for theme mode. Currently, the code I am using is as follows:
class MyRoot extends StatefulWidget {
// const MyRoot({Key? key}) : super(key: key);
static ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
#override
State<MyRoot> createState() => _MyRootState();
}
class _MyRootState extends State<MyRoot> {
DatabaseHelper? databaseHelper = DatabaseHelper.dhInstance;
ThemeMode? tmSaved;
#override
void initState() {
Future.delayed(Duration.zero, () async => await loadData());
super.initState();
}
#override
Widget build(BuildContext context) {
//to prevent auto rotation of the app
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
return ValueListenableBuilder<ThemeMode>(
valueListenable: MyRoot.themeNotifier,
builder: (_, ThemeMode currentMode, __) {
return Sizer(
builder: (context, orientation, deviceType) {
return MaterialApp(
title: 'My Application',
theme: themeLight, //dart file for theme
darkTheme: themeDark, //dart file for theme
themeMode: tmSaved ?? currentMode,
initialRoute: // my initial root
routes: {
// my routes
.
.
.
// my routes
},
);
},
);
},
);
}
Future<void> loadData() async {
if (databaseHelper != null) {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
if (themeMode != null) {
MyRoot.themeNotifier.value = themeMode;
return;
}
}
MyRoot.themeNotifier.value = ThemeMode.system;
}
}
Currently, this shows a light theme splash screen loading, then converts it into dark with a visible flicker.
ValueListenableBuilder<ThemeMode>(... is to enable real time theme change from settings page in my app which working as intended (taken from A Goodman's article: "Flutter: 2 Ways to Make a Dark/Light Mode Toggle".
main.dart has the below code:
void main() {
runApp(MyRoot());
}
Have you tried loading the setting from sqflite in main() before runApp? If you can manage to do so, you should be able to pass the setting as argument to MyRoot and then the widgets would be loaded from the start with the correct theme. I'm speaking in theory, I can't test what I'm suggesting right now.
Something like:
void main() async {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
runApp(MyRoot(themeMode));
}
[...]
class MyRoot extends StatefulWidget {
ThemeMode? themeMode;
const MyRoot(this.themeMode, {Key? key}) : super(key: key);
static ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
#override
State<MyRoot> createState() => _MyRootState();
}
EDIT
Regarding the nullable value you mentioned in comments, you can change the main like this:
void main() async {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
themeMode ??= ThemeMode.system;
runApp(MyRoot(themeMode!));
}
which makes themeMode non-nullable, and so you can change MyRoot in this way:
class MyRoot extends StatefulWidget {
ThemeMode themeMode;
const MyRoot(required this.themeMode, {Key? key}) : super(key: key);
[...]
}
Regarding the functionality of ValueNotifier, I simply thought of widget.themeMode as the initial value of your tmSaved property in your state, not as a value to be reused in the state logic. Something like this:
class _MyRootState extends State<MyRoot> {
DatabaseHelper? databaseHelper = DatabaseHelper.dhInstance;
late ThemeMode tmSaved;
#override
void initState() {
tmSaved = widget.themeMode;
super.initState();
}
[...]
}
so that your widgets would already have the saved value at the first build.
PS the code in this edit, as well as in the original part, isn't meant to be working by simply pasting it. Some things might need adjustments, like adding final to themeMode in MyRoot.
Make your splashscreen. A main widget which get data from sqlflite
And make splashscreen widget go to the your home widget with remove it using navigation pop-up
for example :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ToDo',
color: // color of background
theme: // theme light ,
darkTheme: // darktheme
themeMode: // choose default theme light - dark - system
home: Splashscreen(),// here create an your own widget of splash screen contains futurebuilder to fecth data and return the mainWidget ( home screen for example)
);
}
}
class Splashscreen extends StatelessWidget {
Future<bool> getData()async{
// get info
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getData(),
builder: (context,snapshot){
// if you want test snapshot
//like this
if(snapshot.hasData) {
return Home();
} else {
return Container(color: /* background color as same as theme's color */);
}
}
);
}
}

Flutter theming: InheritedWidget vs global object

In my application I have a custom ThemeProvider implemented in InheritedWidget (default Theme provided by Flutter is a bit too rigid with regards to what a theme can be):
class ThemeProvider extends InheritedWidget {
final AppTheme theme;
const ThemeProvider({Key? key, required Widget child, required this.theme}): super(key: key, child: child);
static AppTheme of(BuildContext context) {
final provider = context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
return provider?.theme ?? AppTheme.defaultTheme;
}
#override
bool updateShouldNotify(ThemeProvider oldWidget) {
return theme != oldWidget.theme;
}
}
Inside a component I can require some specific portion of the theme as needed:
class StyledIcon extends StatelessWidget {
final IconData icon;
final double? size;
final Color? color;
const StyledIcon(this.icon, {Key? key, this.size, this.color}): super(key: key);
#override
Widget build(BuildContext context) {
final theme = ThemeProvider.of(context).icon;
return Icon(
icon,
size: size ?? theme.size,
color: color ?? theme.color
);
}
}
As far as managing application theme goes, what's the benefit of passing theme data around through an InheritedWidget? Why won't a global theme object suffice?
For most applications it doesn't make much of a difference. Inherited widgets have the advantage that you can scope your theme to certain parts of your app. So if you want to scope your theme at all, use an inherited widget. If you don't care about that you can stick with a global object.

Changing persistent colors for all app widgets in runtime by pressing the button

I would like some advice from you if you have time.
I have created a class that will manage colors for certain widgets in most classes and in various types of widgets (stateless and stateful). Changing the value of AppPreferences.instance().themeIsDark (true / false) by pressing a button and using the setState () method unfortunately the colors change only on the current widget (current window). Is it possible to change the variable to change colors in all classes and widgets where they are used?
class AppTheme {
AppTheme._();
static Color myWhite() => AppPreferences.instance().themeIsDark! ? Color(0xFFFFFFFF) : Color(0xFF000000);
static Color notWhite() => AppPreferences.instance().themeIsDark! ? Color(0xFFECF0F1) : Color(0xFF1A1A1A);
static Color nearlyWhite() => AppPreferences.instance().themeIsDark! ? Color(0xFFFEFEFE) : Color(0xFF222222);
static Color myGray1() => AppPreferences.instance().themeIsDark! ? Color(0xFFC1C1C1) : Color(0xFF333333);
static Color myGray5() => AppPreferences.instance().themeIsDark! ? Color(0xFF767676) : Color(0xFF4F4F4F);
static Color myGray2() => AppPreferences.instance().themeIsDark! ? Color(0xFF4F4F4F) : Color(0xFF767676);
static Color myGray4() => AppPreferences.instance().themeIsDark! ? Color(0xFF333333) : Color(0xFFC1C1C1);
static Color myGray3() => AppPreferences.instance().themeIsDark! ? Color(0xFF222222) : Color(0xFFFEFEFE);
static Color myNotBlack() => AppPreferences.instance().themeIsDark! ? Color(0xFF1A1A1A) : Color(0xFFECF0F1);
static Color myBlack() => AppPreferences.instance().themeIsDark! ? Color(0xFF000000) : Color(0xFFFFFFFF);
}
Switch(
value: AppPreferences.instance().themeIsDark!,
onChanged: (value) async {
setState(() {
AppPreferences.instance().themeIsDark = value;
AppPrefFlutterStorage.saveAppPrefToFlutterStorage(AppPreferences.instance());
});
},
activeTrackColor: AppTheme.myBlue,
activeColor: AppTheme.myGray1(),
inactiveThumbColor: AppTheme.myGray1(),
),
AppPreferences.instance (). ThemeIsDark I keep it locally in FlutterSecureStorage and after restarting the application the colors change their values everywhere but in runtime unfortunately not.
I would like to change the persistent colors throughout the application by pressing a button. I don't know if it's possible. Thank you in advance.
There are a few ways you could accomplish themes/skins. The "most official" way would be to create material ThemeData objects and pass them to your MaterialApp().
However, if you have assets or attributes that need to change but aren't included in ThemeData, for example background images, you could use a class MyThemeManager singleton that is a Provider, and watch MyThemeManager in your widgets' build() methods.
class MyTheme {
Color edgeColor;
String bgImageName;
//...etc...
}
//ChangeNotifier will tell "watching" widgets when theme has changed
class MyThemeManager with ChangeNotifier {
//This Map will hold available themes.
Map<String,MyTheme> myThemes;
//This will hold the current theme
MyTheme myCurrentTheme;
//Create MyThemeManager as a singleton
static final MyThemeManager _instance = MyThemeManager._pConstructor();
MyThemeManager._pConstructor() {
//Create your different themes here in myThemes Map
myCurrentTheme = myThemes["default"]; //name of your default theme
}
factory MyThemeManager() {
return _instance;
}
//The method to change your themes
void changeTheme(string themeName) {
myCurrentTheme = myThemes[themeName];
notifyListeners(); //tell watching widgets to redraw
}
}
Then in a stateful or stateless widget's build():
build(BuildContext context) {
//This will "watch" MyThemeManager and when it calls
//notifyListeners() this widget will redraw
MyTheme theme = context.watch<MyThemeManager>().myCurrentTheme;
return Container(
color: theme.edgeColor,
...
);
}
when you want to change your theme:
MyThemeManager().changeTheme("lightTheme");
And any watching widget will update
In your root widget you pass to runApp(), wrap the top child with Provider() or MultiProvider():
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => MyThemeManager(), lazy: false),
//you can put more providers here if you have them
],
child: MyMaterialApp()
);
}
}

Flutter how to get brightness without MediaQuery?

My goal is to create an app where the user can choose his preferred theme.
I'm saving the user's choice with shared preferences so I can load it the next app start.
The user can either select:
- Dark Mode (Independent from the OS Settings)
- Light Mode (Independent from the OS Settings)
- System (Changes between Dark Mode and Light mode depending on the OS settings)
With the help of BLoC, I almost achieved what I want. But the problem is that I need to pass the brightness inside my Bloc event. And to get the system (OS) brightness I need to make use of
MediaQuery.of(context).platformBrightness
But the Bloc gets initiated before MaterialApp so that MediaQuery is unavailable. Sure I can pass the brightness later(from a child widget of MaterialApp) but then (for example, if the user has dark mode activated) it goes from light to dark but visible for a really short time for the user(Because inside the InitialState I passed in light mode).
class MyApp extends StatelessWidget {
final RecipeRepository recipeRepository;
MyApp({Key key, #required this.recipeRepository})
: assert(recipeRepository != null),
super(key: key);
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<ThemeBloc>(create: (context) =>
ThemeBloc(),),
],
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state){
return MaterialApp(
theme: state.themeData,
title: 'Flutter Weather',
localizationsDelegates: [
FlutterI18nDelegate(fallbackFile: 'en',),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
supportedLocales: [
const Locale("en"),
const Locale("de"),
],
home: Home(recipeRepository: recipeRepository),
);
},
),
);
}
}
ThemeBloc:
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
#override
ThemeState get initialState =>
ThemeState(themeData: appThemeData[AppTheme.Bright]);
#override
Stream<ThemeState> mapEventToState(
ThemeEvent event,
) async* {
if (event is LoadLastTheme) {
ThemeData themeData = await _loadLastTheme(event.brightness);
yield ThemeState(themeData: themeData);
}
if (event is ThemeChanged) {
await _saveAppTheme(event.theme);
yield ThemeState(themeData: appThemeData[event.theme]);
}
}
Future<ThemeData> _loadLastTheme(Brightness brightness) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
String themeString = prefs.getString(SharedPrefKeys.appThemeKey);
print("saved theme: $themeString");
if ((prefs.getString(SharedPrefKeys.appThemeKey) != null) &&
themeString != "AppTheme.System") {
switch (themeString) {
case "AppTheme.Bright":
{
return appThemeData[AppTheme.Bright];
}
break;
///Selected dark mode
case "AppTheme.Dark":
{
return appThemeData[AppTheme.Dark];
}
break;
}
}
print("brightness: $brightness");
if (brightness == Brightness.dark) {
return appThemeData[AppTheme.Dark];
} else {
return appThemeData[AppTheme.Bright];
}
}
Future<void> _saveAppTheme(AppTheme appTheme) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString(SharedPrefKeys.appThemeKey, appTheme.toString());
}
}
If you absolutely must do it like this, you can get MediaQuery data directly from the low-level window object like this:
final brightness = MediaQueryData.fromWindow(WidgetsBinding.instance.window).platformBrightness;
However, I would strongly recommend you consider that if you need access to MediaQuery from within your bloc, you should instead move your BlocProvider to get instantiated after your MaterialApp so you can access MediaQuery normally.