Zubora Code

Develop and release a simple app using Flutter that plays a sound when an icon is tapped. 1/n Developing the app.

This is the first article in a series summarizing the process of developing and releasing a simple app using Flutter that plays a sound when an icon is tapped.

Published: 1 August, 2023
Revised: 1 August, 2023

Reflection on the past

I've always wanted to develop and release an app, but despite coming up with ideas and starting development, I kept getting busy with my main job and ended up losing motivation, repeatedly giving up halfway. I imagine many others might have experienced something similar.


Goal

So, this time, I aim to create a very simple app and focus on releasing at least one. Personal app development, especially the first one, seems to have a high barrier, but I often hear that those who manage to release one app go on to release two or three more. My goal here is to overcome that first hurdle and release one app. I would be grateful if people think, "Ah, this is good enough for a start."


App Overview

Although the app will be quite simple, having no users would be lonely. Therefore, I've decided to develop an app for my 1-year-old son! He's always curious about smartphones and constantly tries to take them from me.



As of now, it's just a demo, and I plan to add a page for vehicles that can be switched using tabs. But that's it. It's an extremely simple app that doesn't use any data storage, authentication, or state management libraries. It only plays a sound when you tap an icon.


By the way, I'd like to express my gratitude to Sound Effect Lab (@soundeffect_lab) for providing the sound effects and to Irasutoya (@irasutoya) for the illustrations. Thank you very much! 🙇🏻‍♂️


I will associate all the articles with the tag #first_flutter_tutorial.

I've made all the source code available on GitHub at the following link:

https://github.com/tkugimot/touch_and_hear


Setup

To create a new app following the steps from the "Hello World in Flutter" tutorial, I push it to GitHub with the name "touch_and_hear."

$ flutter create touch-and-hear
$ git init
$ git add .
$ git commit -m 'Initial Commit'
$ git remote add origin git@github.com:tkugimot/touch_and_hear.git
$ git push origin main
$ git checkout -b feature/sound-app

Preparation

Download 6 animal sound files and 6 vehicle sound files from Sound Effect Lab.

Next, download 12 corresponding illustrations from Irasutoya.

Development

Place image and sound files into the assets directory

First, place the downloaded image and sound files directly in the assets directory at the root.

$ git status
On branch feature/sound-app
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   assets/images/bicycle.png
        new file:   assets/images/bike.png
        new file:   assets/images/cat.png
        new file:   assets/images/dog.png
        new file:   assets/images/elephant.png
        new file:   assets/images/fune.png
        new file:   assets/images/horse.png
        new file:   assets/images/kyukyusya.png
        new file:   assets/images/leo.png
        new file:   assets/images/patocar.png
        new file:   assets/images/piyo.png
        new file:   assets/images/super_car.png
        new file:   assets/sounds/bicycle.mp3
        new file:   assets/sounds/bike.mp3
        new file:   assets/sounds/cat.mp3
        new file:   assets/sounds/dog.mp3
        new file:   assets/sounds/elephant.mp3
        new file:   assets/sounds/fune.mp3
        new file:   assets/sounds/horse.mp3
        new file:   assets/sounds/kyukyusya.mp3
        new file:   assets/sounds/leo.mp3
        new file:   assets/sounds/patocar.mp3
        new file:   assets/sounds/piyo.mp3
        new file:   assets/sounds/super_car.mp3

$ git add .
$ git commit -m 'Add assets'


Modify pubspec.yaml

Next, modify the pubspec.yaml to load the assets from the app.

-  # assets:
-  #   - images/a_dot_burr.jpeg
-  #   - images/a_dot_ham.jpeg
+  assets:
+    - assets/sounds/
+    - assets/images/

Introduce the soundpool library

Now, install the soundpool library, which is necessary for playing music.

$ flutter pub add soundpool
$ git add .
$ git commit -m 'Update pubspec.yaml'

*From this point onwards, I won't list commit commands, so please commit changes as appropriate.


Implementation of the Sound class responsible for sound playback

Next, create the Sound class responsible for handling sound playback.

I referred to the implementation in the following site: https://gaprot.jp/2021/09/07/flutter_se/. Thank you very much! 🙇🏻‍♂️

import 'dart:io';

import 'package:flutter/services.dart';
import 'package:soundpool/soundpool.dart';

enum SoundIds {
  DogButton,
  CatButton,
  ElephantButton,
  LeoButton,
  HorseButton,
  PiyoButton,
  BicycleButton,
  BikeButton,
  FuneButton,
  KyuKyuSyaButton,
  PatocarButton,
  SuperCarButton
}

class Sound {
  String os = Platform.operatingSystem;
  bool isIOS = Platform.isIOS;
  late Soundpool _soundPool;

  final Map<SoundIds, int> _seContainer = Map<SoundIds, int>();
  final Map<int, int> _streamContainer = Map<int, int>();

