一般来说,go_router用于从一个页面导航到另一个页面.我们导航的页面将显示在前一页的前面.但是如何在showModalBottomSheet中实现这一点呢?

我有一个模式,只显示半个屏幕.然后,当我单击导航到新页面的按钮时,该页面显示在模式的前面.但我想要的是新页面仍然显示内半模式.

推荐答案

// 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:math' as math;

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:english_words/english_words.dart';

void main() {
  runApp(MusicAppDemo());
}

class MusicAppDemo extends StatelessWidget {
  MusicAppDemo({super.key});

  final MusicDatabase database = MusicDatabase.mock();

  final GoRouter _router = GoRouter(
    initialLocation: '/library',
    routes: <RouteBase>[
      ShellRoute(
        builder: (BuildContext context, GoRouterState state, Widget child) {
          return MusicAppShell(
            currentIndex: switch (state.uri.path) {
              var p when p.startsWith('/recents') => 1,
              var p when p.startsWith('/search') => 2,
              _ => 0,
            },
            child: child,
          );
        },
        routes: <RouteBase>[
          GoRoute(
            path: '/library',
            pageBuilder: (context, state) {
              return FadeTransitionPage(
                child: const LibraryScreen(),
                key: state.pageKey,
              );
            },
            routes: <RouteBase>[
              GoRoute(
                path: 'album/:albumId',
                builder: (BuildContext context, GoRouterState state) {
                  return AlbumScreen(
                    albumId: state.pathParameters['albumId'],
                  );
                },
                routes: [
                  GoRoute(
                    path: 'song/:songId',
                    // Display on the root Navigator
                    builder: (BuildContext context, GoRouterState state) {
                      return SongScreen(
                        songId: state.pathParameters['songId']!,
                      );
                    },
                  ),
                ],
              ),
            ],
          ),
          GoRoute(
            path: '/recents',
            pageBuilder: (context, state) {
              return FadeTransitionPage(
                child: const RecentlyPlayedScreen(),
                key: state.pageKey,
              );
            },
            routes: <RouteBase>[
              GoRoute(
                path: 'song/:songId',
                // Display on the root Navigator
                builder: (BuildContext context, GoRouterState state) {
                  return SongScreen(
                    songId: state.pathParameters['songId']!,
                  );
                },
              ),
            ],
          ),
          GoRoute(
            path: '/search',
            pageBuilder: (context, state) {
              final query = state.uri.queryParameters['q'] ?? '';
              return FadeTransitionPage(
                child: SearchScreen(
                  query: query,
                ),
                key: state.pageKey,
              );
            },
          ),
        ],
      ),
      /// This one is outsise ShellRoute so it won't share any state
      GoRoute(
        path: '/settings',
        pageBuilder: (context, state) {
          return FadeTransitionPage(
            child: const SettingsScreen(),
            key: state.pageKey,
          );
        },
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Music app',
      theme: ThemeData(
        colorSchemeSeed: Colors.pink,
        useMaterial3: true,
      ),
      routerConfig: _router,
      builder: (context, child) {
        return MusicDatabaseScope(
          state: database,
          child: child!,
        );
      },
    );
  }
}

class MusicAppShell extends StatelessWidget {
  final Widget child;
  final int currentIndex;

