This commit is contained in:
toly
2023-12-11 08:48:30 +08:00
parent be554094cd
commit 838d6aa668
63 changed files with 0 additions and 7608 deletions

View File

@@ -1,244 +0,0 @@
// 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);
}
}

View File

@@ -1,171 +0,0 @@
// 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);
}

View File

@@ -1,46 +0,0 @@
// 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!;
}

View File

@@ -1,7 +0,0 @@
// 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';

View File

@@ -1,23 +0,0 @@
// 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>[];
}

View File

@@ -1,32 +0,0 @@
// 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;
}

View File

@@ -1,76 +0,0 @@
// 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),
];
}

View File

@@ -1,49 +0,0 @@
// 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}'),
),
),
],
),
),
);
}
}

View File

@@ -1,31 +0,0 @@
// 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}');
},
),
);
}

View File

@@ -1,77 +0,0 @@
// 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)'),
),
],
),
),
);
}
}

View File

@@ -1,118 +0,0 @@
// 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;
}
}
}

View File

@@ -1,71 +0,0 @@
// 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,
),
],
),
);
}

View File

@@ -1,101 +0,0 @@
// 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'),
)
],
);
}

View File

@@ -1,77 +0,0 @@
// 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'),
),
),
],
),
),
),
),
);
}

View File

@@ -1,37 +0,0 @@
// 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,
),
);
}

View File

@@ -1,37 +0,0 @@
// 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,
),
);
}

View File

@@ -1,87 +0,0 @@
// 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"),
),
);
}
}

View File

@@ -1,175 +0,0 @@
import 'dart:ui';
import 'package:flutter/material.dart';
abstract class CodeDecoration {
final Color activeColor;
final Color inactiveColor;
final Size cursorSize;
final Color cursorColor;
final TextStyle textStyle;
final String? obscureText;
CodeDecoration({
required this.activeColor,
required this.inactiveColor,
required this.textStyle,
required this.cursorSize,
required this.cursorColor,
this.obscureText,
});
void paint(
Canvas canvas, Size size, int alpha, String text, int count, double gap) {
double boxWidth = (size.width - (count - 1) * gap) / count;
/// 绘制装饰
paintDecoration(canvas, size, text, count, gap);
/// 绘制游标
paintCursor(canvas, alpha,size, text.length, boxWidth, gap);
/// 绘制文字
paintText(canvas,size, text, boxWidth, gap);
}
void paintCursor(
Canvas canvas, int alpha,Size size, int count, double boxWidth, double gap) {
Paint cursorPaint = Paint()
..color = cursorColor.withAlpha(alpha)
..strokeWidth = cursorSize.width
..style = PaintingStyle.fill
..isAntiAlias = true;
double startX = count * (boxWidth + gap) + boxWidth / 2;
double startY = size.height/2-cursorSize.height/2;
var endX = startX + cursorSize.width;
var endY = size.height/2 + cursorSize.height/2;
// var endY = size.height - 28.0 - 12;
// canvas.drawLine(Offset(startX, startY), Offset(startX, endY), cursorPaint);
//绘制圆角光标
Rect rect = Rect.fromLTRB(startX, startY, endX, endY);
RRect rrect =
RRect.fromRectAndRadius(rect, Radius.circular(cursorSize.width));
canvas.drawRRect(rrect, cursorPaint);
}
void paintText(Canvas canvas, Size size,String text, double boxWidth, double gap) {
/// 画文本
double startX = 0.0;
/// Determine whether display obscureText.
bool obscure = obscureText != null;
for (int i = 0; i < text.runes.length; i++) {
int rune = text.runes.elementAt(i);
String code = obscure ? obscureText! : String.fromCharCode(rune);
TextPainter textPainter = TextPainter(
text: TextSpan(
style: textStyle,
text: code,
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
/// Layout the text.
textPainter.layout();
var startY = size.height/2-textPainter.height/2;
startX = boxWidth * i + boxWidth / 2 - textPainter.width / 2 + gap * i;
textPainter.paint(canvas, Offset(startX, startY));
}
}
void paintDecoration(
Canvas canvas, Size size, String text, int count, double gap);
}
class UnderlineCodeDecoration extends CodeDecoration {
final double strokeWidth;
UnderlineCodeDecoration(
{required this.strokeWidth,
required super.activeColor,
required super.inactiveColor,
required super.cursorColor,
required super.textStyle,
required super.cursorSize,
super.obscureText});
@override
void paintDecoration(
Canvas canvas, Size size, String text, int count, double gap) {
Paint underlinePaint = Paint()
..color = activeColor
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..isAntiAlias = true;
/// 画下划线
double singleWidth = (size.width - (count - 1) * gap) / count;
var startX = 0.0;
var startY = size.height;
for (int i = 0; i < count; i++) {
if (i == text.length) {
underlinePaint.color = activeColor;
// underlinePaint.strokeWidth = strokeWidth;
} else {
underlinePaint.color = inactiveColor;
// underlinePaint.strokeWidth = strokeWidth;
}
canvas.drawLine(Offset(startX, startY),
Offset(startX + singleWidth, startY), underlinePaint);
startX += singleWidth + gap;
}
}
}
class RRectCodeDecoration extends CodeDecoration {
final double strokeWidth;
final double height;
RRectCodeDecoration(
{required this.height,
required this.strokeWidth,
required super.activeColor,
required super.inactiveColor,
required super.cursorColor,
required super.textStyle,
required super.cursorSize,
super.obscureText});
@override
void paintDecoration(
Canvas canvas, Size size, String text, int count, double gap) {
Paint paint = Paint()
..color = activeColor
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..isAntiAlias = true;
/// 画下划线
double singleWidth = (size.width - (count - 1) * gap) / count;
var startX = 0.0;
var startY = size.height;
for (int i = 0; i < count; i++) {
if (i == text.length) {
paint.color = activeColor;
} else {
paint.color = inactiveColor;
}
RRect rRect = RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(startX, 0),
Offset(startX + singleWidth, startY),
),
Radius.circular(6));
canvas.drawRRect(rRect, paint);
// canvas.drawLine(Offset(startX, startY),
// Offset(startX + singleWidth, startY), underlinePaint);
startX += singleWidth + gap;
}
}
}

View File

