Multi – Temas usando Riverpod no Flutter

Tempo de leitura: 9 minutes

Usar aplicativos móveis tornou-se uma tarefa integral no mundo de hoje. Existe um aplicativo para quase tudo e muitos aplicativos para o mesmo caso de uso. Portanto, reter usuários tornou-se uma tarefa difícil nos dias de hoje. Em situações como essas, a UI/UX do aplicativo desempenha um papel muito crítico para atrair e fazer com que o usuário permaneça no aplicativo.

Os temas têm um papel importante no aumento da experiência do usuário do aplicativo, pois o usuário pode definir o tema do aplicativo como desejar. Assim, o usuário consegue criar uma experiência mais personalizada e confortável com o aplicativo.

Em 2019, o Google introduziu o Dark Theme no Google I/O e imediatamente mais tarde a Apple na WWDC introduziu o Dark Mode. Ambos os gigantes da tecnologia introduziram o modo claro e escuro para melhorar a experiência do usuário no uso móvel. Isso abriu as portas para trazer o tema claro e escuro para outros aplicativos também e muitos aplicativos suportam o modo claro e escuro básico.

Vamos dar um passo à frente no Theming, adicionando vários temas ao aplicativo.

No artigo irei explicar como adicionar vários temas aos aplicativos Flutter e como você pode usar o riverpod para gerenciar os temas do aplicativo.

Este artigo pressupõe que você tenha algum conhecimento básico de Flutter e Provider ou Inherited Widgets.

 

Começando:

Antes de começarmos – – é isso que vamos construir. Um aplicativo onde o usuário pode alternar entre vários temas sem recarregar/reiniciar o aplicativo.

Primeiro de tudo, crie um novo aplicativo Flutter e mostre um HomeScreen() em branco como o widget inicial no MaterialApp lateral. Definiremos esta tela inicial mais tarde, quando tivermos criado nossos temas.

Criando Temas:

Vamos começar criando temas. Crie uma nova pasta chamada Style dentro de lib e crie um arquivo dart chamado estilos.dart dentro da pasta Style. Crie uma classe chamada Styles dentro do arquivo estilos.dart. Todo o código de estilo será escrito dentro desta classe.

Crie uma função chamada themeData do tipo de retorno ThemeData que leva dois parâmetros: BuildContext e int Esta função retornará o ThemeData que iremos criar em breve e o valor int decidirá quais dados do tema retornar.

ThemeData themeData(int index, BuildContext context){ 
}

 

Compreendendo o ThemeData:

A classe ThemeData define a configuração do tema visual geral para um MaterialApp ou uma subárvore de widget dentro do aplicativo. Esta propriedade do tema MaterialApp pode ser usada para configurar a aparência de todo o aplicativo.

ThemeData possui as seguintes propriedades que podemos alterar e configurar o tema dos aplicativos. Abaixo estão as propriedades básicas do ThemeData que iremos configurar. Todas essas propriedades são autoexplicativas. Você pode ler mais sobre eles no documento oficial do flutter – ThemeData

ThemeData(
      primaryColor: 
      backgroundColor:
      indicatorColor: 
      hintColor: 
      errorColor: 
      highlightColor: 
      focusColor: 
      disabledColor: 
      cardColor: 
      brightness: 
      buttonTheme: 
      appBarTheme: 
    );

Crie um tema para o modo claro

Crie uma nova função chamada setWhiteTheme que recebe um BuildContext como parâmetro e tem ThemeData como tipo de retorno. Iremos configurar nosso tema Dia/Luz/Branco nesta função. Vamos atribuir cores ao ThemeData e retornar este themeData. Para fazer isso, adicione o seguinte código à função.

