I'm new to Flutter development and have been following the tutorials and codelabs recommended on flutter.dev to get started. As a first exercise, I decided to recreate the Spotify Desktop welcome page, aiming for the most accurate representation possible in terms of responsiveness and element sizes. I believe this is an excellent way to truly learn a framework in depth.
Currently, I am focusing on the bottom bar of the app. However, I'm encountering difficulties in achieving the desired behavior of the bottom bar when resizing the app window. To better illustrate the issue, I've created a short gif demonstrating the expected behavior (real record of the Spotify Linux Desktop app): .
Specifically, I am struggling to resize the slider of the player accurately so that it matches the minimum and maximum sizes found in the Spotify app. When I shrink the app window, the slider does not stop resizing when it reaches the minimum size I have set. I expect the widget to overflow with a warning when I shrink the window size below the minimum widget size.
Here is the code for my current bottom bar implementation. Please note that not everything is set up correctly yet, as it is still a work in progress and I have mainly focused on the second code snippet below with the slider widget only.
import 'package:flutter/material.dart';
import 'dart:developer' as developer;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Spotify Clone',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: AppBottomBar(),
);
}
}
class AppBottomBar extends StatefulWidget {
@override
_AppBottomBarState createState() => _AppBottomBarState();
}
class _AppBottomBarState extends State<AppBottomBar> {
@override
Widget build(BuildContext context) {
return Material(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// This correctly centers the middle element.
// However, when resizing, both left and right elements are considered to be equal in width
// and the some overlapping appears before for the right element.
// Also, in the Spotify app, the middle element is supposed to be the one being expanded, no the other way around.
children: [
Expanded(child: BottomBarSongOverview()),
MusicController(),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AppController(),
],
)),
],
),
);
}
}
String formatDuration(int totalSeconds) {
final duration = Duration(seconds: totalSeconds);
final hours = duration.inHours;
var minutes = duration.inMinutes.remainder(60).toString();
final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
if (hours > 0) {
minutes = minutes.padLeft(2, '0');
return '$hours:$minutes:$seconds';
} else {
return '$minutes:$seconds';
}
}
class MusicController extends StatefulWidget {
@override
_MusicControllerState createState() => _MusicControllerState();
}
class _MusicControllerState extends State<MusicController> {
double duration = 340;
var currentTime = 0.0;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
constraints: const BoxConstraints(
minWidth: 300,
maxWidth: 700,
),
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => {developer.log('Enable shuffle')},
icon: const Icon(
Icons.shuffle,
semanticLabel: "Enable shuffle",
)),
IconButton(
onPressed: () => {developer.log('Previous')},
icon: const Icon(
Icons.skip_previous,
semanticLabel: "Previous",
)),
IconButton(
onPressed: () => {developer.log('Play')},
icon: const Icon(Icons.play_arrow, semanticLabel: "Play")),
IconButton(
onPressed: () => {developer.log('Next')},
icon: const Icon(Icons.skip_next, semanticLabel: "Next")),
IconButton(
onPressed: () => {developer.log('Enable repeat')},
icon: const Icon(Icons.repeat, semanticLabel: "Repeat")),
],
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(formatDuration(currentTime.ceil())),
Expanded(
child: Slider.adaptive(
value: currentTime,
min: 0.0,
max: duration,
onChanged: (value) {
setState(() {
currentTime = value;
});
},
),
),
Text(formatDuration(duration.ceil())),
],
)
],
),
),
);
}
}
class BottomBarSongOverview extends StatefulWidget {
@override
State<BottomBarSongOverview> createState() => _BottomBarSongOverviewState();
}
class _BottomBarSongOverviewState extends State<BottomBarSongOverview> {
var songName = "Please help me understand Flutter";
var artistName = "MetaHG";
var isFavorite = false;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Image.network(
'https://picsum.photos/200',
width: 50,
height: 50,
),
const SizedBox(width: 8),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(songName),
const SizedBox(height: 8),
Text(artistName),
],
),
const SizedBox(width: 8),
IconButton(
onPressed: () => {developer.log('Save to your library')},
icon: isFavorite
? const Icon(Icons.favorite)
: const Icon(Icons.favorite_border))
],
),
);
}
}
class AppController extends StatefulWidget {
@override
_AppControllerState createState() => _AppControllerState();
}
class _AppControllerState extends State<AppController> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.mic, semanticLabel: "Lyrics"),
onPressed: () => {developer.log("Lyrics")},
),
IconButton(
icon: const Icon(Icons.queue_music, semanticLabel: "Queue"),
onPressed: () => {developer.log("Queue")},
),
IconButton(
icon: const Icon(Icons.phonelink,
semanticLabel: "Connect to a device"),
onPressed: () => {developer.log("Connect to a device")},
),
SoundController(),
IconButton(
icon: const Icon(Icons.fullscreen, semanticLabel: "Full screen"),
onPressed: () => {developer.log("Full screen")},
),
],
),
);
}
}
class SoundController extends StatefulWidget {
@override
_SoundControllerState createState() => _SoundControllerState();
}
class _SoundControllerState extends State<SoundController> {
var volume = 0.5;
var isMute = false;
IconData getVolumeIcon(double value) {
if (isMute) {
return Icons.volume_off;
}
if (value == 0) {
return Icons.volume_mute;
} else if (value <= 0.5) {
return Icons.volume_down;
} else {
return Icons.volume_up;
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(
getVolumeIcon(volume),
semanticLabel: isMute ? 'Unmute' : 'Mute',
),
onPressed: () {
developer.log("Mute");
setState(() {
isMute = !isMute;
});
},
),
SizedBox(
height: 24.0,
child: Slider.adaptive(
value: volume,
onChanged: (value) {
setState(() {
volume = value;
});
}),
),
],
);
}
}
Additionally, I have isolated the code for the slider widget only to better understand widget size constraints. My attempt for constraining the widget size were focused mainly on this piece of code.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Spotify Clone',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Material(child: SliderTest()),
);
}
}
class SliderTest extends StatefulWidget {
@override
_SliderTestState createState() => _SliderTestState();
}
class _SliderTestState extends State<SliderTest> {
double duration = 340;
var currentTime = 0.0;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: EdgeInsets.all(8.0),
constraints: const BoxConstraints(
minWidth: 300,
maxWidth: 700,
),
child: Row(
children: [
Text(currentTime.ceil().toString()),
Expanded(
child: SizedBox(
child: Slider.adaptive(
value: currentTime,
min: 0.0,
max: duration,
onChanged: (value) {
setState(() {
currentTime = value;
});
},
),
),
),
Text(duration.ceil().toString()),
],
),
),
);
}
}
Now, onto my questions:
- What am I missing regarding constraints? My understanding is that the slider adapts itself to the maximum size of the parent widget, which is smaller than the specified minimum size of the slider widget. Could this be the issue? And if so, why does the parent take precedence over the child in this case?
- How can I make my middle widget behave correctly in terms of minimum and maximum sizes?
I would greatly appreciate any insights or suggestions on how to overcome this challenge and achieve the desired behavior. Thank you in advance for your help!
Best regards,
MetaHG