flutterdartgoogle-signin

Flutter web google_sign_in widget twitching continuously on user interaction


I am currently encountering a problem where my google_sign_in button rendered from the Web is twitching as shown here:

GIF: GOOGLE SIGN IN WIDGET TWITCHING

I have followed the example given from the pub dev https://pub.dev/packages/google_sign_in and have achieved success in sign in and sign out, the functionality works, it's just that the widget keeps on twitching and it doesn't look great at all.

I wonder if I was doing something obviously wrong, because I could not find anyone else having the same problem as me anywhere. So here's my Flutter widget code that should be quite straight forward:

import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:google_sign_in_web/web_only.dart' as web;
import 'package:pos_billing/constants/env.dart';
import 'package:pos_billing/pages/authentication/helper/auth_state.dart';
import 'package:provider/provider.dart';
import 'package:pos_billing/pages/widgets/default/custom_toast.dart';

class GoogleSignInButton extends StatefulWidget {
  const GoogleSignInButton({super.key});

  @override
  State<GoogleSignInButton> createState() => _GoogleSignInButtonState();
}

class _GoogleSignInButtonState extends State<GoogleSignInButton> {
  final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
  StreamSubscription<GoogleSignInAuthenticationEvent>? _authSub;

  bool _handlingAuth = false;
  bool _loading = false;

  // initializing google sign in
  @override
  void initState() {
    super.initState();
    _googleSignIn.initialize(clientId: EnvConfig.googleClientId).then((
      _,
    ) async {
      _authSub = _googleSignIn.authenticationEvents.listen(
        _handleAuthenticationEvent,
        onError: _handleAuthenticationError,
      );
      _googleSignIn.attemptLightweightAuthentication();
    });
  }

  @override
  void dispose() {
    _authSub?.cancel();
    super.dispose();
  }

  // listens when authentication event occurs
  Future<void> _handleAuthenticationEvent(
    GoogleSignInAuthenticationEvent event,
  ) async {
    if (_handlingAuth) {
      return;
    } else {
      setState(() => _handlingAuth = true);
    }

    final GoogleSignInAccount? user = switch (event) {
      GoogleSignInAuthenticationEventSignIn() => event.user,
      GoogleSignInAuthenticationEventSignOut() => null,
    };

    if (user != null) {
      try {
        final idToken = user.authentication.idToken;
        final auth = context.read<AuthModel>();
        // API callback to authenticate in backend
        await auth.loginGoogle(idToken!);
        if (!mounted) return;
        await auth.onLoginCallback(context, user.email);
      } catch (e) {
        _onAuthError(e);
      }
      setState(() => _handlingAuth = false);
    }
  }

  // handles auth errors
  Future<void> _handleAuthenticationError(Object e) async {
    await context.read<AuthModel>().logout();
    if (!mounted) return;
    String? message = 'Unknown error: $e';
    if (e is GoogleSignInException) {
      if (e.code.name == 'canceled') return;
      message = e.code.name;
    }
    CustomToast.error(title: message);
  }

  Future<void> _authenticate() async {
    if (!_googleSignIn.supportsAuthenticate()) return;

    setState(() => _loading = true);
    try {
      await _googleSignIn.authenticate();
    } catch (e) {
      _onAuthError(e);
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  void _onAuthError(Object error) {
    if (!mounted) return;
    String msg = '';
    if (error is GoogleSignInException) {
      msg = switch (error.code) {
        GoogleSignInExceptionCode.canceled => 'Sign in canceled',
        _ => 'GoogleSignInException ${error.code}: ${error.description}',
      };
    }
    msg = 'Sign in failed: $error';
    CustomToast.error(title: msg);
  }

  @override
  Widget build(BuildContext context) {
    if (kIsWeb && !_googleSignIn.supportsAuthenticate()) {
      return web.renderButton(
        configuration: web.GSIButtonConfiguration(type: web.GSIButtonType.icon),
      );
    }

    return Material(
      child: InkWell(
        onTap: _loading ? null : _authenticate,
        child: Container(
          padding: const EdgeInsets.all(10),
          decoration: BoxDecoration(
            border: Border.all(color: Color(0xFFDADCE0)),
            borderRadius: BorderRadius.circular(4),
          ),
          child: SvgPicture.asset(
            'lib/assets/google_logo.svg',
            width: 18,
            height: 18,
          ),
        ),
      ),
    );
  }
}

So my question is why does this happen and how can I solve this twitching widget issue, is it a bug from the package? Thank you for your time!


Solution

  • After reviewing my code, I realize that I had an unused parent widget which was causing the issue.

    GestureDetector(
        onTap: () => FocusScope.of(context).unfocus(),
        ...
            // GoogleSignInButton() is placed as a children of GestureDetector
        ...
    )
    

    By removing the GestureDetector, it no longer rebuilds on tapping/unfocus, thus the twitching is gone! Thank you @Niyam Prassanna Kunder for the heads up.