My Flutter Flight – Welcome to the Dark Mode

Meine App hat kürzlich einen Dark-Mode erhalten, gefolgt von der Anfrage, ob ich nicht einen Schalter dafür einbauen könnte. Danach ging es nur noch darum, das beste Beispiel zu finden und alle davon abweichenden Fälle ebenfalls abzudecken. Mein Ergebnis möchte ich hier teilen.

Das Problem

Standardmäßig wird das Theme aus der MyApp Klasse verwendet. Indem der themeMode auf ThemeMode.system gesetzt wird, kann der jeweilige Modus dem Gerät angepasst werden. Wenn nun aber das Gerät im dunklen Modus läuft und ich die App lieber im hellen Modus hätte, will ich das einstellen können. Außerdem soll die App nicht neu gestartet werden müssen, damit die Änderung sichtbar wird.

Ein weiteres Problem, das mir erst später aufgefallen ist betrifft den Fall, dass der Benutzer seinen ausgewählten Modus gerne beibehalten möchte, also auch beim erneuten Start der App.
Ich habe meinen Beitrag entsprechend erweitert.

Die Provider-Lösung

Meine Hauptquelle ist dieses YouTube Video von Johannes Milke (englisch) und sein zugehöriges Get-Repository. Ich füge hier die relevanten Ausschnitte meines Quellcodes ein. Im Vergleich zu Johannes habe ich das normale und dunkle Thema direkt in der App definiert – obwohl ich seine Lösung eleganter finde.

Hier ist der relevante Ausschnitt meiner main.dart.

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) => ChangeNotifierProvider(
      create: (context) => ThemeProvider(),
      builder: (context, _){
    final themeProvider = Provider.of<ThemeProvider>(context);

    return new MaterialApp(

        title: 'My light and dark app',
        theme: ThemeData(
          brightness: Brightness.light,
          primaryColor: Color.fromRGBO(40, 105, 40, 1),
          accentColor: Colors.black,
          fontFamily: 'Raleway',
        ),
        darkTheme: ThemeData(
          brightness: Brightness.dark,
          primaryColor: Color.fromRGBO(100, 255, 218, 1),
          accentColor: Colors.white,
          fontFamily: 'Raleway',
        ),
        themeMode: themeProvider.themeMode,
    );
  });
}

Und mein theme_provider.dart. Ich habe mich dafür entschieden, nach dem hellen, statt dem dunklen Theme zu fragen. Aus keinem besonderen Grund, einfach weil ich das etwas… positiver fand. Wenn die App initialisiert oder das Theme umgestellt wird, dann wird der Modus in den sharedPreferences gespeichert.

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class ThemeProvider extends ChangeNotifier {
    class ThemeProvider extends ChangeNotifier {
    Preferences prefs = Preferences();
    ThemeMode themeMode = ThemeMode.system;

    void initThemeProvider() async {
      themeMode = getThemeMode();
      notifyListeners();
    }

    ThemeMode getThemeMode() {
      if (prefs == null || prefs.isLightMode == null) return ThemeMode.system;
      return prefs.isLightMode ? ThemeMode.light : ThemeMode.dark;
    }

    bool get isLightMode {
      themeMode = getThemeMode();
      return themeMode == ThemeMode.light;
    }

    void toggleTheme(bool isOn) {
      prefs.setIsLightMode(isOn);
      themeMode = getThemeMode();
      notifyListeners();
    }
}

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {

  Preferences prefs;

  @override
 void initState() {
   super.initState();
   prefs = new Preferences();
     await prefs.load();
     Provider.of<ThemeProvider>(context, listen: false).initThemeProvider();
   } on Exception catch (e) {
     // TODO handle exception
   }
 }
}

Mein Schalter befindet sich innerhalb eines ListTile auf der Einstellungs-Seite.

final themeProvider = Provider.of<ThemeProvider>(context);

Diese Zeile muss vorher initialisiert werden. Dann ist hier der Code des Listeneintrags:

new SwitchListTile(
                  value: themeProvider.isLightMode,
                  onChanged: (value) => Provider.of<ThemeProvider>(context, listen: false).toggleTheme(value),
                  title: themeProvider.isLightMode ?
                    Text(DemoLocalizations.of(context).trans('light_mode')) :
                    Text(DemoLocalizations.of(context).trans('dark_mode')),
              ),

Zusätzlicher Code

Ich nutze eine Utils-Klasse um ein einheitliches Hintergrundbild zu definieren. Um dieses auch entsprechend des gewählten Modus auszutauschen, muss ich lediglich den Provider aus dem BuildContext abfragen.

static getBackgroundDecoration(BuildContext context) {
    final themeProvider = Provider.of<ThemeProvider>(context);
    return BoxDecoration(
      image: new DecorationImage(
        image: themeProvider.isLightMode ?
          new AssetImage("resources/images/pergament20.png") :
          new AssetImage("resources/images/pergament20_dark.png"),
        fit: BoxFit.fill,
      ),
    );
  }

Das Ergebnis

Das Umschalten erfolgt nicht ganz unverzüglich (hauptsächlich wegen des Hintergrundbildes), aber die meiste Zeit wird der Anwender wohl kaum mit dem Hell und Dunkel herum spielen und sich über die Verzögerung ärgern.
Es funktioniert wieder reibungslos und lässt sich mit erfreulich wenig Code abbilden. Auch wenn ich bestimmt noch die eine oder andere Stelle übersehen habe, an der eine Farbe mit Hilfe des Providers dynamisch angepasst werden muss.

Beim Start der App wird zuerst der Modus des Systems genutzt und dann mit dem Initialisieren des Providers angepasst.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *