0

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): enter image description here.

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:

  1. 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?
  2. 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

MetaHG
  • 21
  • 3
  • try `CustomMultiChildLayout` - the docs say: "CustomMultiChildLayout is appropriate when there are complex relationships between the size and positioning of multiple widgets" – pskink Jun 08 '23 at 15:31
  • @pskink Thank you for the suggestion, I will check this class. How can you tell that the relationships between my widgets are complex enough to address the problem using this class? From my beginner point of view, the relationships between the widgets do not look that complex. – MetaHG Jun 09 '23 at 12:05

0 Answers0