@@ -1,215 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'decoration/code_decoration.dart';
import 'input_painter.dart';
typedef AsyncSubmit = Future<bool> Function(String value);
/// @desc 短信验证码输入框
/// @time 2019-05-14 16:16
/// @author Cheney
class TolyCodeInput extends StatefulWidget {
final int count;
final AsyncSubmit onSubmit;
final CodeDecoration decoration;
final TextInputType keyboardType;
final bool autoFocus;
final FocusNode? focusNode;
final TextInputAction textInputAction;
const TolyCodeInput(
{this.count = 6,
required this.onSubmit,
this.decoration = const UnderlineDecoration(),
this.keyboardType = TextInputType.number,
this.focusNode,
this.autoFocus = false,
this.textInputAction = TextInputAction.done,
super.key});
@override
State createState() {
return TolyCodeInputState();
}
}
class TolyCodeInputState extends State<TolyCodeInput>
with SingleTickerProviderStateMixin {
///输入监听器
final TextEditingController _controller = TextEditingController();
late AnimationController _animaCtrl;
late Animation<int> _animation;
late FocusNode _focusNode;
@override
void initState() {
_focusNode = widget.focusNode ?? FocusNode();
_controller.addListener(_listenValueChange);
initAnimation();
super.initState();
}
void initAnimation() {
_animaCtrl = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = IntTween(begin: 0, end: 255).animate(_animaCtrl)
..addStatusListener(_stateChange);
_animaCtrl.forward();
}
void _listenValueChange() {
submit(_controller.text);
}
void submit(String text) async{
if (text.length >= widget.count) {
bool success = await widget.onSubmit.call(text.substring(0, widget.count));
if(success){
_focusNode.unfocus();
}else{
_controller.text = "";
}
}
}
@override
void dispose() {
/// Only execute when the controller is autoDispose.
_controller.dispose();
_animaCtrl.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: InputPainter(
RRectCodeDecoration(
height: 56,
strokeWidth: 1,
activeColor: const Color(0xff010101),
inactiveColor: const Color(0xffB6B6B6),
cursorColor: Color(0xff3776E9),
textStyle: TextStyle(color: Colors.black),
cursorSize: Size(1, 24),
),
controller: _controller,
count: widget.count,
decoration: widget.decoration,
alpha: _animation,
),
child: RepaintBoundary(
child: TextField(
controller: _controller,
/// Fake the text style.
style: const TextStyle(
color: Colors.transparent,
),
cursorColor: Colors.transparent,
cursorWidth: 0.0,
autocorrect: false,
textAlign: TextAlign.center,
enableInteractiveSelection: false,
maxLength: widget.count,
onSubmitted: submit,
keyboardType: widget.keyboardType,
focusNode: _focusNode,
autofocus: widget.autoFocus,
textInputAction: widget.textInputAction,
obscureText: true,
decoration: const InputDecoration(
counterText: '',
contentPadding: EdgeInsets.symmetric(vertical: 24),
border: OutlineInputBorder(
borderSide: BorderSide.none,
),
),
),
),
);
}
void _stateChange(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_animaCtrl.reverse();
} else if (status == AnimationStatus.dismissed) {
_animaCtrl.forward();
}
}
}
/// 默认的样式
const TextStyle defaultStyle = TextStyle(
/// Default text color.
color: Color(0x80000000),
/// Default text size.
fontSize: 24.0,
);
abstract class CodeDecoration {
/// The style of painting text.
final TextStyle? textStyle;
final ObscureStyle? obscureStyle;
const CodeDecoration({
this.textStyle,
this.obscureStyle,
});
}
/// The object determine the obscure display
class ObscureStyle {
/// Determine whether replace [obscureText] with number.
final bool isTextObscure;
/// The display text when [isTextObscure] is true
final String obscureText;
const ObscureStyle({
this.isTextObscure = false,
this.obscureText = '*',
}) : assert(obscureText.length == 1);
}
/// The object determine the underline color etc.
class UnderlineDecoration extends CodeDecoration {
/// The space between text and underline.
final double gapSpace;
/// The color of the underline.
final Color color;
/// The height of the underline.
final double lineHeight;
/// The underline changed color when user enter pin.
final Color? enteredColor;
const UnderlineDecoration({
TextStyle? textStyle,
ObscureStyle? obscureStyle,
this.enteredColor = const Color(0xff3776E9),
this.gapSpace = 15.0,
this.color = const Color(0x24000000),
this.lineHeight = 0.5,
}) : super(
textStyle: textStyle,
obscureStyle: obscureStyle,
);
}

View File

@@ -1,141 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'decoration/code_decoration.dart' as c;
import 'input.dart';
class InputPainter extends CustomPainter {
final TextEditingController controller;
final int count;
final double space;
final CodeDecoration decoration;
final c.CodeDecoration underlineCodeDecoration;
final Animation<int> alpha;
InputPainter(
this.underlineCodeDecoration, {
required this.controller,
required this.count,
required this.decoration,
this.space = 4.0,
required this.alpha,
}) : super(repaint: Listenable.merge([controller, alpha]));
void _drawUnderLine(Canvas canvas, Size size) {
/// Force convert to [UnderlineDecoration].
var dr = decoration as UnderlineDecoration;
Paint underlinePaint = Paint()
..color = dr.color
..strokeWidth = dr.lineHeight
..style = PaintingStyle.stroke
..isAntiAlias = true;
var startX = 0.0;
var startY = size.height;
/// 画下划线
double singleWidth = (size.width - (count - 1) * dr.gapSpace) / count;
for (int i = 0; i < count; i++) {
if (i == controller.text.length && dr.enteredColor != null) {
underlinePaint.color = dr.enteredColor!;
underlinePaint.strokeWidth = 1;
} else {
underlinePaint.color = dr.color;
underlinePaint.strokeWidth = 0.5;
}
canvas.drawLine(Offset(startX, startY),
Offset(startX + singleWidth, startY), underlinePaint);
startX += singleWidth + dr.gapSpace;
}
/// 画文本
var index = 0;
startX = 0.0;
startY = 28;
/// Determine whether display obscureText.
bool obscureOn;
obscureOn = decoration.obscureStyle != null &&
decoration.obscureStyle!.isTextObscure;
/// The text style of pin.
TextStyle textStyle;
if (decoration.textStyle == null) {
textStyle = defaultStyle;
} else {
textStyle = decoration.textStyle!;
}
controller.text.runes.forEach((rune) {
String code;
if (obscureOn) {
code = decoration.obscureStyle!.obscureText;
} else {
code = String.fromCharCode(rune);
}
TextPainter textPainter = TextPainter(
text: TextSpan(
style: textStyle,
text: code,
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
/// Layout the text.
textPainter.layout();
startX = singleWidth * index +
singleWidth / 2 -
textPainter.width / 2 +
dr.gapSpace * index;
textPainter.paint(canvas, Offset(startX, startY));
index++;
});
///画光标 如果外部有传,则直接使用外部
Color cursorColor = dr.enteredColor ?? const Color(0xff3776E9);
cursorColor = cursorColor.withAlpha(alpha.value);
double cursorWidth = 1;
double cursorHeight = 24;
//LogUtil.v("animation.value=$alpha");
Paint cursorPaint = Paint()
..color = cursorColor
..strokeWidth = cursorWidth
..style = PaintingStyle.stroke
..isAntiAlias = true;
startX =
controller.text.length * (singleWidth + dr.gapSpace) + singleWidth / 2;
var endX = startX + cursorWidth;
var endY = startY + cursorHeight;
// var endY = size.height - 28.0 - 12;
// canvas.drawLine(Offset(startX, startY), Offset(startX, endY), cursorPaint);
//绘制圆角光标
Rect rect = Rect.fromLTRB(startX, startY, endX, endY);
RRect rrect = RRect.fromRectAndRadius(rect, Radius.circular(cursorWidth));
canvas.drawRRect(rrect, cursorPaint);
}
@override
void paint(Canvas canvas, Size size) {
// _drawUnderLine(canvas, size);
underlineCodeDecoration.paint(
canvas,
size,
alpha.value,
controller.text,
count,
space,
);
}
@override
bool shouldRepaint(covariant InputPainter oldDelegate) {
return oldDelegate.controller.text != controller.text;
}
}

View File

@@ -1,25 +0,0 @@
import 'package:flutter/material.dart';
import 'input.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: TolyCodeInput(
autoFocus: true,
onSubmit: (value) async {
print(value);
return true;
},
)),
),
);
}
}

