This commit is contained in:
toly
2023-09-22 09:15:11 +08:00
parent d456e3c523
commit e95c34018e
132 changed files with 8527 additions and 17 deletions

171
lib/13/go/books/main.dart Normal file
View File

@@ -0,0 +1,171 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'src/auth.dart';
import 'src/data/author.dart';
import 'src/data/book.dart';
import 'src/data/library.dart';
import 'src/screens/author_details.dart';
import 'src/screens/authors.dart';
import 'src/screens/book_details.dart';
import 'src/screens/books.dart';
import 'src/screens/scaffold.dart';
import 'src/screens/settings.dart';
import 'src/screens/sign_in.dart';
void main() => runApp(Bookstore());
/// The book store view.
class Bookstore extends StatelessWidget {
/// Creates a [Bookstore].
Bookstore({super.key});
final ValueKey<String> _scaffoldKey = const ValueKey<String>('App scaffold');
@override
Widget build(BuildContext context) => BookstoreAuthScope(
notifier: _auth,
child: MaterialApp.router(
routerConfig: _router,
),
);
final BookstoreAuth _auth = BookstoreAuth();
late final GoRouter _router = GoRouter(
routes: <GoRoute>[
GoRoute(
path: '/',
redirect: (_, __) => '/books',
),
GoRoute(
path: '/signin',
pageBuilder: (BuildContext context, GoRouterState state) =>
FadeTransitionPage(
key: state.pageKey,
child: SignInScreen(
onSignIn: (Credentials credentials) {
BookstoreAuthScope.of(context)
.signIn(credentials.username, credentials.password);
},
),
),
),
GoRoute(
path: '/books',
redirect: (_, __) => '/books/popular',
),
GoRoute(
path: '/book/:bookId',
redirect: (BuildContext context, GoRouterState state) =>
'/books/all/${state.pathParameters['bookId']}',
),
GoRoute(
path: '/books/:kind(new|all|popular)',
pageBuilder: (BuildContext context, GoRouterState state) =>
FadeTransitionPage(
key: _scaffoldKey,
child: BookstoreScaffold(
selectedTab: ScaffoldTab.books,
child: BooksScreen(state.pathParameters['kind']!),
),
),
routes: <GoRoute>[
GoRoute(
path: ':bookId',
builder: (BuildContext context, GoRouterState state) {
final String bookId = state.pathParameters['bookId']!;
final Book? selectedBook = libraryInstance.allBooks
.firstWhereOrNull((Book b) => b.id.toString() == bookId);
return BookDetailsScreen(book: selectedBook);
},
),
],
),
GoRoute(
path: '/author/:authorId',
redirect: (BuildContext context, GoRouterState state) =>
'/authors/${state.pathParameters['authorId']}',
),
GoRoute(
path: '/authors',
pageBuilder: (BuildContext context, GoRouterState state) =>
FadeTransitionPage(
key: _scaffoldKey,
child: const BookstoreScaffold(
selectedTab: ScaffoldTab.authors,
child: AuthorsScreen(),
),
),
routes: <GoRoute>[
GoRoute(
path: ':authorId',
builder: (BuildContext context, GoRouterState state) {
final int authorId = int.parse(state.pathParameters['authorId']!);
final Author? selectedAuthor = libraryInstance.allAuthors
.firstWhereOrNull((Author a) => a.id == authorId);
return AuthorDetailsScreen(author: selectedAuthor);
},
),
],
),
GoRoute(
path: '/settings',
pageBuilder: (BuildContext context, GoRouterState state) =>
FadeTransitionPage(
key: _scaffoldKey,
child: const BookstoreScaffold(
selectedTab: ScaffoldTab.settings,
child: SettingsScreen(),
),
),
),
],
redirect: _guard,
refreshListenable: _auth,
debugLogDiagnostics: true,
);
String? _guard(BuildContext context, GoRouterState state) {
final bool signedIn = _auth.signedIn;
final bool signingIn = state.matchedLocation == '/signin';
// Go to /signin if the user is not signed in
if (!signedIn && !signingIn) {
return '/signin';
}
// Go to /books if the user is signed in and tries to go to /signin.
else if (signedIn && signingIn) {
return '/books';
}
// no redirect
return null;
}
}
/// A page that fades in an out.
class FadeTransitionPage extends CustomTransitionPage<void> {
/// Creates a [FadeTransitionPage].
FadeTransitionPage({
required LocalKey super.key,
required super.child,
}) : super(
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) =>
FadeTransition(
opacity: animation.drive(_curveTween),
child: child,
));
static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn);
}

