Patrick
Patrick

Reputation: 3759

How to apply different Themes to parts of a Flutter app?

My app has different parts and I want them to have different theme colors, including for all the subroutes in the navigation.

But if I use a Theme, it's not applied to the widgets in the subroutes. I also tried to use nested MaterialApps but this won't work because I can't pop back to the root menu. I'd prefer not to have to pass a Color parameter to all the screens. What should I do?

Here is a test code:

import 'package:flutter/material.dart';

main() {
  runApp(MaterialApp(home: _Test()));
}

class _Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ElevatedButton(
              child: Text('Red section'),
              onPressed: () {
                Navigator.push(context, MaterialPageRoute(
                  builder: (context) {
                    return Theme(
                      data: ThemeData(colorScheme: ColorScheme.light(primary: Colors.red)),
                      child: _TestSubRoute(),
                    );
                  },
                ));
              },
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              child: Text('Green section'),
              onPressed: () {
                Navigator.push(context, MaterialPageRoute(
                  builder: (context) {
                    return Theme(
                      data: ThemeData(colorScheme: ColorScheme.light(primary: Colors.green)),
                      child: _TestSubRoute(),
                    );
                  },
                ));
              },
            ),
          ],
        ),
      ),
    );
  }
}

class _TestSubRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
      appBar: AppBar(
        title: Text('Should keep the same color through the navigation...'),
        actions: [
          IconButton(
            icon: Icon(Icons.help),
            onPressed: () {
              showDialog(
                context: context,
                builder: (context) {
                  return AlertDialog(
                    title: Text('Hello'),
                    actions: [
                      TextButton(
                        child: Text('OK'),
                        onPressed: () => Navigator.pop(context),
                      ),
                    ],
                  );
                },
              );
            },
          ),
        ],
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Push...'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => _TestSubRoute()),
            );
          },
        ),
      ),
    );
  }
}

Upvotes: 5

Views: 7986

Answers (6)

diegoveloper
diegoveloper

Reputation: 103551

I use another approach for this solution, InheritedWidget.

Basically:

  • Define your InheritedWidget where you will call the updated theme.
  • Create a StatefulWidget wrapper class that contains the InheritedWidget and the MaterialApp(where you going to update the Theme).
  • Keep your first screen with its own theme : data: ThemeData.light(),.
  • The only thing you need to do is to call ThemeProvider.of(context).updateTheme(yournewtheme) before pushing the new screen.

Result:

enter image description here

This is the code I used, based on your example:


main() {
  runApp(
    ThemeWrapper(
      child: _Test(),
    ),
  );
}

class ThemeProvider extends InheritedWidget {
  final ValueChanged<ThemeData> onThemeUpdated;

  const ThemeProvider({
    required this.onThemeUpdated,
    required super.child,
    super.key,
  });

  static ThemeProvider of(BuildContext context) =>
      context.findAncestorWidgetOfExactType<ThemeProvider>()!;

  void updateTheme(ThemeData data) {
    onThemeUpdated(data);
  }

  @override
  bool updateShouldNotify(covariant ThemeProvider oldWidget) => false;
}

class ThemeWrapper extends StatefulWidget {
  const ThemeWrapper({required this.child, super.key});
  final Widget child;

  @override
  State<ThemeWrapper> createState() => _ThemeWrapperState();
}

class _ThemeWrapperState extends State<ThemeWrapper> {
  ThemeData? _theme;

  @override
  Widget build(BuildContext context) {
    return ThemeProvider(
      onThemeUpdated: (newTheme) {
        setState(() {
          _theme = newTheme;
        });
      },
      child: MaterialApp(
        theme: _theme,
        home: _Test(),
      ),
    );
  }
}

class _Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.light(),
      child: Scaffold(
        appBar: AppBar(),
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton(
                child: const Text('Red section'),
                onPressed: () {
                  ThemeProvider.of(context).updateTheme(
                    ThemeData(
                      colorScheme: const ColorScheme.light(primary: Colors.red),
                    ),
                  );
                  Navigator.push(context, MaterialPageRoute(
                    builder: (context) {
                      return _TestSubRoute();
                    },
                  ));
                },
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                child: const Text('Green section'),
                onPressed: () {
                  ThemeProvider.of(context).updateTheme(
                    ThemeData(
                      colorScheme:
                          const ColorScheme.light(primary: Colors.green),
                    ),
                  );
                  Navigator.push(context, MaterialPageRoute(
                    builder: (context) {
                      return _TestSubRoute();
                    },
                  ));
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _TestSubRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
      appBar: AppBar(
        title:
            const Text('Should keep the same color through the navigation...'),
        actions: [
          IconButton(
            icon: const Icon(Icons.help),
            onPressed: () {
              showDialog(
                context: context,
                builder: (context) {
                  return AlertDialog(
                    title: const Text('Hello'),
                    actions: [
                      TextButton(
                        child: const Text('OK'),
                        onPressed: () => Navigator.pop(context),
                      ),
                    ],
                  );
                },
              );
            },
          ),
        ],
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Push...'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => _TestSubRoute()),
            );
          },
        ),
      ),
    );
  }
}