View File

@@ -1,87 +0,0 @@
// 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'),
),
),
);
}
}

View File

@@ -1,181 +0,0 @@
// 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'),
);
}
}

View File

@@ -1,139 +0,0 @@
// 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'),
),
);
}
}

View File

@@ -1,460 +0,0 @@
// 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'),
),
],
),
);
}
}

View File

@@ -1,110 +0,0 @@
// 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'),
),
],
),
),
);
}

View File

@@ -1,133 +0,0 @@
// 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),
),
],
),
);
}
}

View File

@@ -1,110 +0,0 @@
// 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'),
),
],
),
),
);
}

View File

@@ -1,167 +0,0 @@
// 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'),
),
],
),
),
);
}

View File

@@ -1,101 +0,0 @@
// 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)'),
),
),
],
),
),
);
}

View File

@@ -1,95 +0,0 @@
// 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'),
),
],
),
),
);
}

View File

@@ -1,102 +0,0 @@
// 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'),
),
],
),
),
);
}

View File

@@ -1,236 +0,0 @@
// 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)),
],
),
);
}
}

View File

@@ -1,163 +0,0 @@
// 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'),
),
)
],
),
),
),
);
}

View File

@@ -1,169 +0,0 @@
// 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),
),
],
),
);
}
}

View File

@@ -1,146 +0,0 @@
// 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'),
),
);
}
}

View File

@@ -1,108 +0,0 @@
// 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,
);
}
}

View File

@@ -1,289 +0,0 @@
// 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,
),
),
);
}
}

View File

@@ -1,324 +0,0 @@
// 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)),
),
]
],
),
);
}
}

View File

@@ -1,175 +0,0 @@
// 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),
);
}
}

View File

@@ -1,183 +0,0 @@
// Copyright 2014 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.
// This sample demonstrates allowing a context menu to be shown in a widget
// subtree in response to user gestures.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const ContextMenuControllerExampleApp());
/// A builder that includes an Offset to draw the context menu at.
typedef ContextMenuBuilder = Widget Function(BuildContext context, Offset offset);
class ContextMenuControllerExampleApp extends StatefulWidget {
const ContextMenuControllerExampleApp({super.key});
@override
State<ContextMenuControllerExampleApp> createState() => _ContextMenuControllerExampleAppState();
}
class _ContextMenuControllerExampleAppState extends State<ContextMenuControllerExampleApp> {
void _showDialog(BuildContext context) {
Navigator.of(context).push(
DialogRoute<void>(
context: context,
builder: (BuildContext context) => const AlertDialog(title: Text('You clicked print!')),
),
);
}
@override
void initState() {
super.initState();
// On web, disable the browser's context menu since this example uses a custom
// Flutter-rendered context menu.
if (kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
}
@override
void dispose() {
if (kIsWeb) {
BrowserContextMenu.enableContextMenu();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Context menu outside of text'),
),
body: _ContextMenuRegion(
contextMenuBuilder: (BuildContext context, Offset offset) {
// The custom context menu will look like the default context menu
// on the current platform with a single 'Print' button.
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: TextSelectionToolbarAnchors(
primaryAnchor: offset,
),
buttonItems: <ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: () {
ContextMenuController.removeAny();
_showDialog(context);
},
label: 'Print',
),
],
);
},
// In this case this wraps a big open space in a GestureDetector in
// order to show the context menu, but it could also wrap a single
// widget like an Image to give it a context menu.
child: ListView(
children: <Widget>[
Container(height: 20.0),
const Text(
'Right click (desktop) or long press (mobile) anywhere, not just on this text, to show the custom menu.'),
],
),
),
),
);
}
}
/// Shows and hides the context menu based on user gestures.
///
/// By default, shows the menu on right clicks and long presses.
class _ContextMenuRegion extends StatefulWidget {
/// Creates an instance of [_ContextMenuRegion].
const _ContextMenuRegion({
required this.child,
required this.contextMenuBuilder,
});
/// Builds the context menu.
final ContextMenuBuilder contextMenuBuilder;
/// The child widget that will be listened to for gestures.
final Widget child;
@override
State<_ContextMenuRegion> createState() => _ContextMenuRegionState();
}
class _ContextMenuRegionState extends State<_ContextMenuRegion> {
Offset? _longPressOffset;
final ContextMenuController _contextMenuController = ContextMenuController();
static bool get _longPressEnabled {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
return true;
case TargetPlatform.macOS:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return false;
}
}
void _onSecondaryTapUp(TapUpDetails details) {
_show(details.globalPosition);
}
void _onTap() {
if (!_contextMenuController.isShown) {
return;
}
_hide();
}
void _onLongPressStart(LongPressStartDetails details) {
_longPressOffset = details.globalPosition;
}
void _onLongPress() {
assert(_longPressOffset != null);
_show(_longPressOffset!);
_longPressOffset = null;
}
void _show(Offset position) {
_contextMenuController.show(
context: context,
contextMenuBuilder: (BuildContext context) {
return widget.contextMenuBuilder(context, position);
},
);
}
void _hide() {
_contextMenuController.remove();
}
@override
void dispose() {
_hide();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onSecondaryTapUp: _onSecondaryTapUp,
onTap: _onTap,
onLongPress: _longPressEnabled ? _onLongPress : null,
onLongPressStart: _longPressEnabled ? _onLongPressStart : null,
child: widget.child,
);
}
}

