9

I'm trying to redirect to login in case the token has been expired in Flutter

Trying to get the posts:

body: new Container(
    padding: new EdgeInsets.only(bottom: 8.0),
    color: Color(0xff2c3e4e),
    child: FutureBuilder<List<Post>>(
        future: api.getposts(),
        builder: (context, snapshot) {
            // doing stuff with response
        }
    )
)

getposts and catching the error:

Future<List<Post>> getposts() async {
    url = 'url';
    var response = await http.get(url,
        headers: {HttpHeaders.authorizationHeader: 'bearer ' + token},
    );
    //part I can't understand how to get to work, how do I push? This gives error about context not being defined.
    if (response.body.toString().contains('Token is Expired')) {
        Navigator.push(
            context,
            MaterialPageRoute(
                builder: (context) =>
                    LoginScreen()),
        );
    }
}

So the question is, how do I use the Navigator correctly in such cases and I can redirect back to loginScreen in case the token has been expired and needs to be renewed? Like mentioned in the comment in the code example, the context gives me "undefined".

Is this even possible the way I am doing it or am I simply handling the whole check completely wrong?

vahdet
  • 6,357
  • 9
  • 51
  • 106
EmJeiEn
  • 1,343
  • 5
  • 17
  • 30
  • You can use redux to manage a global state, I have written a small application with splashScreen here: https://github.com/EdHuamani/flutter_redux_sample_1 – EdHuamani Mar 07 '19 at 23:39
  • Checking the content text for a string is highly likely to break in the future when some other string is returned. You're much better off checking for the HTTP status codes (exactly why they were designed) e.g. `if response.statusCode == 401...` – dKen Nov 18 '21 at 12:58

2 Answers2

5

Code should have single resonpsibility. Your getPost method are doing 2 things at the same time. You should break this function such that it either successfully get the the post, or throw exception, and its caller will handle the exception. Its caller btw must be within build method, because only build method has BuildContext context, something like this:

      if (response.body.toString().contains('Token is Expired')) {
        throw new Exception("Token is Expired") // you may want to implement different exception class
      }
      body: new Container(
        padding: new EdgeInsets.only(bottom: 8.0),
        color: Color(0xff2c3e4e),
        child: FutureBuilder<List<Post>>(
            future: api.getposts(),
            builder: (context, snapshot) {
              if (snapshot.hasError) {
                // you might want to handle different exception, such as token expires, no network, server down, etc.
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => LoginScreen()),
                );
                return Container(); // return empty widget
              }
              if (snapshot.hasData) {
                // do somthing with data
                return Text('Some data here');
              }
              // has no data
              return Text("Loading...");

            }),
      ),

UPDATE

Thanks to @TruongSinh I got it figured out.

Followed his example and figured out the build navigator method which works:

if (snapshot.hasError) {

              @override
              void run() {
                scheduleMicrotask(() {

                  Navigator.pushReplacement(
                    context,
                    MaterialPageRoute(builder: (context) => LoginScreen()),
                  );
                });
              }
              run();
}
EmJeiEn
  • 1,343
  • 5
  • 17
  • 30
TruongSinh
  • 4,662
  • 32
  • 52
  • Error catching seems to work perfectly, thank you. However the Navigator's return gives me "error: Missing return value after 'return'. Tried without i t as well but then I'm getting an error: Failed assertion: line 1554 pos 12: '!_debugLocked': is not true. – EmJeiEn Mar 08 '19 at 03:02
  • hmm, maybe it always require a widget when return, did you try an empty Widget, such as `return Container();`? – TruongSinh Mar 08 '19 at 03:45
  • Container unfortunately returned getter null, but I found that it has to be wrapped into scheduleMicrotask since the tree has not yet ran through. I did just that and got it to login page, however it loops indefinitely, doing the redirect multiple times in nevery second. Trying to find a proper documentation for the scheduleMicrotask and why it is acting like that at the moment. – EmJeiEn Mar 08 '19 at 04:41
  • Thank you once again. Got it figured out! Instead of .push I used .pushReplacement and wrapped it to void function which I then fired up. – EmJeiEn Mar 08 '19 at 17:15
  • I don't understand why we need so much logic in the view, this is ridiculus – Rizerzero Jun 13 '21 at 08:18
4

Update: added a package containing several guards like this

I did it with a StreamBuilder to react on change and be able to display a LoadingScreen when we don't know yet if the user is connected.

StreamBuilder authGuard = StreamBuilder(
  stream: Auth.authState$,
  builder: (context, snapshot) {
    switch (snapshot.data) {
      case AuthState.PENDING:
        return LoadingScreen();
      case AuthState.UNAUTHENTICATED:
        return SignInScreen();
      case AuthState.AUTHENTICATED:
        return HomeScreen();
      default:
        return LoadingScreen();
    }
  },
);

So it will change screen depending on the AuthState:

return MaterialApp(
  // ...
  home: authGuard,
);

And my auth class

import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:rxdart/rxdart.dart';

enum AuthState { PENDING, AUTHENTICATED, UNAUTHENTICATED }

class Auth {
  static final FirebaseAuth _auth = FirebaseAuth.instance;
  static final GoogleSignIn _googleSignIn = GoogleSignIn();

  static Stream<AuthState> authState$ = FirebaseAuth.instance.onAuthStateChanged
      .map((state) =>
          state != null ? AuthState.AUTHENTICATED : AuthState.UNAUTHENTICATED)
      .startWith(AuthState.PENDING);

  static Future<FirebaseUser> signInWithGoogle() async {
    // ...
  }
}
Ced
  • 15,847
  • 14
  • 87
  • 146