View File

@@ -0,0 +1,46 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
/// A mock authentication service.
class BookstoreAuth extends ChangeNotifier {
bool _signedIn = false;
/// Whether user has signed in.
bool get signedIn => _signedIn;
/// Signs out the current user.
Future<void> signOut() async {
await Future<void>.delayed(const Duration(milliseconds: 200));
// Sign out.
_signedIn = false;
notifyListeners();
}
/// Signs in a user.
Future<bool> signIn(String username, String password) async {
await Future<void>.delayed(const Duration(milliseconds: 200));
// Sign in. Allow any password.
_signedIn = true;
notifyListeners();
return _signedIn;
}
}
/// An inherited notifier to host [BookstoreAuth] for the subtree.
class BookstoreAuthScope extends InheritedNotifier<BookstoreAuth> {
/// Creates a [BookstoreAuthScope].
const BookstoreAuthScope({
required BookstoreAuth super.notifier,
required super.child,
super.key,
});
/// Gets the [BookstoreAuth] above the context.
static BookstoreAuth of(BuildContext context) => context
.dependOnInheritedWidgetOfExactType<BookstoreAuthScope>()!
.notifier!;
}

View File

@@ -0,0 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'data/author.dart';
export 'data/book.dart';
export 'data/library.dart';

View File

@@ -0,0 +1,23 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'book.dart';
/// Author data class.
class Author {
/// Creates an author data object.
Author({
required this.id,
required this.name,
});
/// The id of the author.
final int id;
/// The name of the author.
final String name;
/// The books of the author.
final List<Book> books = <Book>[];
}

View File

@@ -0,0 +1,32 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'author.dart';
/// Book data class.
class Book {
/// Creates a book data object.
Book({
required this.id,
required this.title,
required this.isPopular,
required this.isNew,
required this.author,
});
/// The id of the book.
final int id;
/// The title of the book.
final String title;
/// The author of the book.
final Author author;
/// Whether the book is popular.
final bool isPopular;
/// Whether the book is new.
final bool isNew;
}

View File

@@ -0,0 +1,76 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'author.dart';
import 'book.dart';
/// Library data mock.
final Library libraryInstance = Library()
..addBook(
title: 'Left Hand of Darkness',
authorName: 'Ursula K. Le Guin',
isPopular: true,
isNew: true)
..addBook(
title: 'Too Like the Lightning',
authorName: 'Ada Palmer',
isPopular: false,
isNew: true)
..addBook(
title: 'Kindred',
authorName: 'Octavia E. Butler',
isPopular: true,
isNew: false)
..addBook(
title: 'The Lathe of Heaven',
authorName: 'Ursula K. Le Guin',
isPopular: false,
isNew: false);
/// A library that contains books and authors.
class Library {
/// The books in the library.
final List<Book> allBooks = <Book>[];
/// The authors in the library.
final List<Author> allAuthors = <Author>[];
/// Adds a book into the library.
void addBook({
required String title,
required String authorName,
required bool isPopular,
required bool isNew,
}) {
final Author author = allAuthors.firstWhere(
(Author author) => author.name == authorName,
orElse: () {
final Author value = Author(id: allAuthors.length, name: authorName);
allAuthors.add(value);
return value;
},
);
final Book book = Book(
id: allBooks.length,
title: title,
isPopular: isPopular,
isNew: isNew,
author: author,
);
author.books.add(book);
allBooks.add(book);
}
/// The list of popular books in the library.
List<Book> get popularBooks => <Book>[
...allBooks.where((Book book) => book.isPopular),
];
/// The list of new books in the library.
List<Book> get newBooks => <Book>[
...allBooks.where((Book book) => book.isNew),
];
}

View File