Upvotes: 3

Rahul
Rahul

Reputation: 4341

Instead of creating multiple MaterialApp, you can wrap your scaffold in Navigator and Theme widget. This way, same theme will be applied to all the sub-routes.

Now, if you want to pop to root navigator, you may choose to do Navigator.of(context, rootNavigator: true).pop();

This will pop the entire nested Navigators.

Upvotes: 0

Patrick
Patrick

Reputation: 3759

Here is the solution that I finally found. Each section is in a MaterialApp with its own ThemeData. To get back to the root screen, I use a BackButton with

Navigator.of(context, rootNavigator: true).pop()

Contrary to other solutions, this does not force to change all sub-screens, but only the first screen of each section.

Upvotes: 1

OlegBezr
OlegBezr

Reputation: 305

Please find a nice workaround below. The idea is - you get the theme of the route and then apply it to the subroutes.


main() {
  runApp(MaterialApp(home: _Test()));
}

class _Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ElevatedButton(
              child: Text('Red section'),
              onPressed: () {
                Navigator.push(context, MaterialPageRoute(
                  builder: (context) {
                    return Theme(
                      data: ThemeData(colorScheme: ColorScheme.light(primary: Colors.red)),
                      child: _TestSubRoute(),
                    );
                  },
                ));
              },
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              child: Text('Green section'),
              onPressed: () {
                Navigator.push(context, MaterialPageRoute(
                  builder: (context) {
                    return Theme(
                      data: ThemeData(colorScheme: ColorScheme.light(primary: Colors.green)),
                      child: _TestSubRoute(),
                    );
                  },
                ));
              },
            ),
          ],
        ),
      ),
    );
  }
}

class _TestSubRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
      appBar: AppBar(
        title: Text('Should keep the same color through the navigation...'),
        actions: [
          IconButton(
            icon: Icon(Icons.help),
            onPressed: () {
              showDialog(
                context: context,
                builder: (context) {
                  return AlertDialog(
                    title: Text('Hello'),
                    actions: [
                      TextButton(
                        child: Text('OK'),
                        onPressed: () => Navigator.pop(context),
                      ),
                    ],
                  );
                },
              );
            },
          ),
        ],
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Push...'),
          onPressed: () {
            final theme = Theme.of(context);

            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => Theme(
                data: theme,
                child: _TestSubRoute()
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}```

Upvotes: 0

baek
baek

Reputation: 450

I ran your code - You are calling Navigator.push() on _TestSubRoute() from _Test and supplying ThemeData.

You are then calling Navigator.push()on _TestSubRoute() from within _TestSubRoute() itself but not supplying ThemeData.

If you really just want to add a global theme you can do it in your main like this:

main() {
  runApp(MaterialApp(home: _Test(), theme: ThemeData(colorScheme: ColorScheme.light(primary: Colors.red)),));
}

Otherwise, I've noticed that both your widgets are Stateless which means that they will not change or hold state based on Theme.data which is only scoped within Navigator.push()in _Test()

If you want to dynamically change themes for _TestSubRoute(), you will need to use stateful widgets and store ThemeData in the parent widget and change that based on your requirement e.g., onPressed(), onTap() etc.

If you want to still want use just stateless widgets for trivial apps, you will need to specify the ThemeData each time you use Navigator.push() as flutter is not going to set any state for those widgets.

If you want to add a global theme as mentioned by other answers, in your code you can do it like this:

Here is some info from the flutter website:

A widget is either stateful or stateless. If a widget can change—when a user interacts with it, for example—it’s stateful.

A stateless widget never changes. Icon, IconButton, and Text are examples of stateless widgets. Stateless widgets subclass StatelessWidget.

A stateful widget is dynamic: for example, it can change its appearance in response to events triggered by user interactions or when it receives data. Checkbox, Radio, Slider, InkWell, Form, and TextField are examples of stateful widgets. Stateful widgets subclass StatefulWidget.

Hope this helps!

Upvotes: 0

Gwhyyy
Gwhyyy

Reputation: 9206

you can set a custom different ThemeData to a part of the widget tree using the Theme widget like this:

Theme(
  data: ThemeData.light().copyWith(
    primaryColor: Colors.purple,
  ),
  child: YourWidget(),
);

in Flutter the Theme is an InheritedWidget so that the ThemeData you set to the data property will be applied to the whole subtree of the child property, even if you separate your widget into multiples widgets..., they will refer to the closet Theme widget in the widget tree using the context.

the MaterialApp has the default one nested inside of it, so when we do the:

Theme.of(context).primaryColor,

it gets us the primaryColor from the MaterialApp's nested Theme since it's the closet.

but when you specify a Theme widget to a part of your widget tree like the example above, anything inside the YourWidget() will refer to that ThemeData

see https://api.flutter.dev/flutter/material/Theme-class.html

Upvotes: 8

Related Questions