  Sound() {
    this._soundPool = Soundpool.fromOptions(
        options: SoundpoolOptions(
            streamType: StreamType.music, maxStreams: 5 // 5音同時発音に対応させる
            ));
    () async {
      var dogButton = await rootBundle
          .load("assets/sounds/dog.mp3")
          .then((value) => this._soundPool.load(value));
      var catButton = await rootBundle
          .load("assets/sounds/cat.mp3")
          .then((value) => this._soundPool.load(value));
      var elephantButton = await rootBundle
          .load("assets/sounds/elephant.mp3")
          .then((value) => this._soundPool.load(value));
      var leoButton = await rootBundle
          .load("assets/sounds/leo.mp3")
          .then((value) => this._soundPool.load(value));
      var horseButton = await rootBundle
          .load("assets/sounds/horse.mp3")
          .then((value) => this._soundPool.load(value));
      var piyoButton = await rootBundle
          .load("assets/sounds/piyo.mp3")
          .then((value) => this._soundPool.load(value));
      var bicycleButton = await rootBundle
          .load("assets/sounds/bicycle.mp3")
          .then((value) => this._soundPool.load(value));
      var bikeButton = await rootBundle
          .load("assets/sounds/bike.mp3")
          .then((value) => this._soundPool.load(value));
      var funeButton = await rootBundle
          .load("assets/sounds/fune.mp3")
          .then((value) => this._soundPool.load(value));
      var kyukyusyaButton = await rootBundle
          .load("assets/sounds/kyukyusya.mp3")
          .then((value) => this._soundPool.load(value));
      var patocarButton = await rootBundle
          .load("assets/sounds/patocar.mp3")
          .then((value) => this._soundPool.load(value));
      var superCarButton = await rootBundle
          .load("assets/sounds/super_car.mp3")
          .then((value) => this._soundPool.load(value));

      this._seContainer[SoundIds.DogButton] = dogButton;
      this._seContainer[SoundIds.CatButton] = catButton;
      this._seContainer[SoundIds.ElephantButton] = elephantButton;
      this._seContainer[SoundIds.LeoButton] = leoButton;
      this._seContainer[SoundIds.HorseButton] = horseButton;
      this._seContainer[SoundIds.PiyoButton] = piyoButton;
      this._seContainer[SoundIds.BicycleButton] = bicycleButton;
      this._seContainer[SoundIds.BikeButton] = bikeButton;
      this._seContainer[SoundIds.FuneButton] = funeButton;
      this._seContainer[SoundIds.KyuKyuSyaButton] = kyukyusyaButton;
      this._seContainer[SoundIds.PatocarButton] = patocarButton;
      this._seContainer[SoundIds.SuperCarButton] = superCarButton;

      this._streamContainer[dogButton] = 0;
      this._streamContainer[catButton] = 1;
      this._streamContainer[elephantButton] = 2;
      this._streamContainer[leoButton] = 3;
      this._streamContainer[horseButton] = 4;
      this._streamContainer[piyoButton] = 5;
      this._streamContainer[bicycleButton] = 6;
      this._streamContainer[bikeButton] = 7;
      this._streamContainer[funeButton] = 8;
      this._streamContainer[kyukyusyaButton] = 9;
      this._streamContainer[patocarButton] = 10;
      this._streamContainer[superCarButton] = 11;
    }();
  }

  void play(SoundIds ids) async {
    var seId = this._seContainer[ids];
    if (seId != null) {
      var streamId = this._streamContainer[seId] ?? 0;
      if (streamId > 0 && isIOS) {
        await _soundPool.stop(streamId);
      }
      this._streamContainer[seId] = await _soundPool.play(seId);
    } else {
      print("se resource not found! ids: $ids");
    }
  }

  Future<void> dispose() async {
    await _soundPool.release();
    _soundPool.dispose();
    return Future.value(0);
  }
}

Visual Implementation

Finally, I'll display the icons and implement the sound playback when they are tapped in the main.dart.

import 'package:flutter/material.dart';
import 'Sound.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ぴよぴよサウンドパーク',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'ぴよぴよサウンドパーク'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

Sound sound = Sound();

class _MyHomePageState extends State<MyHomePage> {
  int _selectedIndex = 0;
  static const TextStyle optionStyle =
      TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
  final List<Widget> _widgetOptions = <Widget>[
    GridView.count(
        // Create a grid with 2 columns. If you change the scrollDirection to
        // horizontal, this produces 2 rows.
        crossAxisCount: 2,
        // Generate 100 widgets that display their index in the List.
        children: [
          IconButton(
              icon: Image.asset('assets/images/dog.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.DogButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/cat.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.CatButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/elephant.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.ElephantButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/leo.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.LeoButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/horse.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.HorseButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/piyo.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.PiyoButton);
              }),
        ]),
    GridView.count(
        // Create a grid with 2 columns. If you change the scrollDirection to
        // horizontal, this produces 2 rows.
        crossAxisCount: 2,
        // Generate 100 widgets that display their index in the List.
        children: [
          IconButton(
              icon: Image.asset('assets/images/bicycle.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.BicycleButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/bike.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.BikeButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/fune.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.FuneButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/kyukyusya.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.KyuKyuSyaButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/patocar.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.PatocarButton);
              }),
          IconButton(
              icon: Image.asset('assets/images/super_car.png'),
              iconSize: 50,
              onPressed: () {
                sound.play(SoundIds.SuperCarButton);
              }),
        ]),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: _widgetOptions.elementAt(_selectedIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.pets),
            label: 'どうぶつ',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.directions_car),
            label: 'のりもの',
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

The app development is now complete. You can find the pull request for the changes here: https://github.com/tkugimot/touch_and_hear/pull/1?w=1


In the upcoming articles, I will cover the following necessary steps before release:

  • Multi-language support (Japanese + English)
  • Integrating Crashlytics (for monitoring)
  • Integrating AdMob (for advertisements)
  • Creating and setting app icons/feature graphics
  • Creating support and privacy policy sites
  • Adding a drawer (for app usage/contact/privacy policy links)
  • Releasing for Android
  • Releasing for iOS

I hope you found this development process helpful. If you have any questions or need further assistance, feel free to reach out. Good luck with your app development!

Toshimitsu Kugimoto

Software Engineer

Specializing in backend and web frontend development for payment and media at work, the go-to languages for these tasks include Java and TypeScript. Additionally, currently exploring Flutter app development as a side project.