blob: d91db6ed2dd7666a93a01bbb9ad86c2b0a47b0d5 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// This scenario demonstrates how to use redirect to handle a asynchronous
// sign-in flow.
//
// The `StreamAuth` is a mock of google_sign_in. This example wraps it with an
// InheritedNotifier, StreamAuthScope, and relies on
// `dependOnInheritedWidgetOfExactType` to create a dependency between the
// notifier and go_router's parsing pipeline. When StreamAuth broadcasts new
// event, the dependency will cause the go_router to reparse the current url
// which will also trigger the redirect.
void main() => runApp(StreamAuthScope(child: App()));
/// The main app.
class App extends StatelessWidget {
/// Creates an [App].
App({super.key});
/// The title of the app.
static const String title = 'GoRouter Example: Redirection';
// add the login info into the tree as app state that can change over time
@override
Widget build(BuildContext context) => MaterialApp.router(
routerConfig: _router,
title: title,
debugShowCheckedModeBanner: false,
);
late final GoRouter _router = GoRouter(
routes: <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
),
GoRoute(
path: '/login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
),
],
// redirect to the login page if the user is not logged in
redirect: (BuildContext context, GoRouterState state) async {
// Using `of` method creates a dependency of StreamAuthScope. It will
// cause go_router to reparse current route if StreamAuth has new sign-in
// information.
final bool loggedIn = await StreamAuthScope.of(context).isSignedIn();
final bool loggingIn = state.subloc == '/login';
if (!loggedIn) {
return '/login';
}
// if the user is logged in but still on the login page, send them to
// the home page
if (loggingIn) {
return '/';
}
// no need to redirect at all
return null;
},
);
}
/// The login screen.
class LoginScreen extends StatefulWidget {
/// Creates a [LoginScreen].
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with TickerProviderStateMixin {
bool loggingIn = false;
late final AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..addListener(() {
setState(() {});
});
controller.repeat();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text(App.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (loggingIn) CircularProgressIndicator(value: controller.value),
if (!loggingIn)
ElevatedButton(
onPressed: () {
StreamAuthScope.of(context).signIn('test-user');
setState(() {
loggingIn = true;
});
},
child: const Text('Login'),
),
],
),
),
);
}
/// The home screen.
class HomeScreen extends StatelessWidget {
/// Creates a [HomeScreen].
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final StreamAuth info = StreamAuthScope.of(context);
return Scaffold(
appBar: AppBar(
title: const Text(App.title),
actions: <Widget>[
IconButton(
onPressed: () => info.signOut(),
tooltip: 'Logout: ${info.currentUser}',
icon: const Icon(Icons.logout),
)
],
),
body: const Center(
child: Text('HomeScreen'),
),
);
}
}
/// A scope that provides [StreamAuth] for the subtree.
class StreamAuthScope extends InheritedNotifier<StreamAuthNotifier> {
/// Creates a [StreamAuthScope] sign in scope.
StreamAuthScope({
super.key,
required super.child,
}) : super(
notifier: StreamAuthNotifier(),
);
/// Gets the [StreamAuth].
static StreamAuth of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<StreamAuthScope>()!
.notifier!
.streamAuth;
}
}
/// A class that converts [StreamAuth] into a [ChangeNotifier].
class StreamAuthNotifier extends ChangeNotifier {
/// Creates a [StreamAuthNotifier].
StreamAuthNotifier() : streamAuth = StreamAuth() {
streamAuth.onCurrentUserChanged.listen((String? string) {
notifyListeners();
});
}
/// The stream auth client.
final StreamAuth streamAuth;
}
/// An asynchronous log in services mock with stream similar to google_sign_in.
///
/// This class adds an artificial delay of 3 second when logging in an user, and
/// will automatically clear the login session after [refreshInterval].
class StreamAuth {
/// Creates an [StreamAuth] that clear the current user session in
/// [refeshInterval] second.
StreamAuth({this.refreshInterval = 20})
: _userStreamController = StreamController<String?>.broadcast() {
_userStreamController.stream.listen((String? currentUser) {
_currentUser = currentUser;
});
}
/// The current user.
String? get currentUser => _currentUser;
String? _currentUser;
/// Checks whether current user is signed in with an artificial delay to mimic
/// async operation.
Future<bool> isSignedIn() async {
await Future<void>.delayed(const Duration(seconds: 1));
return _currentUser != null;
}
/// A stream that notifies when current user has changed.
Stream<String?> get onCurrentUserChanged => _userStreamController.stream;
final StreamController<String?> _userStreamController;
/// The interval that automatically signs out the user.
final int refreshInterval;
Timer? _timer;
Timer _createRefreshTimer() {
return Timer(Duration(seconds: refreshInterval), () {
_userStreamController.add(null);
_timer = null;
});
}
/// Signs in a user with an artificial delay to mimic async operation.
Future<void> signIn(String newUserName) async {
await Future<void>.delayed(const Duration(seconds: 3));
_userStreamController.add(newUserName);
_timer?.cancel();
_timer = _createRefreshTimer();
}
/// Signs out the current user.
Future<void> signOut() async {
_timer?.cancel();
_timer = null;
_userStreamController.add(null);
}
}