Skip to content
Snippets Groups Projects
Commit 5455f3a4 authored by Adrien BONNIN's avatar Adrien BONNIN
Browse files

:sparkles: Ajout de la nested navigation

parent 6109c962
No related branches found
No related tags found
No related merge requests found
Pipeline #79027 passed
import 'package:article_flutter_riverpod/example.dart';
import 'package:article_flutter_riverpod/screens/example_group_screen.dart';
import 'package:article_flutter_riverpod/screens/example_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'shell');
class App extends StatelessWidget {
App(
this.groups, {
Key? key,
}) : router = buildRouter(groups),
super(key: key);
final List<ExampleGroup> groups;
final GoRouter router;
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter par la pratique',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routeInformationProvider: router.routeInformationProvider,
routerDelegate: router.routerDelegate,
routeInformationParser: router.routeInformationParser,
);
}
static GoRouter buildRouter(List<ExampleGroup> groups) {
return GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: groups[0].path,
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (_, __, child) => ScaffoldWithNavBar(groups, child: child),
routes: [
for (ExampleGroup group in groups) //
GoRoute(
path: group.path,
builder: (_, __) => ExampleGroupScreen(group),
routes: [
for (Example example in group.examples) //
GoRoute(
path: example.path,
builder: (_, __) => ExampleScreen(example),
),
],
),
],
)
],
);
}
}
class ScaffoldWithNavBar extends StatelessWidget {
const ScaffoldWithNavBar(
this.groups, {
required this.child,
Key? key,
}) : super(key: key);
final List<ExampleGroup> groups;
final Widget child;
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
items: [
for (ExampleGroup group in groups) //
BottomNavigationBarItem(
icon: Icon(group.icon),
label: group.title,
),
],
currentIndex: _calculateSelectedIndex(context),
onTap: (index) => _onItemTapped(index, context),
),
);
}
int _calculateSelectedIndex(BuildContext context) {
final GoRouter route = GoRouter.of(context);
final String location = route.location;
for (var i = 0; i < groups.length; i++) {
if (location.startsWith(groups[i].path)) {
return i;
}
}
return 0;
}
void _onItemTapped(int index, BuildContext context) {
context.go(groups[index].path);
}
}
......@@ -3,10 +3,14 @@ import 'package:flutter/material.dart';
class ExampleGroup {
const ExampleGroup({
required this.title,
required this.icon,
required this.path,
required this.examples,
});
final String title;
final IconData icon;
final String path;
final List<Example> examples;
}
......@@ -25,28 +29,3 @@ class Example {
final WidgetBuilder builder;
final bool isScrollable;
}
class ExampleContainer extends StatelessWidget {
const ExampleContainer(
this.example, {
Key? key,
}) : super(key: key);
final Example example;
@override
Widget build(BuildContext context) {
final child = example.builder(context);
return Scaffold(
appBar: AppBar(
title: Text(example.title),
),
body: Center(
child: example.isScrollable
? SingleChildScrollView(child: child) //
: child,
),
);
}
}
import 'package:article_flutter_riverpod/app.dart';
import 'package:article_flutter_riverpod/article_02/consumer_example.dart';
import 'package:article_flutter_riverpod/article_02/parent_example.dart';
import 'package:article_flutter_riverpod/article_02/stateful_example.dart';
......@@ -12,12 +13,12 @@ import 'package:article_flutter_riverpod/presentation/07_overrides_example.dart'
import 'package:article_flutter_riverpod/presentation/08_future_provider_example.dart' as future_provider_example;
import 'package:article_flutter_riverpod/presentation/09_async_value_example.dart' as async_value_example;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:sticky_headers/sticky_headers.dart';
final examples = [
final _groups = [
ExampleGroup(
title: 'Article 01',
icon: Icons.filter_1,
path: '/article_01',
examples: [
Example(
title: 'Consumer',
......@@ -47,6 +48,8 @@ final examples = [
),
ExampleGroup(
title: 'Presentation',
icon: Icons.screenshot_monitor,
path: '/presentation',
examples: [
Example(
title: 'Introduction',
......@@ -109,106 +112,5 @@ final examples = [
];
void main() {
runApp(ArticleApp());
}
class ArticleApp extends StatelessWidget {
ArticleApp({Key? key}) : super(key: key);
final _router = GoRouter(routes: [
GoRoute(
path: '/',
builder: (_, __) => const HomePage(),
routes: [
for (ExampleGroup exampleTitle in examples) //
for (Example example in exampleTitle.examples) //
GoRoute(
path: example.path,
builder: (_, __) => ExampleContainer(example),
)
],
),
]);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: 'Flutter par la pratique',
theme: ThemeData(
primarySwatch: Colors.blue,
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter par la pratique'),
),
body: ListView.builder(
itemBuilder: (_, index) => ExampleGroupListItem(examples[index]),
itemCount: examples.length,
),
);
}
}
class ExampleGroupListItem extends StatelessWidget {
const ExampleGroupListItem(this.exampleGroup, {Key? key}) : super(key: key);
final ExampleGroup exampleGroup;
@override
Widget build(BuildContext context) {
return StickyHeader(
header: Container(
height: 50,
color: Colors.white,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
exampleGroup.title,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.subtitle2,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (Example example in exampleGroup.examples) //
ExampleListItem(example)
],
),
);
}
}
class ExampleListItem extends StatelessWidget {
const ExampleListItem(this.example, {Key? key}) : super(key: key);
final Example example;
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(
example.icon,
color: Theme.of(context).primaryColor,
),
title: Text(
example.title,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push('/${example.path}'),
);
}
runApp(App(_groups));
}
......@@ -52,7 +52,9 @@ class _TodoListState extends ConsumerState<TodoList> {
void _onCheckedChanged(Todo todo, bool? value) {
final updatedTodo = todo.copyWith(checked: value != null && value);
ref.read(todosProvider.notifier).state = ref.read(todosProvider.notifier).state.copyWithTodo(updatedTodo);
final todos = ref.read(todosProvider);
ref.read(todosProvider.notifier).state = todos.copyWithTodo(updatedTodo);
}
}
......
import 'package:article_flutter_riverpod/presentation/01_introduction_example.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
......@@ -160,12 +161,19 @@ class TodosStateNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
final TodosRepository todosRepository;
void load() async {
state = await AsyncValue.guard(() => todosRepository.getTodos());
final todos = await AsyncValue.guard(() => todosRepository.getTodos());
if (mounted) {
state = todos;
}
}
void updateTodo(Todo updatedTodo) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => todosRepository.updateTodos(updatedTodo));
final todos = await AsyncValue.guard(() => todosRepository.updateTodos(updatedTodo));
if (mounted) {
state = todos;
}
}
}
......
import 'package:article_flutter_riverpod/example.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class ExampleGroupScreen extends StatelessWidget {
const ExampleGroupScreen(
this.group, {
Key? key,
}) : super(key: key);
final ExampleGroup group;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(group.title),
),
body: ListView.builder(
itemBuilder: (_, index) => ExampleListItem(
group,
group.examples[index],
),
itemCount: group.examples.length,
),
);
}
}
class ExampleListItem extends StatelessWidget {
const ExampleListItem(
this.group,
this.example, {
Key? key,
}) : super(key: key);
final ExampleGroup group;
final Example example;
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(
example.icon,
color: Theme.of(context).primaryColor,
),
title: Text(
example.title,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _onItemTapped(context, example),
);
}
void _onItemTapped(BuildContext context, Example example) {
context.go("${group.path}/${example.path}");
}
}
import 'package:article_flutter_riverpod/example.dart';
import 'package:flutter/material.dart';
class ExampleScreen extends StatelessWidget {
const ExampleScreen(
this.example, {
Key? key,
}) : super(key: key);
final Example example;
@override
Widget build(BuildContext context) {
final child = example.builder(context);
return Scaffold(
appBar: AppBar(
title: Text(example.title),
),
body: Center(
child: example.isScrollable
? SingleChildScrollView(child: child) //
: child,
),
);
}
}
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TodoExample extends StatelessWidget {
const TodoExample({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const ProviderScope(
child: TodoList(),
);
}
}
class TodoList extends ConsumerStatefulWidget {
const TodoList({Key? key}) : super(key: key);
@override
ConsumerState<TodoList> createState() => _TodoListState();
}
class _TodoListState extends ConsumerState<TodoList> {
bool _isUncheckedFilter = false;
@override
Widget build(BuildContext context) {
final todos = ref.watch(todosProvider.select((tds) => tds.whereTodos(_isUncheckedFilter)));
return Column(
children: [
TodoListFilter(
filter: _isUncheckedFilter,
onFilterChanged: _onFilterChanged,
),
const Divider(height: 1),
Expanded(
child: ListView.builder(
itemBuilder: (_, index) =>
TodoListItem(
todos[index],
onCheckedChanged: (value) => _onCheckedChanged(todos[index], value),
),
itemCount: todos.length,
),
),
],
);
}
void _onFilterChanged(bool value) {
setState(() => _isUncheckedFilter = value);
}
void _onCheckedChanged(Todo todo, bool? value) {
final updatedTodo = todo.copyWith(checked: value != null && value);
final todos = ref.read(todosProvider.notifier).state;
ref.read(todosProvider.notifier).state = todos.copyWithTodo(updatedTodo);
}
}
class TodoListFilter extends StatelessWidget {
const TodoListFilter({
Key? key,
required this.filter,
required this.onFilterChanged,
}) : super(key: key);
final bool filter;
final ValueChanged<bool> onFilterChanged;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Unchecked"),
Switch(
value: filter,
onChanged: onFilterChanged,
),
],
),
);
}
}
class TodoListItem extends StatelessWidget {
const TodoListItem(this.todo, {
Key? key,
this.onCheckedChanged,
}) : super(key: key);
final Todo todo;
final ValueChanged<bool?>? onCheckedChanged;
@override
Widget build(BuildContext context) {
return CheckboxListTile(
value: todo.checked,
onChanged: onCheckedChanged,
title: Text(
todo.title,
overflow: TextOverflow.ellipsis,
),
controlAffinity: ListTileControlAffinity.leading,
);
}
}
final todosProvider = StateProvider<List<Todo>>((ref) => todos);
const todos = [
Todo(
id: 0,
title: 'Unit test passed',
checked: true,
),
Todo(
id: 1,
title: 'Code reviewed',
checked: true,
),
Todo(
id: 2,
title: 'Acceptance criteria for each issue met',
),
Todo(
id: 3,
title: 'Functional tests passed',
),
Todo(
id: 4,
title: 'Non-functional requirements met',
),
Todo(
id: 5,
title: 'Product owner accepts the User Story',
),
];
class Todo {
const Todo({
required this.id,
required this.title,
this.checked = false,
});
final int id;
final String title;
final bool checked;
Todo copyWith({
int? id,
String? title,
bool? checked,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
checked: checked ?? this.checked,
);
}
}
extension TodoListExtension on List<Todo> {
List<Todo> whereTodos(bool isUnchecked) => //
where((todo) => !isUnchecked || !todo.checked).toList();
List<Todo> copyWithTodo(Todo updatedTodo) => //
[for (Todo todo in this) todo.id == updatedTodo.id ? updatedTodo : todo];
}
```
\ No newline at end of file
......@@ -92,7 +92,7 @@ packages:
name: go_router
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.0"
version: "4.5.0"
js:
dependency: transitive
description:
......@@ -175,13 +175,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.2+1"
sticky_headers:
dependency: "direct main"
description:
name: sticky_headers
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0+2"
stream_channel:
dependency: transitive
description:
......
......@@ -35,8 +35,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
flutter_riverpod: ^2.0.0-dev.9
go_router: ^4.3.0
sticky_headers: ^0.3.0
go_router: ^4.5.0
dev_dependencies:
flutter_test:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment