Building An Android Audio Player In Flutter: I

So over the past 2 weeks I have been working on an Android app in my free time. It started out as a way for me to use as many of Flutter’s features as I can and see how it all works out. I wanted a project that will allow me apply the BLoC pattern extensively, use platform channels, overlays, databases, shared preferences and also apply animations. The best idea that came to me was to build an Audio player in Android.


I began building the app when I stumbled on a design by Johny Vino and decided to replicate some parts it. It is not a faithful replication however it is close enough for my intentions. Sometime in the near future I will be doing an app that is faithful to the design.

The code for this project can be found on Github. It is still a work in progress and as at the time of writing this article, some features are yet to be added. Still it has enough features to make for a good learning example for anyone new in Flutter and hoping to see how apps can be built with it.

In this series, I will be sharing the different parts of the app as I build them. I will not attempt to go into detail as there wont be nearly enough time for that. I will just be showing the thinking behind the way I decided to implement the app. I will also be talking about unit testing and widget testing at the end of each article so as to cover as many parts of building a Flutter app as possible.


Code Organization

The different parts of the code are divided into different folders withing the Beatz/lib folder to help with easy location of the files. The blocs are contained within the blocs folder, pages are contained within the pages folder.

Therefore, when I mention “home_page_bloc file” for example, you should know it will be located in the blocs folder.


Main.dart

import 'package:beatz/blocs/bloc_provider.dart';
import 'package:beatz/blocs/home_page_bloc.dart';
import 'package:beatz/pages/home_page.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Beatz',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.pinkAccent,
primaryColorDark: Colors.pink,
primaryColorLight: Colors.deepOrangeAccent,
accentColor: Colors.purpleAccent,
),
home: BlocProvider<HomePageBloc>(
bloc: HomePageBloc(),
child: HomePage(title: 'Beatz'),
),
);
}
}

The main.dart file is just consists of the App itself which is a stateless widget composed of a MaterialApp widget which in itself is made from a BlocProvider. Right off the bat, A BLOC is used to help with displaying the content of the home page.

If you are not familiar with the BloC pattern this article by Didier Boelens should get you up to speed. After reading that you may also want to take a look at this one. Or just follow Didier Boelens all together. He has some of the most lucid articles I have ever read.


BlocProvider.dart

import 'package:flutter/material.dart';

abstract class BlocBase {
void dispose();
}

class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}) : super(key: key);

final T bloc;
final Widget child;

@override
_BlocProviderState<T> createState() => _BlocProviderState<T>();

static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}

static Type _typeOf<T>() => T;
}

class _BlocProviderState<T> extends State<BlocProvider<BlocBase>> {
@override
void dispose() {
widget.bloc.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return widget.child;
}
}

This includes 2 classes; the base class, BlocBase, an abstract class which provides an interface for all other BloC component created within the app to implement and BlocProvider, the widget that is used to pass the BloC down the widget tree that needs it. We’ve already seen how a BlocProvider is used to provide the HomePageBloc in the main.dart file.

The Home Page

These are the files necessary to render the homepage. These files include home_page.dart and home_page_bloc.dart.

import 'package:beatz/blocs/albums_page_bloc.dart';
import 'package:beatz/blocs/bloc_provider.dart';
import 'package:beatz/blocs/home_page_bloc.dart';
import 'package:beatz/blocs/playlist_page_bloc.dart';
import 'package:beatz/blocs/songs_page_bloc.dart';
import 'package:beatz/pages/albums_page.dart';
import 'package:beatz/pages/playlist_page.dart';
import 'package:beatz/pages/songs_page.dart';
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;

@override
_HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
HomePageBloc _bloc;
AnimationController _controller;
Animation<double> _heightAnimation;

final _widgetOptions = [
BlocProvider<AlbumsPageBloc>(
bloc: AlbumsPageBloc(),
child: AlbumsPage(),
),
BlocProvider<SongsPageBloc>(
bloc: SongsPageBloc(),
child: SongsPage(),
),
BlocProvider<PlaylistPageBloc>(
bloc: PlaylistPageBloc(),
child: PlaylistPage(),
),
];

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 4000), vsync: this);
_heightAnimation =
Tween<double>(begin: 0.0, end: 220.0).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
));
_controller.forward();
}

