正文
TLDR
You can find the final source code
here
.
This piano uses only
5032
bytes of Dart Code!
What you will learn
-
Working with Dark Mode
-
Forcing app to be in landscape
-
Working with custom files bundled with the app
-
Working with midi and sounds in flutter
-
Working with
StatefulWidget
-
Using
SafeArea
and
Semantics
-
Building an app with minimal code
What you need
-
Flutter SDK
Installed (
More Info
)
-
A
.sf2
SoundFont File like
this one
-
Physical iOS device (iOS Simulator does not work with this plugin for playing the sounds) or Android Emulator/Device
Setting Up
You can either create a new project with Android Studio or VSCode using the GUI or navigate to the location you want your project and using this command in the terminal:
lutter create -i swift -a kotlin flutter_piano
. Make sure to include
Swift and Kotlin Support!
Now that you have your project created it should look like this.
Let’s start by adding some dependencies to our `pubspec.yaml’
dependencies:
flutter:
sdk: flutter
tonic: ^0.2.3
flutter_midi: ^0.1.1+3
cupertino_icons: ^0.1.2
and add the .sf2 file
assets:
- assets/sounds/Piano.sf2
If you haven’t already create a new folder at the top of your project call
assets
and a subfolder called
sounds
and place the .sf2 file there and make sure it is named
Piano.sf2
Because our app will only work in landscape we need to update those settings as well.
navigate to the
/android/app/src/main/AndroidManifest.xml
and add this line inside
<activity
in the
<application
:
android:screenOrientation="landscape"
Example:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.appleeducate.flutter_piano">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="flutter_piano"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
navigate to
/ios/Runner/info.plist
and change:
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
Now we can start with the UI! When you run the application now it should start in landscape!
Step 1
To make it eaiser to read lets remove the comments. Use “find and replace” and search for
\/\/.*
choose the “select all occurrances” button and hit
backspace
to delete.
Hit save and you should see the code format for you.
The ‘main.dart’ file should look like this:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Step 2
Delete the
MyHomePage
widget so you are left with this.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
You should get an error and thats ok, we will fix that next.
Replace
MyHomePage(title: 'Flutter Demo Home Page')
with a
Scaffold()
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(),
);
}
}
Step 3
Change
MyApp
to a
StatefulWidget
. You can do this quickly by selecting
MyApp
and choose “Convert to StatefulWidget” with the helper.
It should look like this now:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(),
);
}
}
Step 4
Change the theme to dark. You can do this by setting the
ThemeData
in
MaterialApp
change:
theme: ThemeData(
primarySwatch: Colors.blue,
),
to this
theme: ThemeData.dark(),
Add and
AppBar
to the
Scaffold
appBar: AppBar(title: Text("Flutter Piano")),
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.dark(),
home: Scaffold(
appBar: AppBar(title: Text("Flutter Piano")),
),
);
}
}
Now build and run your app, it should look like this.
Step 5
We need to add some imports to the top:
import 'package:flutter/services.dart';
import 'package:flutter_midi/flutter_midi.dart';
If you get an error make sure they are added in the
pubspec.yaml
from earlier, then restart the app. Be sure to run
flutter packages get
everytime you add a dependency.
Now we can add out
initState()
to our app.
@override
initState() {
FlutterMidi.unmute();
rootBundle.load("assets/sounds/Piano.sf2").then((sf2) {
FlutterMidi.prepare(sf2: sf2, name: "Piano.sf2");
});
super.initState();
}
Run the app and make sure you do not get any errors. If you are running this on the iOS Simulator you will get the following error:
Could Not Load Midi on this Device. (Cannot run on simulator), have you included the sound font?
It is ok for developing the UI but once we start with the midi you will need to plug in a real device.
Your code so far should look like this:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_midi/flutter_midi.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
initState() {
FlutterMidi.unmute();
rootBundle.load("assets/sounds/Piano.sf2").then((sf2) {
FlutterMidi.prepare(sf2: sf2, name: "Piano.sf2");
});
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.dark(),
home: Scaffold(
appBar: AppBar(title: Text("Flutter Piano")),
),
);
}
}
Step 6
To make Flutter development faster we start with containers and colors so we can make sure everything is the right size.
Lets start by adding a
Drawer
with a
ListView
to our
Scaffold
.
home: Scaffold(
appBar: AppBar(title: Text("Flutter Piano")),
drawer: Drawer(child: SafeArea(child: ListView(children: <Widget>[]))),
),
You should now get a menu icon that when you press looks like this.
Now lets add a ListView that scrolls Horizontially to the body of the
Scaffold
body: ListView.builder(
itemCount: 7,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
return Container();
},
)
We need 7
itemCount
for 7 octaves on the Piano.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_midi/flutter_midi.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
initState() {
FlutterMidi.unmute();
rootBundle.load("assets/sounds/Piano.sf2").then((sf2) {
FlutterMidi.prepare(sf2: sf2, name: "Piano.sf2");
});
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.dark(),
home: Scaffold(
appBar: AppBar(title: Text("Flutter Piano")),
drawer:
Drawer(child: SafeArea(child: ListView(children: <Widget>[]))),
body: ListView.builder(
itemCount: 7,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
return Container();
},
)),
);
}
}
Step 7
Now we need to build the octave section that will be repeated. Since every octave is identical we can repeat the octaves with minor adjustments.
Lets add some parameters for use to define for our UI. Add these underneath the initState function.
double get keyWidth => 80 + (80 * _widthRatio);
double _widthRatio = 0.0;
bool _showLabels = true;
We will use these to dynamily update the keys.
Under the
itemBuilder
lets define which octave we are working with by adding:
final int i = index * 12;
Our code should look like this now:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_midi/flutter_midi.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
initState() {
FlutterMidi.unmute();
rootBundle.load("assets/sounds/Piano.sf2").then((sf2) {
FlutterMidi.prepare(sf2: sf2, name: "Piano.sf2");
});
super.initState();
}
double get keyWidth => 80 + (80 * _widthRatio);
double _widthRatio = 0.0;
bool _showLabels = true;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.dark(),
home: Scaffold(
appBar: AppBar(title: Text("Flutter Piano")),
drawer:
Drawer(child: SafeArea(child: ListView(children: <Widget>[]))),
body: ListView.builder(
itemCount: 7,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
final int i = index * 12;
return Container();
},
)),
);
}
}
Step 8
Now we need to add a
Stack
for our octave:
return SafeArea(
child: Stack(children: <Widget>[
Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
_buildKey(24 + i, false),
_buildKey(26 + i, false),
_buildKey(28 + i, false),
_buildKey(29 + i, false),
_buildKey(31 + i, false),
_buildKey(33 + i, false),
_buildKey(35 + i, false),
]),
Positioned(
left: 0.0,
right: 0.0,
bottom: 100,
top: 0.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(width: keyWidth * .5),
_buildKey(25 + i, true),
_buildKey(27 + i, true),
Container(width: keyWidth),
_buildKey(30 + i, true),
_buildKey(32 + i, true),
_buildKey(34 + i, true),
Container(width: keyWidth * .5),
])),
]),
);
Here we have defined which midi notes are played for each octave.
Now add the function
_buildKey
underneath our
build
function.
Widget _buildKey(int midi, bool accidental) {
if (accidental) {
return Container(
width: keyWidth,
color: Colors.black,
margin: EdgeInsets.symmetric(horizontal: 2.0),
padding: EdgeInsets.symmetric(horizontal: keyWidth * .1),
child: Material(
elevation: 6.0,
borderRadius: borderRadius,
shadowColor: Color(0x802196F3),
));
}
return Container(
width: keyWidth,
color: Colors.white,
margin: EdgeInsets.symmetric(horizontal: 2.0));
}
Also add
borderRadius
to the bottom of
main.dart
const BorderRadiusGeometry borderRadius = BorderRadius.only(
bottomLeft: Radius.circular(10.0), bottomRight: Radius.circular(10.0));
Your app should look like this:
Your code should look like this:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_midi/flutter_midi.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
initState() {
FlutterMidi.unmute();
rootBundle.load("assets/sounds/Piano.sf2").then((sf2) {
FlutterMidi.prepare(sf2: sf2, name: "Piano.sf2");
});
super.initState();
}
double get keyWidth => 80 + (80 * _widthRatio);
double _widthRatio = 0.0;
bool _showLabels = true;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.dark(),
home: Scaffold(
appBar: AppBar(title: Text("Flutter Piano")),
drawer:
Drawer(child: SafeArea(child: ListView(children: <Widget>[]))),
body: ListView.builder(
itemCount: 7,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
final int i = index * 12;
return SafeArea(
child: Stack(children: <Widget>[
Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
_buildKey(24 + i, false),
_buildKey(26 + i, false),
_buildKey(28 + i, false),
_buildKey(29 + i, false),
_buildKey(31 + i, false),
_buildKey(33 + i, false),
_buildKey(35 + i, false),
]),
Positioned(
left: 0.0,
right: 0.0,
bottom: 100,
top: 0.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(width: keyWidth * .5),
_buildKey(25 + i, true),
_buildKey(27 + i, true),
Container(width: keyWidth),
_buildKey(30 + i, true),
_buildKey(32 + i, true),
_buildKey(34 + i, true),
Container(width: keyWidth * .5),
])),
]),
);
},
)),
);
}
Widget _buildKey(int midi, bool accidental) {
if (accidental) {
return Container(
width: keyWidth,
color: Colors.black,
margin: EdgeInsets.symmetric(horizontal: 2.0),
padding: EdgeInsets.symmetric(horizontal: keyWidth * .1),
child: Material(
elevation: 6.0,
borderRadius: borderRadius,
shadowColor: Color(0x802196F3),
));
}
return Container(
width: keyWidth,
color: Colors.white,
margin: EdgeInsets.symmetric(horizontal: 2.0));
}
}
const BorderRadiusGeometry borderRadius = BorderRadius.only(
bottomLeft: Radius.circular(10.0), bottomRight: Radius.circular(10.0));
Step 9
Time to add midi by adding the following import to the top of the file:
import 'package:tonic/tonic.dart';
In the
-buildKey
function you can add this line:
final pitchName = Pitch.fromMidiNumber(midi).toString();
We can also create the piano key itself underneath it:
final pianoKey = Stack(
children: <Widget>[
Semantics(
button: true,
hint: pitchName,
child: Material(
borderRadius: borderRadius,
color: accidental ? Colors.black : Colors.white,
child: InkWell(
borderRadius: borderRadius,
highlightColor: Colors.grey,
onTap: () {},
onTapDown: (_) => FlutterMidi.playMidiNote(midi: midi),
))),
Positioned(
left: 0.0,
right: 0.0,
bottom: 20.0,
child: _showLabels
? Text(pitchName,
textAlign: TextAlign.center,
style: TextStyle(
color: !accidental ? Colors.black : Colors.white))
: Container()),
],
);
Remove the color from the container and replace it with
child: pianoKey,
if (accidental) {
return Container(
width: keyWidth,
margin: EdgeInsets.symmetric(horizontal: 2.0),
padding: EdgeInsets.symmetric(horizontal: keyWidth * .1),
child: Material(
elevation: 6.0,
borderRadius: borderRadius,
shadowColor: Color(0x802196F3),
child: pianoKey));
}
return Container(
width: keyWidth,
child: pianoKey,
margin: EdgeInsets.symmetric(horizontal: 2.0));
The complete function should look like this:
Widget _buildKey(int midi, bool accidental) {
final pitchName = Pitch.fromMidiNumber(midi).toString();
final pianoKey = Stack(
children: <Widget>[
Semantics(
button: true,
hint: pitchName,
child: Material(
borderRadius: borderRadius,
color: accidental ? Colors.black : Colors.white,
child: InkWell(
borderRadius: borderRadius,
highlightColor: Colors.grey,
onTap: () {},
onTapDown: (_) => FlutterMidi.playMidiNote(midi: midi),
))),
Positioned(
left: 0.0,
right: 0.0,
bottom: 20.0,
child: _showLabels
? Text(pitchName,
textAlign: TextAlign.center,
style: TextStyle(
color: !accidental ? Colors.black : Colors.white))
: Container()),
],
);
if (accidental) {
return Container(
width: keyWidth,
margin: EdgeInsets.symmetric(horizontal: 2.0),
padding: EdgeInsets.symmetric(horizontal: keyWidth * .1),
child: Material(
elevation: 6.0,
borderRadius: borderRadius,
shadowColor: Color(0x802196F3),
child: pianoKey));
}
return Container(
width: keyWidth,
child: pianoKey,
margin: EdgeInsets.symmetric(horizontal: 2.0));
}
Now when you run the app it should look like this: