Multi – Temas usando Riverpod no Flutter
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.
Conteudo
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 nolistView
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 widgetProviderScope
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 widgetConsumer
, 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 oindex
dentro de nossa funçãothemeData
final themeNotifer = ref.watch(themeProvider);
- Chame a função
themeData
no parâmetrotheme
: do aplicativo material e passe oindex
e ocontext
selecionados dentro da função. Você obterá o índice da variávelthemeNotifier
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çãothemeData
.
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áticatextTheme
da classeStyles
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.