View File

@@ -1,89 +0,0 @@
// Copyright 2014 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.
// This example demonstrates showing the default buttons, but customizing their
// appearance.
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const EditableTextToolbarBuilderExampleApp());
class EditableTextToolbarBuilderExampleApp extends StatefulWidget {
const EditableTextToolbarBuilderExampleApp({super.key});
@override
State<EditableTextToolbarBuilderExampleApp> createState() => _EditableTextToolbarBuilderExampleAppState();
}
class _EditableTextToolbarBuilderExampleAppState extends State<EditableTextToolbarBuilderExampleApp> {
final TextEditingController _controller = TextEditingController(
text: 'Right click (desktop) or long press (mobile) to see the menu with custom buttons.',
);
@override
void initState() {
super.initState();
// On web, disable the browser's context menu since this example uses a custom
// Flutter-rendered context menu.
if (kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
}
@override
void dispose() {
if (kIsWeb) {
BrowserContextMenu.enableContextMenu();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Custom button appearance'),
),
body: Center(
child: Column(
children: <Widget>[
const SizedBox(height: 20.0),
TextField(
controller: _controller,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return AdaptiveTextSelectionToolbar(
anchors: editableTextState.contextMenuAnchors,
// Build the default buttons, but make them look custom.
// In a real project you may want to build different
// buttons depending on the platform.
children: editableTextState.contextMenuButtonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoButton(
borderRadius: null,
color: const Color(0xffaaaa00),
disabledColor: const Color(0xffaaaaff),
onPressed: buttonItem.onPressed,
padding: const EdgeInsets.all(10.0),
pressedOpacity: 0.7,
child: SizedBox(
width: 200.0,
child: Text(
CupertinoTextSelectionToolbarButton.getButtonLabel(context, buttonItem),
),
),
);
}).toList(),
);
},
),
],
),
),
),
);
}
}

View File

@@ -1,109 +0,0 @@
// Copyright 2014 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.
// This example demonstrates showing a custom context menu only when some
// narrowly defined text is selected.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const EditableTextToolbarBuilderExampleApp());
const String emailAddress = 'me@example.com';
const String text = 'Select the email address and open the menu: $emailAddress';
class EditableTextToolbarBuilderExampleApp extends StatefulWidget {
const EditableTextToolbarBuilderExampleApp({super.key});
@override
State<EditableTextToolbarBuilderExampleApp> createState() => _EditableTextToolbarBuilderExampleAppState();
}
class _EditableTextToolbarBuilderExampleAppState extends State<EditableTextToolbarBuilderExampleApp> {
final TextEditingController _controller = TextEditingController(
text: text,
);
void _showDialog(BuildContext context) {
Navigator.of(context).push(
DialogRoute<void>(
context: context,
builder: (BuildContext context) => const AlertDialog(title: Text('You clicked send email!')),
),
);
}
@override
void initState() {
super.initState();
// On web, disable the browser's context menu since this example uses a custom
// Flutter-rendered context menu.
if (kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
}
@override
void dispose() {
if (kIsWeb) {
BrowserContextMenu.enableContextMenu();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Custom button for emails'),
),
body: Center(
child: Column(
children: <Widget>[
Container(height: 20.0),
TextField(
controller: _controller,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
final List<ContextMenuButtonItem> buttonItems = editableTextState.contextMenuButtonItems;
// Here we add an "Email" button to the default TextField
// context menu for the current platform, but only if an email
// address is currently selected.
final TextEditingValue value = _controller.value;
if (_isValidEmail(value.selection.textInside(value.text))) {
buttonItems.insert(
0,
ContextMenuButtonItem(
label: 'Send email',
onPressed: () {
ContextMenuController.removeAny();
_showDialog(context);
},
),
);
}
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
},
),
],
),
),
),
);
}
}
bool _isValidEmail(String text) {
return RegExp(
r'(?<name>[a-zA-Z0-9]+)'
r'@'
r'(?<domain>[a-zA-Z0-9]+)'
r'\.'
r'(?<topLevelDomain>[a-zA-Z0-9]+)',
).hasMatch(text);
}

View File

@@ -1,132 +0,0 @@
// Copyright 2014 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.
// This example demonstrates how to create a custom toolbar that retains the
// look of the default buttons for the current platform.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const EditableTextToolbarBuilderExampleApp());
class EditableTextToolbarBuilderExampleApp extends StatefulWidget {
const EditableTextToolbarBuilderExampleApp({super.key});
@override
State<EditableTextToolbarBuilderExampleApp> createState() => _EditableTextToolbarBuilderExampleAppState();
}
class _EditableTextToolbarBuilderExampleAppState extends State<EditableTextToolbarBuilderExampleApp> {
final TextEditingController _controller = TextEditingController(
text: 'Right click (desktop) or long press (mobile) to see the menu with a custom toolbar.',
);
@override
void initState() {
super.initState();
// On web, disable the browser's context menu since this example uses a custom
// Flutter-rendered context menu.
if (kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
}
@override
void dispose() {
if (kIsWeb) {
BrowserContextMenu.enableContextMenu();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Custom toolbar, default-looking buttons'),
),
body: Center(
child: Column(
children: <Widget>[
const SizedBox(height: 20.0),
TextField(
controller: _controller,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return _MyTextSelectionToolbar(
anchor: editableTextState.contextMenuAnchors.primaryAnchor,
// getAdaptiveButtons creates the default button widgets for
// the current platform.
children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
context,
// These buttons just close the menu when clicked.
<ContextMenuButtonItem>[
ContextMenuButtonItem(
label: 'One',
onPressed: () => ContextMenuController.removeAny(),
),
ContextMenuButtonItem(
label: 'Two',
onPressed: () => ContextMenuController.removeAny(),
),
ContextMenuButtonItem(
label: 'Three',
onPressed: () => ContextMenuController.removeAny(),
),
ContextMenuButtonItem(
label: 'Four',
onPressed: () => ContextMenuController.removeAny(),
),
ContextMenuButtonItem(
label: 'Five',
onPressed: () => ContextMenuController.removeAny(),
),
],
).toList(),
);
},
),
],
),
),
),
);
}
}
/// A simple, yet totally custom, text selection toolbar.
///
/// Displays its children in a scrollable grid.
class _MyTextSelectionToolbar extends StatelessWidget {
const _MyTextSelectionToolbar({
required this.anchor,
required this.children,
});
final Offset anchor;
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: anchor.dy,
left: anchor.dx,
child: Container(
width: 200.0,
height: 200.0,
color: Colors.cyanAccent.withOpacity(0.5),
child: GridView.count(
padding: const EdgeInsets.all(12.0),
crossAxisCount: 2,
children: children,
),
),
),
],
);
}
}