@@ -0,0 +1,49 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../data.dart';
import '../widgets/book_list.dart';
/// The author detail screen.
class AuthorDetailsScreen extends StatelessWidget {
/// Creates an author detail screen.
const AuthorDetailsScreen({
required this.author,
super.key,
});
/// The author to be displayed.
final Author? author;
@override
Widget build(BuildContext context) {
if (author == null) {
return const Scaffold(
body: Center(
child: Text('No author found.'),
),
);
}
return Scaffold(
appBar: AppBar(
title: Text(author!.name),
),
body: Center(
child: Column(
children: <Widget>[
Expanded(
child: BookList(
books: author!.books,
onTap: (Book book) => context.go('/book/${book.id}'),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,31 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../data.dart';
import '../widgets/author_list.dart';
/// A screen that displays a list of authors.
class AuthorsScreen extends StatelessWidget {
/// Creates an [AuthorsScreen].
const AuthorsScreen({super.key});
/// The title of the screen.
static const String title = 'Authors';
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text(title),
),
body: AuthorList(
authors: libraryInstance.allAuthors,
onTap: (Author author) {
context.go('/author/${author.id}');
},
),
);
}

View File

@@ -0,0 +1,77 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/link.dart';
import '../data.dart';
import 'author_details.dart';
/// A screen to display book details.
class BookDetailsScreen extends StatelessWidget {
/// Creates a [BookDetailsScreen].
const BookDetailsScreen({
super.key,
this.book,
});
/// The book to be displayed.
final Book? book;
@override
Widget build(BuildContext context) {
if (book == null) {
return const Scaffold(
body: Center(
child: Text('No book found.'),
),
);
}
return Scaffold(
appBar: AppBar(
title: Text(book!.title),
),
body: Center(
child: Column(
children: <Widget>[
Text(
book!.title,
style: Theme.of(context).textTheme.headlineMedium,
),
Text(
book!.author.name,
style: Theme.of(context).textTheme.titleMedium,
),
TextButton(
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) =>
AuthorDetailsScreen(author: book!.author),
),
);
},
child: const Text('View author (navigator.push)'),
),
Link(
uri: Uri.parse('/author/${book!.author.id}'),
builder: (BuildContext context, FollowLink? followLink) =>
TextButton(
onPressed: followLink,
child: const Text('View author (Link)'),
),
),
TextButton(
onPressed: () {
context.push('/author/${book!.author.id}');
},
child: const Text('View author (GoRouter.push)'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,118 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../data.dart';
import '../widgets/book_list.dart';
/// A screen that displays a list of books.
class BooksScreen extends StatefulWidget {
/// Creates a [BooksScreen].
const BooksScreen(this.kind, {super.key});
/// Which tab to display.
final String kind;
@override
State<BooksScreen> createState() => _BooksScreenState();
}
class _BooksScreenState extends State<BooksScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
void didUpdateWidget(BooksScreen oldWidget) {
super.didUpdateWidget(oldWidget);
switch (widget.kind) {
case 'popular':
_tabController.index = 0;
break;
case 'new':
_tabController.index = 1;
break;
case 'all':
_tabController.index = 2;
break;
}
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Books'),
bottom: TabBar(
controller: _tabController,
onTap: _handleTabTapped,
tabs: const <Tab>[
Tab(
text: 'Popular',
icon: Icon(Icons.people),
),
Tab(
text: 'New',
icon: Icon(Icons.new_releases),
),
Tab(
text: 'All',
icon: Icon(Icons.list),
),
],
),
),
body: TabBarView(
controller: _tabController,
children: <Widget>[
BookList(
books: libraryInstance.popularBooks,
onTap: _handleBookTapped,
),
BookList(
books: libraryInstance.newBooks,
onTap: _handleBookTapped,
),
BookList(
books: libraryInstance.allBooks,
onTap: _handleBookTapped,
),
],
),
);
void _handleBookTapped(Book book) {
context.go('/book/${book.id}');
}
void _handleTabTapped(int index) {
switch (index) {
case 1:
context.go('/books/new');
break;
case 2:
context.go('/books/all');
break;
case 0:
default:
context.go('/books/popular');
break;
}
}
}

View File

@@ -0,0 +1,71 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:adaptive_navigation/adaptive_navigation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// The enum for scaffold tab.
enum ScaffoldTab {
/// The books tab.
books,
/// The authors tab.
authors,
/// The settings tab.
settings
}
/// The scaffold for the book store.
class BookstoreScaffold extends StatelessWidget {
/// Creates a [BookstoreScaffold].
const BookstoreScaffold({
required this.selectedTab,
required this.child,
super.key,
});
/// Which tab of the scaffold to display.
final ScaffoldTab selectedTab;
/// The scaffold body.
final Widget child;
@override
Widget build(BuildContext context) => Scaffold(
body: AdaptiveNavigationScaffold(
selectedIndex: selectedTab.index,
body: child,
onDestinationSelected: (int idx) {
switch (ScaffoldTab.values[idx]) {
case ScaffoldTab.books:
context.go('/books');
break;
case ScaffoldTab.authors:
context.go('/authors');
break;
case ScaffoldTab.settings:
context.go('/settings');
break;
}
},
destinations: const <AdaptiveScaffoldDestination>[
AdaptiveScaffoldDestination(
title: 'Books',
icon: Icons.book,
),
AdaptiveScaffoldDestination(
title: 'Authors',
icon: Icons.person,
),
AdaptiveScaffoldDestination(
title: 'Settings',
icon: Icons.settings,
),
],
),
);
}

View File

@@ -0,0 +1,101 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/link.dart';
import '../auth.dart';
/// The settings screen.
class SettingsScreen extends StatefulWidget {
/// Creates a [SettingsScreen].
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) => Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: const Card(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 12),
child: SettingsContent(),
),
),
),
),
),
),
);
}
/// The content of a [SettingsScreen].
class SettingsContent extends StatelessWidget {
/// Creates a [SettingsContent].
const SettingsContent({
super.key,
});
@override
Widget build(BuildContext context) => Column(
children: <Widget>[
...<Widget>[
Text(
'Settings',
style: Theme.of(context).textTheme.headlineMedium,
),
ElevatedButton(
onPressed: () {
BookstoreAuthScope.of(context).signOut();
},
child: const Text('Sign out'),
),
Link(
uri: Uri.parse('/book/0'),
builder: (BuildContext context, FollowLink? followLink) =>
TextButton(
onPressed: followLink,
child: const Text('Go directly to /book/0 (Link)'),
),
),
TextButton(
onPressed: () {
context.go('/book/0');
},
child: const Text('Go directly to /book/0 (GoRouter)'),
),
].map<Widget>((Widget w) =>
Padding(padding: const EdgeInsets.all(8), child: w)),
TextButton(
onPressed: () => showDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Alert!'),
content: const Text('The alert description goes here.'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'Cancel'),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
),
child: const Text('Show Dialog'),
)
],
);
}