@override
Widget build(BuildContext context) {
_bloc = BlocProvider.of<HomePageBloc>(context);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
AnimatedBuilder(
animation: _controller,
builder: (_, __) {
return Container(
height: _heightAnimation.value,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.deepOrangeAccent,
Colors.purple,
],
begin: FractionalOffset(1.2, 0.4),
end: FractionalOffset(-0.3, 0.8),
stops: [0.0, 1.0],
),
),
child: Center(
child: CircleAvatar(
radius: 65.0,
child: Image(
image: AssetImage("assets/headphones.png"),
fit: BoxFit.fill,
color: Colors.white,
),
),
),
);
}),
Expanded(
child: StreamBuilder<int>(
initialData: 0,
stream: _bloc.pageIndexStream,
builder: (context, snapshot) {
return Scaffold(
body: _widgetOptions.elementAt(snapshot.data),
bottomNavigationBar: BottomNavigationBar(
currentIndex: snapshot.data,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.album),
title: Text("albums"),
),
BottomNavigationBarItem(
icon: Icon(Icons.queue_music),
title: Text("songs"),
),
BottomNavigationBarItem(
icon: Icon(Icons.playlist_play),
title: Text("playlist"),
),
],
onTap: _onItemSelected,
),
);
}),
),
],
);
}

void _onItemSelected(int index) => _bloc.pageIndex.add(index);

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

The HomePage is going to mainly consist of Animations, BlocProviders, BottomNavigationBarItems, StreamBuilder, etc. The basic idea is as soon as the HomePage loaded, the AlbumPagewidget is displayed inside.

When a BottomNavigationBarItem is clicked, either the SongsPage, PlaylistPage or AlbumPage is displayed based on the index of the item tapped. Each item calls a function that sends their index to the HomePageBloc and this index is passed through the BloC to the StreamBuilder in order to build the appropriate widget.

HomePage

Now let us look at the HomePageBloc.

class HomePageBloc extends BlocBase{

StreamController<int> _pageIndexController= StreamController<int>();
StreamSink<int> get pageIndex=> _pageIndexController.sink;
Stream<int> get pageIndexStream => _pageIndexController.stream;

@override
void dispose() {
_pageIndexController.close();
}

}

The HomePageBloc extends from the BlocBase class. It has a simple StreamController that is used to show different pages based on the item selected in the BottomNavigationBar of the HomePage widget. This is probably going to be the simplest Bloc component in the entire app.


Testing

Of course it will not be appropriate to finish this part without talking about testing in Flutter. First let’s look at the unit test of the HomePageBloc. You will find this in the Beatz/test/blocs folder.

import 'package:beatz/blocs/home_page_bloc.dart';
import 'package:test/test.dart';

main() {
test('test HomePageBloc gives the right output and in order', () async {
HomePageBloc bloc = HomePageBloc();
bloc.pageIndex.add(1);
bloc.pageIndex.add(3);
bloc.pageIndex.add(0);
expect(
bloc.pageIndexStream,
emitsInOrder([
emits((val) => val == 1),
emits((val) => val == 3),
emits((val) => val == 0),
]));
});
}

This shows an example of how to test a StreamController. We pass values into the input of the StreamController and verify that it is the same values that were passed in that came out of the output and also in the same order as they were entered.

Now let’s look at widget testing the HomePage widget itself. It can be found in the Beatz/test/pages folder.

import 'package:beatz/main.dart';
import 'package:beatz/pages/albums_page.dart';
import 'package:beatz/pages/playlist_page.dart';
import 'package:beatz/pages/songs_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('Home page test', (WidgetTester tester) async {
await tester.pumpWidget(new MyApp());

// Verify that appropriate widgets are visible
expect(find.byType(BottomNavigationBar), findsOneWidget);
expect(
find.descendant(
of: find.byType(CircleAvatar), matching: find.byType(Image)),
findsOneWidget);
expect(find.byType(AlbumsPage), findsOneWidget);
expect(find.byType(SongsPage), findsNothing);
expect(find.byType(PlaylistPage), findsNothing);

// Tap the songs icon and verify that appropriate widgets are visible
await tester.tap(find.byIcon(Icons.queue_music));
await tester.pump();
expect(find.byType(BottomNavigationBar), findsOneWidget);
expect(find.byType(AlbumsPage), findsNothing);
expect(find.byType(SongsPage), findsOneWidget);
expect(find.byType(PlaylistPage), findsNothing);

// Tap the playlist icon and verify that appropriate widgets are visible
await tester.tap(find.byIcon(Icons.playlist_play));
await tester.pump();
expect(find.byType(BottomNavigationBar), findsOneWidget);
expect(find.byType(AlbumsPage), findsNothing);
expect(find.byType(SongsPage), findsNothing);
expect(find.byType(PlaylistPage), findsOneWidget);
});
}

We just check that the appropriate widgets are displayed at the appropraite times in the lifetime of the app. We tap on the BottomNavigationBarItems and check again.


I know the explanation in this article might not be clear enough for some people. If you are one of those people I’d advise you to look through the code and study it so get a clearer picture of my intentions.


So that’s it for this first part. To get more of this and other much more interesting articles, follow Flutter Community on Medium and @FlutterComm on Twitter. The community has lots of articles by various different writers covering several Flutter topics for you to read and skill up in Flutter.

Happy Fluttering.