  const MusicAppShell({
    super.key,
    required this.child,
    required this.currentIndex,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      /// This button only purpose is open a bottomsheet so you can select and see that works the same as the NavigationBar, but with this your bottomSheet won't be disposed or closed
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          await showModalBottomSheet(
            context: context,
            isScrollControlled: true,
            useRootNavigator: true,
            enableDrag: true,
            builder: (context) {
              return DraggableScrollableSheet(
                maxChildSize: 0.5,
                expand: false,
                initialChildSize: 0.25,
                builder: (context, scrollController) {
                  final theme = Theme.of(context);
                  return Material(
                    color: theme.scaffoldBackgroundColor,
                    shape: theme.bottomSheetTheme.shape,
                    clipBehavior: Clip.hardEdge,
                    child: ListView(
                      children: [
                        ListTile(
                          leading: const Icon(Icons.my_library_music_rounded),
                          title: const Text('Library'),
                          onTap: () => _onItemTapped(0, context),
                        ),
                        ListTile(
                          leading: const Icon(Icons.timelapse),
                          title: const Text('Recently Played'),
                          onTap: () => _onItemTapped(1, context),
                        ),
                        ListTile(
                          leading: const Icon(Icons.search),
                          title: const Text('Search'),
                          onTap: () => _onItemTapped(2, context),
                        ),
                        ListTile(
                          leading: const Icon(Icons.settings),
                          title: const Text('Settings'),
                          onTap: () => _onItemTapped(3, context),
                        ),
                      ],
                    ),
                  );
                },
              );
            },
          );
        },
        child: const Icon(Icons.more),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.my_library_music_rounded),
            label: 'Library',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.timelapse),
            label: 'Recently Played',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
        ],
        currentIndex: currentIndex,
        onTap: (int idx) => _onItemTapped(idx, context),
      ),
    );
  }

  void _onItemTapped(int index, BuildContext context) {
    switch (index) {
      case 1:
        GoRouter.of(context).go('/recents');
        break;
      case 2:
        GoRouter.of(context).go('/search');
        break;
      case 3:
        GoRouter.of(context).go('/settings');
        break;
      case 0:
      default:
        GoRouter.of(context).go('/library');
        break;
    }
  }
}


/// Part of Dartpad's example but can be ignored
class LibraryScreen extends StatelessWidget {
  const LibraryScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Library'),
      ),
      body: ListView.builder(
        itemBuilder: (context, albumId) {
          final album = database.albums[albumId];
          return AlbumTile(
            album: album,
            onTap: () {
              GoRouter.of(context).go('/library/album/$albumId');
            },
          );
        },
        itemCount: database.albums.length,
      ),
    );
  }
}

class RecentlyPlayedScreen extends StatelessWidget {
  const RecentlyPlayedScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final songs = database.recentlyPlayed;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Recently Played'),
      ),
      body: ListView.builder(
        itemBuilder: (context, index) {
          final song = songs[index];
          final albumIdInt = int.tryParse(song.albumId)!;
          final album = database.albums[albumIdInt];
          return SongTile(
            album: album,
            song: song,
            onTap: () {
              GoRouter.of(context).go('/recents/song/${song.fullId}');
            },
          );
        },
        itemCount: songs.length,
      ),
    );
  }
}

class SearchScreen extends StatefulWidget {
  final String query;

  const SearchScreen({super.key, required this.query});

  @override
  State<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  String? _currentQuery;

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final songs = database.search(widget.query);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Search'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(12.0),
            child: TextField(
              decoration: const InputDecoration(
                hintText: 'Search...',
                border: OutlineInputBorder(),
              ),
              onChanged: (String? newSearch) {
                _currentQuery = newSearch;
              },
              onEditingComplete: () {
                GoRouter.of(context).go(
                  '/search?q=$_currentQuery',
                );
              },
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemBuilder: (context, index) {
                final song = songs[index];
                return SongTile(
                  album: database.albums[int.tryParse(song.albumId)!],
                  song: song,
                  onTap: () {
                    GoRouter.of(context).go(
                        '/library/album/${song.albumId}/song/${song.fullId}');
                  },
                );
              },
              itemCount: songs.length,
            ),
          ),
        ],
      ),
    );
  }
}

class AlbumScreen extends StatelessWidget {
  final String? albumId;