ThemeData setWhiteTheme(BuildContext context) {
    return ThemeData(
      primaryColor: Colors.white,
      backgroundColor: Colors.white,
      indicatorColor: Colors.black,
      hintColor: Colors.grey.shade600,
      errorColor: Colors.red.shade500,
      highlightColor: Colors.grey.shade200,
      focusColor: Colors.black,
      disabledColor: Colors.grey.shade300,
      cardColor: Colors.grey.shade100,
      brightness: Brightness.light,
      buttonTheme: Theme.of(context)
          .buttonTheme
          .copyWith(colorScheme: const ColorScheme.light()),
      appBarTheme: const AppBarTheme(
        backgroundColor: Colors.blue,
        elevation: 0,
      ),
    );
  }

Criando um tema no modo escuro

Ótimo, vamos criar um tema para o modo escuro assim como criamos o tema para o modo claro.

Crie uma nova função abaixo de setWhiteTheme e nomeie-a como setBlackTheme conforme segue.

ThemeData setBlackTheme(BuildContext context) {
    return ThemeData(
      primaryColor: Colors.black,
      backgroundColor: Colors.black,
      indicatorColor: Colors.white,
      hintColor: Colors.grey.shade500,
      errorColor: Colors.red.shade900,
      highlightColor: Colors.grey.shade700,
      focusColor: Colors.white,
      disabledColor: Colors.grey.shade800,
      cardColor: const Color.fromARGB(255, 41, 40, 40),
      brightness: Brightness.dark,
      buttonTheme: Theme.of(context)
          .buttonTheme
          .copyWith(colorScheme: const ColorScheme.dark()),
      appBarTheme: const AppBarTheme(
        backgroundColor: Color.fromARGB(255, 28, 28, 28),
        elevation: 0,
      ),
    );
  }

 

Uma função comum para outros temas

Vamos criar uma função comum para gerenciar outros temas também. Passaremos Color como parâmetro nesta função para que possamos alterar a cor da aplicação definindo também este tema.

ThemeData setOtherTheme(
      {required BuildContext context,
      required MaterialColor mColor,
      required Color color}) {
    return ThemeData(
      primarySwatch: mColor,
      primaryColor: mColor,
      backgroundColor: Colors.white,
      indicatorColor: mColor,
      hintColor: mColor.shade200,
      errorColor: mColor.shade500,
      highlightColor: Colors.grey.shade200,
      focusColor: color,
      disabledColor: Colors.grey.shade300,
      cardColor: Colors.grey.shade100,
      brightness: Brightness.light,
      buttonTheme: Theme.of(context)
          .buttonTheme
          .copyWith(colorScheme: const ColorScheme.light()),
      appBarTheme: AppBarTheme(
        backgroundColor: mColor,
        elevation: 0,
      ),
    );
  }

 

Retornando o tema correto conforme necessário

Agora que a configuração dos dados do nosso tema está concluída. vamos chamar essas funções dentro da função themeData que foi criada na parte superior. Role até a função themeData e usaremos um Switch — Case para retornar o tipo de configuração ThemeData que queremos para nosso aplicativo.

  ThemeData themeData(int index, BuildContext context) {
  switch (index) {
    case 0:
      return setWhiteTheme(context);
    case 1:
      return setBlackTheme(context);
    case 2:
      return setOtherTheme(
          context: context, mColor: Colors.red, color: Colors.red);
    case 3:
      return setOtherTheme(
          context: context, mColor: Colors.green, color: Colors.greenAccent);
    case 4:
      return setOtherTheme(
          context: context, mColor: Colors.blue, color: Colors.blueAccent);
    case 5:
      return setOtherTheme(
          context: context,
          mColor: Colors.yellow,
          color: Colors.yellowAccent);
    case 6:
      return setOtherTheme(
          context: context, mColor: Colors.amber, color: Colors.amber);
    case 7:
      return setOtherTheme(
          context: context,
          mColor: Colors.deepPurple,
          color: Colors.deepPurpleAccent);
    default:
      return setWhiteTheme(context);
  }
}

Passamos esse valor int para a função do listView que iremos criar na HomeScreen para selecionar o themeColour.

Certifique-se de que o valor int que você passa denota a mesma cor no listView que denota aqui. Caso contrário, seu aplicativo mostrará um tema diferente que não será o ideal.

Tema de texto personalizado

Usar cores personalizadas é apenas metade da nossa jornada temática. Vamos também criar um TextTheme personalizado que pode ser usado para definir o tema do texto do aplicativo.

Dentro da própria classe Styles, na parte superior, vamos criar um estilo de texto padrão que iremos modificar para criar um tema de texto personalizado.

static const _defaultTextStyle = TextStyle(fontWeight: FontWeight.w500, fontFamily: your_custom_font_family);

Isso nos ajuda a criar um TextStyle personalizado padrão que iremos modificar ainda mais para personalizar como quisermos. Você também pode passar sua própria fonte personalizada aqui.

 

Compreendendo o TextTheme

Assim como ThemeData ajuda a configurar a aparência visual geral do aplicativo. TextTheme ajuda na configuração do texto geral do aplicativo. Podemos usar TextTheme para decidir o tipo de texto que você deseja.

Os temas de texto têm propriedades como displayLarge, displayMedium, displaySmall, headlineMedium, headlineSmall, headlineSmall, titleLarge, bodyLarge, bodyMedium, titleMedium, titleSmall. Definiremos as configurações personalizadas para essas propriedades e as usaremos em nosso aplicativo.

 

Criando temas de texto personalizados

Adicione a seguinte função e o final da sua classe Styles Nesta função personalizamos o _defaultTextStyle criado acima e os atribuímos aos parâmetros do TextTheme

Nesta função usamos o método CopyWith para modificar o _defaultTextStyle e alteramos o tamanho e a cor da fonte. Você está livre para experimentar todas as configurações aqui.

static TextTheme textTheme(BuildContext context) {
    return TextTheme(
      displayLarge _defaultTextStyle.copyWith(
          fontSize: 100,
          color: Theme.of(context).indicatorColor,
          fontWeight: FontWeight.w200),
      displayMedium: _defaultTextStyle.copyWith(
          fontSize: 25, color: Theme.of(context).indicatorColor),
      displaySmall: _defaultTextStyle.copyWith(
          fontSize: 16, color: Theme.of(context).indicatorColor),
      headlineMedium: _defaultTextStyle.copyWith(
          fontSize: 18, color: Theme.of(context).indicatorColor),
      headLineSmall: _defaultTextStyle.copyWith(
          fontSize: 14, color: Theme.of(context).indicatorColor),
      bodyLarge: _defaultTextStyle.copyWith(
          fontSize: 13, color: Theme.of(context).indicatorColor),
      bodyMedium: _defaultTextStyle.copyWith(
          fontSize: 20, color: Theme.of(context).indicatorColor),
      titleMedium: _defaultTextStyle.copyWith(
          fontSize: 12, color: Theme.of(context).hintColor),
      titleSmall: _defaultTextStyle.copyWith(
          fontSize: 10, color: Theme.of(context).hintColor),
    );
  }

Pronto, terminamos de criar a configuração de Style e Theme. Nossos Estilos e Temas estão prontos para serem usados dentro de nossa aplicação.

 

Adicionando Riverpod ao projeto

A seguir usaremos o riverpod para selecionar e adicionar os temas criados acima em nosso aplicativo.

vamos começar adicionando dependências do riverpod ao nosso arquivo pubspec.yaml na seção de dependências como segue. Você pode ter uma versão diferente, mas contanto que seja superior a 2.4.0, você está pronto para prosseguir.

dependencies:
  flutter_riverpod: ^2.4.0

não se esqueça de executar flutter pub get no terminal após adicionar dependências.

 

Criando ThemeProvider

Vamos criar um Theme Provider que nos ajudará a fornecer o tema que selecionamos para todo o nosso aplicativo.

Crie uma pasta chamada Provider e crie um novo arquivo chamado theme_provider.dart . Adicione uma classe chamada ThemeProvider que implementa ChangeNotifier dentro deste arquivo. Isso nos ajudará a notificar quando o tema selecionado for alterado e as alterações serão trazidas para o aplicativo.

