v5
This commit is contained in:
244
lib/go_router/examples/async_redirection.dart
Normal file
244
lib/go_router/examples/async_redirection.dart
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
// 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.matchedLocation == '/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);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
lib/go_router/examples/books/main.dart
Normal file
171
lib/go_router/examples/books/main.dart
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
// 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 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import 'src/auth.dart';
|
||||||
|
import 'src/data/author.dart';
|
||||||
|
import 'src/data/book.dart';
|
||||||
|
import 'src/data/library.dart';
|
||||||
|
import 'src/screens/author_details.dart';
|
||||||
|
import 'src/screens/authors.dart';
|
||||||
|
import 'src/screens/book_details.dart';
|
||||||
|
import 'src/screens/books.dart';
|
||||||
|
import 'src/screens/scaffold.dart';
|
||||||
|
import 'src/screens/settings.dart';
|
||||||
|
import 'src/screens/sign_in.dart';
|
||||||
|
|
||||||
|
void main() => runApp(Bookstore());
|
||||||
|
|
||||||
|
/// The book store view.
|
||||||
|
class Bookstore extends StatelessWidget {
|
||||||
|
/// Creates a [Bookstore].
|
||||||
|
Bookstore({super.key});
|
||||||
|
|
||||||
|
final ValueKey<String> _scaffoldKey = const ValueKey<String>('App scaffold');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => BookstoreAuthScope(
|
||||||
|
notifier: _auth,
|
||||||
|
child: MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final BookstoreAuth _auth = BookstoreAuth();
|
||||||
|
|
||||||
|
late final GoRouter _router = GoRouter(
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
redirect: (_, __) => '/books',
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/signin',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
FadeTransitionPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: SignInScreen(
|
||||||
|
onSignIn: (Credentials credentials) {
|
||||||
|
BookstoreAuthScope.of(context)
|
||||||
|
.signIn(credentials.username, credentials.password);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/books',
|
||||||
|
redirect: (_, __) => '/books/popular',
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/book/:bookId',
|
||||||
|
redirect: (BuildContext context, GoRouterState state) =>
|
||||||
|
'/books/all/${state.pathParameters['bookId']}',
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/books/:kind(new|all|popular)',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
FadeTransitionPage(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
child: BookstoreScaffold(
|
||||||
|
selectedTab: ScaffoldTab.books,
|
||||||
|
child: BooksScreen(state.pathParameters['kind']!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: ':bookId',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
final String bookId = state.pathParameters['bookId']!;
|
||||||
|
final Book? selectedBook = libraryInstance.allBooks
|
||||||
|
.firstWhereOrNull((Book b) => b.id.toString() == bookId);
|
||||||
|
|
||||||
|
return BookDetailsScreen(book: selectedBook);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/author/:authorId',
|
||||||
|
redirect: (BuildContext context, GoRouterState state) =>
|
||||||
|
'/authors/${state.pathParameters['authorId']}',
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/authors',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
FadeTransitionPage(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
child: const BookstoreScaffold(
|
||||||
|
selectedTab: ScaffoldTab.authors,
|
||||||
|
child: AuthorsScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: ':authorId',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
final int authorId = int.parse(state.pathParameters['authorId']!);
|
||||||
|
final Author? selectedAuthor = libraryInstance.allAuthors
|
||||||
|
.firstWhereOrNull((Author a) => a.id == authorId);
|
||||||
|
|
||||||
|
return AuthorDetailsScreen(author: selectedAuthor);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/settings',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
FadeTransitionPage(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
child: const BookstoreScaffold(
|
||||||
|
selectedTab: ScaffoldTab.settings,
|
||||||
|
child: SettingsScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
redirect: _guard,
|
||||||
|
refreshListenable: _auth,
|
||||||
|
debugLogDiagnostics: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
String? _guard(BuildContext context, GoRouterState state) {
|
||||||
|
final bool signedIn = _auth.signedIn;
|
||||||
|
final bool signingIn = state.matchedLocation == '/signin';
|
||||||
|
|
||||||
|
// Go to /signin if the user is not signed in
|
||||||
|
if (!signedIn && !signingIn) {
|
||||||
|
return '/signin';
|
||||||
|
}
|
||||||
|
// Go to /books if the user is signed in and tries to go to /signin.
|
||||||
|
else if (signedIn && signingIn) {
|
||||||
|
return '/books';
|
||||||
|
}
|
||||||
|
|
||||||
|
// no redirect
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A page that fades in an out.
|
||||||
|
class FadeTransitionPage extends CustomTransitionPage<void> {
|
||||||
|
/// Creates a [FadeTransitionPage].
|
||||||
|
FadeTransitionPage({
|
||||||
|
required LocalKey super.key,
|
||||||
|
required super.child,
|
||||||
|
}) : super(
|
||||||
|
transitionsBuilder: (BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child) =>
|
||||||
|
FadeTransition(
|
||||||
|
opacity: animation.drive(_curveTween),
|
||||||
|
child: child,
|
||||||
|
));
|
||||||
|
|
||||||
|
static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn);
|
||||||
|
}
|
||||||
46
lib/go_router/examples/books/src/auth.dart
Normal file
46
lib/go_router/examples/books/src/auth.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// 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 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// A mock authentication service.
|
||||||
|
class BookstoreAuth extends ChangeNotifier {
|
||||||
|
bool _signedIn = false;
|
||||||
|
|
||||||
|
/// Whether user has signed in.
|
||||||
|
bool get signedIn => _signedIn;
|
||||||
|
|
||||||
|
/// Signs out the current user.
|
||||||
|
Future<void> signOut() async {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||||
|
// Sign out.
|
||||||
|
_signedIn = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs in a user.
|
||||||
|
Future<bool> signIn(String username, String password) async {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
// Sign in. Allow any password.
|
||||||
|
_signedIn = true;
|
||||||
|
notifyListeners();
|
||||||
|
return _signedIn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An inherited notifier to host [BookstoreAuth] for the subtree.
|
||||||
|
class BookstoreAuthScope extends InheritedNotifier<BookstoreAuth> {
|
||||||
|
/// Creates a [BookstoreAuthScope].
|
||||||
|
const BookstoreAuthScope({
|
||||||
|
required BookstoreAuth super.notifier,
|
||||||
|
required super.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Gets the [BookstoreAuth] above the context.
|
||||||
|
static BookstoreAuth of(BuildContext context) => context
|
||||||
|
.dependOnInheritedWidgetOfExactType<BookstoreAuthScope>()!
|
||||||
|
.notifier!;
|
||||||
|
}
|
||||||
7
lib/go_router/examples/books/src/data.dart
Normal file
7
lib/go_router/examples/books/src/data.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
export 'data/author.dart';
|
||||||
|
export 'data/book.dart';
|
||||||
|
export 'data/library.dart';
|
||||||
23
lib/go_router/examples/books/src/data/author.dart
Normal file
23
lib/go_router/examples/books/src/data/author.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// 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 'book.dart';
|
||||||
|
|
||||||
|
/// Author data class.
|
||||||
|
class Author {
|
||||||
|
/// Creates an author data object.
|
||||||
|
Author({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The id of the author.
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
/// The name of the author.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The books of the author.
|
||||||
|
final List<Book> books = <Book>[];
|
||||||
|
}
|
||||||
32
lib/go_router/examples/books/src/data/book.dart
Normal file
32
lib/go_router/examples/books/src/data/book.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 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 'author.dart';
|
||||||
|
|
||||||
|
/// Book data class.
|
||||||
|
class Book {
|
||||||
|
/// Creates a book data object.
|
||||||
|
Book({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.isPopular,
|
||||||
|
required this.isNew,
|
||||||
|
required this.author,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The id of the book.
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
/// The title of the book.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The author of the book.
|
||||||
|
final Author author;
|
||||||
|
|
||||||
|
/// Whether the book is popular.
|
||||||
|
final bool isPopular;
|
||||||
|
|
||||||
|
/// Whether the book is new.
|
||||||
|
final bool isNew;
|
||||||
|
}
|
||||||
76
lib/go_router/examples/books/src/data/library.dart
Normal file
76
lib/go_router/examples/books/src/data/library.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// 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 'author.dart';
|
||||||
|
import 'book.dart';
|
||||||
|
|
||||||
|
/// Library data mock.
|
||||||
|
final Library libraryInstance = Library()
|
||||||
|
..addBook(
|
||||||
|
title: 'Left Hand of Darkness',
|
||||||
|
authorName: 'Ursula K. Le Guin',
|
||||||
|
isPopular: true,
|
||||||
|
isNew: true)
|
||||||
|
..addBook(
|
||||||
|
title: 'Too Like the Lightning',
|
||||||
|
authorName: 'Ada Palmer',
|
||||||
|
isPopular: false,
|
||||||
|
isNew: true)
|
||||||
|
..addBook(
|
||||||
|
title: 'Kindred',
|
||||||
|
authorName: 'Octavia E. Butler',
|
||||||
|
isPopular: true,
|
||||||
|
isNew: false)
|
||||||
|
..addBook(
|
||||||
|
title: 'The Lathe of Heaven',
|
||||||
|
authorName: 'Ursula K. Le Guin',
|
||||||
|
isPopular: false,
|
||||||
|
isNew: false);
|
||||||
|
|
||||||
|
/// A library that contains books and authors.
|
||||||
|
class Library {
|
||||||
|
/// The books in the library.
|
||||||
|
final List<Book> allBooks = <Book>[];
|
||||||
|
|
||||||
|
/// The authors in the library.
|
||||||
|
final List<Author> allAuthors = <Author>[];
|
||||||
|
|
||||||
|
/// Adds a book into the library.
|
||||||
|
void addBook({
|
||||||
|
required String title,
|
||||||
|
required String authorName,
|
||||||
|
required bool isPopular,
|
||||||
|
required bool isNew,
|
||||||
|
}) {
|
||||||
|
final Author author = allAuthors.firstWhere(
|
||||||
|
(Author author) => author.name == authorName,
|
||||||
|
orElse: () {
|
||||||
|
final Author value = Author(id: allAuthors.length, name: authorName);
|
||||||
|
allAuthors.add(value);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final Book book = Book(
|
||||||
|
id: allBooks.length,
|
||||||
|
title: title,
|
||||||
|
isPopular: isPopular,
|
||||||
|
isNew: isNew,
|
||||||
|
author: author,
|
||||||
|
);
|
||||||
|
|
||||||
|
author.books.add(book);
|
||||||
|
allBooks.add(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of popular books in the library.
|
||||||
|
List<Book> get popularBooks => <Book>[
|
||||||
|
...allBooks.where((Book book) => book.isPopular),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The list of new books in the library.
|
||||||
|
List<Book> get newBooks => <Book>[
|
||||||
|
...allBooks.where((Book book) => book.isNew),
|
||||||
|
];
|
||||||
|
}
|
||||||
49
lib/go_router/examples/books/src/screens/author_details.dart
Normal file
49
lib/go_router/examples/books/src/screens/author_details.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../data.dart';
|
||||||
|
import '../widgets/book_list.dart';
|
||||||
|
|
||||||
|
/// The author detail screen.
|
||||||
|
class AuthorDetailsScreen extends StatelessWidget {
|
||||||
|
/// Creates an author detail screen.
|
||||||
|
const AuthorDetailsScreen({
|
||||||
|
required this.author,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The author to be displayed.
|
||||||
|
final Author? author;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (author == null) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Text('No author found.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(author!.name),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: BookList(
|
||||||
|
books: author!.books,
|
||||||
|
onTap: (Book book) => context.go('/book/${book.id}'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/go_router/examples/books/src/screens/authors.dart
Normal file
31
lib/go_router/examples/books/src/screens/authors.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../data.dart';
|
||||||
|
import '../widgets/author_list.dart';
|
||||||
|
|
||||||
|
/// A screen that displays a list of authors.
|
||||||
|
class AuthorsScreen extends StatelessWidget {
|
||||||
|
/// Creates an [AuthorsScreen].
|
||||||
|
const AuthorsScreen({super.key});
|
||||||
|
|
||||||
|
/// The title of the screen.
|
||||||
|
static const String title = 'Authors';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(title),
|
||||||
|
),
|
||||||
|
body: AuthorList(
|
||||||
|
authors: libraryInstance.allAuthors,
|
||||||
|
onTap: (Author author) {
|
||||||
|
context.go('/author/${author.id}');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
77
lib/go_router/examples/books/src/screens/book_details.dart
Normal file
77
lib/go_router/examples/books/src/screens/book_details.dart
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:url_launcher/link.dart';
|
||||||
|
|
||||||
|
import '../data.dart';
|
||||||
|
import 'author_details.dart';
|
||||||
|
|
||||||
|
/// A screen to display book details.
|
||||||
|
class BookDetailsScreen extends StatelessWidget {
|
||||||
|
/// Creates a [BookDetailsScreen].
|
||||||
|
const BookDetailsScreen({
|
||||||
|
super.key,
|
||||||
|
this.book,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The book to be displayed.
|
||||||
|
final Book? book;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (book == null) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Text('No book found.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(book!.title),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
book!.title,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
book!.author.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push<void>(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (BuildContext context) =>
|
||||||
|
AuthorDetailsScreen(author: book!.author),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('View author (navigator.push)'),
|
||||||
|
),
|
||||||
|
Link(
|
||||||
|
uri: Uri.parse('/author/${book!.author.id}'),
|
||||||
|
builder: (BuildContext context, FollowLink? followLink) =>
|
||||||
|
TextButton(
|
||||||
|
onPressed: followLink,
|
||||||
|
child: const Text('View author (Link)'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.push('/author/${book!.author.id}');
|
||||||
|
},
|
||||||
|
child: const Text('View author (GoRouter.push)'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
lib/go_router/examples/books/src/screens/books.dart
Normal file
118
lib/go_router/examples/books/src/screens/books.dart
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../data.dart';
|
||||||
|
import '../widgets/book_list.dart';
|
||||||
|
|
||||||
|
/// A screen that displays a list of books.
|
||||||
|
class BooksScreen extends StatefulWidget {
|
||||||
|
/// Creates a [BooksScreen].
|
||||||
|
const BooksScreen(this.kind, {super.key});
|
||||||
|
|
||||||
|
/// Which tab to display.
|
||||||
|
final String kind;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BooksScreen> createState() => _BooksScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BooksScreenState extends State<BooksScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tabController = TabController(length: 3, vsync: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(BooksScreen oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
switch (widget.kind) {
|
||||||
|
case 'popular':
|
||||||
|
_tabController.index = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'new':
|
||||||
|
_tabController.index = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'all':
|
||||||
|
_tabController.index = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Books'),
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
onTap: _handleTabTapped,
|
||||||
|
tabs: const <Tab>[
|
||||||
|
Tab(
|
||||||
|
text: 'Popular',
|
||||||
|
icon: Icon(Icons.people),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
text: 'New',
|
||||||
|
icon: Icon(Icons.new_releases),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
text: 'All',
|
||||||
|
icon: Icon(Icons.list),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: <Widget>[
|
||||||
|
BookList(
|
||||||
|
books: libraryInstance.popularBooks,
|
||||||
|
onTap: _handleBookTapped,
|
||||||
|
),
|
||||||
|
BookList(
|
||||||
|
books: libraryInstance.newBooks,
|
||||||
|
onTap: _handleBookTapped,
|
||||||
|
),
|
||||||
|
BookList(
|
||||||
|
books: libraryInstance.allBooks,
|
||||||
|
onTap: _handleBookTapped,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _handleBookTapped(Book book) {
|
||||||
|
context.go('/book/${book.id}');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTabTapped(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 1:
|
||||||
|
context.go('/books/new');
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
context.go('/books/all');
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
context.go('/books/popular');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
lib/go_router/examples/books/src/screens/scaffold.dart
Normal file
71
lib/go_router/examples/books/src/screens/scaffold.dart
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// 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 'package:adaptive_navigation/adaptive_navigation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// The enum for scaffold tab.
|
||||||
|
enum ScaffoldTab {
|
||||||
|
/// The books tab.
|
||||||
|
books,
|
||||||
|
|
||||||
|
/// The authors tab.
|
||||||
|
authors,
|
||||||
|
|
||||||
|
/// The settings tab.
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The scaffold for the book store.
|
||||||
|
class BookstoreScaffold extends StatelessWidget {
|
||||||
|
/// Creates a [BookstoreScaffold].
|
||||||
|
const BookstoreScaffold({
|
||||||
|
required this.selectedTab,
|
||||||
|
required this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Which tab of the scaffold to display.
|
||||||
|
final ScaffoldTab selectedTab;
|
||||||
|
|
||||||
|
/// The scaffold body.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
body: AdaptiveNavigationScaffold(
|
||||||
|
selectedIndex: selectedTab.index,
|
||||||
|
body: child,
|
||||||
|
onDestinationSelected: (int idx) {
|
||||||
|
switch (ScaffoldTab.values[idx]) {
|
||||||
|
case ScaffoldTab.books:
|
||||||
|
context.go('/books');
|
||||||
|
break;
|
||||||
|
case ScaffoldTab.authors:
|
||||||
|
context.go('/authors');
|
||||||
|
break;
|
||||||
|
case ScaffoldTab.settings:
|
||||||
|
context.go('/settings');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destinations: const <AdaptiveScaffoldDestination>[
|
||||||
|
AdaptiveScaffoldDestination(
|
||||||
|
title: 'Books',
|
||||||
|
icon: Icons.book,
|
||||||
|
),
|
||||||
|
AdaptiveScaffoldDestination(
|
||||||
|
title: 'Authors',
|
||||||
|
icon: Icons.person,
|
||||||
|
),
|
||||||
|
AdaptiveScaffoldDestination(
|
||||||
|
title: 'Settings',
|
||||||
|
icon: Icons.settings,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
101
lib/go_router/examples/books/src/screens/settings.dart
Normal file
101
lib/go_router/examples/books/src/screens/settings.dart
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:url_launcher/link.dart';
|
||||||
|
|
||||||
|
import '../auth.dart';
|
||||||
|
|
||||||
|
/// The settings screen.
|
||||||
|
class SettingsScreen extends StatefulWidget {
|
||||||
|
/// Creates a [SettingsScreen].
|
||||||
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: const Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 12),
|
||||||
|
child: SettingsContent(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The content of a [SettingsScreen].
|
||||||
|
class SettingsContent extends StatelessWidget {
|
||||||
|
/// Creates a [SettingsContent].
|
||||||
|
const SettingsContent({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Column(
|
||||||
|
children: <Widget>[
|
||||||
|
...<Widget>[
|
||||||
|
Text(
|
||||||
|
'Settings',
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
BookstoreAuthScope.of(context).signOut();
|
||||||
|
},
|
||||||
|
child: const Text('Sign out'),
|
||||||
|
),
|
||||||
|
Link(
|
||||||
|
uri: Uri.parse('/book/0'),
|
||||||
|
builder: (BuildContext context, FollowLink? followLink) =>
|
||||||
|
TextButton(
|
||||||
|
onPressed: followLink,
|
||||||
|
child: const Text('Go directly to /book/0 (Link)'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.go('/book/0');
|
||||||
|
},
|
||||||
|
child: const Text('Go directly to /book/0 (GoRouter)'),
|
||||||
|
),
|
||||||
|
].map<Widget>((Widget w) =>
|
||||||
|
Padding(padding: const EdgeInsets.all(8), child: w)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => AlertDialog(
|
||||||
|
title: const Text('Alert!'),
|
||||||
|
content: const Text('The alert description goes here.'),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, 'Cancel'),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, 'OK'),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('Show Dialog'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
77
lib/go_router/examples/books/src/screens/sign_in.dart
Normal file
77
lib/go_router/examples/books/src/screens/sign_in.dart
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Credential data class.
|
||||||
|
class Credentials {
|
||||||
|
/// Creates a credential data object.
|
||||||
|
Credentials(this.username, this.password);
|
||||||
|
|
||||||
|
/// The username of the credentials.
|
||||||
|
final String username;
|
||||||
|
|
||||||
|
/// The password of the credentials.
|
||||||
|
final String password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sign-in screen.
|
||||||
|
class SignInScreen extends StatefulWidget {
|
||||||
|
/// Creates a sign-in screen.
|
||||||
|
const SignInScreen({
|
||||||
|
required this.onSignIn,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Called when users sign in with [Credentials].
|
||||||
|
final ValueChanged<Credentials> onSignIn;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SignInScreen> createState() => _SignInScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SignInScreenState extends State<SignInScreen> {
|
||||||
|
final TextEditingController _usernameController = TextEditingController();
|
||||||
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Card(
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints.loose(const Size(600, 600)),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Sign in',
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium),
|
||||||
|
TextField(
|
||||||
|
decoration: const InputDecoration(labelText: 'Username'),
|
||||||
|
controller: _usernameController,
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
decoration: const InputDecoration(labelText: 'Password'),
|
||||||
|
obscureText: true,
|
||||||
|
controller: _passwordController,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
widget.onSignIn(Credentials(
|
||||||
|
_usernameController.value.text,
|
||||||
|
_passwordController.value.text));
|
||||||
|
},
|
||||||
|
child: const Text('Sign in'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
37
lib/go_router/examples/books/src/widgets/author_list.dart
Normal file
37
lib/go_router/examples/books/src/widgets/author_list.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../data.dart';
|
||||||
|
|
||||||
|
/// The author list view.
|
||||||
|
class AuthorList extends StatelessWidget {
|
||||||
|
/// Creates an [AuthorList].
|
||||||
|
const AuthorList({
|
||||||
|
required this.authors,
|
||||||
|
this.onTap,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The list of authors to be shown.
|
||||||
|
final List<Author> authors;
|
||||||
|
|
||||||
|
/// Called when the user taps an author.
|
||||||
|
final ValueChanged<Author>? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => ListView.builder(
|
||||||
|
itemCount: authors.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) => ListTile(
|
||||||
|
title: Text(
|
||||||
|
authors[index].name,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'${authors[index].books.length} books',
|
||||||
|
),
|
||||||
|
onTap: onTap != null ? () => onTap!(authors[index]) : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
37
lib/go_router/examples/books/src/widgets/book_list.dart
Normal file
37
lib/go_router/examples/books/src/widgets/book_list.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../data.dart';
|
||||||
|
|
||||||
|
/// The book list view.
|
||||||
|
class BookList extends StatelessWidget {
|
||||||
|
/// Creates an [BookList].
|
||||||
|
const BookList({
|
||||||
|
required this.books,
|
||||||
|
this.onTap,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The list of books to be displayed.
|
||||||
|
final List<Book> books;
|
||||||
|
|
||||||
|
/// Called when the user taps a book.
|
||||||
|
final ValueChanged<Book>? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => ListView.builder(
|
||||||
|
itemCount: books.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) => ListTile(
|
||||||
|
title: Text(
|
||||||
|
books[index].title,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
books[index].author.name,
|
||||||
|
),
|
||||||
|
onTap: onTap != null ? () => onTap!(books[index]) : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
87
lib/go_router/examples/exception_handling.dart
Normal file
87
lib/go_router/examples/exception_handling.dart
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// This sample app shows how to use `GoRouter.onException` to redirect on
|
||||||
|
/// exception.
|
||||||
|
///
|
||||||
|
/// The first route '/' is mapped to [HomeScreen], and the second route
|
||||||
|
/// '/404' is mapped to [NotFoundScreen].
|
||||||
|
///
|
||||||
|
/// Any other unknown route or exception is redirected to `/404`.
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
/// The route configuration.
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
onException: (_, GoRouterState state, GoRouter router) {
|
||||||
|
router.go('/404', extra: state.uri.toString());
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const HomeScreen();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/404',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return NotFoundScreen(uri: state.extra as String? ?? '');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The main app.
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
/// Constructs a [MyApp]
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The home screen
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [HomeScreen]
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Home Screen')),
|
||||||
|
body: Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => context.go('/some-unknown-route'),
|
||||||
|
child: const Text('Simulates user entering unknown url'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The not found screen
|
||||||
|
class NotFoundScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [HomeScreen]
|
||||||
|
const NotFoundScreen({super.key, required this.uri});
|
||||||
|
|
||||||
|
/// The uri that can not be found.
|
||||||
|
final String uri;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Page Not Found')),
|
||||||
|
body: Center(
|
||||||
|
child: Text("Can't find a page for: $uri"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/go_router/examples/main.dart
Normal file
87
lib/go_router/examples/main.dart
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// This sample app shows an app with two screens.
|
||||||
|
///
|
||||||
|
/// The first route '/' is mapped to [HomeScreen], and the second route
|
||||||
|
/// '/details' is mapped to [DetailsScreen].
|
||||||
|
///
|
||||||
|
/// The buttons use context.go() to navigate to each destination. On mobile
|
||||||
|
/// devices, each destination is deep-linkable and on the web, can be navigated
|
||||||
|
/// to using the address bar.
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
/// The route configuration.
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const HomeScreen();
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const DetailsScreen();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The main app.
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
/// Constructs a [MyApp]
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The home screen
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [HomeScreen]
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Home Screen')),
|
||||||
|
body: Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => context.go('/details'),
|
||||||
|
child: const Text('Go to the Details screen'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The details screen
|
||||||
|
class DetailsScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [DetailsScreen]
|
||||||
|
const DetailsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Details Screen')),
|
||||||
|
body: Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
child: const Text('Go back to the Home screen'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
181
lib/go_router/examples/named_routes.dart
Normal file
181
lib/go_router/examples/named_routes.dart
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
// This scenario demonstrates how to navigate using named locations instead of
|
||||||
|
// URLs.
|
||||||
|
//
|
||||||
|
// Instead of hardcoding the URI locations , you can also use the named
|
||||||
|
// locations. To use this API, give a unique name to each GoRoute. The name can
|
||||||
|
// then be used in context.namedLocation to be translate back to the actual URL
|
||||||
|
// location.
|
||||||
|
|
||||||
|
/// Family data class.
|
||||||
|
class Family {
|
||||||
|
/// Create a family.
|
||||||
|
const Family({required this.name, required this.people});
|
||||||
|
|
||||||
|
/// The last name of the family.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The people in the family.
|
||||||
|
final Map<String, Person> people;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Person data class.
|
||||||
|
class Person {
|
||||||
|
/// Creates a person.
|
||||||
|
const Person({required this.name, required this.age});
|
||||||
|
|
||||||
|
/// The first name of the person.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The age of the person.
|
||||||
|
final int age;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Map<String, Family> _families = <String, Family>{
|
||||||
|
'f1': Family(
|
||||||
|
name: 'Doe',
|
||||||
|
people: <String, Person>{
|
||||||
|
'p1': Person(name: 'Jane', age: 23),
|
||||||
|
'p2': Person(name: 'John', age: 6),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
'f2': Family(
|
||||||
|
name: 'Wong',
|
||||||
|
people: <String, Person>{
|
||||||
|
'p1': Person(name: 'June', age: 51),
|
||||||
|
'p2': Person(name: 'Xin', age: 44),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() => runApp(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: Named Routes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: title,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
late final GoRouter _router = GoRouter(
|
||||||
|
debugLogDiagnostics: true,
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
name: 'home',
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const HomeScreen(),
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
name: 'family',
|
||||||
|
path: 'family/:fid',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
FamilyScreen(fid: state.pathParameters['fid']!),
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
name: 'person',
|
||||||
|
path: 'person/:pid',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return PersonScreen(
|
||||||
|
fid: state.pathParameters['fid']!,
|
||||||
|
pid: state.pathParameters['pid']!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The home screen that shows a list of families.
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
/// Creates a [HomeScreen].
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(App.title),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
for (final MapEntry<String, Family> entry in _families.entries)
|
||||||
|
ListTile(
|
||||||
|
title: Text(entry.value.name),
|
||||||
|
onTap: () => context.go(context.namedLocation('family',
|
||||||
|
pathParameters: <String, String>{'fid': entry.key})),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen that shows a list of persons in a family.
|
||||||
|
class FamilyScreen extends StatelessWidget {
|
||||||
|
/// Creates a [FamilyScreen].
|
||||||
|
const FamilyScreen({required this.fid, super.key});
|
||||||
|
|
||||||
|
/// The id family to display.
|
||||||
|
final String fid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Map<String, Person> people = _families[fid]!.people;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(_families[fid]!.name)),
|
||||||
|
body: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
for (final MapEntry<String, Person> entry in people.entries)
|
||||||
|
ListTile(
|
||||||
|
title: Text(entry.value.name),
|
||||||
|
onTap: () => context.go(context.namedLocation(
|
||||||
|
'person',
|
||||||
|
pathParameters: <String, String>{'fid': fid, 'pid': entry.key},
|
||||||
|
queryParameters: <String, String>{'qid': 'quid'},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The person screen.
|
||||||
|
class PersonScreen extends StatelessWidget {
|
||||||
|
/// Creates a [PersonScreen].
|
||||||
|
const PersonScreen({required this.fid, required this.pid, super.key});
|
||||||
|
|
||||||
|
/// The id of family this person belong to.
|
||||||
|
final String fid;
|
||||||
|
|
||||||
|
/// The id of the person to be displayed.
|
||||||
|
final String pid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Family family = _families[fid]!;
|
||||||
|
final Person person = family.people[pid]!;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(person.name)),
|
||||||
|
body: Text('${person.name} ${family.name} is ${person.age} years old'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
lib/go_router/examples/on_exit.dart
Normal file
139
lib/go_router/examples/on_exit.dart
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// This sample app demonstrates how to use GoRoute.onExit.
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
/// The route configuration.
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const HomeScreen();
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const DetailsScreen();
|
||||||
|
},
|
||||||
|
onExit: (BuildContext context) async {
|
||||||
|
final bool? confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: const Text('Are you sure to leave this page?'),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Confirm'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return confirmed ?? false;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'settings',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const SettingsScreen();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The main app.
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
/// Constructs a [MyApp]
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The home screen
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [HomeScreen]
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Home Screen')),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/details'),
|
||||||
|
child: const Text('Go to the Details screen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The details screen
|
||||||
|
class DetailsScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [DetailsScreen]
|
||||||
|
const DetailsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Details Screen')),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: const Text('go back'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.go('/settings');
|
||||||
|
},
|
||||||
|
child: const Text('go to settings'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The settings screen
|
||||||
|
class SettingsScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [SettingsScreen]
|
||||||
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Settings Screen')),
|
||||||
|
body: const Center(
|
||||||
|
child: Text('Settings'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
460
lib/go_router/examples/others/custom_stateful_shell_route.dart
Normal file
460
lib/go_router/examples/others/custom_stateful_shell_route.dart
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
// 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 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState> _rootNavigatorKey =
|
||||||
|
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||||
|
final GlobalKey<NavigatorState> _tabANavigatorKey =
|
||||||
|
GlobalKey<NavigatorState>(debugLabel: 'tabANav');
|
||||||
|
|
||||||
|
// This example demonstrates how to setup nested navigation using a
|
||||||
|
// BottomNavigationBar, where each bar item uses its own persistent navigator,
|
||||||
|
// i.e. navigation state is maintained separately for each item. This setup also
|
||||||
|
// enables deep linking into nested pages.
|
||||||
|
//
|
||||||
|
// This example also demonstrates how build a nested shell with a custom
|
||||||
|
// container for the branch Navigators (in this case a TabBarView).
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(NestedTabNavigationExampleApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An example demonstrating how to use nested navigators
|
||||||
|
class NestedTabNavigationExampleApp extends StatelessWidget {
|
||||||
|
/// Creates a NestedTabNavigationExampleApp
|
||||||
|
NestedTabNavigationExampleApp({super.key});
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
navigatorKey: _rootNavigatorKey,
|
||||||
|
initialLocation: '/a',
|
||||||
|
routes: <RouteBase>[
|
||||||
|
StatefulShellRoute(
|
||||||
|
builder: (BuildContext context, GoRouterState state,
|
||||||
|
StatefulNavigationShell navigationShell) {
|
||||||
|
// This nested StatefulShellRoute demonstrates the use of a
|
||||||
|
// custom container for the branch Navigators. In this implementation,
|
||||||
|
// no customization is done in the builder function (navigationShell
|
||||||
|
// itself is simply used as the Widget for the route). Instead, the
|
||||||
|
// navigatorContainerBuilder function below is provided to
|
||||||
|
// customize the container for the branch Navigators.
|
||||||
|
return navigationShell;
|
||||||
|
},
|
||||||
|
navigatorContainerBuilder: (BuildContext context,
|
||||||
|
StatefulNavigationShell navigationShell, List<Widget> children) {
|
||||||
|
// Returning a customized container for the branch
|
||||||
|
// Navigators (i.e. the `List<Widget> children` argument).
|
||||||
|
//
|
||||||
|
// See ScaffoldWithNavBar for more details on how the children
|
||||||
|
// are managed (using AnimatedBranchContainer).
|
||||||
|
return ScaffoldWithNavBar(
|
||||||
|
navigationShell: navigationShell, children: children);
|
||||||
|
},
|
||||||
|
branches: <StatefulShellBranch>[
|
||||||
|
// The route branch for the first tab of the bottom navigation bar.
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: _tabANavigatorKey,
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
// The screen to display as the root in the first tab of the
|
||||||
|
// bottom navigation bar.
|
||||||
|
path: '/a',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const RootScreenA(),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
// The details screen to display stacked on navigator of the
|
||||||
|
// first tab. This will cover screen A but not the application
|
||||||
|
// shell (bottom navigation bar).
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const DetailsScreen(label: 'A'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// The route branch for the third tab of the bottom navigation bar.
|
||||||
|
StatefulShellBranch(
|
||||||
|
// StatefulShellBranch will automatically use the first descendant
|
||||||
|
// GoRoute as the initial location of the branch. If another route
|
||||||
|
// is desired, specify the location of it using the defaultLocation
|
||||||
|
// parameter.
|
||||||
|
// defaultLocation: '/c2',
|
||||||
|
routes: <RouteBase>[
|
||||||
|
StatefulShellRoute(
|
||||||
|
builder: (BuildContext context, GoRouterState state,
|
||||||
|
StatefulNavigationShell navigationShell) {
|
||||||
|
// Just like with the top level StatefulShellRoute, no
|
||||||
|
// customization is done in the builder function.
|
||||||
|
return navigationShell;
|
||||||
|
},
|
||||||
|
navigatorContainerBuilder: (BuildContext context,
|
||||||
|
StatefulNavigationShell navigationShell,
|
||||||
|
List<Widget> children) {
|
||||||
|
// Returning a customized container for the branch
|
||||||
|
// Navigators (i.e. the `List<Widget> children` argument).
|
||||||
|
//
|
||||||
|
// See TabbedRootScreen for more details on how the children
|
||||||
|
// are managed (in a TabBarView).
|
||||||
|
return TabbedRootScreen(
|
||||||
|
navigationShell: navigationShell, children: children);
|
||||||
|
},
|
||||||
|
// This bottom tab uses a nested shell, wrapping sub routes in a
|
||||||
|
// top TabBar.
|
||||||
|
branches: <StatefulShellBranch>[
|
||||||
|
StatefulShellBranch(routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/b1',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const TabScreen(
|
||||||
|
label: 'B1', detailsPath: '/b1/details'),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
builder:
|
||||||
|
(BuildContext context, GoRouterState state) =>
|
||||||
|
const DetailsScreen(
|
||||||
|
label: 'B1',
|
||||||
|
withScaffold: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
StatefulShellBranch(routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/b2',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const TabScreen(
|
||||||
|
label: 'B2', detailsPath: '/b2/details'),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
builder:
|
||||||
|
(BuildContext context, GoRouterState state) =>
|
||||||
|
const DetailsScreen(
|
||||||
|
label: 'B2',
|
||||||
|
withScaffold: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
title: 'Flutter Demo',
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
),
|
||||||
|
routerConfig: _router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the "shell" for the app by building a Scaffold with a
|
||||||
|
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||||
|
class ScaffoldWithNavBar extends StatelessWidget {
|
||||||
|
/// Constructs an [ScaffoldWithNavBar].
|
||||||
|
const ScaffoldWithNavBar({
|
||||||
|
required this.navigationShell,
|
||||||
|
required this.children,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
|
||||||
|
|
||||||
|
/// The navigation shell and container for the branch Navigators.
|
||||||
|
final StatefulNavigationShell navigationShell;
|
||||||
|
|
||||||
|
/// The children (branch Navigators) to display in a custom container
|
||||||
|
/// ([AnimatedBranchContainer]).
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: AnimatedBranchContainer(
|
||||||
|
currentIndex: navigationShell.currentIndex,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
// Here, the items of BottomNavigationBar are hard coded. In a real
|
||||||
|
// world scenario, the items would most likely be generated from the
|
||||||
|
// branches of the shell route, which can be fetched using
|
||||||
|
// `navigationShell.route.branches`.
|
||||||
|
items: const <BottomNavigationBarItem>[
|
||||||
|
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
|
||||||
|
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
|
||||||
|
],
|
||||||
|
currentIndex: navigationShell.currentIndex,
|
||||||
|
onTap: (int index) => _onTap(context, index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigate to the current location of the branch at the provided index when
|
||||||
|
/// tapping an item in the BottomNavigationBar.
|
||||||
|
void _onTap(BuildContext context, int index) {
|
||||||
|
// When navigating to a new branch, it's recommended to use the goBranch
|
||||||
|
// method, as doing so makes sure the last navigation state of the
|
||||||
|
// Navigator for the branch is restored.
|
||||||
|
navigationShell.goBranch(
|
||||||
|
index,
|
||||||
|
// A common pattern when using bottom navigation bars is to support
|
||||||
|
// navigating to the initial location when tapping the item that is
|
||||||
|
// already active. This example demonstrates how to support this behavior,
|
||||||
|
// using the initialLocation parameter of goBranch.
|
||||||
|
initialLocation: index == navigationShell.currentIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom branch Navigator container that provides animated transitions
|
||||||
|
/// when switching branches.
|
||||||
|
class AnimatedBranchContainer extends StatelessWidget {
|
||||||
|
/// Creates a AnimatedBranchContainer
|
||||||
|
const AnimatedBranchContainer(
|
||||||
|
{super.key, required this.currentIndex, required this.children});
|
||||||
|
|
||||||
|
/// The index (in [children]) of the branch Navigator to display.
|
||||||
|
final int currentIndex;
|
||||||
|
|
||||||
|
/// The children (branch Navigators) to display in this container.
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: children.mapIndexed(
|
||||||
|
(int index, Widget navigator) {
|
||||||
|
return AnimatedScale(
|
||||||
|
scale: index == currentIndex ? 1 : 1.5,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: index == currentIndex ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
child: _branchNavigatorWrapper(index, navigator),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _branchNavigatorWrapper(int index, Widget navigator) => IgnorePointer(
|
||||||
|
ignoring: index != currentIndex,
|
||||||
|
child: TickerMode(
|
||||||
|
enabled: index == currentIndex,
|
||||||
|
child: navigator,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget for the root page for the first section of the bottom navigation bar.
|
||||||
|
class RootScreenA extends StatelessWidget {
|
||||||
|
/// Creates a RootScreenA
|
||||||
|
const RootScreenA({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Root of section A'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Screen A', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Padding(padding: EdgeInsets.all(4)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).go('/a/details');
|
||||||
|
},
|
||||||
|
child: const Text('View details'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The details screen for either the A or B screen.
|
||||||
|
class DetailsScreen extends StatefulWidget {
|
||||||
|
/// Constructs a [DetailsScreen].
|
||||||
|
const DetailsScreen({
|
||||||
|
required this.label,
|
||||||
|
this.param,
|
||||||
|
this.withScaffold = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label to display in the center of the screen.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// Optional param
|
||||||
|
final String? param;
|
||||||
|
|
||||||
|
/// Wrap in scaffold
|
||||||
|
final bool withScaffold;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => DetailsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state for DetailsScreen
|
||||||
|
class DetailsScreenState extends State<DetailsScreen> {
|
||||||
|
int _counter = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.withScaffold) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Details Screen - ${widget.label}'),
|
||||||
|
),
|
||||||
|
body: _build(context),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: _build(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Details for ${widget.label} - Counter: $_counter',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Padding(padding: EdgeInsets.all(4)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_counter++;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Increment counter'),
|
||||||
|
),
|
||||||
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
|
if (widget.param != null)
|
||||||
|
Text('Parameter: ${widget.param!}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
|
if (!widget.withScaffold) ...<Widget>[
|
||||||
|
const Padding(padding: EdgeInsets.all(16)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('< Back',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a nested shell using a [TabBar] and [TabBarView].
|
||||||
|
class TabbedRootScreen extends StatefulWidget {
|
||||||
|
/// Constructs a TabbedRootScreen
|
||||||
|
const TabbedRootScreen(
|
||||||
|
{required this.navigationShell, required this.children, super.key});
|
||||||
|
|
||||||
|
/// The current state of the parent StatefulShellRoute.
|
||||||
|
final StatefulNavigationShell navigationShell;
|
||||||
|
|
||||||
|
/// The children (branch Navigators) to display in the [TabBarView].
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _TabbedRootScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabbedRootScreenState extends State<TabbedRootScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final TabController _tabController = TabController(
|
||||||
|
length: widget.children.length,
|
||||||
|
vsync: this,
|
||||||
|
initialIndex: widget.navigationShell.currentIndex);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant TabbedRootScreen oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_tabController.index = widget.navigationShell.currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final List<Tab> tabs = widget.children
|
||||||
|
.mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}'))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Root of Section B (nested TabBar shell)'),
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: tabs,
|
||||||
|
onTap: (int tappedIndex) => _onTabTap(context, tappedIndex),
|
||||||
|
)),
|
||||||
|
body: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: widget.children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTabTap(BuildContext context, int index) {
|
||||||
|
widget.navigationShell.goBranch(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget for the pages in the top tab bar.
|
||||||
|
class TabScreen extends StatelessWidget {
|
||||||
|
/// Creates a RootScreen
|
||||||
|
const TabScreen({required this.label, required this.detailsPath, super.key});
|
||||||
|
|
||||||
|
/// The label
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// The path to the detail page
|
||||||
|
final String detailsPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Screen $label', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Padding(padding: EdgeInsets.all(4)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).go(detailsPath);
|
||||||
|
},
|
||||||
|
child: const Text('View details'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
lib/go_router/examples/others/error_screen.dart
Normal file
110
lib/go_router/examples/others/error_screen.dart
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
void main() => runApp(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: Custom Error Screen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: title,
|
||||||
|
);
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page1Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/page2',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page2Screen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
errorBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
ErrorScreen(state.error!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the first page.
|
||||||
|
class Page1Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page1Screen].
|
||||||
|
const Page1Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/page2'),
|
||||||
|
child: const Text('Go to page 2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the second page.
|
||||||
|
class Page2Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page2Screen].
|
||||||
|
const Page2Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
child: const Text('Go to home page'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the error page.
|
||||||
|
class ErrorScreen extends StatelessWidget {
|
||||||
|
/// Creates an [ErrorScreen].
|
||||||
|
const ErrorScreen(this.error, {super.key});
|
||||||
|
|
||||||
|
/// The error to display.
|
||||||
|
final Exception error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('My "Page Not Found" Screen')),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
SelectableText(error.toString()),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
child: const Text('Home'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
133
lib/go_router/examples/others/extra_param.dart
Normal file
133
lib/go_router/examples/others/extra_param.dart
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// Family data class.
|
||||||
|
class Family {
|
||||||
|
/// Create a family.
|
||||||
|
const Family({required this.name, required this.people});
|
||||||
|
|
||||||
|
/// The last name of the family.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The people in the family.
|
||||||
|
final Map<String, Person> people;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Person data class.
|
||||||
|
class Person {
|
||||||
|
/// Creates a person.
|
||||||
|
const Person({required this.name, required this.age});
|
||||||
|
|
||||||
|
/// The first name of the person.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The age of the person.
|
||||||
|
final int age;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Map<String, Family> _families = <String, Family>{
|
||||||
|
'f1': Family(
|
||||||
|
name: 'Doe',
|
||||||
|
people: <String, Person>{
|
||||||
|
'p1': Person(name: 'Jane', age: 23),
|
||||||
|
'p2': Person(name: 'John', age: 6),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
'f2': Family(
|
||||||
|
name: 'Wong',
|
||||||
|
people: <String, Person>{
|
||||||
|
'p1': Person(name: 'June', age: 51),
|
||||||
|
'p2': Person(name: 'Xin', age: 44),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() => runApp(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: Extra Parameter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: title,
|
||||||
|
);
|
||||||
|
|
||||||
|
late final GoRouter _router = GoRouter(
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
name: 'home',
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const HomeScreen(),
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
name: 'family',
|
||||||
|
path: 'family',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
final Map<String, Object> params =
|
||||||
|
state.extra! as Map<String, String>;
|
||||||
|
final String fid = params['fid']! as String;
|
||||||
|
return FamilyScreen(fid: fid);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The home screen that shows a list of families.
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
/// Creates a [HomeScreen].
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
for (final MapEntry<String, Family> entry in _families.entries)
|
||||||
|
ListTile(
|
||||||
|
title: Text(entry.value.name),
|
||||||
|
onTap: () => context.goNamed('family',
|
||||||
|
extra: <String, String>{'fid': entry.key}),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen that shows a list of persons in a family.
|
||||||
|
class FamilyScreen extends StatelessWidget {
|
||||||
|
/// Creates a [FamilyScreen].
|
||||||
|
const FamilyScreen({required this.fid, super.key});
|
||||||
|
|
||||||
|
/// The family to display.
|
||||||
|
final String fid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Map<String, Person> people = _families[fid]!.people;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(_families[fid]!.name)),
|
||||||
|
body: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
for (final Person p in people.values)
|
||||||
|
ListTile(
|
||||||
|
title: Text(p.name),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
lib/go_router/examples/others/init_loc.dart
Normal file
110
lib/go_router/examples/others/init_loc.dart
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
void main() => runApp(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: Initial Location';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: title,
|
||||||
|
);
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
initialLocation: '/page3',
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page1Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/page2',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page2Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/page3',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page3Screen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the first page.
|
||||||
|
class Page1Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page1Screen].
|
||||||
|
const Page1Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/page2'),
|
||||||
|
child: const Text('Go to page 2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the second page.
|
||||||
|
class Page2Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page2Screen].
|
||||||
|
const Page2Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
child: const Text('Go to home page'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the third page.
|
||||||
|
class Page3Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page3Screen].
|
||||||
|
const Page3Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/page2'),
|
||||||
|
child: const Text('Go to page 2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
167
lib/go_router/examples/others/nav_observer.dart
Normal file
167
lib/go_router/examples/others/nav_observer.dart
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
void main() => runApp(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: Navigator Observer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: title,
|
||||||
|
);
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
observers: <NavigatorObserver>[MyNavObserver()],
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
// if there's no name, path will be used as name for observers
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page1Screen(),
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
name: 'page2',
|
||||||
|
path: 'page2/:p1',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page2Screen(),
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
name: 'page3',
|
||||||
|
path: 'page3',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page3Screen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Navigator observer.
|
||||||
|
class MyNavObserver extends NavigatorObserver {
|
||||||
|
/// Creates a [MyNavObserver].
|
||||||
|
MyNavObserver() {
|
||||||
|
log.onRecord.listen((LogRecord e) => debugPrint('$e'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The logged message.
|
||||||
|
final Logger log = Logger('MyNavObserver');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) =>
|
||||||
|
log.info('didPush: ${route.str}, previousRoute= ${previousRoute?.str}');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) =>
|
||||||
|
log.info('didPop: ${route.str}, previousRoute= ${previousRoute?.str}');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) =>
|
||||||
|
log.info('didRemove: ${route.str}, previousRoute= ${previousRoute?.str}');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
|
||||||
|
log.info('didReplace: new= ${newRoute?.str}, old= ${oldRoute?.str}');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didStartUserGesture(
|
||||||
|
Route<dynamic> route,
|
||||||
|
Route<dynamic>? previousRoute,
|
||||||
|
) =>
|
||||||
|
log.info('didStartUserGesture: ${route.str}, '
|
||||||
|
'previousRoute= ${previousRoute?.str}');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didStopUserGesture() => log.info('didStopUserGesture');
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Route<dynamic> {
|
||||||
|
String get str => 'route(${settings.name}: ${settings.arguments})';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the first page.
|
||||||
|
class Page1Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page1Screen].
|
||||||
|
const Page1Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.goNamed(
|
||||||
|
'page2',
|
||||||
|
pathParameters: <String, String>{'p1': 'pv1'},
|
||||||
|
queryParameters: <String, String>{'q1': 'qv1'},
|
||||||
|
),
|
||||||
|
child: const Text('Go to page 2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the second page.
|
||||||
|
class Page2Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page2Screen].
|
||||||
|
const Page2Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.goNamed(
|
||||||
|
'page3',
|
||||||
|
pathParameters: <String, String>{'p1': 'pv2'},
|
||||||
|
),
|
||||||
|
child: const Text('Go to page 3'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the third page.
|
||||||
|
class Page3Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page3Screen].
|
||||||
|
const Page3Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
child: const Text('Go to home page'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
101
lib/go_router/examples/others/push.dart
Normal file
101
lib/go_router/examples/others/push.dart
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
void main() => runApp(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: Push';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: title,
|
||||||
|
);
|
||||||
|
|
||||||
|
late final GoRouter _router = GoRouter(
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page1ScreenWithPush(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/page2',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
Page2ScreenWithPush(
|
||||||
|
int.parse(state.uri.queryParameters['push-count']!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the first page.
|
||||||
|
class Page1ScreenWithPush extends StatelessWidget {
|
||||||
|
/// Creates a [Page1ScreenWithPush].
|
||||||
|
const Page1ScreenWithPush({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('${App.title}: page 1')),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.push('/page2?push-count=1'),
|
||||||
|
child: const Text('Push page 2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the second page.
|
||||||
|
class Page2ScreenWithPush extends StatelessWidget {
|
||||||
|
/// Creates a [Page2ScreenWithPush].
|
||||||
|
const Page2ScreenWithPush(this.pushCount, {super.key});
|
||||||
|
|
||||||
|
/// The push count.
|
||||||
|
final int pushCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('${App.title}: page 2 w/ push count $pushCount'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
child: const Text('Go to home page'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => context.push(
|
||||||
|
'/page2?push-count=${pushCount + 1}',
|
||||||
|
),
|
||||||
|
child: const Text('Push page 2 (again)'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
95
lib/go_router/examples/others/router_neglect.dart
Normal file
95
lib/go_router/examples/others/router_neglect.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
void main() => runApp(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: Router neglect';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: title,
|
||||||
|
);
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
// turn off history tracking in the browser for this navigation
|
||||||
|
routerNeglect: true,
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page1Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/page2',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page2Screen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the first page.
|
||||||
|
class Page1Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page1Screen].
|
||||||
|
const Page1Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/page2'),
|
||||||
|
child: const Text('Go to page 2'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
// turn off history tracking in the browser for this navigation;
|
||||||
|
// note that this isn't necessary when you've set routerNeglect
|
||||||
|
// but it does illustrate the technique
|
||||||
|
onPressed: () => Router.neglect(
|
||||||
|
context,
|
||||||
|
() => context.push('/page2'),
|
||||||
|
),
|
||||||
|
child: const Text('Push page 2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the second page.
|
||||||
|
class Page2Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page2Screen].
|
||||||
|
const Page2Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
child: const Text('Go to home page'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
102
lib/go_router/examples/others/state_restoration.dart
Normal file
102
lib/go_router/examples/others/state_restoration.dart
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
void main() => runApp(
|
||||||
|
const RootRestorationScope(restorationId: 'root', child: App()),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The main app.
|
||||||
|
class App extends StatefulWidget {
|
||||||
|
/// Creates an [App].
|
||||||
|
const App({super.key});
|
||||||
|
|
||||||
|
/// The title of the app.
|
||||||
|
static const String title = 'GoRouter Example: State Restoration';
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<App> createState() => _AppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppState extends State<App> with RestorationMixin {
|
||||||
|
@override
|
||||||
|
String get restorationId => 'wrapper';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||||
|
// Implement restoreState for your app
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: App.title,
|
||||||
|
restorationScopeId: 'app',
|
||||||
|
);
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
routes: <GoRoute>[
|
||||||
|
// restorationId set for the route automatically
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page1Screen(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// restorationId set for the route automatically
|
||||||
|
GoRoute(
|
||||||
|
path: '/page2',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const Page2Screen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
restorationScopeId: 'router',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the first page.
|
||||||
|
class Page1Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page1Screen].
|
||||||
|
const Page1Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/page2'),
|
||||||
|
child: const Text('Go to page 2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen of the second page.
|
||||||
|
class Page2Screen extends StatelessWidget {
|
||||||
|
/// Creates a [Page2Screen].
|
||||||
|
const Page2Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
child: const Text('Go to home page'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
void main() => runApp(RestorableStatefulShellRouteExampleApp());
|
||||||
|
|
||||||
|
/// An example demonstrating how to use StatefulShellRoute with state
|
||||||
|
/// restoration.
|
||||||
|
class RestorableStatefulShellRouteExampleApp extends StatelessWidget {
|
||||||
|
/// Creates a NestedTabNavigationExampleApp
|
||||||
|
RestorableStatefulShellRouteExampleApp({super.key});
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
initialLocation: '/a',
|
||||||
|
restorationScopeId: 'router',
|
||||||
|
routes: <RouteBase>[
|
||||||
|
StatefulShellRoute.indexedStack(
|
||||||
|
restorationScopeId: 'shell1',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state,
|
||||||
|
StatefulNavigationShell navigationShell) {
|
||||||
|
return MaterialPage<void>(
|
||||||
|
restorationId: 'shellWidget1',
|
||||||
|
child: ScaffoldWithNavBar(navigationShell: navigationShell));
|
||||||
|
},
|
||||||
|
branches: <StatefulShellBranch>[
|
||||||
|
// The route branch for the first tab of the bottom navigation bar.
|
||||||
|
StatefulShellBranch(
|
||||||
|
restorationScopeId: 'branchA',
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
// The screen to display as the root in the first tab of the
|
||||||
|
// bottom navigation bar.
|
||||||
|
path: '/a',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const MaterialPage<void>(
|
||||||
|
restorationId: 'screenA',
|
||||||
|
child:
|
||||||
|
RootScreen(label: 'A', detailsPath: '/a/details')),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
// The details screen to display stacked on navigator of the
|
||||||
|
// first tab. This will cover screen A but not the application
|
||||||
|
// shell (bottom navigation bar).
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const MaterialPage<void>(
|
||||||
|
restorationId: 'screenADetail',
|
||||||
|
child: DetailsScreen(label: 'A')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// The route branch for the second tab of the bottom navigation bar.
|
||||||
|
StatefulShellBranch(
|
||||||
|
restorationScopeId: 'branchB',
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
// The screen to display as the root in the second tab of the
|
||||||
|
// bottom navigation bar.
|
||||||
|
path: '/b',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const MaterialPage<void>(
|
||||||
|
restorationId: 'screenB',
|
||||||
|
child:
|
||||||
|
RootScreen(label: 'B', detailsPath: '/b/details')),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
// The details screen to display stacked on navigator of the
|
||||||
|
// first tab. This will cover screen A but not the application
|
||||||
|
// shell (bottom navigation bar).
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const MaterialPage<void>(
|
||||||
|
restorationId: 'screenBDetail',
|
||||||
|
child: DetailsScreen(label: 'B')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
restorationScopeId: 'app',
|
||||||
|
title: 'Flutter Demo',
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
),
|
||||||
|
routerConfig: _router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the "shell" for the app by building a Scaffold with a
|
||||||
|
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||||
|
class ScaffoldWithNavBar extends StatelessWidget {
|
||||||
|
/// Constructs an [ScaffoldWithNavBar].
|
||||||
|
const ScaffoldWithNavBar({
|
||||||
|
required this.navigationShell,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
|
||||||
|
|
||||||
|
/// The navigation shell and container for the branch Navigators.
|
||||||
|
final StatefulNavigationShell navigationShell;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: navigationShell,
|
||||||
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
items: const <BottomNavigationBarItem>[
|
||||||
|
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
|
||||||
|
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
|
||||||
|
],
|
||||||
|
currentIndex: navigationShell.currentIndex,
|
||||||
|
onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget for the root/initial pages in the bottom navigation bar.
|
||||||
|
class RootScreen extends StatelessWidget {
|
||||||
|
/// Creates a RootScreen
|
||||||
|
const RootScreen({
|
||||||
|
required this.label,
|
||||||
|
required this.detailsPath,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// The path to the detail page
|
||||||
|
final String detailsPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Root of section $label'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Screen $label',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Padding(padding: EdgeInsets.all(4)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).go(detailsPath);
|
||||||
|
},
|
||||||
|
child: const Text('View details'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The details screen for either the A or B screen.
|
||||||
|
class DetailsScreen extends StatefulWidget {
|
||||||
|
/// Constructs a [DetailsScreen].
|
||||||
|
const DetailsScreen({
|
||||||
|
required this.label,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label to display in the center of the screen.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => DetailsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state for DetailsScreen
|
||||||
|
class DetailsScreenState extends State<DetailsScreen> with RestorationMixin {
|
||||||
|
final RestorableInt _counter = RestorableInt(0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get restorationId => 'DetailsScreen-${widget.label}';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||||
|
registerForRestoration(_counter, 'counter');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_counter.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Details Screen - ${widget.label}'),
|
||||||
|
),
|
||||||
|
body: _build(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Details for ${widget.label} - Counter: ${_counter.value}',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Padding(padding: EdgeInsets.all(4)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_counter.value++;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Increment counter'),
|
||||||
|
),
|
||||||
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
163
lib/go_router/examples/others/transitions.dart
Normal file
163
lib/go_router/examples/others/transitions.dart
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
void main() => runApp(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: Custom Transitions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: title,
|
||||||
|
);
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
redirect: (_, __) => '/none',
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/fade',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
CustomTransitionPage<void>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const ExampleTransitionsScreen(
|
||||||
|
kind: 'fade',
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
transitionsBuilder: (BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child) =>
|
||||||
|
FadeTransition(opacity: animation, child: child),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/scale',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
CustomTransitionPage<void>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const ExampleTransitionsScreen(
|
||||||
|
kind: 'scale',
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
transitionsBuilder: (BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child) =>
|
||||||
|
ScaleTransition(scale: animation, child: child),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/slide',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
CustomTransitionPage<void>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const ExampleTransitionsScreen(
|
||||||
|
kind: 'slide',
|
||||||
|
color: Colors.yellow,
|
||||||
|
),
|
||||||
|
transitionsBuilder: (BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child) =>
|
||||||
|
SlideTransition(
|
||||||
|
position: animation.drive(
|
||||||
|
Tween<Offset>(
|
||||||
|
begin: const Offset(0.25, 0.25),
|
||||||
|
end: Offset.zero,
|
||||||
|
).chain(CurveTween(curve: Curves.easeIn)),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/rotation',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
CustomTransitionPage<void>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const ExampleTransitionsScreen(
|
||||||
|
kind: 'rotation',
|
||||||
|
color: Colors.purple,
|
||||||
|
),
|
||||||
|
transitionsBuilder: (BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child) =>
|
||||||
|
RotationTransition(turns: animation, child: child),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/none',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||||
|
NoTransitionPage<void>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const ExampleTransitionsScreen(
|
||||||
|
kind: 'none',
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An Example transitions screen.
|
||||||
|
class ExampleTransitionsScreen extends StatelessWidget {
|
||||||
|
/// Creates an [ExampleTransitionsScreen].
|
||||||
|
const ExampleTransitionsScreen({
|
||||||
|
required this.color,
|
||||||
|
required this.kind,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The available transition kinds.
|
||||||
|
static final List<String> kinds = <String>[
|
||||||
|
'fade',
|
||||||
|
'scale',
|
||||||
|
'slide',
|
||||||
|
'rotation',
|
||||||
|
'none'
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The color of the container.
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// The transition kind.
|
||||||
|
final String kind;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: Text('${App.title}: $kind')),
|
||||||
|
body: Container(
|
||||||
|
color: color,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
for (final String kind in kinds)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => context.go('/$kind'),
|
||||||
|
child: Text('$kind transition'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
169
lib/go_router/examples/path_and_query_parameters.dart
Normal file
169
lib/go_router/examples/path_and_query_parameters.dart
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
// This scenario demonstrates how to use path parameters and query parameters.
|
||||||
|
//
|
||||||
|
// The route segments that start with ':' are treated as path parameters when
|
||||||
|
// defining GoRoute[s]. The parameter values can be accessed through
|
||||||
|
// GoRouterState.pathParameters.
|
||||||
|
//
|
||||||
|
// The query parameters are automatically stored in GoRouterState.queryParameters.
|
||||||
|
|
||||||
|
/// Family data class.
|
||||||
|
class Family {
|
||||||
|
/// Create a family.
|
||||||
|
const Family({required this.name, required this.people});
|
||||||
|
|
||||||
|
/// The last name of the family.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The people in the family.
|
||||||
|
final Map<String, Person> people;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Person data class.
|
||||||
|
class Person {
|
||||||
|
/// Creates a person.
|
||||||
|
const Person({required this.name, required this.age});
|
||||||
|
|
||||||
|
/// The first name of the person.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The age of the person.
|
||||||
|
final int age;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Map<String, Family> _families = <String, Family>{
|
||||||
|
'f1': Family(
|
||||||
|
name: 'Doe',
|
||||||
|
people: <String, Person>{
|
||||||
|
'p1': Person(name: 'Jane', age: 23),
|
||||||
|
'p2': Person(name: 'John', age: 6),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
'f2': Family(
|
||||||
|
name: 'Wong',
|
||||||
|
people: <String, Person>{
|
||||||
|
'p1': Person(name: 'June', age: 51),
|
||||||
|
'p2': Person(name: 'Xin', age: 44),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() => runApp(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: Query Parameters';
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
routes: <GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
name: 'family',
|
||||||
|
path: 'family/:fid',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return FamilyScreen(
|
||||||
|
fid: state.pathParameters['fid']!,
|
||||||
|
asc: state.uri.queryParameters['sort'] == 'asc',
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The home screen that shows a list of families.
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
/// Creates a [HomeScreen].
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(App.title),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
for (final MapEntry<String, Family> entry in _families.entries)
|
||||||
|
ListTile(
|
||||||
|
title: Text(entry.value.name),
|
||||||
|
onTap: () => context.go('/family/${entry.key}'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen that shows a list of persons in a family.
|
||||||
|
class FamilyScreen extends StatelessWidget {
|
||||||
|
/// Creates a [FamilyScreen].
|
||||||
|
const FamilyScreen({required this.fid, required this.asc, super.key});
|
||||||
|
|
||||||
|
/// The family to display.
|
||||||
|
final String fid;
|
||||||
|
|
||||||
|
/// Whether to sort the name in ascending order.
|
||||||
|
final bool asc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Map<String, String> newQueries;
|
||||||
|
final List<String> names = _families[fid]!
|
||||||
|
.people
|
||||||
|
.values
|
||||||
|
.map<String>((Person p) => p.name)
|
||||||
|
.toList();
|
||||||
|
names.sort();
|
||||||
|
if (asc) {
|
||||||
|
newQueries = const <String, String>{'sort': 'desc'};
|
||||||
|
} else {
|
||||||
|
newQueries = const <String, String>{'sort': 'asc'};
|
||||||
|
}
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_families[fid]!.name),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.goNamed('family',
|
||||||
|
pathParameters: <String, String>{'fid': fid},
|
||||||
|
queryParameters: newQueries),
|
||||||
|
tooltip: 'sort ascending or descending',
|
||||||
|
icon: const Icon(Icons.sort),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
for (final String name in asc ? names : names.reversed)
|
||||||
|
ListTile(
|
||||||
|
title: Text(name),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
146
lib/go_router/examples/redirection.dart
Normal file
146
lib/go_router/examples/redirection.dart
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
// This scenario demonstrates how to use redirect to handle a sign-in flow.
|
||||||
|
//
|
||||||
|
// The GoRouter.redirect method is called before the app is navigate to a
|
||||||
|
// new page. You can choose to redirect to a different page by returning a
|
||||||
|
// non-null URL string.
|
||||||
|
|
||||||
|
/// The login information.
|
||||||
|
class LoginInfo extends ChangeNotifier {
|
||||||
|
/// The username of login.
|
||||||
|
String get userName => _userName;
|
||||||
|
String _userName = '';
|
||||||
|
|
||||||
|
/// Whether a user has logged in.
|
||||||
|
bool get loggedIn => _userName.isNotEmpty;
|
||||||
|
|
||||||
|
/// Logs in a user.
|
||||||
|
void login(String userName) {
|
||||||
|
_userName = userName;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logs out the current user.
|
||||||
|
void logout() {
|
||||||
|
_userName = '';
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() => runApp(App());
|
||||||
|
|
||||||
|
/// The main app.
|
||||||
|
class App extends StatelessWidget {
|
||||||
|
/// Creates an [App].
|
||||||
|
App({super.key});
|
||||||
|
|
||||||
|
final LoginInfo _loginInfo = LoginInfo();
|
||||||
|
|
||||||
|
/// 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) => ChangeNotifierProvider<LoginInfo>.value(
|
||||||
|
value: _loginInfo,
|
||||||
|
child: 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) {
|
||||||
|
// if the user is not logged in, they need to login
|
||||||
|
final bool loggedIn = _loginInfo.loggedIn;
|
||||||
|
final bool loggingIn = state.matchedLocation == '/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;
|
||||||
|
},
|
||||||
|
|
||||||
|
// changes on the listenable will cause the router to refresh it's route
|
||||||
|
refreshListenable: _loginInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The login screen.
|
||||||
|
class LoginScreen extends StatelessWidget {
|
||||||
|
/// Creates a [LoginScreen].
|
||||||
|
const LoginScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(App.title)),
|
||||||
|
body: Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
// log a user in, letting all the listeners know
|
||||||
|
context.read<LoginInfo>().login('test-user');
|
||||||
|
|
||||||
|
// router will automatically redirect from /login to / using
|
||||||
|
// refreshListenable
|
||||||
|
},
|
||||||
|
child: const Text('Login'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The home screen.
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
/// Creates a [HomeScreen].
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final LoginInfo info = context.read<LoginInfo>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(App.title),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: info.logout,
|
||||||
|
tooltip: 'Logout: ${info.userName}',
|
||||||
|
icon: const Icon(Icons.logout),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Text('HomeScreen'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
lib/go_router/examples/routing_config.dart
Normal file
108
lib/go_router/examples/routing_config.dart
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// This app shows how to dynamically add more route into routing config
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
/// The main app.
|
||||||
|
class MyApp extends StatefulWidget {
|
||||||
|
/// Constructs a [MyApp]
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyApp> createState() => _MyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyAppState extends State<MyApp> {
|
||||||
|
bool isNewRouteAdded = false;
|
||||||
|
|
||||||
|
late final ValueNotifier<RoutingConfig> myConfig =
|
||||||
|
ValueNotifier<RoutingConfig>(_generateRoutingConfig());
|
||||||
|
|
||||||
|
late final GoRouter router = GoRouter.routingConfig(
|
||||||
|
routingConfig: myConfig,
|
||||||
|
errorBuilder: (_, GoRouterState state) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Page not found')),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('${state.uri} does not exist'),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => router.go('/'),
|
||||||
|
child: const Text('Go to home')),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
|
||||||
|
RoutingConfig _generateRoutingConfig() {
|
||||||
|
return RoutingConfig(
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (_, __) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Home')),
|
||||||
|
body: Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: isNewRouteAdded
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
isNewRouteAdded = true;
|
||||||
|
// Modify the routing config.
|
||||||
|
myConfig.value = _generateRoutingConfig();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: isNewRouteAdded
|
||||||
|
? const Text('A route has been added')
|
||||||
|
: const Text('Add a new route'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
router.go('/new-route');
|
||||||
|
},
|
||||||
|
child: const Text('Try going to /new-route'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (isNewRouteAdded)
|
||||||
|
GoRoute(
|
||||||
|
path: '/new-route',
|
||||||
|
builder: (_, __) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('A new Route')),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => router.go('/'),
|
||||||
|
child: const Text('Go to home')),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
routerConfig: router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
289
lib/go_router/examples/shell_route.dart
Normal file
289
lib/go_router/examples/shell_route.dart
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState> _rootNavigatorKey =
|
||||||
|
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||||
|
final GlobalKey<NavigatorState> _shellNavigatorKey =
|
||||||
|
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||||
|
|
||||||
|
// This scenario demonstrates how to set up nested navigation using ShellRoute,
|
||||||
|
// which is a pattern where an additional Navigator is placed in the widget tree
|
||||||
|
// to be used instead of the root navigator. This allows deep-links to display
|
||||||
|
// pages along with other UI components such as a BottomNavigationBar.
|
||||||
|
//
|
||||||
|
// This example demonstrates how to display a route within a ShellRoute and also
|
||||||
|
// push a screen using a different navigator (such as the root Navigator) by
|
||||||
|
// providing a `parentNavigatorKey`.
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(ShellRouteExampleApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An example demonstrating how to use [ShellRoute]
|
||||||
|
class ShellRouteExampleApp extends StatelessWidget {
|
||||||
|
/// Creates a [ShellRouteExampleApp]
|
||||||
|
ShellRouteExampleApp({super.key});
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
navigatorKey: _rootNavigatorKey,
|
||||||
|
initialLocation: '/a',
|
||||||
|
debugLogDiagnostics: true,
|
||||||
|
routes: <RouteBase>[
|
||||||
|
/// Application shell
|
||||||
|
ShellRoute(
|
||||||
|
navigatorKey: _shellNavigatorKey,
|
||||||
|
builder: (BuildContext context, GoRouterState state, Widget child) {
|
||||||
|
return ScaffoldWithNavBar(child: child);
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
/// The first screen to display in the bottom navigation bar.
|
||||||
|
GoRoute(
|
||||||
|
path: '/a',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const ScreenA();
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
// The details screen to display stacked on the inner Navigator.
|
||||||
|
// This will cover screen A but not the application shell.
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const DetailsScreen(label: 'A');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
/// Displayed when the second item in the the bottom navigation bar is
|
||||||
|
/// selected.
|
||||||
|
GoRoute(
|
||||||
|
path: '/b',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const ScreenB();
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
/// Same as "/a/details", but displayed on the root Navigator by
|
||||||
|
/// specifying [parentNavigatorKey]. This will cover both screen B
|
||||||
|
/// and the application shell.
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
parentNavigatorKey: _rootNavigatorKey,
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const DetailsScreen(label: 'B');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
/// The third screen to display in the bottom navigation bar.
|
||||||
|
GoRoute(
|
||||||
|
path: '/c',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const ScreenC();
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
// The details screen to display stacked on the inner Navigator.
|
||||||
|
// This will cover screen A but not the application shell.
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const DetailsScreen(label: 'C');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
title: 'Flutter Demo',
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
),
|
||||||
|
routerConfig: _router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the "shell" for the app by building a Scaffold with a
|
||||||
|
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||||
|
class ScaffoldWithNavBar extends StatelessWidget {
|
||||||
|
/// Constructs an [ScaffoldWithNavBar].
|
||||||
|
const ScaffoldWithNavBar({
|
||||||
|
required this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The widget to display in the body of the Scaffold.
|
||||||
|
/// In this sample, it is a Navigator.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: child,
|
||||||
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
items: const <BottomNavigationBarItem>[
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.home),
|
||||||
|
label: 'A Screen',
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.business),
|
||||||
|
label: 'B Screen',
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.notification_important_rounded),
|
||||||
|
label: 'C Screen',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
currentIndex: _calculateSelectedIndex(context),
|
||||||
|
onTap: (int idx) => _onItemTapped(idx, context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _calculateSelectedIndex(BuildContext context) {
|
||||||
|
final String location = GoRouterState.of(context).uri.toString();
|
||||||
|
if (location.startsWith('/a')) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (location.startsWith('/b')) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (location.startsWith('/c')) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onItemTapped(int index, BuildContext context) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
GoRouter.of(context).go('/a');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
GoRouter.of(context).go('/b');
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
GoRouter.of(context).go('/c');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first screen in the bottom navigation bar.
|
||||||
|
class ScreenA extends StatelessWidget {
|
||||||
|
/// Constructs a [ScreenA] widget.
|
||||||
|
const ScreenA({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Screen A'),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).go('/a/details');
|
||||||
|
},
|
||||||
|
child: const Text('View A details'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The second screen in the bottom navigation bar.
|
||||||
|
class ScreenB extends StatelessWidget {
|
||||||
|
/// Constructs a [ScreenB] widget.
|
||||||
|
const ScreenB({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Screen B'),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).go('/b/details');
|
||||||
|
},
|
||||||
|
child: const Text('View B details'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The third screen in the bottom navigation bar.
|
||||||
|
class ScreenC extends StatelessWidget {
|
||||||
|
/// Constructs a [ScreenC] widget.
|
||||||
|
const ScreenC({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Screen C'),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).go('/c/details');
|
||||||
|
},
|
||||||
|
child: const Text('View C details'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The details screen for either the A, B or C screen.
|
||||||
|
class DetailsScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [DetailsScreen].
|
||||||
|
const DetailsScreen({
|
||||||
|
required this.label,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label to display in the center of the screen.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Details Screen'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Text(
|
||||||
|
'Details for $label',
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
324
lib/go_router/examples/stateful_shell_route.dart
Normal file
324
lib/go_router/examples/stateful_shell_route.dart
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState> _rootNavigatorKey =
|
||||||
|
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||||
|
final GlobalKey<NavigatorState> _sectionANavigatorKey =
|
||||||
|
GlobalKey<NavigatorState>(debugLabel: 'sectionANav');
|
||||||
|
|
||||||
|
// This example demonstrates how to setup nested navigation using a
|
||||||
|
// BottomNavigationBar, where each bar item uses its own persistent navigator,
|
||||||
|
// i.e. navigation state is maintained separately for each item. This setup also
|
||||||
|
// enables deep linking into nested pages.
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(NestedTabNavigationExampleApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An example demonstrating how to use nested navigators
|
||||||
|
class NestedTabNavigationExampleApp extends StatelessWidget {
|
||||||
|
/// Creates a NestedTabNavigationExampleApp
|
||||||
|
NestedTabNavigationExampleApp({super.key});
|
||||||
|
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
navigatorKey: _rootNavigatorKey,
|
||||||
|
initialLocation: '/a',
|
||||||
|
routes: <RouteBase>[
|
||||||
|
StatefulShellRoute.indexedStack(
|
||||||
|
builder: (BuildContext context, GoRouterState state,
|
||||||
|
StatefulNavigationShell navigationShell) {
|
||||||
|
// Return the widget that implements the custom shell (in this case
|
||||||
|
// using a BottomNavigationBar). The StatefulNavigationShell is passed
|
||||||
|
// to be able access the state of the shell and to navigate to other
|
||||||
|
// branches in a stateful way.
|
||||||
|
return ScaffoldWithNavBar(navigationShell: navigationShell);
|
||||||
|
},
|
||||||
|
branches: <StatefulShellBranch>[
|
||||||
|
// The route branch for the first tab of the bottom navigation bar.
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: _sectionANavigatorKey,
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
// The screen to display as the root in the first tab of the
|
||||||
|
// bottom navigation bar.
|
||||||
|
path: '/a',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const RootScreen(label: 'A', detailsPath: '/a/details'),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
// The details screen to display stacked on navigator of the
|
||||||
|
// first tab. This will cover screen A but not the application
|
||||||
|
// shell (bottom navigation bar).
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const DetailsScreen(label: 'A'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// The route branch for the second tab of the bottom navigation bar.
|
||||||
|
StatefulShellBranch(
|
||||||
|
// It's not necessary to provide a navigatorKey if it isn't also
|
||||||
|
// needed elsewhere. If not provided, a default key will be used.
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
// The screen to display as the root in the second tab of the
|
||||||
|
// bottom navigation bar.
|
||||||
|
path: '/b',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const RootScreen(
|
||||||
|
label: 'B',
|
||||||
|
detailsPath: '/b/details/1',
|
||||||
|
secondDetailsPath: '/b/details/2',
|
||||||
|
),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: 'details/:param',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
DetailsScreen(
|
||||||
|
label: 'B',
|
||||||
|
param: state.pathParameters['param'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// The route branch for the third tab of the bottom navigation bar.
|
||||||
|
StatefulShellBranch(
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
// The screen to display as the root in the third tab of the
|
||||||
|
// bottom navigation bar.
|
||||||
|
path: '/c',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
const RootScreen(
|
||||||
|
label: 'C',
|
||||||
|
detailsPath: '/c/details',
|
||||||
|
),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
builder: (BuildContext context, GoRouterState state) =>
|
||||||
|
DetailsScreen(
|
||||||
|
label: 'C',
|
||||||
|
extra: state.extra,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
title: 'Flutter Demo',
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
),
|
||||||
|
routerConfig: _router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the "shell" for the app by building a Scaffold with a
|
||||||
|
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||||
|
class ScaffoldWithNavBar extends StatelessWidget {
|
||||||
|
/// Constructs an [ScaffoldWithNavBar].
|
||||||
|
const ScaffoldWithNavBar({
|
||||||
|
required this.navigationShell,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
|
||||||
|
|
||||||
|
/// The navigation shell and container for the branch Navigators.
|
||||||
|
final StatefulNavigationShell navigationShell;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: navigationShell,
|
||||||
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
// Here, the items of BottomNavigationBar are hard coded. In a real
|
||||||
|
// world scenario, the items would most likely be generated from the
|
||||||
|
// branches of the shell route, which can be fetched using
|
||||||
|
// `navigationShell.route.branches`.
|
||||||
|
items: const <BottomNavigationBarItem>[
|
||||||
|
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
|
||||||
|
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
|
||||||
|
BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'),
|
||||||
|
],
|
||||||
|
currentIndex: navigationShell.currentIndex,
|
||||||
|
onTap: (int index) => _onTap(context, index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigate to the current location of the branch at the provided index when
|
||||||
|
/// tapping an item in the BottomNavigationBar.
|
||||||
|
void _onTap(BuildContext context, int index) {
|
||||||
|
// When navigating to a new branch, it's recommended to use the goBranch
|
||||||
|
// method, as doing so makes sure the last navigation state of the
|
||||||
|
// Navigator for the branch is restored.
|
||||||
|
navigationShell.goBranch(
|
||||||
|
index,
|
||||||
|
// A common pattern when using bottom navigation bars is to support
|
||||||
|
// navigating to the initial location when tapping the item that is
|
||||||
|
// already active. This example demonstrates how to support this behavior,
|
||||||
|
// using the initialLocation parameter of goBranch.
|
||||||
|
initialLocation: index == navigationShell.currentIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget for the root/initial pages in the bottom navigation bar.
|
||||||
|
class RootScreen extends StatelessWidget {
|
||||||
|
/// Creates a RootScreen
|
||||||
|
const RootScreen({
|
||||||
|
required this.label,
|
||||||
|
required this.detailsPath,
|
||||||
|
this.secondDetailsPath,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// The path to the detail page
|
||||||
|
final String detailsPath;
|
||||||
|
|
||||||
|
/// The path to another detail page
|
||||||
|
final String? secondDetailsPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Root of section $label'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Screen $label',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Padding(padding: EdgeInsets.all(4)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).go(detailsPath, extra: '$label-XYZ');
|
||||||
|
},
|
||||||
|
child: const Text('View details'),
|
||||||
|
),
|
||||||
|
const Padding(padding: EdgeInsets.all(4)),
|
||||||
|
if (secondDetailsPath != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).go(secondDetailsPath!);
|
||||||
|
},
|
||||||
|
child: const Text('View more details'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The details screen for either the A or B screen.
|
||||||
|
class DetailsScreen extends StatefulWidget {
|
||||||
|
/// Constructs a [DetailsScreen].
|
||||||
|
const DetailsScreen({
|
||||||
|
required this.label,
|
||||||
|
this.param,
|
||||||
|
this.extra,
|
||||||
|
this.withScaffold = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label to display in the center of the screen.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// Optional param
|
||||||
|
final String? param;
|
||||||
|
|
||||||
|
/// Optional extra object
|
||||||
|
final Object? extra;
|
||||||
|
|
||||||
|
/// Wrap in scaffold
|
||||||
|
final bool withScaffold;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => DetailsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state for DetailsScreen
|
||||||
|
class DetailsScreenState extends State<DetailsScreen> {
|
||||||
|
int _counter = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.withScaffold) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Details Screen - ${widget.label}'),
|
||||||
|
),
|
||||||
|
body: _build(context),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: _build(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Details for ${widget.label} - Counter: $_counter',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Padding(padding: EdgeInsets.all(4)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_counter++;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Increment counter'),
|
||||||
|
),
|
||||||
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
|
if (widget.param != null)
|
||||||
|
Text('Parameter: ${widget.param!}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
|
if (widget.extra != null)
|
||||||
|
Text('Extra: ${widget.extra!}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
if (!widget.withScaffold) ...<Widget>[
|
||||||
|
const Padding(padding: EdgeInsets.all(16)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('< Back',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
175
lib/go_router/examples/transition_animations.dart
Normal file
175
lib/go_router/examples/transition_animations.dart
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// To use a custom transition animation, provide a `pageBuilder` with a
|
||||||
|
/// CustomTransitionPage.
|
||||||
|
///
|
||||||
|
/// To learn more about animation in Flutter, check out the [Introduction to
|
||||||
|
/// animations](https://docs.flutter.dev/development/ui/animations) page on
|
||||||
|
/// flutter.dev.
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
/// The route configuration.
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const HomeScreen();
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: 'details',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) {
|
||||||
|
return CustomTransitionPage<void>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const DetailsScreen(),
|
||||||
|
transitionDuration: const Duration(milliseconds: 150),
|
||||||
|
transitionsBuilder: (BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child) {
|
||||||
|
// Change the opacity of the screen using a Curve based on the the animation's
|
||||||
|
// value
|
||||||
|
return FadeTransition(
|
||||||
|
opacity:
|
||||||
|
CurveTween(curve: Curves.easeInOut).animate(animation),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'dismissible-details',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) {
|
||||||
|
return CustomTransitionPage<void>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const DismissibleDetails(),
|
||||||
|
barrierDismissible: true,
|
||||||
|
barrierColor: Colors.black38,
|
||||||
|
opaque: false,
|
||||||
|
transitionDuration: Duration.zero,
|
||||||
|
transitionsBuilder: (_, __, ___, Widget child) => child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'custom-reverse-transition-duration',
|
||||||
|
pageBuilder: (BuildContext context, GoRouterState state) {
|
||||||
|
return CustomTransitionPage<void>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const DetailsScreen(),
|
||||||
|
barrierDismissible: true,
|
||||||
|
barrierColor: Colors.black38,
|
||||||
|
opaque: false,
|
||||||
|
transitionDuration: const Duration(milliseconds: 500),
|
||||||
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
||||||
|
transitionsBuilder: (BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The main app.
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
/// Constructs a [MyApp]
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The home screen
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [HomeScreen]
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Home Screen')),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/details'),
|
||||||
|
child: const Text('Go to the Details screen'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/dismissible-details'),
|
||||||
|
child: const Text('Go to the Dismissible Details screen'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () =>
|
||||||
|
context.go('/custom-reverse-transition-duration'),
|
||||||
|
child: const Text(
|
||||||
|
'Go to the Custom Reverse Transition Duration Screen',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The details screen
|
||||||
|
class DetailsScreen extends StatelessWidget {
|
||||||
|
/// Constructs a [DetailsScreen]
|
||||||
|
const DetailsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Details Screen')),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.go('/'),
|
||||||
|
child: const Text('Go back to the Home screen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dismissible details screen
|
||||||
|
class DismissibleDetails extends StatelessWidget {
|
||||||
|
/// Constructs a [DismissibleDetails]
|
||||||
|
const DismissibleDetails({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(48),
|
||||||
|
child: ColoredBox(color: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
lib/go_router/v0/app.dart
Normal file
1
lib/go_router/v0/app.dart
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export 'app/unit_app.dart';
|
||||||
221
lib/go_router/v0/app/navigation/router/app_router_delegate.dart
Normal file
221
lib/go_router/v0/app/navigation/router/app_router_delegate.dart
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../pages/color/color_detail_page.dart';
|
||||||
|
import '../../../pages/color/color_page.dart';
|
||||||
|
import '../../../pages/empty/empty_page.dart';
|
||||||
|
import '../../../pages/settings/settings_page.dart';
|
||||||
|
import '../../../pages/counter/counter_page.dart';
|
||||||
|
import '../../../pages/sort/sort_page.dart';
|
||||||
|
import '../transition/fade_transition_page.dart';
|
||||||
|
import '../../../pages/color/color_add_page.dart';
|
||||||
|
|
||||||
|
const List<String> kDestinationsPaths = [
|
||||||
|
'/color',
|
||||||
|
'/counter',
|
||||||
|
'/user',
|
||||||
|
'/settings',
|
||||||
|
];
|
||||||
|
|
||||||
|
AppRouterDelegate router = AppRouterDelegate();
|
||||||
|
|
||||||
|
class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
|
||||||
|
String _path = '/color';
|
||||||
|
|
||||||
|
String get path => _path;
|
||||||
|
|
||||||
|
int? get activeIndex {
|
||||||
|
if(path.startsWith('/color')) return 0;
|
||||||
|
if(path.startsWith('/counter')) return 1;
|
||||||
|
if(path.startsWith('/user')) return 2;
|
||||||
|
if(path.startsWith('/settings')) return 3;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String,Completer<dynamic>> _completerMap = {};
|
||||||
|
|
||||||
|
Completer<dynamic>? completer;
|
||||||
|
|
||||||
|
final Map<String,List<Page>> _alivePageMap = {};
|
||||||
|
|
||||||
|
void setPathKeepLive(String value){
|
||||||
|
_alivePageMap[value] = _buildPageByPath(value);
|
||||||
|
path = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String,dynamic> _pathExtraMap = {};
|
||||||
|
|
||||||
|
FutureOr<dynamic> changePath(String value,{bool forResult=false,Object? extra}){
|
||||||
|
if(forResult){
|
||||||
|
_completerMap[value] = Completer();
|
||||||
|
}
|
||||||
|
if(extra!=null){
|
||||||
|
_pathExtraMap[value] = extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(forResult){
|
||||||
|
return _completerMap[value]!.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
set path(String value) {
|
||||||
|
if (_path == value) return;
|
||||||
|
_path = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<Page> pages = [];
|
||||||
|
if(_alivePageMap.containsKey(path)){
|
||||||
|
pages = _alivePageMap[path]!;
|
||||||
|
}else{
|
||||||
|
for (var element in _alivePageMap.values) {
|
||||||
|
pages.addAll(element);
|
||||||
|
}
|
||||||
|
pages.addAll(_buildPageByPath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Navigator(
|
||||||
|
onPopPage: _onPopPage,
|
||||||
|
pages: pages.toSet().toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Page> _buildPageByPath(String path) {
|
||||||
|
Widget? child;
|
||||||
|
if(path.startsWith('/color')){
|
||||||
|
return buildColorPages(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path == kDestinationsPaths[1]) {
|
||||||
|
child = const CounterPage();
|
||||||
|
}
|
||||||
|
if (path == kDestinationsPaths[2]) {
|
||||||
|
child = const SortPage();
|
||||||
|
}
|
||||||
|
if (path == kDestinationsPaths[3]) {
|
||||||
|
child = const SettingPage();
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
FadeTransitionPage(
|
||||||
|
key: ValueKey(path),
|
||||||
|
child: child ?? const EmptyPage(),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Page> buildColorPages(String path){
|
||||||
|
List<Page> result = [];
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
for (String segment in uri.pathSegments) {
|
||||||
|
if(segment == 'color'){
|
||||||
|
result.add( const FadeTransitionPage(
|
||||||
|
key: ValueKey('/color'),
|
||||||
|
child:ColorPage(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if(segment =='detail'){
|
||||||
|
final Map<String, String> queryParams = uri.queryParameters;
|
||||||
|
String? selectedColor = queryParams['color'];
|
||||||
|
|
||||||
|
if (selectedColor != null) {
|
||||||
|
Color color = Color(int.parse(selectedColor, radix: 16));
|
||||||
|
result.add( FadeTransitionPage(
|
||||||
|
key: const ValueKey('/color/detail'),
|
||||||
|
child:ColorDetailPage(color: color),
|
||||||
|
));
|
||||||
|
}else{
|
||||||
|
Color? selectedColor = _pathExtraMap[path];
|
||||||
|
if (selectedColor != null) {
|
||||||
|
result.add( FadeTransitionPage(
|
||||||
|
key: const ValueKey('/color/detail'),
|
||||||
|
child:ColorDetailPage(color: selectedColor),
|
||||||
|
));
|
||||||
|
_pathExtraMap.remove(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(segment == 'add'){
|
||||||
|
result.add( const FadeTransitionPage(
|
||||||
|
key: ValueKey('/color/add'),
|
||||||
|
child:ColorAddPage(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> popRoute() async {
|
||||||
|
print('=======popRoute=========');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _onPopPage(Route route, result) {
|
||||||
|
if(_completerMap.containsKey(path)){
|
||||||
|
_completerMap[path]?.complete(result);
|
||||||
|
_completerMap.remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
path = backPath(path);
|
||||||
|
return route.didPop(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
String backPath(String path){
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
if(uri.pathSegments.length==1) return path;
|
||||||
|
List<String> parts = List.of(uri.pathSegments)..removeLast();
|
||||||
|
return '/${parts.join('/')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setNewRoutePath(configuration) async {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// class AppRouterDelegate extends RouterDelegate<String> with ChangeNotifier, PopNavigatorRouterDelegateMixin {
|
||||||
|
//
|
||||||
|
// List<String> _value = ['/'];
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// List<String> get value => _value;
|
||||||
|
//
|
||||||
|
// set value(List<String> value){
|
||||||
|
// _value = value;
|
||||||
|
// notifyListeners();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return Navigator(
|
||||||
|
// onPopPage: _onPopPage,
|
||||||
|
// pages: _value.map((e) => _pageMap[e]!).toList(),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// final Map<String, Page> _pageMap = const {
|
||||||
|
// '/': MaterialPage(child: HomePage()),
|
||||||
|
// 'a': MaterialPage(child: PageA()),
|
||||||
|
// 'b': MaterialPage(child: PageB()),
|
||||||
|
// 'c': MaterialPage(child: PageC()),
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// bool _onPopPage(Route route, result) {
|
||||||
|
// _value = List.of(_value)..removeLast();
|
||||||
|
// notifyListeners();
|
||||||
|
// return route.didPop(result);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// GlobalKey<NavigatorState>? navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Future<void> setNewRoutePath(String configuration) async{
|
||||||
|
// }
|
||||||
|
// }
|
||||||
105
lib/go_router/v0/app/navigation/router/iroute.dart
Normal file
105
lib/go_router/v0/app/navigation/router/iroute.dart
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
import '../../../pages/color/color_add_page.dart';
|
||||||
|
import '../../../pages/color/color_detail_page.dart';
|
||||||
|
import '../../../pages/color/color_page.dart';
|
||||||
|
import '../transition/fade_transition_page.dart';
|
||||||
|
|
||||||
|
class IRoute {
|
||||||
|
final String path;
|
||||||
|
final IRoutePageBuilder builder;
|
||||||
|
final List<IRoute> children;
|
||||||
|
|
||||||
|
const IRoute({
|
||||||
|
required this.path,
|
||||||
|
this.children = const [],
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'IRoute{path: $path, children: $children}';
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> list() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef IRoutePageBuilder = Page? Function(
|
||||||
|
BuildContext context, IRouteData data);
|
||||||
|
|
||||||
|
class IRouteData {
|
||||||
|
final Object? extra;
|
||||||
|
final bool forResult;
|
||||||
|
final Uri uri;
|
||||||
|
final bool keepAlive;
|
||||||
|
|
||||||
|
IRouteData({
|
||||||
|
required this.extra,
|
||||||
|
required this.uri,
|
||||||
|
required this.forResult,
|
||||||
|
required this.keepAlive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IRoute> kDestinationsIRoutes = [
|
||||||
|
IRoute(
|
||||||
|
path: '/color',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/color'),
|
||||||
|
child: ColorPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
IRoute(
|
||||||
|
path: '/color/detail',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
final Map<String, String> queryParams = data.uri.queryParameters;
|
||||||
|
String? selectedColor = queryParams['color'];
|
||||||
|
if (selectedColor != null) {
|
||||||
|
Color color = Color(int.parse(selectedColor, radix: 16));
|
||||||
|
return FadeTransitionPage(
|
||||||
|
key: const ValueKey('/color/detail'),
|
||||||
|
child: ColorDetailPage(color: color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IRoute(
|
||||||
|
path: '/color/add',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/color/add'),
|
||||||
|
child: ColorAddPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IRoute(
|
||||||
|
path: '/counter',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/counter'),
|
||||||
|
child: ColorAddPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
IRoute(
|
||||||
|
path: '/user',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/user'),
|
||||||
|
child: ColorAddPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
IRoute(
|
||||||
|
path: '/settings',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/settings'),
|
||||||
|
child: ColorAddPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||||
|
// for details. All rights reserved. Use of this source code is governed by a
|
||||||
|
// BSD-style license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FadeTransitionPage<T> extends Page<T> {
|
||||||
|
final Widget child;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
const FadeTransitionPage({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.duration = const Duration(milliseconds: 300),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Route<T> createRoute(BuildContext context) =>
|
||||||
|
PageBasedFadeTransitionRoute<T>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
|
||||||
|
final FadeTransitionPage<T> _page;
|
||||||
|
|
||||||
|
PageBasedFadeTransitionRoute(this._page) : super(settings: _page);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => _page.duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation) {
|
||||||
|
var curveTween = CurveTween(curve: Curves.easeIn);
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation.drive(curveTween),
|
||||||
|
child: (settings as FadeTransitionPage).child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation, Widget child) =>
|
||||||
|
child;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||||
|
// for details. All rights reserved. Use of this source code is governed by a
|
||||||
|
// BSD-style license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NoTransitionPage<T> extends Page<T> {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const NoTransitionPage({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Route<T> createRoute(BuildContext context) => NoTransitionRoute<T>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoTransitionRoute<T> extends PageRoute<T> {
|
||||||
|
|
||||||
|
final NoTransitionPage<T> _page;
|
||||||
|
|
||||||
|
NoTransitionRoute(this._page) : super(settings: _page);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => const Duration(milliseconds: 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation) {
|
||||||
|
return (settings as NoTransitionPage).child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation, Widget child) =>
|
||||||
|
child;
|
||||||
|
}
|
||||||
34
lib/go_router/v0/app/navigation/views/app_navigation.dart
Normal file
34
lib/go_router/v0/app/navigation/views/app_navigation.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../router/app_router_delegate.dart';
|
||||||
|
import 'app_navigation_rail.dart';
|
||||||
|
import 'app_top_bar.dart';
|
||||||
|
|
||||||
|
class AppNavigation extends StatelessWidget {
|
||||||
|
const AppNavigation({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
double px1 = 1/View.of(context).devicePixelRatio;
|
||||||
|
return Scaffold(
|
||||||
|
body: Row(
|
||||||
|
children: [
|
||||||
|
const AppNavigationRail(),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const AppTopBar(),
|
||||||
|
Divider(height: px1,),
|
||||||
|
Expanded(
|
||||||
|
child: Router(
|
||||||
|
routerDelegate: router,
|
||||||
|
backButtonDispatcher: RootBackButtonDispatcher(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/components.dart';
|
||||||
|
import '../router/app_router_delegate.dart';
|
||||||
|
|
||||||
|
class AppNavigationRail extends StatefulWidget {
|
||||||
|
const AppNavigationRail({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationRail> createState() => _AppNavigationRailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationRailState extends State<AppNavigationRail> {
|
||||||
|
|
||||||
|
final List<MenuMeta> deskNavBarMenus = const [
|
||||||
|
MenuMeta(label: '颜色板', icon: Icons.color_lens_outlined),
|
||||||
|
MenuMeta(label: '计数器', icon: Icons.add_chart),
|
||||||
|
MenuMeta(label: '我的', icon: Icons.person),
|
||||||
|
MenuMeta(label: '设置', icon: Icons.settings),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
router.addListener(_onRouterChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
router.removeListener(_onRouterChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DragToMoveWrap(
|
||||||
|
child: TolyNavigationRail(
|
||||||
|
items: deskNavBarMenus,
|
||||||
|
leading: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 18.0),
|
||||||
|
child: FlutterLogo(),
|
||||||
|
),
|
||||||
|
tail: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 6.0),
|
||||||
|
child: Text('V0.0.4',style: TextStyle(color: Colors.white,fontSize: 12),),
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xff3975c6),
|
||||||
|
onDestinationSelected: _onDestinationSelected,
|
||||||
|
selectedIndex: router.activeIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDestinationSelected(int index) {
|
||||||
|
if(index==1){
|
||||||
|
router.setPathKeepLive(kDestinationsPaths[index]);
|
||||||
|
}else{
|
||||||
|
router.path = kDestinationsPaths[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRouterChange() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
64
lib/go_router/v0/app/navigation/views/app_router_editor.dart
Normal file
64
lib/go_router/v0/app/navigation/views/app_router_editor.dart
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/toly_ui/button/hover_icon_button.dart';
|
||||||
|
import '../router/app_router_delegate.dart';
|
||||||
|
|
||||||
|
class AppRouterEditor extends StatefulWidget {
|
||||||
|
final ValueChanged<String>? onSubmit;
|
||||||
|
const AppRouterEditor({super.key, this.onSubmit});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppRouterEditor> createState() => _AppRouterEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppRouterEditorState extends State<AppRouterEditor> {
|
||||||
|
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_onRouteChange();
|
||||||
|
router.addListener(_onRouteChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRouteChange() {
|
||||||
|
_controller.text=router.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
router.removeListener(_onRouteChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
child: CupertinoTextField(
|
||||||
|
controller: _controller,
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
padding: EdgeInsets.only(left:12,top: 6,bottom: 6,right: 32),
|
||||||
|
placeholder: '输入路由地址导航',
|
||||||
|
onSubmitted: widget.onSubmit,
|
||||||
|
decoration: BoxDecoration(color: Color(0xffF1F2F3),borderRadius: BorderRadius.circular(6)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: HoverIconButton(
|
||||||
|
icon: Icons.directions_outlined,
|
||||||
|
defaultColor: Color(0xff68696B),
|
||||||
|
onPressed:()=>widget.onSubmit?.call(_controller.text),
|
||||||
|
size: 20
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
104
lib/go_router/v0/app/navigation/views/app_top_bar.dart
Normal file
104
lib/go_router/v0/app/navigation/views/app_top_bar.dart
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/components.dart';
|
||||||
|
import '../router/app_router_delegate.dart';
|
||||||
|
import 'app_router_editor.dart';
|
||||||
|
|
||||||
|
class AppTopBar extends StatelessWidget {
|
||||||
|
const AppTopBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DragToMoveWrap(
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
height: 46,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
const RouterIndicator(),
|
||||||
|
Expanded(
|
||||||
|
child: Row(children: [
|
||||||
|
const Spacer(),
|
||||||
|
SizedBox(
|
||||||
|
width: 250,
|
||||||
|
child: AppRouterEditor(
|
||||||
|
onSubmit: (path) => router.path = path,
|
||||||
|
)),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
child: VerticalDivider(
|
||||||
|
width: 32,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
])),
|
||||||
|
const WindowButtons()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RouterIndicator extends StatefulWidget {
|
||||||
|
const RouterIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RouterIndicator> createState() => _RouterIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> kRouteLabelMap = {
|
||||||
|
'/color': '颜色板',
|
||||||
|
'/color/add': '添加颜色',
|
||||||
|
'/color/detail': '颜色详情',
|
||||||
|
'/counter': '计数器',
|
||||||
|
'/user': '我的',
|
||||||
|
'/settings': '系统设置',
|
||||||
|
};
|
||||||
|
|
||||||
|
class _RouterIndicatorState extends State<RouterIndicator> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
router.addListener(_onRouterChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
router.removeListener(_onRouterChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TolyBreadcrumb(
|
||||||
|
items: pathToBreadcrumbItems(router.path),
|
||||||
|
onTapItem: (item) {
|
||||||
|
if (item.to != null) {
|
||||||
|
router.path = item.to!;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRouterChange() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BreadcrumbItem> pathToBreadcrumbItems(String path) {
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
List<BreadcrumbItem> result = [];
|
||||||
|
String to = '';
|
||||||
|
|
||||||
|
String distPath = '';
|
||||||
|
for (String segment in uri.pathSegments) {
|
||||||
|
distPath += '/$segment';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String segment in uri.pathSegments) {
|
||||||
|
to += '/$segment';
|
||||||
|
String label = kRouteLabelMap[to] ?? '未知路由';
|
||||||
|
result.add(BreadcrumbItem(to: to, label: label, active: to == distPath));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/go_router/v0/app/unit_app.dart
Normal file
29
lib/go_router/v0/app/unit_app.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'navigation/router/app_router_delegate.dart';
|
||||||
|
import 'navigation/views/app_navigation.dart';
|
||||||
|
import 'navigation/views/app_navigation_rail.dart';
|
||||||
|
|
||||||
|
class UnitApp extends StatelessWidget {
|
||||||
|
const UnitApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeData(
|
||||||
|
fontFamily: "宋体",
|
||||||
|
scaffoldBackgroundColor: Colors.white,
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
elevation: 0,
|
||||||
|
iconTheme: IconThemeData(color: Colors.black),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
))),
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: AppNavigation()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
102
lib/go_router/v0/pages/color/color_add_page.dart
Normal file
102
lib/go_router/v0/pages/color/color_add_page.dart
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
|
|
||||||
|
class ColorAddPage extends StatefulWidget {
|
||||||
|
const ColorAddPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ColorAddPage> createState() => _ColorAddPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ColorAddPageState extends State<ColorAddPage> {
|
||||||
|
late Color _color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_color = randomColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
String text = '# ${_color.value.toRadixString(16)}';
|
||||||
|
return Scaffold(
|
||||||
|
bottomNavigationBar: Container(
|
||||||
|
margin: EdgeInsets.only(right:20,bottom: 20),
|
||||||
|
// color: Colors.redAccent,
|
||||||
|
child: Row(
|
||||||
|
textDirection:TextDirection.rtl,
|
||||||
|
children: [
|
||||||
|
ElevatedButton(onPressed: (){
|
||||||
|
Navigator.of(context).pop(_color);
|
||||||
|
|
||||||
|
}, child: Text('添加')),
|
||||||
|
SizedBox(width: 12,),
|
||||||
|
OutlinedButton(onPressed: (){
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}, child: Text('取消')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0,vertical: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(text,style: TextStyle(color: _color,fontSize: 24,letterSpacing: 4),)),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(left: 10),
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: Icon(
|
||||||
|
Icons.sell_outlined,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _color,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ColorPicker(
|
||||||
|
colorPickerWidth:200,
|
||||||
|
// enableAlpha: false,
|
||||||
|
displayThumbColor:true,
|
||||||
|
pickerColor: _color,
|
||||||
|
paletteType: PaletteType.hueWheel,
|
||||||
|
onColorChanged: changeColor,
|
||||||
|
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Random _random = Random();
|
||||||
|
|
||||||
|
Color get randomColor {
|
||||||
|
return Color.fromARGB(
|
||||||
|
255,
|
||||||
|
_random.nextInt(256),
|
||||||
|
_random.nextInt(256),
|
||||||
|
_random.nextInt(256),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectColor() {
|
||||||
|
Navigator.of(context).pop(_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeColor(Color value) {
|
||||||
|
_color = value;
|
||||||
|
setState(() {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/go_router/v0/pages/color/color_detail_page.dart
Normal file
34
lib/go_router/v0/pages/color/color_detail_page.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class ColorDetailPage extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
const ColorDetailPage({super.key, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
const TextStyle style = TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white
|
||||||
|
);
|
||||||
|
String text = '# ${color.value.toRadixString(16)}';
|
||||||
|
return Scaffold(
|
||||||
|
// appBar: AppBar(
|
||||||
|
// systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
// statusBarColor: Colors.transparent,
|
||||||
|
// statusBarIconBrightness: Brightness.light
|
||||||
|
// ),
|
||||||
|
// iconTheme: IconThemeData(color: Colors.white),
|
||||||
|
// titleTextStyle:TextStyle(color: Colors.white,fontSize: 18) ,
|
||||||
|
// backgroundColor: color,
|
||||||
|
// title: Text('颜色界面',),),
|
||||||
|
body: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: color,
|
||||||
|
child: Text(text ,style: style,),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
lib/go_router/v0/pages/color/color_page.dart
Normal file
54
lib/go_router/v0/pages/color/color_page.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/project/colors_panel.dart';
|
||||||
|
import '../../app/navigation/router/app_router_delegate.dart';
|
||||||
|
|
||||||
|
class ColorPage extends StatefulWidget {
|
||||||
|
const ColorPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ColorPage> createState() => _ColorPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ColorPageState extends State<ColorPage> {
|
||||||
|
final List<Color> _colors = [
|
||||||
|
Colors.red, Colors.black, Colors.blue, Colors.green, Colors.orange,
|
||||||
|
Colors.pink, Colors.purple, Colors.indigo, Colors.amber, Colors.cyan,
|
||||||
|
Colors.redAccent, Colors.grey, Colors.blueAccent, Colors.greenAccent, Colors.orangeAccent,
|
||||||
|
Colors.pinkAccent, Colors.purpleAccent, Colors.indigoAccent, Colors.amberAccent, Colors.cyanAccent,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
// appBar: AppBar(title:const Text('颜色主页')),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: _toAddPage,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
body: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ColorsPanel(
|
||||||
|
colors: _colors,
|
||||||
|
onSelect: _selectColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectColor(Color color){
|
||||||
|
String value = color.value.toRadixString(16);
|
||||||
|
router.path = '/color/detail?color=$value';
|
||||||
|
// router.setPathForData('/color/detail',color);
|
||||||
|
// router.setPathKeepLive('/color/detail?color=$value');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toAddPage() async {
|
||||||
|
Color? color = await router.changePath('/color/add',forResult: true);
|
||||||
|
if (color != null) {
|
||||||
|
setState(() {
|
||||||
|
_colors.add(color);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
lib/go_router/v0/pages/counter/counter_page.dart
Normal file
43
lib/go_router/v0/pages/counter/counter_page.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CounterPage extends StatefulWidget {
|
||||||
|
const CounterPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CounterPage> createState() => _CounterPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CounterPageState extends State<CounterPage> {
|
||||||
|
int _counter = 0;
|
||||||
|
|
||||||
|
void _incrementCounter() {
|
||||||
|
setState(() {
|
||||||
|
_counter++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text(
|
||||||
|
'You have pushed the button this many times:',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$_counter',
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: _incrementCounter,
|
||||||
|
tooltip: 'Increment',
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user