  const AlbumScreen({
    required this.albumId,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final albumIdInt = int.tryParse(albumId ?? '');
    final album = database.albums[albumIdInt!];
    return Scaffold(
      appBar: AppBar(
        title: Text('Album - ${album.title}'),
      ),
      body: Center(
        child: Column(
          children: [
            Row(
              children: [
                SizedBox(
                  width: 200,
                  height: 200,
                  child: Container(
                    color: album.color,
                    margin: const EdgeInsets.all(8),
                  ),
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      album.title,
                      style: Theme.of(context).textTheme.headlineMedium,
                    ),
                    Text(
                      album.artist,
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                  ],
                ),
              ],
            ),
            Expanded(
              child: ListView.builder(
                itemBuilder: (context, index) {
                  final song = album.songs[index];
                  return ListTile(
                    title: Text(song.title),
                    leading: SizedBox(
                      width: 50,
                      height: 50,
                      child: Container(
                        color: album.color,
                        margin: const EdgeInsets.all(8),
                      ),
                    ),
                    trailing: SongDuration(
                      duration: song.duration,
                    ),
                    onTap: () {
                      GoRouter.of(context)
                          .go('/library/album/$albumId/song/${song.fullId}');
                    },
                  );
                },
                itemCount: album.songs.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class SongScreen extends StatelessWidget {
  final String songId;

  const SongScreen({
    super.key,
    required this.songId,
  });

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final song = database.getSongById(songId);
    final albumIdInt = int.tryParse(song.albumId);
    final album = database.albums[albumIdInt!];

    return Scaffold(
      appBar: AppBar(
        title: Text('Song - ${song.title}'),
      ),
      body: Column(
        children: [
          Row(
            children: [
              SizedBox(
                width: 300,
                height: 300,
                child: Container(
                  color: album.color,
                  margin: const EdgeInsets.all(8),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      song.title,
                      style: Theme.of(context).textTheme.displayMedium,
                    ),
                    Text(
                      album.title,
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                  ],
                ),
              )
            ],
          )
        ],
      ),
    );
  }
}

class MusicDatabase {
  final List<Album> albums;
  final List<Song> recentlyPlayed;
  final Map<String, Song> _allSongs = {};

  MusicDatabase(this.albums, this.recentlyPlayed) {
    _populateAllSongs();
  }

  factory MusicDatabase.mock() {
    final albums = _mockAlbums().toList();
    final recentlyPlayed = _mockRecentlyPlayed(albums).toList();
    return MusicDatabase(albums, recentlyPlayed);
  }

  Song getSongById(String songId) {
    if (_allSongs.containsKey(songId)) {
      return _allSongs[songId]!;
    }
    throw ('No song with ID $songId found.');
  }

  List<Song> search(String searchString) {
    final songs = <Song>[];
    for (var song in _allSongs.values) {
      final album = albums[int.tryParse(song.albumId)!];
      if (song.title.contains(searchString) ||
          album.title.contains(searchString)) {
        songs.add(song);
      }
    }
    return songs;
  }

  void _populateAllSongs() {
    for (var album in albums) {
      for (var song in album.songs) {
        _allSongs[song.fullId] = song;
      }
    }
  }

  static MusicDatabase of(BuildContext context) {
    final routeStateScope =
        context.dependOnInheritedWidgetOfExactType<MusicDatabaseScope>();
    if (routeStateScope == null) throw ('No RouteState in scope!');
    return routeStateScope.state;
  }

  static Iterable<Album> _mockAlbums() sync* {
    for (var i = 0; i < Colors.primaries.length; i++) {
      final color = Colors.primaries[i];
      final title = WordPair.random().toString();
      final artist = WordPair.random().toString();
      final songs = <Song>[];
      for (var j = 0; j < 12; j++) {
        final minutes = math.Random().nextInt(3) + 3;
        final seconds = math.Random().nextInt(60);
        final title = WordPair.random();
        final duration = Duration(minutes: minutes, seconds: seconds);
        final song = Song('$j', '$i', '$title', duration);

        songs.add(song);
      }
      yield Album('$i', title, artist, color, songs);
    }
  }

  static Iterable<Song> _mockRecentlyPlayed(List<Album> albums) sync* {
    for (var album in albums) {
      final songIndex = math.Random().nextInt(album.songs.length);
      yield album.songs[songIndex];
    }
  }
}

class MusicDatabaseScope extends InheritedWidget {
  final MusicDatabase state;

  const MusicDatabaseScope({
    required this.state,
    required super.child,
    super.key,
  });

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return oldWidget is MusicDatabaseScope && state != oldWidget.state;
  }
}

class Album {
  final String id;
  final String title;
  final String artist;
  final Color color;
  final List<Song> songs;

  Album(this.id, this.title, this.artist, this.color, this.songs);
}

class Song {
  final String id;
  final String albumId;
  final String title;
  final Duration duration;

  Song(this.id, this.albumId, this.title, this.duration);

  String get fullId => '$albumId-$id';
}

class AlbumTile extends StatelessWidget {
  final Album album;
  final VoidCallback? onTap;

  const AlbumTile({super.key, required this.album, this.onTap});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: SizedBox(
        width: 50,
        height: 50,
        child: Container(
          color: album.color,
        ),
      ),
      title: Text(album.title),
      subtitle: Text(album.artist),
      onTap: onTap,
    );
  }
}

class SongTile extends StatelessWidget {
  final Album album;
  final Song song;
  final VoidCallback? onTap;

  const SongTile(
      {super.key, required this.album, required this.song, this.onTap});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: SizedBox(
        width: 50,
        height: 50,
        child: Container(
          color: album.color,
          margin: const EdgeInsets.all(8),
        ),
      ),
      title: Text(song.title),
      trailing: SongDuration(
        duration: song.duration,
      ),
      onTap: onTap,
    );
  }
}

class SongDuration extends StatelessWidget {
  final Duration duration;

