dart

Flutter(Andorid限定)バックグラウンドでアラームを鳴らすサンプルコード

目覚まし時計などのサンプルに

Flutterでバックグラウンドでアラームを鳴らすサンプルコードです。

サンプルはボタン押したら5秒後に

バックグラウンドでアラームが鳴り

ローカル通知が出るようにしています。

目覚まし時計を作る際の参考にしていただければ幸いです。

使用しているAPIに「android_alarm_manager_plus」があるので、Andorid限定です。

サンプルの動き

『5秒後にサウンド再生』を押下

5秒後に通知が現れ、サウンドが鳴る

通知を押すと最初の画面になるので、

『サウンド停止』を押すと

音が止まります。

概要

『時間がきたら通知してアラームを鳴らす』

という単純な処理ですが

Flutterからしてみれば

  • 時間がきたら処理する
  • 時間がきたらローカル通知を出す
  • サウンドを鳴らす

という3つの処理に分かれます。

よって、それらの3つの処理を組み合わせてコーディングしてあります。

それぞれのAPI

時間が来たら処理をするAPIは

時間がきたらローカル通知を出すAPIは

サウンドを鳴らすのは

がそれぞれ担当しています。

サンプルコード

百聞は一見に如かず

ということで、早速サンプルコードです。

pubspec.yaml

name: backgroundalarm
description: backgroun Alarm
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
  sdk: '>=2.18.4 <3.0.0'
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  android_alarm_manager_plus: ^2.1.0
  flutter_ringtone_player: ^3.2.0
  flutter_local_notifications: 12.0.4
  flutter_native_timezone: ^2.0.0
flutter:
  uses-material-design: true

今回は

  1. android_alarm_manager_plus
  2. flutter_ringtone_player
  3. flutter_local_notifications
  4. flutter_native_timezone

の4つのAPIを利用しています。

main.dart

