Flutter theming: InheritedWidget vs global object - flutter

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.

Related

How to theme with Hive and Riverpod | 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`
...
);
}
}

Does keyword late work as I expect when constructing Widgets

I have a convenience StatelessWidget that returns the appropriate widget for one of three display size breakpoints:
/// Return the most appropriate widget for the current display size.
///
/// If a widget for current display size is not available, chose the closest smaller variant.
/// A [mobile] size widget is required.
class SizeAppropriate extends StatelessWidget {
// ignore: prefer_const_constructors_in_immutables
SizeAppropriate(
this.context,
{
required this.mobile,
this.tablet,
this.desktop,
Key? key
}
) : super(key: key);
final BuildContext context;
late final Widget mobile;
late final Widget? tablet;
late final Widget? desktop;
#override
Widget build(BuildContext context) {
switch (getDisplaySize(context)) {
case DisplaySize.mobile:
return mobile;
case DisplaySize.tablet:
return tablet ?? mobile;
case DisplaySize.desktop:
return desktop ?? tablet ?? mobile;
}
}
}
I then use it like this:
SizeAppropriate(
context,
mobile: const Text('mobile'),
desktop: const Text('desktop'),
)
Is the keyword late working here as intended, building only the correct variant, or am I hogging the performance, because all variants are constructed (or am I even creating an anti-pattern)?
Should I use builder functions instead?
Edit:
This answer makes me think I'm correct. Although the last two sentences make me think I'm not correct.
When you do this, the initializer becomes lazy. Instead of running it as soon as the instance is constructed, it is deferred and run lazily the first time the field is accessed. In other words, it works exactly like an initializer on a top-level variable or static field. This can be handy when the initialization expression is costly and may not be needed.
When I do log('mobile') and log('desktop') in the respective widgets being constructed, I only get one type of message in the console.
Edit 2 – minimum working example:
This is my main.dart in a newly generated project (VS Code command >Flutter: New Project – Application). It might be worth noting, that I am testing this on Linux.
import 'package:flutter/material.dart';
import 'dart:developer';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: SizeAppropriate(
context,
mobile: const Mobile(),
tablet: const Tablet(),
desktop: const Desktop(),
),
),
);
}
}
class Mobile extends StatelessWidget {
const Mobile({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
log('mobile');
return const Text('mobile');
}
}
class Tablet extends StatelessWidget {
const Tablet({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
log('tablet');
return const Text('tablet');
}
}
class Desktop extends StatelessWidget {
const Desktop({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
log('desktop');
return const Text('desktop');
}
}
enum DisplaySize {
desktop,
tablet,
mobile,
}
DisplaySize getDisplaySize(BuildContext context) {
Size size = MediaQuery.of(context).size;
if (size.width < 768) {
return DisplaySize.mobile;
}
else if (size.width < 1200) {
return DisplaySize.tablet;
}
else {
return DisplaySize.desktop;
}
}
class SizeAppropriate extends StatelessWidget {
// ignore: prefer_const_constructors_in_immutables
SizeAppropriate(
this.context,
{
required this.mobile,
this.tablet,
this.desktop,
Key? key
}
) : super(key: key);
final BuildContext context;
late final Widget mobile;
late final Widget? tablet;
late final Widget? desktop;
#override
Widget build(BuildContext context) {
switch (getDisplaySize(context)) {
case DisplaySize.mobile:
return mobile;
case DisplaySize.tablet:
return tablet ?? mobile;
case DisplaySize.desktop:
return desktop ?? tablet ?? mobile;
}
}
}
late does not do what you want. It's only for the null-safety feature and when you do or don't get warnings about it. Those two texts get built every time regardless of environment, because they need to be there when they are passed to your widget.
If you to only build the appropriate widgets for each size when the size is known, you indeed need indeed pass two builders, one of which you call if appropriate.
For two constant Text widgets, that would be too much overhead, but I am assuming you want "heavier" widget trees for both options in the end.

How to create custom types of widgets in Flutter?

I am trying to create a couple of widgets A that all should belong to another type of widget B, so that in the end all of them could be passed to a constructor that accepts only widgets of type B, but not other custom widgets like Container, Text, etc.
I tried something like this:
Parent class:
class ProDynamicWidget {
const ProDynamicWidget({
required this.height
});
final double height;
}
Child classes:
class ProDynamicTitle extends ProDynamicWidget {
final String title;
ProDynamicTitle({
required this.title
}) : super(height: 50.0);
// Here should be a build method for the title
}
############################################################
class ProDynamicImage extends ProDynamicWidget {
final String imageUrl;
ProDynamicImage({
required this.imageUrl
}) : super(height: 70.0);
// Here should be a build method for the image
}
I then wanted to create a widget that only accept widgets of type ProDynamicWidget:
class TestOneWidget extends StatelessWidget {
const TestOneWidget({
Key? key,
required this.widget
}) : super(key: key);
final ProDynamicWidget widget;
#override
Widget build(BuildContext context) {
return Container();
}
}
I do not really get how can now end up with child widgets that have separate build methods and a way the constructur at the end only accepts these child widgets instead of every type of widget.
Make ProDynamicWidget abstract and let it extend StatelessWidget:
abstract class ProDynamicWidget extends StatelessWidget{
const ProDynamicWidget({
required this.height
});
final double height;
}
Next, ProDynamicTitle and ProDynamicImage simply extend ProDynamicWidget and will thus have to define the build method:
class ProDynamicTitle extends ProDynamicWidget {
final String title;
const ProDynamicTitle({
required this.title
}) : super(height: 50.0);
#override
Widget build(BuildContext context) {
return Text(title);
}
}
class ProDynamicImage extends ProDynamicWidget {
final String imageUrl;
const ProDynamicImage({
required this.imageUrl
}) : super(height: 70.0);
#override
Widget build(BuildContext context) {
return Image(image: NetworkImage(imageUrl));
}
}
You can keep TestOneWidget as is. It will only accept descendants of ProDynamicWidget.

Flutter: add a simple wrapper to the Text widget

I came from a React world and trying to get my head around Flutter and Dart.
I'm using the Text widget with the same parameters a lot, so it seems reasonable to think of a way to reuse code. I created a wrapper that uses it:
import 'package:flutter/material.dart';
TextStyle getThemeProperty(type, TextTheme textTheme) {
switch (type) {
case 'headline1':
return textTheme.headline1;
case 'headline2':
return textTheme.headline2;
case 'headline3':
return textTheme.headline3;
default:
return textTheme.bodyText2;
}
}
class CustomText extends StatelessWidget {
const CustomText({Key key, this.type, this.text, this.color}) : super(key: key);
final type;
final text;
final color;
#override
Widget build(BuildContext context) {
var textTheme = Theme.of(context).textTheme;
var style = getThemeProperty(type, textTheme);
if (this.color != null) style.color = this.color;
return Text(
this.text,
style: style,
);
}
}
// Usage
CustomText(
text: 'Some Heading',
type: 'headline2',
color: Colors.black
)
The idea is to set the color if the color property is passed as a parameter, but Dart's compiler doesn't like it. It throws me the error: ''color' can't be used as a setter because it's final.
Try finding a different setter, or making 'color' non-final.'
I'm planning to do the same to fontWeight and textAlign properties as well. How am I able to make this work, I mean, to add new props to the style object on demand?
The reason why the dart compiler is unhappy is just because the color property of the TextStyle is declared as final. Therefore to use a new color, you have to create a new instance of the TextStyle.
Luckily, the TextStyle class comes with a copyWith method that returns an edited copy of your TextStyle
final type;
final text;
final color;
#override
Widget build(BuildContext context) {
var textTheme = Theme.of(context).textTheme;
var style = getThemeProperty(type, textTheme);
return Text(
this.text,
// Added this...
style: style.copyWith(color: color ?? style.color),
);
}
As a side note, when making reusable widgets, it's always a good idea to type your parameters. This is because any type of variable can be used. So instead of passing a String for text, you may pass an int
// DON'T DO THIS
final type;
final text;
final color;
// DO THIS
final String type;
final String text;
final Color color;
Also adding the this keyword to reference a variable in a class without variable shadowing is unnecessary.
// DON'T
this.text
// DO
text

AppLocalizations without calling `of(context)` every time

I find the localization procedure using the official Flutter localization plugin cumbersome. To display a localized string I have to call AppLocalizations.of(context).myAppTitle - not exactly sleek or easy to glance over in a huge nested Widget tree with lots of localized strings. Not to mention it looks ugly.
Is there a way to make the usage nicer? For example, can I use a global variable or a static class with a AppLocalizations instance member to make the access easier? For example declaring a top level AppLocalizations variable
// Somewhere in the global scope
AppLocalizations l;
// main.dart
class _MyAppState extends State<MyApp>{
#override
void initState() {
super.initState();
getLocaleSomehow().then((locale){
l = Localization(locale);
setState((){});
});
}
}
Then I could simply call
Text(l.myAppTitle)
So in an essence what I'm asking is "what are the dangers and/or disadvantages of not calling AppLocalizations.of(context)?"
If I really do need to use the .of(BuildContext) method to access the AppLocalizations instance - can I at least store it in my StatefulWidget? I'm thinking something like
class DetailsPage extends StatefulWidget{
AppLocalizations _l;
#override
Widget build(BuildContext context) {
_l = AppLocalizations.of(context);
// ... build widgets ...
}
}
Or is there any other way to make the localization less cumbersome?
Yes, it is needed.
You could work around it, but that is a bad idea.
The reason for this is that Localization.of<T>(context, T) may update over time. A few situations where it does are:
The locale changed
The LocalizationsDelegate obtained was asynchronously loaded
MaterialApp/CupertinoApp got updated with new translations
If you're not properly calling Localization.of inside build as you should, then in those scenarios your UI may fail to properly update.
It is totally fine to store the Localization object inside of your State and it works very well in that case.
If you want to only make it look nicer, you could also just declare the variable in the build method:
#override
Widget build(BuildContext context) {
final l = Localization.of(context);
return Text(l.myAppTitle);
}
In a StatefulWidget, you could also re-assign the variable in didChangeDependencies or just assign it once using the null-aware ??= operator because the object will not change over time:
class _MyStatefulWidgetState extends State<MyStatefulWidget> with WidgetsBindingObserver {
Localization l;
#override
didChangeDependencies() {
WidgetsBinding.instance.addObserver(this);
l ??= Localization.of(context);
super.didChangeDependencies();
}
#override
void didChangeLocales(List<Locale> locale) {
l = Localization.of(context);
super.didChangeLocales(locale);
}
#override
dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Widget build(BuildContext context) => Text(l.myAppTite);
}
In didChangeLocales, you can re-assign every time. This makes sure that the variable always holds the appropriate locale and is initialized at first build (with didChangeDependencies). Notice that I also included a WidgetsBindingObserver, which you need to handle as shown in the code.
By using Flutter extensions you can now simply extend The StatelessWidget and StatefulWidget, or the generic Widget to provide a translate method. Two different solutions:
1. on Widget
extension TranslationHelper on Widget {
String tr(BuildContext context, String key) {
return AppLocalizations.of(context).translate(key);
}
}
Then in the build method of a StatelessWidget you can call tr(context, 'title'). For the build method of a StatefulWidget you have to call widget.tr(context, 'title').
2. on StatelessWidget and StatefulWidget
For a more consistent calling of the translate function you can extend StatelessWidget and StatefulWidget, respectively:
extension TranslationHelperStateless on StatelessWidget {
String tr(BuildContext context, String key) {
return AppLocalizations.of(context).translate(key);
}
}
extension TranslationHelperStateful<T extends StatefulWidget> on State<T> {
String tr(BuildContext context, String key) {
return AppLocalizations.of(context).translate(key);
}
}
For both build methods in StatelessWidget and StatefulWidget you can call:
tr(context, 'title')
With StatefulWidget there is one risk as a developer you need to avoid. Which is calling the tr() method in a place where you can access context but where the build method has not ran yet, like initState. Make sure to call tr() always in the build methods.
3. on StatelessWidget and StatefulWidget, but not using the translate method
You can extend StatelessWidget and StatefulWidget and return the AppLocalizations, like this:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension TranslationHelperStateless on StatelessWidget {
AppLocalizations tr(BuildContext context) {
return AppLocalizations.of(context)!;
}
}
extension TranslationHelperStateful<T extends StatefulWidget> on State<T> {
AppLocalizations tr(BuildContext context) {
return AppLocalizations.of(context)!;
}
}
For both build methods in StatelessWidget and StatefulWidget you can call:
tr(context).title
or
tr(context).helloUser(name)
I combined some of the info from the other responses here (specialy Fleximex's) to a solution which I found quite interesting - and that's the one I'm using. I created an extension on the BuildContext itself.
extension BuildContextHelper on BuildContext {
AppLocalizations get l {
// if no locale was found, returns a default
return AppLocalizations.of(this) ?? AppLocalizationsEn();
}
}
Using this extension (and importing it), one can use it like this:
context.l.appTitle
or
context.l.helloUser(name)
IMHO it's clean and readable, it's not the shortest though.
You can create your own text widget and do localization there.You can replace all your text widgets with your own MyText widget.
class MyText extends StatelessWidget {
String data;
InlineSpan textSpan;
TextStyle style;
StrutStyle strutStyle;
TextAlign textAlign;
TextDirection textDirection;
Locale locale;
bool softWrap;
TextOverflow overflow;
double textScaleFactor;
int maxLines;
String semanticsLabel;
TextWidthBasis textWidthBasis;
MyText(
this.data, {
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
});
#override
Widget build(BuildContext context) {
return Text(
Localization.of(context).data,
style: style,
semanticsLabel: semanticsLabel,
locale: locale,
key: key,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
softWrap: softWrap,
strutStyle: strutStyle,
textDirection: textDirection,
textScaleFactor: textScaleFactor,
textWidthBasis: textWidthBasis,
);
}
}