diff --git a/lib/go_router/examples/async_redirection.dart b/lib/go_router/examples/async_redirection.dart new file mode 100644 index 0000000..f84c14e --- /dev/null +++ b/lib/go_router/examples/async_redirection.dart @@ -0,0 +1,244 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +// This scenario demonstrates how to use redirect to handle a asynchronous +// sign-in flow. +// +// The `StreamAuth` is a mock of google_sign_in. This example wraps it with an +// InheritedNotifier, StreamAuthScope, and relies on +// `dependOnInheritedWidgetOfExactType` to create a dependency between the +// notifier and go_router's parsing pipeline. When StreamAuth broadcasts new +// event, the dependency will cause the go_router to reparse the current url +// which will also trigger the redirect. + +void main() => runApp(StreamAuthScope(child: App())); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Redirection'; + + // add the login info into the tree as app state that can change over time + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + 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 createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State + 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: [ + 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: [ + 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 { + /// 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()! + .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.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 isSignedIn() async { + await Future.delayed(const Duration(seconds: 1)); + return _currentUser != null; + } + + /// A stream that notifies when current user has changed. + Stream get onCurrentUserChanged => _userStreamController.stream; + final StreamController _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 signIn(String newUserName) async { + await Future.delayed(const Duration(seconds: 3)); + _userStreamController.add(newUserName); + _timer?.cancel(); + _timer = _createRefreshTimer(); + } + + /// Signs out the current user. + Future signOut() async { + _timer?.cancel(); + _timer = null; + _userStreamController.add(null); + } +} diff --git a/lib/go_router/examples/books/main.dart b/lib/go_router/examples/books/main.dart new file mode 100644 index 0000000..eb757e3 --- /dev/null +++ b/lib/go_router/examples/books/main.dart @@ -0,0 +1,171 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'src/auth.dart'; +import 'src/data/author.dart'; +import 'src/data/book.dart'; +import 'src/data/library.dart'; +import 'src/screens/author_details.dart'; +import 'src/screens/authors.dart'; +import 'src/screens/book_details.dart'; +import 'src/screens/books.dart'; +import 'src/screens/scaffold.dart'; +import 'src/screens/settings.dart'; +import 'src/screens/sign_in.dart'; + +void main() => runApp(Bookstore()); + +/// The book store view. +class Bookstore extends StatelessWidget { + /// Creates a [Bookstore]. + Bookstore({super.key}); + + final ValueKey _scaffoldKey = const ValueKey('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( + 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( + 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( + 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 { + /// Creates a [FadeTransitionPage]. + FadeTransitionPage({ + required LocalKey super.key, + required super.child, + }) : super( + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + FadeTransition( + opacity: animation.drive(_curveTween), + child: child, + )); + + static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn); +} diff --git a/lib/go_router/examples/books/src/auth.dart b/lib/go_router/examples/books/src/auth.dart new file mode 100644 index 0000000..b9c353c --- /dev/null +++ b/lib/go_router/examples/books/src/auth.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +/// A mock authentication service. +class BookstoreAuth extends ChangeNotifier { + bool _signedIn = false; + + /// Whether user has signed in. + bool get signedIn => _signedIn; + + /// Signs out the current user. + Future signOut() async { + await Future.delayed(const Duration(milliseconds: 200)); + // Sign out. + _signedIn = false; + notifyListeners(); + } + + /// Signs in a user. + Future signIn(String username, String password) async { + await Future.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 { + /// 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()! + .notifier!; +} diff --git a/lib/go_router/examples/books/src/data.dart b/lib/go_router/examples/books/src/data.dart new file mode 100644 index 0000000..109082e --- /dev/null +++ b/lib/go_router/examples/books/src/data.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'data/author.dart'; +export 'data/book.dart'; +export 'data/library.dart'; diff --git a/lib/go_router/examples/books/src/data/author.dart b/lib/go_router/examples/books/src/data/author.dart new file mode 100644 index 0000000..b58db11 --- /dev/null +++ b/lib/go_router/examples/books/src/data/author.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'book.dart'; + +/// Author data class. +class Author { + /// Creates an author data object. + Author({ + required this.id, + required this.name, + }); + + /// The id of the author. + final int id; + + /// The name of the author. + final String name; + + /// The books of the author. + final List books = []; +} diff --git a/lib/go_router/examples/books/src/data/book.dart b/lib/go_router/examples/books/src/data/book.dart new file mode 100644 index 0000000..cd2c94f --- /dev/null +++ b/lib/go_router/examples/books/src/data/book.dart @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'author.dart'; + +/// Book data class. +class Book { + /// Creates a book data object. + Book({ + required this.id, + required this.title, + required this.isPopular, + required this.isNew, + required this.author, + }); + + /// The id of the book. + final int id; + + /// The title of the book. + final String title; + + /// The author of the book. + final Author author; + + /// Whether the book is popular. + final bool isPopular; + + /// Whether the book is new. + final bool isNew; +} diff --git a/lib/go_router/examples/books/src/data/library.dart b/lib/go_router/examples/books/src/data/library.dart new file mode 100644 index 0000000..075b825 --- /dev/null +++ b/lib/go_router/examples/books/src/data/library.dart @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'author.dart'; +import 'book.dart'; + +/// Library data mock. +final Library libraryInstance = Library() + ..addBook( + title: 'Left Hand of Darkness', + authorName: 'Ursula K. Le Guin', + isPopular: true, + isNew: true) + ..addBook( + title: 'Too Like the Lightning', + authorName: 'Ada Palmer', + isPopular: false, + isNew: true) + ..addBook( + title: 'Kindred', + authorName: 'Octavia E. Butler', + isPopular: true, + isNew: false) + ..addBook( + title: 'The Lathe of Heaven', + authorName: 'Ursula K. Le Guin', + isPopular: false, + isNew: false); + +/// A library that contains books and authors. +class Library { + /// The books in the library. + final List allBooks = []; + + /// The authors in the library. + final List allAuthors = []; + + /// 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 get popularBooks => [ + ...allBooks.where((Book book) => book.isPopular), + ]; + + /// The list of new books in the library. + List get newBooks => [ + ...allBooks.where((Book book) => book.isNew), + ]; +} diff --git a/lib/go_router/examples/books/src/screens/author_details.dart b/lib/go_router/examples/books/src/screens/author_details.dart new file mode 100644 index 0000000..3aff898 --- /dev/null +++ b/lib/go_router/examples/books/src/screens/author_details.dart @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../data.dart'; +import '../widgets/book_list.dart'; + +/// The author detail screen. +class AuthorDetailsScreen extends StatelessWidget { + /// Creates an author detail screen. + const AuthorDetailsScreen({ + required this.author, + super.key, + }); + + /// The author to be displayed. + final Author? author; + + @override + Widget build(BuildContext context) { + if (author == null) { + return const Scaffold( + body: Center( + child: Text('No author found.'), + ), + ); + } + return Scaffold( + appBar: AppBar( + title: Text(author!.name), + ), + body: Center( + child: Column( + children: [ + Expanded( + child: BookList( + books: author!.books, + onTap: (Book book) => context.go('/book/${book.id}'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/go_router/examples/books/src/screens/authors.dart b/lib/go_router/examples/books/src/screens/authors.dart new file mode 100644 index 0000000..0eeb1c3 --- /dev/null +++ b/lib/go_router/examples/books/src/screens/authors.dart @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../data.dart'; +import '../widgets/author_list.dart'; + +/// A screen that displays a list of authors. +class AuthorsScreen extends StatelessWidget { + /// Creates an [AuthorsScreen]. + const AuthorsScreen({super.key}); + + /// The title of the screen. + static const String title = 'Authors'; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text(title), + ), + body: AuthorList( + authors: libraryInstance.allAuthors, + onTap: (Author author) { + context.go('/author/${author.id}'); + }, + ), + ); +} diff --git a/lib/go_router/examples/books/src/screens/book_details.dart b/lib/go_router/examples/books/src/screens/book_details.dart new file mode 100644 index 0000000..9a51a83 --- /dev/null +++ b/lib/go_router/examples/books/src/screens/book_details.dart @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/link.dart'; + +import '../data.dart'; +import 'author_details.dart'; + +/// A screen to display book details. +class BookDetailsScreen extends StatelessWidget { + /// Creates a [BookDetailsScreen]. + const BookDetailsScreen({ + super.key, + this.book, + }); + + /// The book to be displayed. + final Book? book; + + @override + Widget build(BuildContext context) { + if (book == null) { + return const Scaffold( + body: Center( + child: Text('No book found.'), + ), + ); + } + return Scaffold( + appBar: AppBar( + title: Text(book!.title), + ), + body: Center( + child: Column( + children: [ + 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( + MaterialPageRoute( + 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)'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/go_router/examples/books/src/screens/books.dart b/lib/go_router/examples/books/src/screens/books.dart new file mode 100644 index 0000000..b0e7ca2 --- /dev/null +++ b/lib/go_router/examples/books/src/screens/books.dart @@ -0,0 +1,118 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../data.dart'; +import '../widgets/book_list.dart'; + +/// A screen that displays a list of books. +class BooksScreen extends StatefulWidget { + /// Creates a [BooksScreen]. + const BooksScreen(this.kind, {super.key}); + + /// Which tab to display. + final String kind; + + @override + State createState() => _BooksScreenState(); +} + +class _BooksScreenState extends State + 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( + 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: [ + 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; + } + } +} diff --git a/lib/go_router/examples/books/src/screens/scaffold.dart b/lib/go_router/examples/books/src/screens/scaffold.dart new file mode 100644 index 0000000..2ffaf9a --- /dev/null +++ b/lib/go_router/examples/books/src/screens/scaffold.dart @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:adaptive_navigation/adaptive_navigation.dart'; +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +/// The enum for scaffold tab. +enum ScaffoldTab { + /// The books tab. + books, + + /// The authors tab. + authors, + + /// The settings tab. + settings +} + +/// The scaffold for the book store. +class BookstoreScaffold extends StatelessWidget { + /// Creates a [BookstoreScaffold]. + const BookstoreScaffold({ + required this.selectedTab, + required this.child, + super.key, + }); + + /// Which tab of the scaffold to display. + final ScaffoldTab selectedTab; + + /// The scaffold body. + final Widget child; + + @override + Widget build(BuildContext context) => Scaffold( + body: AdaptiveNavigationScaffold( + selectedIndex: selectedTab.index, + body: child, + onDestinationSelected: (int idx) { + switch (ScaffoldTab.values[idx]) { + case ScaffoldTab.books: + context.go('/books'); + break; + case ScaffoldTab.authors: + context.go('/authors'); + break; + case ScaffoldTab.settings: + context.go('/settings'); + break; + } + }, + destinations: const [ + AdaptiveScaffoldDestination( + title: 'Books', + icon: Icons.book, + ), + AdaptiveScaffoldDestination( + title: 'Authors', + icon: Icons.person, + ), + AdaptiveScaffoldDestination( + title: 'Settings', + icon: Icons.settings, + ), + ], + ), + ); +} diff --git a/lib/go_router/examples/books/src/screens/settings.dart b/lib/go_router/examples/books/src/screens/settings.dart new file mode 100644 index 0000000..a098e30 --- /dev/null +++ b/lib/go_router/examples/books/src/screens/settings.dart @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/link.dart'; + +import '../auth.dart'; + +/// The settings screen. +class SettingsScreen extends StatefulWidget { + /// Creates a [SettingsScreen]. + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + @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: [ + ...[ + 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 w) => + Padding(padding: const EdgeInsets.all(8), child: w)), + TextButton( + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Alert!'), + content: const Text('The alert description goes here.'), + actions: [ + 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'), + ) + ], + ); +} diff --git a/lib/go_router/examples/books/src/screens/sign_in.dart b/lib/go_router/examples/books/src/screens/sign_in.dart new file mode 100644 index 0000000..e02c870 --- /dev/null +++ b/lib/go_router/examples/books/src/screens/sign_in.dart @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Credential data class. +class Credentials { + /// Creates a credential data object. + Credentials(this.username, this.password); + + /// The username of the credentials. + final String username; + + /// The password of the credentials. + final String password; +} + +/// The sign-in screen. +class SignInScreen extends StatefulWidget { + /// Creates a sign-in screen. + const SignInScreen({ + required this.onSignIn, + super.key, + }); + + /// Called when users sign in with [Credentials]. + final ValueChanged onSignIn; + + @override + State createState() => _SignInScreenState(); +} + +class _SignInScreenState extends State { + 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: [ + 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'), + ), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/lib/go_router/examples/books/src/widgets/author_list.dart b/lib/go_router/examples/books/src/widgets/author_list.dart new file mode 100644 index 0000000..371e30a --- /dev/null +++ b/lib/go_router/examples/books/src/widgets/author_list.dart @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../data.dart'; + +/// The author list view. +class AuthorList extends StatelessWidget { + /// Creates an [AuthorList]. + const AuthorList({ + required this.authors, + this.onTap, + super.key, + }); + + /// The list of authors to be shown. + final List authors; + + /// Called when the user taps an author. + final ValueChanged? 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, + ), + ); +} diff --git a/lib/go_router/examples/books/src/widgets/book_list.dart b/lib/go_router/examples/books/src/widgets/book_list.dart new file mode 100644 index 0000000..3e2761f --- /dev/null +++ b/lib/go_router/examples/books/src/widgets/book_list.dart @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../data.dart'; + +/// The book list view. +class BookList extends StatelessWidget { + /// Creates an [BookList]. + const BookList({ + required this.books, + this.onTap, + super.key, + }); + + /// The list of books to be displayed. + final List books; + + /// Called when the user taps a book. + final ValueChanged? 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, + ), + ); +} diff --git a/lib/go_router/examples/exception_handling.dart b/lib/go_router/examples/exception_handling.dart new file mode 100644 index 0000000..82c10e3 --- /dev/null +++ b/lib/go_router/examples/exception_handling.dart @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This sample app shows how to use `GoRouter.onException` to redirect on +/// exception. +/// +/// The first route '/' is mapped to [HomeScreen], and the second route +/// '/404' is mapped to [NotFoundScreen]. +/// +/// Any other unknown route or exception is redirected to `/404`. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + onException: (_, GoRouterState state, GoRouter router) { + router.go('/404', extra: state.uri.toString()); + }, + routes: [ + 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"), + ), + ); + } +} diff --git a/lib/go_router/examples/main.dart b/lib/go_router/examples/main.dart new file mode 100644 index 0000000..d159648 --- /dev/null +++ b/lib/go_router/examples/main.dart @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This sample app shows an app with two screens. +/// +/// The first route '/' is mapped to [HomeScreen], and the second route +/// '/details' is mapped to [DetailsScreen]. +/// +/// The buttons use context.go() to navigate to each destination. On mobile +/// devices, each destination is deep-linkable and on the web, can be navigated +/// to using the address bar. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + 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'), + ), + ), + ); + } +} diff --git a/lib/go_router/examples/named_routes.dart b/lib/go_router/examples/named_routes.dart new file mode 100644 index 0000000..9685fc4 --- /dev/null +++ b/lib/go_router/examples/named_routes.dart @@ -0,0 +1,181 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +// This scenario demonstrates how to navigate using named locations instead of +// URLs. +// +// Instead of hardcoding the URI locations , you can also use the named +// locations. To use this API, give a unique name to each GoRoute. The name can +// then be used in context.namedLocation to be translate back to the actual URL +// location. + +/// Family data class. +class Family { + /// Create a family. + const Family({required this.name, required this.people}); + + /// The last name of the family. + final String name; + + /// The people in the family. + final Map 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 _families = { + 'f1': Family( + name: 'Doe', + people: { + 'p1': Person(name: 'Jane', age: 23), + 'p2': Person(name: 'John', age: 6), + }, + ), + 'f2': Family( + name: 'Wong', + people: { + '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( + name: 'home', + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (BuildContext context, GoRouterState state) => + FamilyScreen(fid: state.pathParameters['fid']!), + routes: [ + 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: [ + for (final MapEntry entry in _families.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.go(context.namedLocation('family', + pathParameters: {'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 people = _families[fid]!.people; + return Scaffold( + appBar: AppBar(title: Text(_families[fid]!.name)), + body: ListView( + children: [ + for (final MapEntry entry in people.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.go(context.namedLocation( + 'person', + pathParameters: {'fid': fid, 'pid': entry.key}, + queryParameters: {'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'), + ); + } +} diff --git a/lib/go_router/examples/on_exit.dart b/lib/go_router/examples/on_exit.dart new file mode 100644 index 0000000..fba83a7 --- /dev/null +++ b/lib/go_router/examples/on_exit.dart @@ -0,0 +1,139 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This sample app demonstrates how to use GoRoute.onExit. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(); + }, + onExit: (BuildContext context) async { + final bool? confirmed = await showDialog( + context: context, + builder: (_) { + return AlertDialog( + content: const Text('Are you sure to leave this page?'), + actions: [ + 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: [ + 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: [ + 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'), + ), + ); + } +} diff --git a/lib/go_router/examples/others/custom_stateful_shell_route.dart b/lib/go_router/examples/others/custom_stateful_shell_route.dart new file mode 100644 index 0000000..5fbe2b6 --- /dev/null +++ b/lib/go_router/examples/others/custom_stateful_shell_route.dart @@ -0,0 +1,460 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _tabANavigatorKey = + GlobalKey(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: [ + 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 children) { + // Returning a customized container for the branch + // Navigators (i.e. the `List children` argument). + // + // See ScaffoldWithNavBar for more details on how the children + // are managed (using AnimatedBranchContainer). + return ScaffoldWithNavBar( + navigationShell: navigationShell, children: children); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _tabANavigatorKey, + routes: [ + 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: [ + // 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: [ + 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 children) { + // Returning a customized container for the branch + // Navigators (i.e. the `List 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(routes: [ + GoRoute( + path: '/b1', + builder: (BuildContext context, GoRouterState state) => + const TabScreen( + label: 'B1', detailsPath: '/b1/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B1', + withScaffold: false, + ), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b2', + builder: (BuildContext context, GoRouterState state) => + const TabScreen( + label: 'B2', detailsPath: '/b2/details'), + routes: [ + 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('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 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(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 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: [ + 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 createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + 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: [ + 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) ...[ + 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 children; + + @override + State createState() => _TabbedRootScreenState(); +} + +class _TabbedRootScreenState extends State + 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 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: [ + 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'), + ), + ], + ), + ); + } +} diff --git a/lib/go_router/examples/others/error_screen.dart b/lib/go_router/examples/others/error_screen.dart new file mode 100644 index 0000000..5c071fc --- /dev/null +++ b/lib/go_router/examples/others/error_screen.dart @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Custom Error Screen'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + routes: [ + GoRoute( + 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: [ + 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: [ + 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: [ + SelectableText(error.toString()), + TextButton( + onPressed: () => context.go('/'), + child: const Text('Home'), + ), + ], + ), + ), + ); +} diff --git a/lib/go_router/examples/others/extra_param.dart b/lib/go_router/examples/others/extra_param.dart new file mode 100644 index 0000000..eaef755 --- /dev/null +++ b/lib/go_router/examples/others/extra_param.dart @@ -0,0 +1,133 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// Family data class. +class Family { + /// Create a family. + const Family({required this.name, required this.people}); + + /// The last name of the family. + final String name; + + /// The people in the family. + final Map 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 _families = { + 'f1': Family( + name: 'Doe', + people: { + 'p1': Person(name: 'Jane', age: 23), + 'p2': Person(name: 'John', age: 6), + }, + ), + 'f2': Family( + name: 'Wong', + people: { + '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( + name: 'home', + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family', + builder: (BuildContext context, GoRouterState state) { + final Map params = + state.extra! as Map; + 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: [ + for (final MapEntry entry in _families.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.goNamed('family', + extra: {'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 people = _families[fid]!.people; + return Scaffold( + appBar: AppBar(title: Text(_families[fid]!.name)), + body: ListView( + children: [ + for (final Person p in people.values) + ListTile( + title: Text(p.name), + ), + ], + ), + ); + } +} diff --git a/lib/go_router/examples/others/init_loc.dart b/lib/go_router/examples/others/init_loc.dart new file mode 100644 index 0000000..4f61b00 --- /dev/null +++ b/lib/go_router/examples/others/init_loc.dart @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Initial Location'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + initialLocation: '/page3', + routes: [ + GoRoute( + 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: [ + 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: [ + 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: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} diff --git a/lib/go_router/examples/others/nav_observer.dart b/lib/go_router/examples/others/nav_observer.dart new file mode 100644 index 0000000..038d337 --- /dev/null +++ b/lib/go_router/examples/others/nav_observer.dart @@ -0,0 +1,167 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Navigator Observer'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + observers: [MyNavObserver()], + routes: [ + GoRoute( + // if there's no name, path will be used as name for observers + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + routes: [ + GoRoute( + name: 'page2', + path: 'page2/:p1', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + routes: [ + 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 route, Route? previousRoute) => + log.info('didPush: ${route.str}, previousRoute= ${previousRoute?.str}'); + + @override + void didPop(Route route, Route? previousRoute) => + log.info('didPop: ${route.str}, previousRoute= ${previousRoute?.str}'); + + @override + void didRemove(Route route, Route? previousRoute) => + log.info('didRemove: ${route.str}, previousRoute= ${previousRoute?.str}'); + + @override + void didReplace({Route? newRoute, Route? oldRoute}) => + log.info('didReplace: new= ${newRoute?.str}, old= ${oldRoute?.str}'); + + @override + void didStartUserGesture( + Route route, + Route? previousRoute, + ) => + log.info('didStartUserGesture: ${route.str}, ' + 'previousRoute= ${previousRoute?.str}'); + + @override + void didStopUserGesture() => log.info('didStopUserGesture'); +} + +extension on Route { + 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: [ + ElevatedButton( + onPressed: () => context.goNamed( + 'page2', + pathParameters: {'p1': 'pv1'}, + queryParameters: {'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: [ + ElevatedButton( + onPressed: () => context.goNamed( + 'page3', + pathParameters: {'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: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/lib/go_router/examples/others/push.dart b/lib/go_router/examples/others/push.dart new file mode 100644 index 0000000..0cea894 --- /dev/null +++ b/lib/go_router/examples/others/push.dart @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Push'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + 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: [ + 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: [ + 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)'), + ), + ), + ], + ), + ), + ); +} diff --git a/lib/go_router/examples/others/router_neglect.dart b/lib/go_router/examples/others/router_neglect.dart new file mode 100644 index 0000000..75686a1 --- /dev/null +++ b/lib/go_router/examples/others/router_neglect.dart @@ -0,0 +1,95 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Router neglect'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + // turn off history tracking in the browser for this navigation + routerNeglect: true, + routes: [ + GoRoute( + 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: [ + 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: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/lib/go_router/examples/others/state_restoration.dart b/lib/go_router/examples/others/state_restoration.dart new file mode 100644 index 0000000..93e8aca --- /dev/null +++ b/lib/go_router/examples/others/state_restoration.dart @@ -0,0 +1,102 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp( + const RootRestorationScope(restorationId: 'root', child: App()), + ); + +/// The main app. +class App extends StatefulWidget { + /// Creates an [App]. + const App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: State Restoration'; + + @override + State createState() => _AppState(); +} + +class _AppState extends State 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: [ + // 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: [ + 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: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/lib/go_router/examples/others/stateful_shell_state_restoration.dart b/lib/go_router/examples/others/stateful_shell_state_restoration.dart new file mode 100644 index 0000000..aeecd11 --- /dev/null +++ b/lib/go_router/examples/others/stateful_shell_state_restoration.dart @@ -0,0 +1,236 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(RestorableStatefulShellRouteExampleApp()); + +/// An example demonstrating how to use StatefulShellRoute with state +/// restoration. +class RestorableStatefulShellRouteExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + RestorableStatefulShellRouteExampleApp({super.key}); + + final GoRouter _router = GoRouter( + initialLocation: '/a', + restorationScopeId: 'router', + routes: [ + StatefulShellRoute.indexedStack( + restorationScopeId: 'shell1', + pageBuilder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + return MaterialPage( + restorationId: 'shellWidget1', + child: ScaffoldWithNavBar(navigationShell: navigationShell)); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ + 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( + restorationId: 'screenA', + child: + RootScreen(label: 'A', detailsPath: '/a/details')), + routes: [ + // 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( + restorationId: 'screenADetail', + child: DetailsScreen(label: 'A')), + ), + ], + ), + ], + ), + // The route branch for the second tab of the bottom navigation bar. + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ + 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( + restorationId: 'screenB', + child: + RootScreen(label: 'B', detailsPath: '/b/details')), + routes: [ + // 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( + 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('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(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: [ + 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 createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State 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: [ + 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)), + ], + ), + ); + } +} diff --git a/lib/go_router/examples/others/transitions.dart b/lib/go_router/examples/others/transitions.dart new file mode 100644 index 0000000..e4b9a3e --- /dev/null +++ b/lib/go_router/examples/others/transitions.dart @@ -0,0 +1,163 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Custom Transitions'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (_, __) => '/none', + ), + GoRoute( + path: '/fade', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'fade', + color: Colors.red, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + FadeTransition(opacity: animation, child: child), + ), + ), + GoRoute( + path: '/scale', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'scale', + color: Colors.green, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + ScaleTransition(scale: animation, child: child), + ), + ), + GoRoute( + path: '/slide', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'slide', + color: Colors.yellow, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + SlideTransition( + position: animation.drive( + Tween( + 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( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'rotation', + color: Colors.purple, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + RotationTransition(turns: animation, child: child), + ), + ), + GoRoute( + path: '/none', + pageBuilder: (BuildContext context, GoRouterState state) => + NoTransitionPage( + 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 kinds = [ + '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: [ + for (final String kind in kinds) + Padding( + padding: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () => context.go('/$kind'), + child: Text('$kind transition'), + ), + ) + ], + ), + ), + ), + ); +} diff --git a/lib/go_router/examples/path_and_query_parameters.dart b/lib/go_router/examples/path_and_query_parameters.dart new file mode 100644 index 0000000..3a8f289 --- /dev/null +++ b/lib/go_router/examples/path_and_query_parameters.dart @@ -0,0 +1,169 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +// This scenario demonstrates how to use path parameters and query parameters. +// +// The route segments that start with ':' are treated as path parameters when +// defining GoRoute[s]. The parameter values can be accessed through +// GoRouterState.pathParameters. +// +// The query parameters are automatically stored in GoRouterState.queryParameters. + +/// Family data class. +class Family { + /// Create a family. + const Family({required this.name, required this.people}); + + /// The last name of the family. + final String name; + + /// The people in the family. + final Map 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 _families = { + 'f1': Family( + name: 'Doe', + people: { + 'p1': Person(name: 'Jane', age: 23), + 'p2': Person(name: 'John', age: 6), + }, + ), + 'f2': Family( + name: 'Wong', + people: { + '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( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + 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: [ + for (final MapEntry 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 newQueries; + final List names = _families[fid]! + .people + .values + .map((Person p) => p.name) + .toList(); + names.sort(); + if (asc) { + newQueries = const {'sort': 'desc'}; + } else { + newQueries = const {'sort': 'asc'}; + } + return Scaffold( + appBar: AppBar( + title: Text(_families[fid]!.name), + actions: [ + IconButton( + onPressed: () => context.goNamed('family', + pathParameters: {'fid': fid}, + queryParameters: newQueries), + tooltip: 'sort ascending or descending', + icon: const Icon(Icons.sort), + ) + ], + ), + body: ListView( + children: [ + for (final String name in asc ? names : names.reversed) + ListTile( + title: Text(name), + ), + ], + ), + ); + } +} diff --git a/lib/go_router/examples/redirection.dart b/lib/go_router/examples/redirection.dart new file mode 100644 index 0000000..aded4b7 --- /dev/null +++ b/lib/go_router/examples/redirection.dart @@ -0,0 +1,146 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +// This scenario demonstrates how to use redirect to handle a sign-in flow. +// +// The GoRouter.redirect method is called before the app is navigate to a +// new page. You can choose to redirect to a different page by returning a +// non-null URL string. + +/// The login information. +class LoginInfo extends ChangeNotifier { + /// The username of login. + String get userName => _userName; + String _userName = ''; + + /// Whether a user has logged in. + bool get loggedIn => _userName.isNotEmpty; + + /// Logs in a user. + void login(String userName) { + _userName = userName; + notifyListeners(); + } + + /// Logs out the current user. + void logout() { + _userName = ''; + notifyListeners(); + } +} + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + final LoginInfo _loginInfo = LoginInfo(); + + /// The title of the app. + static const String title = 'GoRouter Example: Redirection'; + + // add the login info into the tree as app state that can change over time + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: _loginInfo, + child: MaterialApp.router( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ), + ); + + late final GoRouter _router = GoRouter( + routes: [ + 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().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(); + + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + actions: [ + IconButton( + onPressed: info.logout, + tooltip: 'Logout: ${info.userName}', + icon: const Icon(Icons.logout), + ) + ], + ), + body: const Center( + child: Text('HomeScreen'), + ), + ); + } +} diff --git a/lib/go_router/examples/routing_config.dart b/lib/go_router/examples/routing_config.dart new file mode 100644 index 0000000..3425520 --- /dev/null +++ b/lib/go_router/examples/routing_config.dart @@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This app shows how to dynamically add more route into routing config +void main() => runApp(const MyApp()); + +/// The main app. +class MyApp extends StatefulWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + bool isNewRouteAdded = false; + + late final ValueNotifier myConfig = + ValueNotifier(_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: [ + Text('${state.uri} does not exist'), + ElevatedButton( + onPressed: () => router.go('/'), + child: const Text('Go to home')), + ], + )), + )); + + RoutingConfig _generateRoutingConfig() { + return RoutingConfig( + routes: [ + GoRoute( + path: '/', + builder: (_, __) { + return Scaffold( + appBar: AppBar(title: const Text('Home')), + body: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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: [ + ElevatedButton( + onPressed: () => router.go('/'), + child: const Text('Go to home')), + ], + )), + ); + }, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: router, + ); + } +} diff --git a/lib/go_router/examples/shell_route.dart b/lib/go_router/examples/shell_route.dart new file mode 100644 index 0000000..70878b8 --- /dev/null +++ b/lib/go_router/examples/shell_route.dart @@ -0,0 +1,289 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _shellNavigatorKey = + GlobalKey(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: [ + /// Application shell + ShellRoute( + navigatorKey: _shellNavigatorKey, + builder: (BuildContext context, GoRouterState state, Widget child) { + return ScaffoldWithNavBar(child: child); + }, + routes: [ + /// The first screen to display in the bottom navigation bar. + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return const ScreenA(); + }, + routes: [ + // 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: [ + /// 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: [ + // 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( + 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: [ + 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: [ + 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: [ + 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, + ), + ), + ); + } +} diff --git a/lib/go_router/examples/stateful_shell_route.dart b/lib/go_router/examples/stateful_shell_route.dart new file mode 100644 index 0000000..eb0a67b --- /dev/null +++ b/lib/go_router/examples/stateful_shell_route.dart @@ -0,0 +1,324 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _sectionANavigatorKey = + GlobalKey(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: [ + 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: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _sectionANavigatorKey, + routes: [ + 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: [ + // 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: [ + 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: [ + 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: [ + 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: [ + 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('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(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: [ + 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 createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + 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: [ + 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) ...[ + const Padding(padding: EdgeInsets.all(16)), + TextButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ), + ] + ], + ), + ); + } +} diff --git a/lib/go_router/examples/transition_animations.dart b/lib/go_router/examples/transition_animations.dart new file mode 100644 index 0000000..f245719 --- /dev/null +++ b/lib/go_router/examples/transition_animations.dart @@ -0,0 +1,175 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// To use a custom transition animation, provide a `pageBuilder` with a +/// CustomTransitionPage. +/// +/// To learn more about animation in Flutter, check out the [Introduction to +/// animations](https://docs.flutter.dev/development/ui/animations) page on +/// flutter.dev. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) { + return CustomTransitionPage( + key: state.pageKey, + child: const DetailsScreen(), + transitionDuration: const Duration(milliseconds: 150), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation 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( + 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( + 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 animation, + Animation 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: [ + 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: [ + 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), + ); + } +} diff --git a/lib/go_router/v0/app.dart b/lib/go_router/v0/app.dart new file mode 100644 index 0000000..c9460c2 --- /dev/null +++ b/lib/go_router/v0/app.dart @@ -0,0 +1 @@ +export 'app/unit_app.dart'; \ No newline at end of file diff --git a/lib/go_router/v0/app/navigation/router/app_router_delegate.dart b/lib/go_router/v0/app/navigation/router/app_router_delegate.dart new file mode 100644 index 0000000..3e51873 --- /dev/null +++ b/lib/go_router/v0/app/navigation/router/app_router_delegate.dart @@ -0,0 +1,221 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import '../../../pages/color/color_detail_page.dart'; +import '../../../pages/color/color_page.dart'; +import '../../../pages/empty/empty_page.dart'; +import '../../../pages/settings/settings_page.dart'; +import '../../../pages/counter/counter_page.dart'; +import '../../../pages/sort/sort_page.dart'; +import '../transition/fade_transition_page.dart'; +import '../../../pages/color/color_add_page.dart'; + +const List kDestinationsPaths = [ + '/color', + '/counter', + '/user', + '/settings', +]; + +AppRouterDelegate router = AppRouterDelegate(); + +class AppRouterDelegate extends RouterDelegate 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> _completerMap = {}; + + Completer? completer; + + final Map> _alivePageMap = {}; + + void setPathKeepLive(String value){ + _alivePageMap[value] = _buildPageByPath(value); + path = value; + } + + final Map _pathExtraMap = {}; + + FutureOr 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 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 _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 buildColorPages(String path){ + List 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 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 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 parts = List.of(uri.pathSegments)..removeLast(); + return '/${parts.join('/')}'; + } + + @override + Future setNewRoutePath(configuration) async {} +} + + + +// class AppRouterDelegate extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { +// +// List _value = ['/']; +// +// +// List get value => _value; +// +// set value(List value){ +// _value = value; +// notifyListeners(); +// } +// +// @override +// Widget build(BuildContext context) { +// return Navigator( +// onPopPage: _onPopPage, +// pages: _value.map((e) => _pageMap[e]!).toList(), +// ); +// } +// +// final Map _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? navigatorKey = GlobalKey(); +// +// @override +// Future setNewRoutePath(String configuration) async{ +// } +// } diff --git a/lib/go_router/v0/app/navigation/router/iroute.dart b/lib/go_router/v0/app/navigation/router/iroute.dart new file mode 100644 index 0000000..ef2160d --- /dev/null +++ b/lib/go_router/v0/app/navigation/router/iroute.dart @@ -0,0 +1,105 @@ +import 'package:flutter/cupertino.dart'; + +import '../../../pages/color/color_add_page.dart'; +import '../../../pages/color/color_detail_page.dart'; +import '../../../pages/color/color_page.dart'; +import '../transition/fade_transition_page.dart'; + +class IRoute { + final String path; + final IRoutePageBuilder builder; + final List children; + + const IRoute({ + required this.path, + this.children = const [], + required this.builder, + }); + + @override + String toString() { + return 'IRoute{path: $path, children: $children}'; + } + + List 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 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 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(), + ); + }), +]; diff --git a/lib/go_router/v0/app/navigation/transition/fade_transition_page.dart b/lib/go_router/v0/app/navigation/transition/fade_transition_page.dart new file mode 100644 index 0000000..552171b --- /dev/null +++ b/lib/go_router/v0/app/navigation/transition/fade_transition_page.dart @@ -0,0 +1,53 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +class FadeTransitionPage extends Page { + final Widget child; + final Duration duration; + + const FadeTransitionPage({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 300), + }); + + @override + Route createRoute(BuildContext context) => + PageBasedFadeTransitionRoute(this); +} + +class PageBasedFadeTransitionRoute extends PageRoute { + final FadeTransitionPage _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 animation, + Animation secondaryAnimation) { + var curveTween = CurveTween(curve: Curves.easeIn); + return FadeTransition( + opacity: animation.drive(curveTween), + child: (settings as FadeTransitionPage).child, + ); + } + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) => + child; +} diff --git a/lib/go_router/v0/app/navigation/transition/no_transition_page.dart b/lib/go_router/v0/app/navigation/transition/no_transition_page.dart new file mode 100644 index 0000000..291910b --- /dev/null +++ b/lib/go_router/v0/app/navigation/transition/no_transition_page.dart @@ -0,0 +1,47 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +class NoTransitionPage extends Page { + final Widget child; + + const NoTransitionPage({ + super.key, + required this.child, + }); + + @override + Route createRoute(BuildContext context) => NoTransitionRoute(this); +} + +class NoTransitionRoute extends PageRoute { + + final NoTransitionPage _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 animation, + Animation secondaryAnimation) { + return (settings as NoTransitionPage).child; + } + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) => + child; +} diff --git a/lib/go_router/v0/app/navigation/views/app_navigation.dart b/lib/go_router/v0/app/navigation/views/app_navigation.dart new file mode 100644 index 0000000..adcab6c --- /dev/null +++ b/lib/go_router/v0/app/navigation/views/app_navigation.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import '../router/app_router_delegate.dart'; +import 'app_navigation_rail.dart'; +import 'app_top_bar.dart'; + +class AppNavigation extends StatelessWidget { + const AppNavigation({super.key}); + + @override + Widget build(BuildContext context) { + double px1 = 1/View.of(context).devicePixelRatio; + return Scaffold( + body: Row( + children: [ + const AppNavigationRail(), + Expanded( + child: Column( + children: [ + const AppTopBar(), + Divider(height: px1,), + Expanded( + child: Router( + routerDelegate: router, + backButtonDispatcher: RootBackButtonDispatcher(), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/go_router/v0/app/navigation/views/app_navigation_rail.dart b/lib/go_router/v0/app/navigation/views/app_navigation_rail.dart new file mode 100644 index 0000000..1ab9714 --- /dev/null +++ b/lib/go_router/v0/app/navigation/views/app_navigation_rail.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:iroute/components/components.dart'; +import '../router/app_router_delegate.dart'; + +class AppNavigationRail extends StatefulWidget { + const AppNavigationRail({super.key}); + + @override + State createState() => _AppNavigationRailState(); +} + +class _AppNavigationRailState extends State { + + final List 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(() {}); + } +} diff --git a/lib/go_router/v0/app/navigation/views/app_router_editor.dart b/lib/go_router/v0/app/navigation/views/app_router_editor.dart new file mode 100644 index 0000000..10d5701 --- /dev/null +++ b/lib/go_router/v0/app/navigation/views/app_router_editor.dart @@ -0,0 +1,64 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:iroute/components/toly_ui/button/hover_icon_button.dart'; +import '../router/app_router_delegate.dart'; + +class AppRouterEditor extends StatefulWidget { + final ValueChanged? onSubmit; + const AppRouterEditor({super.key, this.onSubmit}); + + @override + State createState() => _AppRouterEditorState(); +} + +class _AppRouterEditorState extends State { + + 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 + ), + ) + ], + ); + } +} diff --git a/lib/go_router/v0/app/navigation/views/app_top_bar.dart b/lib/go_router/v0/app/navigation/views/app_top_bar.dart new file mode 100644 index 0000000..1b95110 --- /dev/null +++ b/lib/go_router/v0/app/navigation/views/app_top_bar.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:iroute/components/components.dart'; +import '../router/app_router_delegate.dart'; +import 'app_router_editor.dart'; + +class AppTopBar extends StatelessWidget { + const AppTopBar({super.key}); + + @override + Widget build(BuildContext context) { + return DragToMoveWrap( + child: Container( + alignment: Alignment.center, + height: 46, + child: Row( + children: [ + const SizedBox(width: 16), + const RouterIndicator(), + Expanded( + child: Row(children: [ + const Spacer(), + SizedBox( + width: 250, + child: AppRouterEditor( + onSubmit: (path) => router.path = path, + )), + const Padding( + padding: EdgeInsets.symmetric(vertical: 12.0), + child: VerticalDivider( + width: 32, + ), + ) + ])), + const WindowButtons() + ], + ), + ), + ); + } +} + +class RouterIndicator extends StatefulWidget { + const RouterIndicator({super.key}); + + @override + State createState() => _RouterIndicatorState(); +} + +Map kRouteLabelMap = { + '/color': '颜色板', + '/color/add': '添加颜色', + '/color/detail': '颜色详情', + '/counter': '计数器', + '/user': '我的', + '/settings': '系统设置', +}; + +class _RouterIndicatorState extends State { + @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 pathToBreadcrumbItems(String path) { + Uri uri = Uri.parse(path); + List 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; + } +} diff --git a/lib/go_router/v0/app/unit_app.dart b/lib/go_router/v0/app/unit_app.dart new file mode 100644 index 0000000..60d28f8 --- /dev/null +++ b/lib/go_router/v0/app/unit_app.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'navigation/router/app_router_delegate.dart'; +import 'navigation/views/app_navigation.dart'; +import 'navigation/views/app_navigation_rail.dart'; + +class UnitApp extends StatelessWidget { + const UnitApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + fontFamily: "宋体", + scaffoldBackgroundColor: Colors.white, + appBarTheme: const AppBarTheme( + elevation: 0, + iconTheme: IconThemeData(color: Colors.black), + titleTextStyle: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ))), + debugShowCheckedModeBanner: false, + home: AppNavigation() + ); + } +} + + diff --git a/lib/go_router/v0/pages/color/color_add_page.dart b/lib/go_router/v0/pages/color/color_add_page.dart new file mode 100644 index 0000000..c6ca0cd --- /dev/null +++ b/lib/go_router/v0/pages/color/color_add_page.dart @@ -0,0 +1,102 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; + +class ColorAddPage extends StatefulWidget { + const ColorAddPage({super.key}); + + @override + State createState() => _ColorAddPageState(); +} + +class _ColorAddPageState extends State { + 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(() { + + }); + } +} diff --git a/lib/go_router/v0/pages/color/color_detail_page.dart b/lib/go_router/v0/pages/color/color_detail_page.dart new file mode 100644 index 0000000..17fcd17 --- /dev/null +++ b/lib/go_router/v0/pages/color/color_detail_page.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class ColorDetailPage extends StatelessWidget { + final Color color; + const ColorDetailPage({super.key, required this.color}); + + @override + Widget build(BuildContext context) { + + const TextStyle style = TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.white + ); + String text = '# ${color.value.toRadixString(16)}'; + return Scaffold( + // appBar: AppBar( + // systemOverlayStyle: SystemUiOverlayStyle( + // statusBarColor: Colors.transparent, + // statusBarIconBrightness: Brightness.light + // ), + // iconTheme: IconThemeData(color: Colors.white), + // titleTextStyle:TextStyle(color: Colors.white,fontSize: 18) , + // backgroundColor: color, + // title: Text('颜色界面',),), + body: Container( + alignment: Alignment.center, + color: color, + child: Text(text ,style: style,), + ), + ); + } +} diff --git a/lib/go_router/v0/pages/color/color_page.dart b/lib/go_router/v0/pages/color/color_page.dart new file mode 100644 index 0000000..6764129 --- /dev/null +++ b/lib/go_router/v0/pages/color/color_page.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:iroute/components/project/colors_panel.dart'; +import '../../app/navigation/router/app_router_delegate.dart'; + +class ColorPage extends StatefulWidget { + const ColorPage({super.key}); + + @override + State createState() => _ColorPageState(); +} + +class _ColorPageState extends State { + final List _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); + }); + } + } +} \ No newline at end of file diff --git a/lib/go_router/v0/pages/counter/counter_page.dart b/lib/go_router/v0/pages/counter/counter_page.dart new file mode 100644 index 0000000..b5b2e17 --- /dev/null +++ b/lib/go_router/v0/pages/counter/counter_page.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class CounterPage extends StatefulWidget { + const CounterPage({super.key}); + + @override + State createState() => _CounterPageState(); +} + +class _CounterPageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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), + ), + ); + } +} \ No newline at end of file diff --git a/lib/go_router/v0/pages/empty/empty_page.dart b/lib/go_router/v0/pages/empty/empty_page.dart new file mode 100644 index 0000000..b05f56f --- /dev/null +++ b/lib/go_router/v0/pages/empty/empty_page.dart @@ -0,0 +1,30 @@ +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), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/go_router/v0/pages/settings/settings_page.dart b/lib/go_router/v0/pages/settings/settings_page.dart new file mode 100644 index 0000000..0b53503 --- /dev/null +++ b/lib/go_router/v0/pages/settings/settings_page.dart @@ -0,0 +1,11 @@ +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'))); + } +} diff --git a/lib/go_router/v0/pages/sort/sort_page.dart b/lib/go_router/v0/pages/sort/sort_page.dart new file mode 100644 index 0000000..8172cc0 --- /dev/null +++ b/lib/go_router/v0/pages/sort/sort_page.dart @@ -0,0 +1,859 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class SortPage extends StatefulWidget { + const SortPage({Key? key}) : super(key: key); + + @override + State createState() => _SortPageState(); +} + +class _SortPageState extends State { + //存放随机数组 + List numbers = []; + + //订阅流 + StreamController> 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 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 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 _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 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: [ + 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( + initialData: numbers, + stream: streamController.stream, + builder: (context, snapshot) { + List numbers = snapshot.data as List; + 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: [ + 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; + } +} diff --git a/lib/go_router/v0/pages/user/user_page.dart b/lib/go_router/v0/pages/user/user_page.dart new file mode 100644 index 0000000..aba9710 --- /dev/null +++ b/lib/go_router/v0/pages/user/user_page.dart @@ -0,0 +1,11 @@ +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'))); + } +} diff --git a/lib/v5/app/navigation/router/iroute.dart b/lib/v5/app/navigation/router/iroute.dart index b703af9..ef2160d 100644 --- a/lib/v5/app/navigation/router/iroute.dart +++ b/lib/v5/app/navigation/router/iroute.dart @@ -1,37 +1,105 @@ +import 'package:flutter/cupertino.dart'; + +import '../../../pages/color/color_add_page.dart'; +import '../../../pages/color/color_detail_page.dart'; +import '../../../pages/color/color_page.dart'; +import '../transition/fade_transition_page.dart'; + class IRoute { final String path; + final IRoutePageBuilder builder; final List children; - const IRoute({required this.path, this.children = const []}); + const IRoute({ + required this.path, + this.children = const [], + required this.builder, + }); @override String toString() { return 'IRoute{path: $path, children: $children}'; } - List list(){ - + List list() { return []; } - } +typedef IRoutePageBuilder = Page? Function( + BuildContext context, IRouteData data); -const List kDestinationsIRoutes = [ +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 kDestinationsIRoutes = [ IRoute( path: '/color', + builder: (ctx, data) { + return const FadeTransitionPage( + key: ValueKey('/color'), + child: ColorPage(), + ); + }, children: [ - IRoute(path: '/color/add'), - IRoute(path: '/color/detail'), + IRoute( + path: '/color/detail', + builder: (ctx, data) { + final Map 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', - ), + path: '/counter', + builder: (ctx, data) { + return const FadeTransitionPage( + key: ValueKey('/counter'), + child: ColorAddPage(), + ); + }), IRoute( - path: '/user', - ), + path: '/user', + builder: (ctx, data) { + return const FadeTransitionPage( + key: ValueKey('/user'), + child: ColorAddPage(), + ); + }), IRoute( - path: '/settings', - ), + path: '/settings', + builder: (ctx, data) { + return const FadeTransitionPage( + key: ValueKey('/settings'), + child: ColorAddPage(), + ); + }), ]; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ea24344..e180b20 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,11 +6,13 @@ import FlutterMacOS import Foundation import screen_retriever +import shared_preferences_foundation import url_launcher_macos import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index c1a95d4..8cbe93b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,14 +65,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" - equatable: - dependency: "direct main" - description: - name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.5" fake_async: dependency: transitive description: @@ -81,6 +73,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.0" flutter: dependency: "direct main" description: flutter @@ -116,10 +124,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "2aa884667eeda3a1c461f31e72af1f77984ab0f29450d8fb12ec1f7bc53eea14" + sha256: a206cc4621a644531a2e05e7774616ab4d9d85eab1f3b0e255f3102937fccab1 url: "https://pub.flutter-io.cn" source: hosted - version: "10.1.0" + version: "12.0.0" lints: dependency: transitive description: @@ -129,7 +137,7 @@ packages: source: hosted version: "2.1.1" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" @@ -176,14 +184,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.8.3" - path_to_regexp: - dependency: "direct main" + path_provider_linux: + dependency: transitive description: - name: path_to_regexp - sha256: "169d78fbd55e61ea8873bcca545979f559d22238f66facdd7ef30870c7f53327" + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.0" + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" plugin_platform_interface: dependency: transitive description: @@ -200,14 +232,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.0.5" - quiver: - dependency: "direct main" - description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.2.1" screen_retriever: dependency: transitive description: @@ -216,6 +240,62 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.1.9" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" sky_engine: dependency: transitive description: flutter @@ -269,13 +349,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.6.0" - toly_menu: - dependency: "direct main" - description: - path: "E:\\Projects\\Flutter\\packages\\toly_menu" - relative: false - source: path - version: "0.0.1" url_launcher: dependency: "direct main" description: @@ -340,14 +413,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.8" - url_strategy: - dependency: "direct main" - description: - name: url_strategy - sha256: "42b68b42a9864c4d710401add17ad06e28f1c1d5500c93b98c431f6b0ea4ab87" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.0" vector_math: dependency: transitive description: @@ -364,6 +429,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4-beta" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.9" window_manager: dependency: "direct main" description: @@ -372,6 +445,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.3.7" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.3" sdks: - dart: ">=3.1.1 <4.0.0" + dart: ">=3.1.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index d4f622c..897e528 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,23 +28,21 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + flutter: sdk: flutter flutter_colorpicker: ^1.0.3 window_manager: ^0.3.7 - adaptive_navigation: ^0.0.4 - go_router: ^10.1.0 - provider: 6.0.5 - url_launcher: ^6.0.7 - equatable: ^2.0.5 - url_strategy: ^0.2.0 - quiver: ^3.1.0 - path_to_regexp: ^0.4.0 + go_router: ^12.0.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - toly_menu: - path: E:\Projects\Flutter\packages\toly_menu + + logging: ^1.0.0 + provider: 6.0.5 + shared_preferences: ^2.0.11 + url_launcher: ^6.0.7 + adaptive_navigation: ^0.0.4 dev_dependencies: flutter_test: diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 10bc521..1adc377 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -28,7 +28,7 @@ bool FlutterWindow::OnCreate() { SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { -// this->Show(); + this->Show(); //delete this->Show() });