import 'package:flutter/material.dart';
//時間になったらバックグラウンドで音楽を鳴らすため
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
//時間になったらローカル通知を出すため
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
//ローカル通知で使うMethodChannelのため
import 'package:flutter/services.dart';
//ローカル通知で使うStreamControllerのため
import 'dart:async';
//音楽を慣らすため
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';
//ローカル通知の時間をセットするためタイムゾーンの定義が必要
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:flutter_native_timezone/flutter_native_timezone.dart';
//kIsWeb(Web判定)を使うため
import 'package:flutter/foundation.dart';
//Platform判定のため
import 'dart:io';
//アラーム用のID
const int alarmID = 123456789;
//鳴らしたい秒数
const int alramSecond = 5;
//ローカル通知のための準備
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
String? selectedNotificationPayload;
final StreamController didReceiveLocalNotificationStream = StreamController.broadcast();
final StreamController selectNotificationStream = StreamController.broadcast();
const MethodChannel platform = MethodChannel('dexterx.dev/flutter_local_notifications_example');
class ReceivedNotification {
  ReceivedNotification({
    required this.id,
    required this.title,
    required this.body,
    required this.payload,
  });
  final int id;
  final String? title;
  final String? body;
  final String? payload;
}
const String navigationActionId = 'id_3';
@pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse notificationResponse) {
  // ignore: avoid_print
  print('notification(${notificationResponse.id}) action tapped: '
      '${notificationResponse.actionId} with'
      ' payload: ${notificationResponse.payload}');
  if (notificationResponse.input?.isNotEmpty ?? false) {
    // ignore: avoid_print
    print(
        'notification action tapped with input: ${notificationResponse.input}');
  }
}
//タイムゾーン初期化メソッド定義
Future _configureLocalTimeZone() async {
  if (kIsWeb || Platform.isLinux) {
    return;
  }
  tz.initializeTimeZones();
  final String timeZoneName = await FlutterNativeTimezone.getLocalTimezone();
  tz.setLocalLocation(tz.getLocation(timeZoneName));
}
Future main() async{
  //アラームのための初期化
  WidgetsFlutterBinding.ensureInitialized();
  await AndroidAlarmManager.initialize();
  //タイムゾーン初期化
  await _configureLocalTimeZone();
  //通知のための初期化
  final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb &&
      Platform.isLinux
      ? null
      : await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
  if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
    selectedNotificationPayload =
        notificationAppLaunchDetails!.notificationResponse?.payload;
  }
  const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
  const InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid);
  await flutterLocalNotificationsPlugin.initialize(initializationSettings,
    onDidReceiveNotificationResponse:
        (NotificationResponse notificationResponse) {
      switch (notificationResponse.notificationResponseType) {
        case NotificationResponseType.selectedNotification:
          selectNotificationStream.add(notificationResponse.payload);
          break;
        case NotificationResponseType.selectedNotificationAction:
          if (notificationResponse.actionId == navigationActionId) {
            selectNotificationStream.add(notificationResponse.payload);
          }
          break;
      }
    },
    onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
  );
  //メイン画面呼び出し
  runApp(const MyApp());
}
//メイン画面
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo BackGround Play Sound Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'BackGround Play Sound Demo'),
    );
  }
}
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  @pragma('vm:entry-point')
  static Future callSoundStart() async {
    //音楽再生
    FlutterRingtonePlayer.playRingtone(looping: true);
  }
  //アラーム・通知セット
  Future setBackgroundAlarm() async {
    //アラームセット
    await AndroidAlarmManager.oneShot(
     const Duration(seconds: alramSecond),
      alarmID,
      callSoundStart,
      alarmClock: true,
      allowWhileIdle: true,
      exact: true,
      wakeup: true,
    );
    //通知セット
    await flutterLocalNotificationsPlugin.zonedSchedule(
        alarmID,
        '通知テストタイトル',
        '通知テスト本文',
        tz.TZDateTime.now(tz.local).add(const Duration(seconds: alramSecond)),
        const NotificationDetails(
            android: AndroidNotificationDetails(
                'full screen channel id', 'full screen channel name',
                channelDescription: 'full screen channel description',
                priority: Priority.high,
                playSound:false,
                importance: Importance.high,
                fullScreenIntent: true)),
        androidAllowWhileIdle: true,
        uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime);
  }
  //アラーム・通知を止める
   stopBackgroundArlam() async {
     //アラームを止める
     await AndroidAlarmManager.oneShot(
         const Duration(seconds: 0), alarmID, stopSound,
         exact: true, wakeup: true, alarmClock: true, allowWhileIdle: true);
     //通知を止める
     await flutterLocalNotificationsPlugin.cancel(alarmID);
   }
  @pragma('vm:entry-point')
  static stopSound() async {
    FlutterRingtonePlayer.stop();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("バックグラウンド再生"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed:() async {
                setBackgroundAlarm();
              },
              child: const Text("5秒後にサウンド再生"),
            ),
            ElevatedButton(
              onPressed:() async {
                stopBackgroundArlam();
              },
              child: const Text("サウンド停止"),
            ),
          ],
        ),
      ),
  );
  }
}

AndroidManifest.xml

※追加行が2か所あります。

以下ソース内の

  • ここを追加!その1
  • ここを追加!その2
を参照してください。
どちらも

『時間がきたらアラームを鳴らす許可をするための記載』

です。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.backgroundalarm">
   <application
        android:label="backgroundalarm"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
      <!-- ここを追加!その1START -->
       <service
           android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
           android:permission="android.permission.BIND_JOB_SERVICE"
           android:exported="false"/>
       <receiver
           android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
           android:exported="false"/>
       <receiver
           android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
           android:enabled="false"
           android:exported="false">
           <intent-filter>
               <action android:name="android.intent.action.BOOT_COMPLETED" />
           </intent-filter>
       </receiver>
  <!-- ここを追加!その1END -->
    </application>
  <!-- ここを追加!その2START -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <!-- For apps with targetSDK=31 (Android 12) -->
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
    <!-- ここを追加!その2END -->
</manifest>
AndroidManifest.xmlの場所は Projectエクスプローラーから android->app->src->mainの場所にあります。

サンプルコードのリンク先

上記のサンプルコードはGitHub上にも載せてあります。

補足

今回は単純な音をならすだけなので

flutter_ringtone_player

を使用していますが、

もっと本格的に音楽を再生したい!

と言う人は

just_audio

を使うことになると思います。

just_audioのサンプルコードは以下記事を参照してください。

後はサンプルコードを動かしたり、ソースを修正してみたりすればなんとなく動きはつかめると思います。

では!良いflutterライフを!

広告




関連記事