Issue
I’m making a music app and want the now playing status to show at the bottom of most pages. I’ve kind of done this by using bottomNavigationBar: const NowPlayingBar()
on every scaffold that needed the bar. This has 2 issues:
- This technically makes multiple copies of the nav bar for every route that has it
- The nav bar doesn’t "stay above" page transitions (video below)
The only real way that I’ve found of doing this is with the persistent_bottom_nav_bar package, but that doesn’t seem to allow custom widgets (there’s this but NavBarStyle.custom
doesn’t seem to exist). Is there a way of constantly showing the bar on all pages?
Here’s the video showing issue 2: https://streamable.com/gxcswk
And here’s my now playing bar widget (it’s basically just a list tile that listens to changes):
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import '../components/AlbumImage.dart';
import '../services/mediaStateStream.dart';
import '../services/FinampSettingsHelper.dart';
import '../services/processArtist.dart';
import '../services/MusicPlayerBackgroundTask.dart';
class NowPlayingBar extends StatelessWidget {
const NowPlayingBar({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// BottomNavBar's default elevation is 8 (https://api.flutter.dev/flutter/material/BottomNavigationBar/elevation.html)
const elevation = 8.0;
final color = Theme.of(context).bottomNavigationBarTheme.backgroundColor;
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
return Material(
color: color,
elevation: elevation,
child: SafeArea(
child: StreamBuilder<MediaState>(
stream: mediaStateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
final playing = snapshot.data!.playbackState.playing;
// If we have a media item and the player hasn't finished, show
// the now playing bar.
if (snapshot.data!.mediaItem != null) {
return SizedBox(
width: MediaQuery.of(context).size.width,
child: Dismissible(
key: const Key("NowPlayingBar"),
confirmDismiss: (direction) async {
if (direction == DismissDirection.endToStart) {
audioHandler.skipToNext();
} else {
audioHandler.skipToPrevious();
}
return false;
},
background: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
AspectRatio(
aspectRatio: 1,
child: FittedBox(
fit: BoxFit.fitHeight,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Icon(Icons.skip_previous),
),
),
),
AspectRatio(
aspectRatio: 1,
child: FittedBox(
fit: BoxFit.fitHeight,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Icon(Icons.skip_next),
),
),
),
],
),
),
child: ListTile(
onTap: () =>
Navigator.of(context).pushNamed("/nowplaying"),
// We put the album image in a ValueListenableBuilder so that it reacts to offline changes
leading: ValueListenableBuilder(
valueListenable:
FinampSettingsHelper.finampSettingsListener,
builder: (context, _, widget) => AlbumImage(
itemId: snapshot.data!.mediaItem!.extras!["parentId"],
),
),
title: Text(
snapshot.data!.mediaItem!.title,
softWrap: false,
maxLines: 1,
overflow: TextOverflow.fade,
),
subtitle: Text(
processArtist(snapshot.data!.mediaItem!.artist),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (snapshot.data!.playbackState.processingState !=
AudioProcessingState.idle)
IconButton(
// We have a key here because otherwise the
// InkWell moves over to the play/pause button
key: const ValueKey("StopButton"),
icon: const Icon(Icons.stop),
onPressed: () => audioHandler.stop(),
),
playing
? IconButton(
icon: const Icon(Icons.pause),
onPressed: () => audioHandler.pause(),
)
: IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => audioHandler.play(),
),
],
),
),
),
);
} else {
return const SizedBox(
width: 0,
height: 0,
);
}
} else {
return const SizedBox(
width: 0,
height: 0,
);
}
},
),
),
);
}
}
Solution
What you could do is build another navigator which contains the now playing bar as such:
(This is a simplified version of this solution for your use case)
Here is a video of how it would look
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: VisiblePage(),
);
}
}
class VisiblePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
/* You could also make the body of the scaffold a stack, column,
etc. that has the the buildNavigator and the now playing bar as
its children instead of setting the NowPlayingBar as the bottom
nav bar and the buildNavigator as the body. */
return Scaffold(
backgroundColor: Colors.white,
body: _buildNavigator(context),
bottomNavigationBar: NowPlayingBar(),
);
}
Map<String, WidgetBuilder> _routeBuilders(BuildContext context, Map args) {
return {
"/": (context) {
return MainPage();
},
'/page1': (context) {
return Page1();
},
'/page2': (context) {
return Page2();
}
};
}
Widget _buildNavigator(BuildContext context) {
return Navigator(
onGenerateRoute: (settings) {
final args = settings.arguments ?? {};
var routeBuilders = _routeBuilders(context, args as Map);
return MaterialPageRoute(
fullscreenDialog: true,
settings: settings,
builder: (context) {
return routeBuilders[settings.name]!(context);
},
);
},
);
}
}
To navigate to page 1 simply use:
Navigator.of(context).pushNamed('/page1');
For page 2 (which doesn’t contain the bar) set rootNavigator to true:
Navigator.of(context, rootNavigator: true)
.push(MaterialPageRoute(builder: (context) => Page2()));
This uses the initial navigator in the "MyApp" page instead. You could also make this a named route by adding named routes to the ‘MyApp’ page.
Answered By – Shaveen
Answer Checked By – Clifford M. (FlutterFixes Volunteer)