View File

@@ -1,93 +0,0 @@
// Copyright 2014 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.
// This example demonstrates a custom context menu in non-editable text using
// SelectionArea.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const SelectableRegionToolbarBuilderExampleApp());
const String text =
'I am some text inside of SelectionArea. Right click (desktop) or long press (mobile) me to show the customized context menu.';
class SelectableRegionToolbarBuilderExampleApp extends StatefulWidget {
const SelectableRegionToolbarBuilderExampleApp({super.key});
@override
State<SelectableRegionToolbarBuilderExampleApp> createState() => _SelectableRegionToolbarBuilderExampleAppState();
}
class _SelectableRegionToolbarBuilderExampleAppState extends State<SelectableRegionToolbarBuilderExampleApp> {
void _showDialog(BuildContext context) {
Navigator.of(context).push(
DialogRoute<void>(
context: context,
builder: (BuildContext context) => const AlertDialog(title: Text('You clicked print!')),
),
);
}
@override
void initState() {
super.initState();
// On web, disable the browser's context menu since this example uses a custom
// Flutter-rendered context menu.
if (kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
}
@override
void dispose() {
if (kIsWeb) {
BrowserContextMenu.enableContextMenu();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Context menu anywhere'),
),
body: Center(
child: SizedBox(
width: 200.0,
child: SelectionArea(
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: selectableRegionState.contextMenuAnchors,
buttonItems: <ContextMenuButtonItem>[
...selectableRegionState.contextMenuButtonItems,
ContextMenuButtonItem(
onPressed: () {
ContextMenuController.removeAny();
_showDialog(context);
},
label: 'Print',
),
],
);
},
child: ListView(
children: const <Widget>[
SizedBox(height: 20.0),
Text(text),
],
),
),
),
),
),
);
}
}

View File

@@ -1 +0,0 @@
export 'app/unit_app.dart';

View File

@@ -1,221 +0,0 @@
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{
// }
// }

View File

@@ -1,105 +0,0 @@
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(),
);
}),
];

View File

@@ -1,53 +0,0 @@
// 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;
}

View File

@@ -1,47 +0,0 @@
// 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;
}

View File

@@ -1,34 +0,0 @@
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(),
),
),
],
),
),
],
),
);
}
}

View File

@@ -1,65 +0,0 @@
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(() {});
}
}

View File

@@ -1,64 +0,0 @@
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
),
)
],
);
}
}

View File

@@ -1,104 +0,0 @@
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;
}
}

View File

@@ -1,29 +0,0 @@
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()
);
}
}

View File

@@ -1,102 +0,0 @@
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(() {
});
}
}

View File

@@ -1,34 +0,0 @@
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,),
),
);
}
}

View File

@@ -1,54 +0,0 @@
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);
});
}
}
}

View File

@@ -1,43 +0,0 @@
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),
),
);
}
}

View File

@@ -1,30 +0,0 @@
import 'package:flutter/material.dart';
class EmptyPage extends StatelessWidget {
const EmptyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(
// title: Text('界面走丢了'),
// ),
body: Scaffold(
body: Center(
child: Wrap(
spacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
direction: Axis.vertical,
children: [
Icon(Icons.nearby_error,size: 64, color: Colors.grey),
Text(
'404 界面丢失',
style: TextStyle(fontSize: 24, color: Colors.grey),
),
],
),
),
),
);
}
}

View File

@@ -1,11 +0,0 @@
import 'package:flutter/material.dart';
class SettingPage extends StatelessWidget {
const SettingPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body:Center(child: Text('SettingPage')));
}
}

View File