View File

@@ -0,0 +1,77 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Credential data class.
class Credentials {
/// Creates a credential data object.
Credentials(this.username, this.password);
/// The username of the credentials.
final String username;
/// The password of the credentials.
final String password;
}
/// The sign-in screen.
class SignInScreen extends StatefulWidget {
/// Creates a sign-in screen.
const SignInScreen({
required this.onSignIn,
super.key,
});
/// Called when users sign in with [Credentials].
final ValueChanged<Credentials> onSignIn;
@override
State<SignInScreen> createState() => _SignInScreenState();
}
class _SignInScreenState extends State<SignInScreen> {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Card(
child: Container(
constraints: BoxConstraints.loose(const Size(600, 600)),
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Sign in',
style: Theme.of(context).textTheme.headlineMedium),
TextField(
decoration: const InputDecoration(labelText: 'Username'),
controller: _usernameController,
),
TextField(
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
controller: _passwordController,
),
Padding(
padding: const EdgeInsets.all(16),
child: TextButton(
onPressed: () async {
widget.onSignIn(Credentials(
_usernameController.value.text,
_passwordController.value.text));
},
child: const Text('Sign in'),
),
),
],
),
),
),
),
);
}

View File

@@ -0,0 +1,37 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../data.dart';
/// The author list view.
class AuthorList extends StatelessWidget {
/// Creates an [AuthorList].
const AuthorList({
required this.authors,
this.onTap,
super.key,
});
/// The list of authors to be shown.
final List<Author> authors;
/// Called when the user taps an author.
final ValueChanged<Author>? onTap;
@override
Widget build(BuildContext context) => ListView.builder(
itemCount: authors.length,
itemBuilder: (BuildContext context, int index) => ListTile(
title: Text(
authors[index].name,
),
subtitle: Text(
'${authors[index].books.length} books',
),
onTap: onTap != null ? () => onTap!(authors[index]) : null,
),
);
}

View File

@@ -0,0 +1,37 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../data.dart';
/// The book list view.
class BookList extends StatelessWidget {
/// Creates an [BookList].
const BookList({
required this.books,
this.onTap,
super.key,
});
/// The list of books to be displayed.
final List<Book> books;
/// Called when the user taps a book.
final ValueChanged<Book>? onTap;
@override
Widget build(BuildContext context) => ListView.builder(
itemCount: books.length,
itemBuilder: (BuildContext context, int index) => ListTile(
title: Text(
books[index].title,
),
subtitle: Text(
books[index].author.name,
),
onTap: onTap != null ? () => onTap!(books[index]) : null,
),
);
}