  const SongDuration({
    required this.duration,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Text(
        '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}');
  }
}

class SettingsScreen extends StatelessWidget {

  const SettingsScreen({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green,
      body: Center(
        child: IconButton(
          icon: const Icon(Icons.home),
          onPressed: () => GoRouter.of(context).go('/library'),
        ),
      ),
    );
  }
}

/// A page that fades in an out.
class FadeTransitionPage extends CustomTransitionPage<void> {
  /// Creates a [FadeTransitionPage].
  FadeTransitionPage({
    required 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);
}

此代码摘自GoRoad DartPad示例,但经过修改,以便您可以看到它是如何工作的

Gist

本例直观地展示了如何使用带有BottomNavigationBar的Shellroute,但我添加了一个打开底板的工厂作为示例,在那里您可以 Select 相同的选项,背景小部件将更改,除了最后一个"设置"之外,我在shell 路径外创建它,以便您可以看到,使用外部的路由将更改整个小部件(处理底板)

enter image description here

Flutter相关问答推荐

相同的Flutter 代码库,但Firebase登录适用于Web应用程序,但无法登录iOS应用程序

Flutter -如何将导航轨道标签靠左对齐?

就像Flutter 中的头文件一样?

在Flutter 中每次点击按钮时都很难调用自定义函数

升级到Ffltter 3.16后,应用程序栏、背景 colored颜色 、按钮大小和间距都会发生变化

Flutter 块消费者仅对特定的状态属性更改做出react

Ffmpeg_kit_fltter:^6.0.3版权问题

无法在仿真器上运行Flutter 项目-文件名太长错误

在flutter中实现类似3D的可滚动堆叠分页列表

如何使 dart 函数变得通用?

如果有空间则居中CustomScrollView

Flutter ListView 在调用 setState 后未更新

Flutter如何在容器外创建环形进度条

Firestore 使用 Flutter 以奇怪的格式保存数据

在 Flutter 中,SpinEdit 叫什么?

如何从sibling ChildWidgetB 触发 ChildWidgetA 中的操作

从子类调用超类的超方法

配置项目:firebase_core时出现问题

构建失败 - 无法解析 io.grpc:grpc-core:[1.28.0]. (是的,我已经升级到 mavenCentral())

检索 api 的值时,我得到_TypeError(类型'Null'不是'String'类型的子类型)