My idea was to create a music playing app. But not just any music playing app, an MC Dub music app complete with top hits such as Universal Gravitation, and Physics Force Problems!
This app includes 11 songs, a volume slider, a live time slider, a pause button, and a play button. The time on the left side updates as the music plays and the time on the right updates depending on the duration of the song.
/*The import Statements import different packages,
*which add different functionalities to this file */
import 'package:flutter/material.dart';
import 'home_screen.dart';
import 'package:audioplayers/audio_cache.dart';
/*Nothing here changes the ways that you can interact with the app.
* what it does is make sure everything is set up and displayed properly
* It also makes sure that the background color is correct and everything is
* titled appropriately*/
void main() => runApp(McMusic());
class McMusic extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MC Dub Music',
theme: ThemeData(
canvasColor: Color(0xFF1A1A1B),
),
home: MainDisplay(),
//home: HomeScreen(),
);
}
}
/*The import Statements import different packages,
*which add different functionalities to this file */
import 'package:flutter/material.dart';
import 'manage_music.dart';
/*In Dart/Flutter, everything that draws on the screen or influences
* the way things look is called a widget. For example, when this
* class says extends StatelessWidget, it is getting attributes from
* its parent class StatelessWidget. The Stateless part means that
* you can not call setState({}). This distinction will be important later*/
class MusicDisplay extends StatelessWidget {
/*This class takes on all attributes from the parent class,
* so to not have some of the functionality from statelessWidget
* we use @override to tell our class to do something that StatelessWidget
* would not do.*/
@override
Widget build(BuildContext context) {
return MaterialApp();
}
}
/*This line creates a new variable called manageMusic.
* Similar to how variables can be set to int or double or String,
* manageMusic is a ManageMusic type variable. we set it equal to the class
* ManageMusic from our manage_music file.*/
ManageMusic manageMusic = ManageMusic();
/*This class extends StatefulWidget, which means that it has a state.
* A state is useful because every time a variable inside a state is updated
* the build function is called and the app is redrawn*/
class MainDisplay extends StatefulWidget {
@override
_MainDisplayState createState() => _MainDisplayState();
}
/*everything in this class is part of the state. that's why it extends
* the State of MainDisplay. The only other important piece of information here
* is the underscore before the name MainDisplayState. This tells the app that
* everything inside here is local. This means that no outside classes would
* be able to reach in and use one of this class's functions.*/
class _MainDisplayState extends State<MainDisplay> {
/*These are the variables with generic starting values that will be
* updated later.*/
double volumeValue = 1.0;
double locationValue = 0.0;
double lengthOfSong = 0.0;
double currentPosition = 0.0;
double currentPositionInSeconds = 0.0;
double currentPositionInDisplayableSeconds = 0.0;
double currentPositionInMinutes = 0.0;
int currentPositionInInt = 0;
Color buttonColor = Color(0xFF1A1A1B);
/*This widget is a Divider widget that I have taken out of my build function
* and externalized. The reason for doing this is that all the lines on the screen
* are the same height and color. I want the driest code I can get easily,
* so I can now call divide() in my build instead of creating a new widget
* every time.*/
Divider divide() {
return Divider(
height: 4.0,
color: Colors.grey[800],
);
}
/*This function updates the value for our current position variable every time
* it is called. Since it is a state variable, every time it is updated build
* is called and the app is redrawn.*/
void handlePositionChange() {
setState(() {
currentPosition = onPositionChange();
});
}
/*This function looks very complicated at first, but really it is just a
* lot of conversions. First, we add a listener that listens for every time
* onAudioPositionChanged updates. The reason that it is in this class
* instead of ManageMusic is that setState needs to be called to
* constantly update the slider widget to show the music's progression
* in real time. All of the conversions are needed to convert the current
* position from a Duration variable to a double, which is the only
* value type that the slider widget can take in. This function also
* returns the value as a double because that is what is most often used
* in this program.*/
double onPositionChange() {
manageMusic.audioPlayer.onAudioPositionChanged.listen((Duration player) {
setState(() {
currentPositionInInt = player.inSeconds;
currentPositionInMinutes = player.inMinutes.toDouble();
currentPositionInSeconds = currentPositionInInt.toDouble();
currentPositionInDisplayableSeconds =
currentPositionInSeconds - currentPositionInMinutes * 60;
});
return currentPositionInInt.toDouble();
});
return currentPositionInInt.toDouble();
}
/*This is the long awaited build widget. everything after the return is
* drawn on the screen every time this function is called.*/
@override
Widget build(BuildContext context) {
manageMusic.convertDuration();
handlePositionChange();
onPositionChange();
return Scaffold(
/*The Column widget is used to position everything in a column.
* I used it so that all of the songs would display on top of each other
* the CrossAxisAlignment.stretch is used to stretch the children out
* across the screen if it does not have a specified width. This causes
* the dividers and sliders to reach across the screen to fill up space.*/
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
/*This widget is used as padding to add space between the widgets
* and the top of the screen.*/
SizedBox(
height: 30.0,
),
/*Here is the widget which we externalized earlier*/
divide(),
/*The following widgets are very repetitive and not very dry.
* They all call the externalized widget in the ManageMusic class
* called buildButton and pass in the link to the song, a button color,
* and the title of the song.*/
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/rock_1.mp3',
buttonColor,
'RockCycle'),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/energy_collisions.m4a',
buttonColor,
'Energy Collisions',
),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/heating_up_-_mc_dub.m4a',
buttonColor,
'Heating up',
),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/maps_1.mp3',
buttonColor,
'Maps'),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/mc_dub_evolution_state_of_mind.mp3',
buttonColor,
'Evolution',
),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/mc_dub_universal_gravitation__space_out__.mp3',
buttonColor,
'Space Out',
),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/mitosis_1.mp3',
buttonColor,
'Mitosis',
),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/ocean_1.mp3',
buttonColor,
'Ocean',
),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/mc_dub_waves_transfer_energy.mp3',
buttonColor,
'Waves Transfer Energy',
),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/physics_force_problems.m4a',
buttonColor,
'Physics Force Problems',
),
divide(),
manageMusic.buildButton(
'https://sanmarinscience.weebly.com/uploads/1/1/5/2/11528286/stars_1.mp3',
buttonColor,
'Stars',
),
divide(),
/*Inside of the column widget, we create a row to horizontally
* position widgets.*/
Row(
children: <Widget>[
/*This icon widget displays the icon next to the volume bar*/
Icon(
Icons.volume_up,
color: Colors.grey,
),
/*This slider widget displays the purple bar. The value is
* set to volume value which is updated every time it is changed.
* the min and max act as the max and min volume. The active color
* is the color of the slider at and before the value.
* the inactive color is the color of everything after the position.
* onChanged listens for a change in the value, and every time it
* detects a change the volume value is set to the current value
* and a manageMusic function is called to change the volume.*/
Expanded(
child: Slider(
value: volumeValue,
min: 0,
max: 1,
activeColor: Colors.purple,
inactiveColor: Colors.grey,
onChanged: (value) {
setState(() {
volumeValue = value;
manageMusic.setVolume(volumeValue);
});
},
),
),
],
),
/*Here we do almost exactly the same thing as above, but now for the
* position. we also add some text widgets to display the position
* and total length of the song.*/
Row(
children: <Widget>[
Text(
'${currentPositionInMinutes.round()}:${currentPositionInDisplayableSeconds.round()}',
style: TextStyle(color: Colors.grey),
),
Expanded(
/*The only major difference in this slider is the max, which
* is set at the length of the song plus a little more so
* that the value can never exceed the max. We also use
* a seek function in the onChanged handler instead of a
* function that sets the volume*/
child: Slider(
value: currentPosition,
min: 0.0,
max: manageMusic.lengthInSecondsConverted + 0.5,
activeColor: Colors.blueGrey,
inactiveColor: Colors.grey,
onChanged: (value) {
manageMusic.seekSong(value.toInt());
setState(() {
currentPosition = value;
});
//setCurrentPosition();
},
),
),
Text(
'${manageMusic.timeInMinutes.round()}:${manageMusic.displayableSeconds.round()}',
style: TextStyle(
color: Colors.grey,
),
),
],
),
Row(
children: <Widget>[
Expanded(
/*These buttons are very simple. I added some padding on the
* top and bottom to position the icons nicely, passed in a
* color, added an icon as a child, and that was it. The
* onPressed of this button calls the pause function,
* and the other button's onPressed calls the resume function.*/
child: FlatButton(
padding: EdgeInsets.fromLTRB(0, 15, 0, 15),
color: Colors.green[600],
child: Icon(
Icons.pause,
),
onPressed: () {
manageMusic.pauseMusic();
},
),
),
Expanded(
child: FlatButton(
padding: EdgeInsets.fromLTRB(0, 15, 0, 15),
color: Colors.green[600],
child: Icon(
Icons.play_arrow,
),
onPressed: () {
manageMusic.resumeMusic();
},
),
),
],
),
],
),
);
}
}
/*The import Statements import different packages,
*which add different functionalities to this file */
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
/*This class does not extend anything, meaning it has no parent to inherit
* any properties from. It also does not have a state, meaning that there is
* no way to call build from this class*/
class ManageMusic {
/*These are all of our variables for this class.*/
AudioPlayer audioPlayer = AudioPlayer();
double lengthInSecondsConverted = 1.0;
double timeInMinutes = 0.0;
double timeInSeconds = 0.0;
double displayableSeconds = 0.0;
double currentPositionInSeconds = 0.0;
int currentPositionInInt = 0;
bool isPlaying = false;
Stream<Duration> currentPosition;
/*This function takes in a song and then plays it. If the result is a 1, that
* means it is playing the song, so we set isPlaying to true. The only tricky
* part of this function is the keywords async and await. these keywords mean
* that the function returns asynchronously. this is because the function is
* making a call to the internet to receive the files. This can't happen
* instantaneously, so we need to put these keywords in to tell the program
* to not continue until the call has come back with a response. */
void playSound(songLocation) async {
int resultForPlay = await audioPlayer.play(songLocation);
if (resultForPlay == 1) {
isPlaying = true;
}
}
/*The Future<int> means that this function is calling a function which is
* asynchronous so it does not return the result immediately. As for the
* functionality, this function makes a call to get the duration of the
* file that is currently playing. it then either returns the length or a 0
* if the length was null or an error occurred.*/
Future<int> getDuration() async {
if (isPlaying == true) {
int songLength = await audioPlayer.getDuration();
return songLength;
} else {
return 0;
}
}
/*This function is yet another async await function. It first makes a call
* to get the duration, then it converts the lengths several times to
* get the value as an int. we then create a new variable called duration.
* this variable is equal to a function that takes in a measurement and
* a value. you can then convert that value to different measurements.
* that is exactly what we do. we get the value in minutes and seconds
* as doubles. we then create a variable called displayableSeconds and
* calculate the amount of trailing seconds without the minutes included.
* the final piece is setting lengthInSecondsConverted to the absolute
* value of lengthInSeconds. (I don't know why but lengthInSeconds returned
* as a negative that was less than the min value on the slider widget.)*/
void convertDuration() async {
int lengthInInt = await getDuration();
double lengthInDouble = lengthInInt.toDouble();
double lengthInSeconds = lengthInDouble / 1000;
Duration duration = Duration(milliseconds: lengthInInt);
timeInMinutes = duration.inMinutes.toDouble();
timeInSeconds = duration.inSeconds.toDouble();
displayableSeconds = timeInSeconds - timeInMinutes * 60;
lengthInSecondsConverted = lengthInSeconds.abs();
}
/*This function pauses the music*/
void pauseMusic() async {
int resultForPause = await audioPlayer.pause();
}
/*This function plays the music*/
void resumeMusic() async {
int resultForPause = await audioPlayer.resume();
}
/*This function changes the volume of the music to the value passed in*/
void setVolume(volume) {
audioPlayer.setVolume(volume);
}
/*This function seeks in the song to the value passed in*/
void seekSong(seconds) {
audioPlayer.seek(Duration(seconds: seconds));
}
/*This is the externalized widget that takes in the songLocation, color, and
* songTitle and returns a button widget that had all of the information
* built in.*/
Expanded buildButton(
songLocation,
color,
songTitle,
) {
return Expanded(
child: FlatButton(
color: color,
child: Text(
'$songTitle',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
playSound(songLocation);
},
),
);
}
}
This app used dart for the functionality and flutter for the styling. Dart is an object oriented programing language similar to React JS and other web frameworks. Flutter has the concept of widgets. These widgets help build the UI of the app. These widgets are all drawn in the build()
function of the class. The build function is called every time a state value is updated. A state value is a variable that lives in the state of a class. These state values can be updated by calling setState()
and passing in the variable you want to update and the value you want to update it to. This only works if your class extends statefulWidget
This is why I update values with setState()
in home_screen.dart
but not manage_music.dart
.
I believe that I was very creative with the idea of this app. This is something that no one else did, and I believe that my creativity paid off. I am very happy with the outcome of the app. I also used my critical thinking skills on this app. I overcame a lot of problems, converted a lot of things from one measurement to another, and overall problem solved very well.
I could definitely have used my time more effectively. I spent the first week of the project just trying to come up with an idea. This was definitely detrimental. This caused a time crunch towards the end, thus leaving a less than perfect app. I would have loved to add a few more functionalities to the app, but my lost time got in the way. I could also have improved on planning. I ended up doing quite well, but I could have down more if I had a list of things to complete on each day. Overall, I believe i did quite well.