adicione uma variável privada que conterá o valor int do tema selecionado que será passado para a função themeData na classe Styles. Vamos dar 0 como valor padrão.

int _themeIndex = 0;

Variáveis privadas não são acessíveis fora da classe, então vamos criar o getter da variável int privada.

int get themeIndex => _themeIndex;

agora finalmente vamos criar uma função que nos ajudará a atribuir o valor int à variável privada que é criada no topo. Esta função recebe um valor int como parâmetro e atribui esse valor à variável int privada. Chamamos notify Listeners para que nosso widget e aplicativo escutem as mudanças ocorridas.

setTheme(int value) {
    _themeIndex = value;
    notifyListeners();
 }

Crie uma variável global abaixo da classe ThemeProvider para tornar nosso provedor de tema acessível por meio do aplicativo. Usamos ChangeNotifierProvider para isso.

final themeProvider = ChangeNotifierProvider<ThemeProvider>((ref) => ThemeProvider());

Compilando nosso aplicativo para Riverpod

Quando você usa o riverpod, há algumas pequenas alterações que você precisa fazer dentro do arquivo main.dart para que o aplicativo funcione.

  • Envolvendo o widget dentro de ProviderScope: No main.dart, envolva o widget que você passa dentro da função runApp com um widget ProviderScope
void main() {
  runApp(ProviderScope(child: MultithemeRiverpod()));
}
  • Warp o MaterialApp com um Consumer Widget para que ele consuma todas as alterações dos dados do tema que passamos. Ao usar o widget Consumer, você deve usar o parâmetro construtor do widget do consumidor para construir seu widget quando houver alguma alteração nos dados.
  @override
  Widget build(BuildContext context) {
    return Consumer(builder: (context, ref, child) {
     // Create a theme notifier variable to use the index
      return MaterialApp(
        // Todo: Add theme styles here 
        debugShowCheckedModeBanner: false,
        home: HomeScreen(),
      );
    });
  }
}

Se você notar que ainda temos 2 TODOs.

  • Vamos começar criando uma variável themeNotifier que será usada para passar o index dentro de nossa função themeData
final themeNotifer = ref.watch(themeProvider);
  • Chame a função themeData no parâmetro theme: do aplicativo material e passe o index e o context selecionados dentro da função. Você obterá o índice da variável themeNotifier que você criou.
theme: styles.themeData(themeNotifer.themeIndex, context),

não se esqueça de inicializar a variável de estilos no topo ou ocorrerá um erro

Styles styles = Styles();

Como usar o tema personalizado

  • Theme.of(context): Use Theme.of(context) para usar a cor personalizada que você configurou dentro da função themeData.
    por exemplo –
// Scaffold background colour
* backgroundColor: Theme.of(context).backgroundColor
// Text Error Border Colour
* color: Theme.of(context).errorColor,
  • Styles.textTheme(context): Use a função estática textTheme da classe Styles para alterar o theme do text. Você pode escolher entre os temas de texto displayLarge, displayMedium, displaySmall, headlineMedium, headlineSmall, headlineSmall, titleLarge, bodyLarge, bodyMedium, titleMedium, titleSmall que desejar.
style: Styles.textTheme(context).headline4,style: Styles.textTheme(context).headline4,

 

Criando a IU

Agora que você sabe como usar os temas, vamos criar uma UI básica para testar nosso tema. Copie e cole o código abaixo no arquivo home_screen.dart.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:multitheme_riverpod/Providers/theme_provider.dart';
import 'package:multitheme_riverpod/Style/style.dart';

class HomeScreen extends StatelessWidget {
  HomeScreen({super.key});
  
