skip to Main Content

I have a stateful widget called AuthenticatingScreen where I’m trying to perform the following flow…

  1. Output message letting the user know we are logging them in
  2. Get user oAuth token (calls to service file)
  3. Update the message to let the user know we are loading their details
  4. Fetch the users details and redirect them away

The problem is that at step three, I’m rebuilding the state, which is in turn causing the build method to be fired again and calling the service again, which triggers an exception.

import 'package:flutter/material.dart';
import 'package:testing/services/auth_service.dart';

class AuthenticatingScreen extends StatefulWidget {
  final String token;

  AuthenticatingScreen(this.token);

  @override
  State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
}

class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
  // step 1) our default message
  String _message = 'Please wait while we log you in...';

  Future<void> _fetchUserDetails() {
    return Future.delayed(const Duration(seconds: 3), () {
      // ToDo: fetch user details from the server
    });
  }

  @override
  Widget build(BuildContext context) {

    // step 2) get our oAuth token
    AuthService.handleCallback(widget.token).then((accessCode) async {
      
      // step 3) update our message
      setState(() => _message = 'We're just getting your details');

      // step 4) retrieve our user details and redirect away
      _fetchUserDetails().then((_) {
        Navigator.of(context).pushNamedAndRemoveUntil(
          '/home',
          (Route<dynamic> route) => false,
        );
      });
    });

    /// output our authenticating screen.
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Padding(
              padding: EdgeInsets.only(bottom: 20.0),
              child: CircularProgressIndicator(),
            ),
            Text(_message),
          ],
        ),
      ),
    );
  }
}

My question being: How can I work around this / extract this logic to only fire when the widget is created, while still having access to the build context for navigation?

I’ve tried making the widget itself stateless and extracting the message and spinner into a separate widget, but changing the input argument alone still doesn’t force a rebuild.

2

Answers


  1. Chosen as BEST ANSWER

    Ok, so I have figured out the solution. It seems making service calls within the build() method is a bad idea.

    Moving my service calls into a void function which can then be called within the initState() method seems to be the way to go.

    import 'package:flutter/material.dart';
    import 'package:testing/screens/home.dart';
    import 'package:testing/services/auth_service.dart';
    
    class AuthenticatingScreen extends StatefulWidget {
      final String token;
    
      AuthenticatingScreen(this.token);
    
      @override
      State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
    }
    
    class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
      /// the default message to display to the user.
      String _message = 'Please wait while we log you in...';
    
      void _authenticateUser(String token) {
        AuthService.handleCallback(widget.token).then((accessCode) async {
          // we've got the users token, now we need to fetch the user details
          setState(() => _message = 'We're just getting your details');
    
          // after fetching the user details, push them to the home screen
          _fetchUserDetails().then((_) {
            Navigator.of(context).pushNamedAndRemoveUntil(
              HomeScreen.name,
              (Route<dynamic> route) => false,
            );
          });
        });
      }
    
      Future<void> _fetchUserDetails() {
        return Future.delayed(const Duration(seconds: 3), () {
          // ToDo: fetch user details from the server
        });
      }
    
      @override
      void initState() {
        super.initState();
        _authenticateUser(widget.token);
      }
    
      @override
      Widget build(BuildContext context) {
        /// output our authenticating screen.
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Padding(
                  padding: EdgeInsets.only(bottom: 20.0),
                  child: CircularProgressIndicator(),
                ),
                Text(_message),
              ],
            ),
          ),
        );
      }
    }
    

    And this way when the build() method is called again for the rebuild, very little details have to be redrawn.


  2. you can do it this way, i usually use getx & controller to achieve this.

    1. separate the UI class & service class preferably in a controller
    2. make the UI class statefull
    3. call the API in onInit() method,as it called only once it will trigger the
      service class
    4. in API method when you get the result 200, initiate the UI transition
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search