@@ -1,859 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
class SortPage extends StatefulWidget {
const SortPage({Key? key}) : super(key: key);
@override
State<SortPage> createState() => _SortPageState();
}
class _SortPageState extends State<SortPage> {
//存放随机数组
List<int> numbers = [];
//订阅流
StreamController<List<int>> streamController = StreamController();
String currentSort = 'bubble';
//柱子的数量 -> 生成排序数组的长度
double sampleSize = 0;
//是否排序
bool isSorted = false;
//是否在排序中
bool isSorting = false;
//排序动画更新的速度
int speed = 0;
static int duration = 1500;
String getTitle() {
switch (currentSort) {
case "bubble":
return "Bubble Sort";
case "coctail":
return "Coctail Sort";
case "comb":
return "Comb Sort";
case "pigeonhole":
return "Pigeonhole Sort";
case "shell":
return "Shell Sort";
case "selection":
return "Selection Sort";
case "cycle":
return "Cycle Sort";
case "heap":
return "Heap Sort";
case "insertion":
return "Insertion Sort";
case "gnome":
return "Gnome Sort";
case "oddeven":
return "OddEven Sort";
case "quick":
return "Quick Sort";
case "merge":
return "Merge Sort";
}
return "";
}
reset() {
isSorted = false;
numbers = [];
for (int i = 0; i < sampleSize; ++i) {
numbers.add(Random().nextInt(500));
}
streamController.add(numbers);
}
Duration getDuration() {
return Duration(microseconds: duration);
}
///动画时间
changeSpeed() {
if (speed >= 3) {
speed = 0;
duration = 1500;
} else {
speed++;
duration = duration ~/ 2;
}
setState(() {});
}
///冒泡排序
bubbleSort() async {
//控制需要进行排序的次数。每一轮循环都会确定一个数字的最终位置。
for (int i = 0; i < numbers.length; ++i) {
//遍历当前未排序的元素,通过相邻的元素比较并交换位置来完成排序。
for (int j = 0; j < numbers.length - i - 1; ++j) {
//如果 _numbers[j] 大于 _numbers[j + 1],则交换它们的位置,确保较大的元素移到右边。
if (numbers[j] > numbers[j + 1]) {
int temp = numbers[j];
numbers[j] = numbers[j + 1];
numbers[j + 1] = temp;
}
//实现一个延迟以便在ui上展示排序的动画效果
await Future.delayed(getDuration(), () {});
streamController.add(numbers);
}
}
}
///鸡尾酒排序(双向冒泡排序)
cocktailSort() async {
bool swapped = true; // 表示是否进行了交换
int start = 0; // 当前未排序部分的起始位置
int end = numbers.length; // 当前未排序部分的结束位置
// 开始排序循环,只有当没有进行交换时才会退出循环
while (swapped == true) {
swapped = false;
// 从左往右遍历需要排序的部分
for (int i = start; i < end - 1; ++i) {
// 对每两个相邻元素进行比较
if (numbers[i] > numbers[i + 1]) {
// 如果前面的元素大于后面的元素,则交换它们的位置
int temp = numbers[i];
numbers[i] = numbers[i + 1];
numbers[i + 1] = temp;
swapped = true; // 进行了交换
}
// 实现动画效果,延迟一段时间后更新数组状态
await Future.delayed(getDuration());
streamController.add(numbers);
}
// 如果没有进行交换,则说明已经排好序,退出循环
if (swapped == false) break;
// 重设为false准备进行下一轮排序
swapped = false;
// 将end设置为上一轮排序的最后一个元素的位置
end = end - 1;
// 从右往左遍历需要排序的部分
for (int i = end - 1; i >= start; i--) {
// 对每两个相邻元素进行比较
if (numbers[i] > numbers[i + 1]) {
// 如果前面的元素大于后面的元素,则交换它们的位置
int temp = numbers[i];
numbers[i] = numbers[i + 1];
numbers[i + 1] = temp;
swapped = true; // 进行了交换
}
// 实现动画效果,延迟一段时间后更新数组状态
await Future.delayed(getDuration());
streamController.add(numbers);
}
// 将start向右移一位准备下一轮排序
start = start + 1;
}
}
///梳排序Comb Sort
combSort() async {
int gap = numbers.length;
bool swapped = true;
// 当间隔不为1或存在交换时执行循环
while (gap != 1 || swapped == true) {
// 通过缩小间隔来逐步将元素归位
gap = getNextGap(gap);
swapped = false;
for (int i = 0; i < numbers.length - gap; i++) {
// 如果当前元素大于间隔位置上的元素,则交换它们的位置
if (numbers[i] > numbers[i + gap]) {
int temp = numbers[i];
numbers[i] = numbers[i + gap];
numbers[i + gap] = temp;
swapped = true;
}
// 实现一个延迟,以便在 UI 上展示排序的动画效果。
await Future.delayed(getDuration());
streamController.add(numbers);
}
}
}
int getNextGap(int gap) {
// 根据当前间隔值计算下一个间隔值
gap = (gap * 10) ~/ 13;
if (gap < 1) return 1;
return gap;
}
///鸽巢排序
pigeonHole() async {
int min = numbers[0];
int max = numbers[0];
int range, i, j, index;
// 找到数组中的最大值和最小值
for (int a = 0; a < numbers.length; a++) {
if (numbers[a] > max) max = numbers[a];
if (numbers[a] < min) min = numbers[a];
}
// 计算鸽巢的个数
range = max - min + 1;
List<int> p = List.generate(range, (i) => 0);
// 将数字分配到各个鸽巢中
for (i = 0; i < numbers.length; i++) {
p[numbers[i] - min]++;
}
index = 0;
// 将鸽巢中的数字取出,重新放回到数组中
for (j = 0; j < range; j++) {
while (p[j]-- > 0) {
numbers[index++] = j + min;
await Future.delayed(getDuration());
streamController.add(numbers);
}
}
}
///希尔排序
shellSort() async {
//定义变量 gap 并初始化为数组长度的一半。每次循环完成后将 gap 减半直到等于 0。
for (int gap = numbers.length ~/ 2; gap > 0; gap ~/= 2) {
//遍历每个子序列并进行插入排序。初始时从第一个子序列的第二个元素开始,即 i = gap以 gap 为步长逐个遍历每个子序列。
for (int i = gap; i < numbers.length; i += 1) {
//将当前遍历到的元素赋值给它
int temp = numbers[i];
//内部使用一个 for 循环来实现插入排序。
//循环开始时定义变量 j 并将其初始化为当前遍历到的元素的下标。通过不断比较前后相隔 gap 的元素大小并交换位置,将当前元素插入到正确的位置。
int j;
for (j = i; j >= gap && numbers[j - gap] > temp; j -= gap) {
numbers[j] = numbers[j - gap];
}
numbers[j] = temp;
await Future.delayed(getDuration());
streamController.add(numbers);
}
}
}
///选择排序
selectionSort() async {
for (int i = 0; i < numbers.length; i++) {
for (int j = i + 1; j < numbers.length; j++) {
// 遍历未排序部分,内层循环控制变量 j
if (numbers[i] > numbers[j]) {
// 判断当前元素是否比后续元素小
int temp = numbers[j];
// 交换当前元素和后续较小的元素
numbers[j] = numbers[i];
numbers[i] = temp;
}
await Future.delayed(getDuration(), () {});
streamController.add(numbers);
}
}
}
///循环排序
cycleSort() async {
int writes = 0;
for (int cycleStart = 0; cycleStart <= numbers.length - 2; cycleStart++) {
int item = numbers[cycleStart];
int pos = cycleStart;
// 在未排序部分中寻找比当前元素小的元素个数
for (int i = cycleStart + 1; i < numbers.length; i++) {
if (numbers[i] < item) pos++;
}
// 如果当前元素已经在正确位置上,则跳过此次迭代
if (pos == cycleStart) {
continue;
}
// 将当前元素放置到正确的位置上,并记录写操作次数
while (item == numbers[pos]) {
pos += 1;
}
if (pos != cycleStart) {
int temp = item;
item = numbers[pos];
numbers[pos] = temp;
writes++;
}
// 循环将位于当前位置的元素放置到正确的位置上
while (pos != cycleStart) {
pos = cycleStart;
// 继续在未排序部分中寻找比当前元素小的元素个数
for (int i = cycleStart + 1; i < numbers.length; i++) {
if (numbers[i] < item) pos += 1;
}
// 将当前元素放置到正确的位置上,并记录写操作次数
while (item == numbers[pos]) {
pos += 1;
}
if (item != numbers[pos]) {
int temp = item;
item = numbers[pos];
numbers[pos] = temp;
writes++;
}
// 添加延迟操作以展示排序过程
await Future.delayed(getDuration());
streamController.add(numbers);
}
}
}
///堆排序
heapSort() async {
// 从最后一个非叶子节点开始,构建最大堆
for (int i = numbers.length ~/ 2; i >= 0; i--) {
await heapify(numbers, numbers.length, i);
streamController.add(numbers);
}
// 依次取出最大堆的根节点(最大值),并进行堆化
for (int i = numbers.length - 1; i >= 0; i--) {
int temp = numbers[0];
numbers[0] = numbers[i];
numbers[i] = temp;
await heapify(numbers, i, 0);
streamController.add(numbers);
}
}
heapify(List<int> arr, int n, int i) async {
int largest = i;
int l = 2 * i + 1; // 左子节点索引
int r = 2 * i + 2; // 右子节点索引
// 如果左子节点存在并且大于父节点,则更新最大值索引
if (l < n && arr[l] > arr[largest]) largest = l;
// 如果右子节点存在并且大于父节点或左子节点,则更新最大值索引
if (r < n && arr[r] > arr[largest]) largest = r;
// 如果最大值索引不等于当前节点索引,则交换节点值,并递归进行堆化
if (largest != i) {
int temp = numbers[i];
numbers[i] = numbers[largest];
numbers[largest] = temp;
heapify(arr, n, largest);
}
await Future.delayed(getDuration()); // 延迟操作,用于可视化排序过程
}
///插入排序
insertionSort() async {
for (int i = 1; i < numbers.length; i++) {
int temp = numbers[i]; // 将当前元素存储到临时变量 temp 中
int j = i - 1; // j 表示已排序部分的最后一个元素的索引
// 在已排序部分从后往前查找,找到合适位置插入当前元素
while (j >= 0 && temp < numbers[j]) {
numbers[j + 1] = numbers[j]; // 当前元素比已排序部分的元素小,将元素后移一位
--j; // 向前遍历
await Future.delayed(getDuration());
streamController.add(numbers); // 更新排序结果
}
numbers[j + 1] = temp; // 插入当前元素到已排序部分的正确位置
await Future.delayed(getDuration(), () {});
streamController.add(numbers); // 更新排序结果
}
}
///地精排序 (侏儒排序)
gnomeSort() async {
int index = 0;
while (index < numbers.length) {
// 当 index 小于数组长度时执行循环
if (index == 0) index++;
if (numbers[index] >= numbers[index - 1]) {
// 如果当前元素大于等于前面的元素,则将 index 加1
index++;
} else {
// 否则,交换这两个元素,并将 index 减1使得元素可以沉到正确位置
int temp = numbers[index];
numbers[index] = numbers[index - 1];
numbers[index - 1] = temp;
index--;
}
await Future.delayed(getDuration());
streamController.add(numbers);
}
return;
}
///奇偶排序(Odd-Even Sort)
oddEvenSort() async {
bool isSorted = false;
while (!isSorted) {
// 当 isSorted 为 false 时执行循环
isSorted = true; // 先假设数组已经排好序
for (int i = 1; i <= numbers.length - 2; i = i + 2) {
// 对奇数索引位置进行比较
if (numbers[i] > numbers[i + 1]) {
// 如果当前元素大于后面的元素,则交换它们的值
int temp = numbers[i];
numbers[i] = numbers[i + 1];
numbers[i + 1] = temp;
isSorted = false; // 若发生了交换,则说明数组仍未完全排序,将 isSorted 设为 false
await Future.delayed(getDuration());
streamController.add(numbers);
}
}
for (int i = 0; i <= numbers.length - 2; i = i + 2) {
// 对偶数索引位置进行比较
if (numbers[i] > numbers[i + 1]) {
// 如果当前元素大于后面的元素,则交换它们的值
int temp = numbers[i];
numbers[i] = numbers[i + 1];
numbers[i + 1] = temp;
isSorted = false;
await Future.delayed(getDuration());
streamController.add(numbers);
}
}
}
return;
}
///快速排序
quickSort(int leftIndex, int rightIndex) async {
// 定义一个名为 _partition 的异步函数,用于划分数组,并返回划分后的基准元素的索引位置
Future<int> _partition(int left, int right) async {
// 选择中间位置的元素作为基准元素
int p = (left + (right - left) / 2).toInt();
// 交换基准元素和最右边的元素
var temp = numbers[p];
numbers[p] = numbers[right];
numbers[right] = temp;
await Future.delayed(getDuration());
streamController.add(numbers);
// 初始化游标 cursor
int cursor = left;
// 遍历数组并根据基准元素将元素交换到左侧或右侧
for (int i = left; i < right; i++) {
if (cf(numbers[i], numbers[right]) <= 0) {
// 如果当前元素小于等于基准元素,则交换它和游标位置的元素
var temp = numbers[i];
numbers[i] = numbers[cursor];
numbers[cursor] = temp;
cursor++;
await Future.delayed(getDuration());
streamController.add(numbers);
}
}
// 将基准元素放置在游标位置
temp = numbers[right];
numbers[right] = numbers[cursor];
numbers[cursor] = temp;
await Future.delayed(getDuration());
streamController.add(numbers);
return cursor; // 返回基准元素的索引位置
}
// 如果左索引小于右索引,则递归地对数组进行快速排序
if (leftIndex < rightIndex) {
int p = await _partition(leftIndex, rightIndex);
await quickSort(leftIndex, p - 1); // 对基准元素左侧的子数组进行快速排序
await quickSort(p + 1, rightIndex); // 对基准元素右侧的子数组进行快速排序
}
}
// 比较函数,用于判断两个元素的大小关系
cf(int a, int b) {
if (a < b) {
return -1; // 若 a 小于 b则返回 -1
} else if (a > b) {
return 1; // 若 a 大于 b则返回 1
} else {
return 0; // 若 a 等于 b则返回 0
}
}
///归并排序
mergeSort(int leftIndex, int rightIndex) async {
// 定义一个名为 merge 的异步函数,用于合并两个有序子数组
Future<void> merge(int leftIndex, int middleIndex, int rightIndex) async {
// 计算左侧子数组和右侧子数组的大小
int leftSize = middleIndex - leftIndex + 1;
int rightSize = rightIndex - middleIndex;
// 创建左侧子数组和右侧子数组
List leftList = List.generate(leftSize, (index) => 0);
List rightList = List.generate(rightSize, (index) => 0);
// 将原始数组中的元素分别复制到左侧子数组和右侧子数组中
for (int i = 0; i < leftSize; i++) {
leftList[i] = numbers[leftIndex + i];
}
for (int j = 0; j < rightSize; j++) {
rightList[j] = numbers[middleIndex + j + 1];
}
// 初始化游标和索引
int i = 0, j = 0;
int k = leftIndex;
// 比较左侧子数组和右侧子数组的元素,并按顺序将较小的元素放入原始数组中
while (i < leftSize && j < rightSize) {
if (leftList[i] <= rightList[j]) {
numbers[k] = leftList[i];
i++;
} else {
numbers[k] = rightList[j];
j++;
}
await Future.delayed(getDuration());
streamController.add(numbers);
k++;
}
// 将左侧子数组或右侧子数组中剩余的元素放入原始数组中
while (i < leftSize) {
numbers[k] = leftList[i];
i++;
k++;
await Future.delayed(getDuration());
streamController.add(numbers);
}
while (j < rightSize) {
numbers[k] = rightList[j];
j++;
k++;
await Future.delayed(getDuration());
streamController.add(numbers);
}
}
// 如果左索引小于右索引,则递归地对数组进行归并排序
if (leftIndex < rightIndex) {
// 计算中间索引位置
int middleIndex = (rightIndex + leftIndex) ~/ 2;
// 分别对左侧子数组和右侧子数组进行归并排序
await mergeSort(leftIndex, middleIndex);
await mergeSort(middleIndex + 1, rightIndex);
await Future.delayed(getDuration());
streamController.add(numbers);
// 合并两个有序子数组
await merge(leftIndex, middleIndex, rightIndex);
}
}
checkAndResetIfSorted() async {
if (isSorted) {
reset();
await Future.delayed(const Duration(milliseconds: 200));
}
}
sort() async {
setState(() {
isSorting = true;
});
await checkAndResetIfSorted();
Stopwatch stopwatch = Stopwatch()..start();
switch (currentSort) {
case "bubble":
await bubbleSort();
break;
case "coctail":
await cocktailSort();
break;
case "comb":
await combSort();
break;
case "pigeonhole":
await pigeonHole();
break;
case "shell":
await shellSort();
break;
case "selection":
await selectionSort();
break;
case "cycle":
await cycleSort();
break;
case "heap":
await heapSort();
break;
case "insertion":
await insertionSort();
break;
case "gnome":
await gnomeSort();
break;
case "oddeven":
await oddEvenSort();
break;
case "quick":
await quickSort(0, sampleSize.toInt() - 1);
break;
case "merge":
await mergeSort(0, sampleSize.toInt() - 1);
break;
}
stopwatch.stop();
print("Sorting completed in ${stopwatch.elapsed.inMilliseconds} ms.");
setState(() {
isSorting = false;
isSorted = true;
});
}
setSort(String type) {
setState(() {
currentSort = type;
});
}
@override
void initState() {
super.initState();
// reset();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
sampleSize = MediaQuery.of(context).size.width / 2;
for (int i = 0; i < sampleSize; ++i) {
//随机往数组中填值
numbers.add(Random().nextInt(500));
}
setState(() {});
}
@override
void dispose() {
streamController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"当前选择的是:${getTitle()}",
style: const TextStyle(fontSize: 14),
),
actions: <Widget>[
PopupMenuButton(
initialValue: currentSort,
itemBuilder: (ctx) {
return const [
PopupMenuItem(
value: 'bubble',
child: Text("Bubble Sort — 冒泡排序"),
),
PopupMenuItem(
value: 'coctail',
child: Text("Coctail Sort — 鸡尾酒排序(双向冒泡排序)"),
),
PopupMenuItem(
value: 'comb',
child: Text("Comb Sort — 梳排序"),
),
PopupMenuItem(
value: 'pigeonhole',
child: Text("pigeonhole Sort — 鸽巢排序"),
),
PopupMenuItem(
value: 'shell',
child: Text("shell Sort — 希尔排序"),
),
PopupMenuItem(
value: 'selection',
child: Text("Selection Sort — 选择排序"),
),
PopupMenuItem(
value: 'cycle',
child: Text("CycleSort — 循环排序"),
),
PopupMenuItem(
value: 'heap',
child: Text("HeapSort — 堆排序"),
),
PopupMenuItem(
value: 'insertion',
child: Text("InsertionSort — 插入排序"),
),
PopupMenuItem(
value: 'gnome',
child: Text("GnomeSort — 地精排序 (侏儒排序)"),
),
PopupMenuItem(
value: 'oddeven',
child: Text("OddEvenSort — 奇偶排序"),
),
PopupMenuItem(
value: 'quick',
child: Text("QuickSort — 快速排序"),
),
PopupMenuItem(
value: 'merge',
child: Text("MergeSort — 归并排序"),
),
];
},
onSelected: (String value) {
reset();
setSort(value);
},
)
],
),
body: StreamBuilder<Object>(
initialData: numbers,
stream: streamController.stream,
builder: (context, snapshot) {
List<int> numbers = snapshot.data as List<int>;
int counter = 0;
return Row(
children: numbers.map((int num) {
counter++;
return CustomPaint(
painter: BarPainter(
width: MediaQuery.of(context).size.width / sampleSize,
value: num,
index: counter,
),
);
}).toList(),
);
},
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
ElevatedButton(
onPressed: isSorting
? null
: () {
reset();
setSort(currentSort);
},
child: const Text("重置")),
ElevatedButton(
onPressed: isSorting ? null : sort, child: const Text("开始排序")),
ElevatedButton(
onPressed: isSorting ? null : changeSpeed,
child: Text(
"${speed + 1}x",
style: const TextStyle(fontSize: 20),
),
),
],
),
),
);
}
}
class BarPainter extends CustomPainter {
//宽度
final double width;
//高度(数组中对应的值)
final int value;
//位置索引
final int index;
BarPainter({required this.width, required this.value, required this.index});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint();
if (value < 500 * .10) {
paint.color = Colors.blue.shade100;
} else if (value < 500 * .20) {
paint.color = Colors.blue.shade200;
} else if (value < 500 * .30) {
paint.color = Colors.blue.shade300;
} else if (value < 500 * .40) {
paint.color = Colors.blue.shade400;
} else if (value < 500 * .50) {
paint.color = Colors.blue.shade500;
} else if (value < 500 * .60) {
paint.color = Colors.blue.shade600;
} else if (value < 500 * .70) {
paint.color = Colors.blue.shade700;
} else if (value < 500 * .80) {
paint.color = Colors.blue.shade800;
} else if (value < 500 * .90) {
paint.color = Colors.blue.shade900;
} else {
paint.color = const Color(0xFF011E51);
}
paint.strokeWidth = width;
paint.strokeCap = StrokeCap.round;
canvas.drawLine(
Offset(index * width, 0),
Offset(
index * width,
value.ceilToDouble(),
),
paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

View File

@@ -1,11 +0,0 @@
import 'package:flutter/material.dart';
class UserPage extends StatelessWidget {
const UserPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body:Center(child: Text('UserPage')));
}
}