  List<Color> colorsList = [
    Colors.white,
    Colors.black,
    Colors.redAccent,
    Colors.green,
    Colors.blue,
    Colors.amber,
    Colors.orange,
    Colors.deepPurpleAccent,
  ];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).backgroundColor,
      appBar: AppBar(
        title: const Text('Home Screen'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              'Title',
              style: Styles.textTheme(context).headline1,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(12.0),
            child: TextField(
              style: Theme.of(context).textTheme.bodyText1,
              decoration: InputDecoration(
                prefixIcon: const Icon(Icons.person),
                suffixIcon: IconButton(
                  icon: Icon(
                    Icons.visibility,
                    color: Theme.of(context).focusColor,
                  ),
                  onPressed: () {},
                ),
                hintText: 'Enter Name',
                hintStyle: Styles.textTheme(context).subtitle1,
                focusedBorder: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(10),
                  borderSide: BorderSide(
                    color: Theme.of(context).focusColor,
                  ),
                ),
                errorBorder: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(10),
                  borderSide: BorderSide(
                    color: Theme.of(context).errorColor,
                  ),
                ),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('Checkbox'),
                Checkbox(
                  value: true,
                  onChanged: (value) {
                    print(value);
                  },
                ),
              ],
            ),
          ),
          // Texto com um parágrafo da internet
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec auctor, nisl eget aliquam tincidunt, nunc nisl aliquam nisl, eget aliquam nisl nunc eget nisl. Donec auctor, nisl eget aliquam tincidunt, nunc nisl aliquam nisl, eget aliquam nisl nunc eget nisl.',
              style: Styles.textTheme(context).bodyText1,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(20.0),
            child: Text(
              'Themes',
              style: Styles.textTheme(context).headline4,
            ),
          ),
          SizedBox(
            width: MediaQuery.of(context).size.width,
            height: 50,
            child: ListView.builder(
              physics: const PageScrollPhysics(),
              scrollDirection: Axis.horizontal,
              itemCount: colorsList.length,
              shrinkWrap: true,
              itemBuilder: ((context, index) {
                return Consumer(
                  builder:
                      (BuildContext context, WidgetRef ref, Widget? child) {
                    final themeNotifer = ref.watch(themeProvider);
                    return GestureDetector(
                      child: Container(
                        height: 100,
                        width: 100,
                        decoration: BoxDecoration(
                            border: Border.all(color: Colors.black, width: 3),
                            color: colorsList[index],
                            shape: BoxShape.circle),
                      ),
                      onTap: () {
                        themeNotifer.setTheme(index);
                      },
                    );
                  },
                );
              }),
            ),
          ),
        ],
      ),
    );
  }
}

Percorra o código, você verá como usamos as propriedades do tema para adicionar um tema personalizado ao widget e ao texto.

Na parte inferior criamos um list view horizontal que nos ajudará na seleção do theme que queremos para nosso aplicativo. No topo temos uma lista de cores que podem ser exibidas na lista abaixo.

Usamos o widget Consumer para distorcer nosso widget GestureDectector e chamamos a função setTheme onde passamos o index da cor que foi selecionada/clicada. A função setTheme define o index para a variável privada _themeIndex que criamos na classe ThemeProvider e este valor é chamado em main.dart pelo valor getter do themeIndex.

Execute o aplicativo e ele deverá funcionar conforme mostrado no gif a seguir.

 

 

Conclusão

Usar o riverpod para gerenciar o estado do aplicativo torna a configuração dos temas muito fácil. Os temas dão ao nosso aplicativo uma forma de personalização que ajuda a aumentar a experiência do usuário do aplicativo. Isso dá ao nosso usuário a opção de escolher qualquer tema que lhe seja confortável.

Você também pode dar ao usuário a opção de selecionar a cor primária e a cor secundária e atribuir esses valores à nossa função themeData.

Use SharedPreference para salvar o tema selecionado pelo usuário, para que o usuário não precise definir o tema sempre que abrir o aplicativo.

Aqui está o código completo enviado ao Github – Multi-Theme-App não se esqueça de curtir/iniciar o repositório.