Search code examples
androidfirebaseflutterriverpodthemeprovider

Flutter Riverpod, how avoid rebuilding whole page using consumer


Ive recently replaced Provider from my project to riverpod which is used with firebase auth to provide auth state.

On my login page, i two textformfields for email and password, and i have the sign in button inside a consumer. Ive also added a theme toggle switch (dark/light mode using ThemeProvider Package) and this is where the problem occurs.

Everytime the switch is toggled, the whole page rebuilds and clears the fields? ive tried stateful/stateless/consumer widget for the page and still cant getting around this. also tried adding keys still rebuilds.

heres my loginpage.dart:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hubx/services/providers.dart';
import 'package:hubx/shared/widgets/loading_widget.dart';
import 'package:hubx/shared/validator.dart';
import 'package:theme_provider/theme_provider.dart';

class LoginPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    final GlobalKey<FormState> _signInFormKey = GlobalKey<FormState>();

    final TextEditingController _emailController = TextEditingController();
    final TextEditingController _passwordController = TextEditingController();

    final FocusNode _emailFocusNode = FocusNode();
    final FocusNode _passwordFocusNode = FocusNode();

    bool _darkTheme = true;
    _darkTheme = (ThemeProvider.controllerOf(context).currentThemeId == 'dark');

    return GestureDetector(
      onTap: () {
        _emailFocusNode.unfocus();
        _passwordFocusNode.unfocus();
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text('Login'),
        ),
        body: SafeArea(
          child: Scrollbar(
            child: Center(
              child: SingleChildScrollView(
                padding: EdgeInsets.all(16),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Card(
                      child: SwitchListTile.adaptive(
                        title: Text('Dark Mode'),
                        value: _darkTheme,
                        onChanged: (val) {
                          (val)
                              ? ThemeProvider.controllerOf(context)
                                  .setTheme('dark')
                              : ThemeProvider.controllerOf(context)
                                  .setTheme('light');
                        },
                      ),
                    ),
                    Card(
                      child: Form(
                        key: _signInFormKey,
                        child: Padding(
                          padding: const EdgeInsets.all(16.0),
                          child: Column(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              Column(
                                children: [
                                  TextFormField(
                                    controller: _emailController,
                                    focusNode: _emailFocusNode,
                                    keyboardType: TextInputType.emailAddress,
                                    textInputAction: TextInputAction.next,
                                    autovalidateMode:
                                        AutovalidateMode.onUserInteraction,
                                    validator: (value) =>
                                        Validator.validateEmail(email: value!),
                                    decoration: InputDecoration(
                                        labelText: 'Email',
                                        prefixIcon: Icon(Icons.email_outlined)),
                                  ),
                                  SizedBox(height: 8),
                                  TextFormField(
                                    controller: _passwordController,
                                    focusNode: _passwordFocusNode,
                                    keyboardType: TextInputType.text,
                                    textInputAction: TextInputAction.go,
                                    autovalidateMode:
                                        AutovalidateMode.onUserInteraction,
                                    validator: (value) =>
                                        Validator.validatePassword(
                                      password: value!,
                                    ),
                                    obscureText: true,
                                    decoration: InputDecoration(
                                      labelText: 'Password',
                                      prefixIcon: Icon(Icons.password_outlined),
                                    ),
                                  ),
                                  SizedBox(height: 32),
                                ],
                              ),
                              Consumer(
                                builder: (context, ref, child) {
                                  final authService =
                                      ref.watch(authServiceProvider);
                                  if (!authService.isLoading) {
                                    return Container(
                                      width: double.maxFinite,
                                      child: ElevatedButton.icon(
                                        onPressed: () async {
                                          _emailFocusNode.unfocus();
                                          _passwordFocusNode.unfocus();
                                          if (_signInFormKey.currentState!
                                              .validate()) {
                                            final authService =
                                                ref.read(authServiceProvider);
                                            authService
                                                .signInWithEmailAndPassword(
                                              _emailController.text,
                                              _passwordController.text,
                                            )
                                                .then((value) async {
                                              if (value == 'Success') {
                                                Navigator
                                                    .pushNamedAndRemoveUntil(
                                                        context,
                                                        AppRoutes
                                                            .RootAuthWrapper,
                                                        (_) => false);
                                              } else {
                                                Fluttertoast.showToast(
                                                    msg: value);
                                              }
                                            });
                                          }
                                        },
                                        icon:
                                            FaIcon(FontAwesomeIcons.signInAlt),
                                        label: Padding(
                                          padding: const EdgeInsets.all(16.0),
                                          child: Text(
                                            'LOGIN',
                                            style: TextStyle(
                                              fontSize: 20,
                                              fontWeight: FontWeight.w500,
                                              letterSpacing: 2,
                                            ),
                                          ),
                                        ),
                                      ),
                                    );
                                  } else {
                                    return loadingWidget();
                                  }
                                },
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

authServiceProvider:

final authServiceProvider = ChangeNotifierProvider.autoDispose<AuthService>(
  (ref) {
    return AuthService();
  },
);

AuthService.dart:

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';

class AuthService extends ChangeNotifier {
  bool isLoading = false;

  Future<String> signInWithEmailAndPassword(
    String email,
    String password,
  ) async {
    _setLoading(true);
    String res;
    try {
      final userCredential =
          await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );

      _setLoading(false);

      res = 'Success';
      return res;
    } on FirebaseAuthException catch (e) {
      _setLoading(false);
      res = e.message.toString();
      print("# Auth Sign In - Error => $e");
      return res;
    }
  }

  Future<void> signOut() async {
    await FirebaseAuth.instance.signOut();
  }

  void _setLoading(bool newValue) {
    isLoading = newValue;
    notifyListeners();
  }
}

Solution

  • This has nothing to do with Riverpod but the way you initialize your controllers inside the build method. When you update your theme build method will rebuild, thats a fact, all your color changes so all widgets thath inheret Theme (all) will rebuild. You say you tried stateful, you tried this?

    class LoginPage extends StatefulWidget {
      LoginPage({Key key}) : super(key: key);
    
      @override
      _LoginPageState createState() => _LoginPageState();
    }
    
    class _LoginPageState extends State<LoginPage> {
      final GlobalKey<FormState> _signInFormKey = GlobalKey<FormState>();
    
      final TextEditingController _emailController = TextEditingController();
      final TextEditingController _passwordController = TextEditingController();
    
      final FocusNode _emailFocusNode = FocusNode();
      final FocusNode _passwordFocusNode = FocusNode();
    
      @override
      void dispose() {
        _emailController.dispose();
        _passwordController.dispose();
        _emailFocusNode.dispose();
        _passwordFocusNode.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        bool _darkTheme = true;
        _darkTheme = (ThemeProvider.controllerOf(context).currentThemeId == 'dark');
        /// you could also wrap your switch button in a consumerWidget just to watch ThemeProvider change to avoid doing this here
    
        return GestureDetector(...);
      }
    }
    

    You could also use a package like flutter_hooks if you want to initialzie stuff in build method, but I wouldn't recommend it right now until you understand some basics of stateful and inhereted widgets