เนื้อหานี้จะเป็นแนวทางการใช้งานการตั้งเวลาการทำงาน โดยเฉพาะ
การทำงานในขณะปิดแอปอยู่ หรือไม่ได้ใช้งานแอป ในกรณีต้องการ
ใช้ความสามารถนี้กับงานพิเศษบางอย่าง เช่น การแจ้งเตือน การตั้งปลุก
และอื่นๆ ลักษณะการทำงานนี้ เราอาจจะคุ้นมาแล้ว ในบทความเกี่ยวกับ
การใช้งาน Flutter Local Notifacation ตามบทความลิ้งค์ด้านล่าง
การใช้งาน Flutter Local Notifications จัดการวิธ๊แจ้งเตือนใน Flutter http://niik.in/1125
ตัวแพ็กเก็จนี้ หลักๆ จะเป็นตัวใช้กำหนดเวลาการทำงานเป็นหลัก นั่นคือเวลาเราใช้งาน เราต้อง
นำไปประยุกต์ใช้ส่วนอื่นๆ เพิ่มเติมอีกที สำหรับแนวทางการนำไปใช้งาน เช่น
- สร้างระบบแจ้งเตือนแบบซ้ำๆ (เช่น แจ้งเตือนกิจกรรมหรือยา)
- อัปเดทข้อมูลเบื้องหลังเป็นระยะๆ (เช่น อัปเดทสถานะของแอปหรือข้อมูลจาก server)
- ทำงานตามเวลาที่กำหนด (เช่น ดาวน์โหลดข้อมูลในเวลากลางคืน)
ติดตั้ง package ที่จำเป็นเพิ่มเติม ตามรายการด้านล่าง
แพ็กเก็จที่จำเป็นต้องติดตั้งเพิ่มเติม สำหรับการทำงานมีดังนี้
android_alarm_manager_plus: ^4.0.4 shared_preferences: ^2.3.2 permission_handler: ^11.3.1
การกำหนดการขอสิทธิ์เข้าถึงการใช้งานข้อมูล และการทำงานบางอย่าง
ไฟล์ android > app > src > main > AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
....
...
<!-- เกี่ยวกับการแจ้งเตือน เพิ่ม 3 ส่วนนี้ -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<!-- For apps with targetSDK 31 (Android 12) and newer -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<application ....
....
</manifest>
และภายใน <application> เพิ่มส่วนนี้เข้าไป
<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>
ไฟล์ android > app > src > main > AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
.......
<application ....
<activity
android:name=".MainActivity"
android:exported="true"
..........
......
android:hardwareAccelerated="true"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:windowSoftInputMode="adjustResize">
..........
......
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<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>
</application>
....
</manifest>
แนวทางการใช้งาน Android alarm manager plus
เนื้อหาการใช้งานนี้เราจะนำเสนอแค่รูปแบบการใช้งานของแพ็กเก็จ ว่าทำอะไรบ้าง ทำยังไง เช่น เรา
สามารถกำหนดการตั้งเวลาอย่างไรได้บ้าง เช่น ตั้งเวลาให้ทำงานในอีก 10 นาทีข้างหน้า หรือตั้งเวลา
ที่ ณ วันที่หรือเวลาใดๆ โดยระบุเจาะจงลงไป หรือตั้งเวลาแบบให้ทำงานทุกๆ กี่นาที ชั่วโมง แบบนี้เป็นต้น
ในที่นี้เราใช้แพ็กเก็จ shared_preferences มาร่วมเพื่อเก็บค่าการนับจำนวนการทำงานของตัวตั้ง
เวลา เพื่อให้เห็นว่า สมมติเราตั้งเวลาไว้แล้วปิดแอปไป การทำงานก็ยังคงอยู่ เพราะมีการทำงานอยู่เบื้อง
หลัง ทำการบวกค่าข้อมูลและบันทึกไว้ และอีกแพ็กเก็จที่พลาดไม่ได้และส่วนใหญ่ควรต้องมีในทุกๆ แอป
ก็คือตัวจัดการ permission หรือการขอสิทธิ์ต่างๆ ในขณะทำงานของแอป หรือก็คือแพ็กเก็จที่ชื่อว่า
permission_handler เราสามารถใช้งานเพื่อขอใช้สิทธิ์ต่างๆ ที่ต้องการ และยังสามารถเปิดไปยังหน้า
จัดการสิทธิ์นั้นๆ ได้ง่ายอีกด้วย
คำอธิบายแสดงในโค้ด โค้ดการใช้งานตัวอย่าง จะคอมเม้นปิดไว้ หากอยากทดสอบตัวไหนก็เปิดใช้งาน
ไฟล์ alarmsimple.dart
import 'dart:isolate';
import 'dart:math';
import 'dart:ui';
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/material.dart';
class AlarmSimple extends StatefulWidget {
static const routeName = '/alarmsimple';
const AlarmSimple({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _AlarmSimpleState();
}
}
class _AlarmSimpleState extends State<AlarmSimple> {
// กำหนดตัวแปรใช้งาน SharedPreferences
late SharedPreferences _prefs;
bool _isPrefsLoaded = false; // ตัวแปรเพื่อเช็คว่าการโหลดเสร็จหรือยัง
// ตัวแปรสำหรับทดสอบ นับจำนวนการต้ั้งเวลา
int _counter = 0;
// ตัวกำหนดสถานะสิทธิ์การตั้งเวลา
PermissionStatus _exactAlarmPermissionStatus = PermissionStatus.granted;
// ตัวแปรสำหรับค่า alarm IDs สำหรับยกเลิก
List<int> alarmIds = [];
/// กำหนดค่าไว้เก็บ SharedPreferences key นับจำนวนการแจ้งเตือนแบบเก็บค่าในแอป
static String countKey = 'count';
/// กำหนดชื่อ port ของการทำงานแยก [SendPort] ทำงานเบื้องหลัง
/// ที่ใช้ร่วมกับ UI isolate (การทำงานผ่าน UI)
static String isolateName = 'isolate';
/// กำหนด port ที่ใช้ทำงานร่วมกันระหว่าง ส่วนทำงานเบื้องหลัง (background isolate)
/// และส่วนทำงานด้านหน้าผ่าน UI (UI isolate)
ReceivePort port = ReceivePort();
@override
void initState() {
super.initState();
_initializePort(); // ตั้งค่าการทำงานของ port ส่งค่าแยกการทำงาน
_initAlarm(); // ตั้งค่าเริ่มต้นการใช้งาน alarm manager
_initPrefs(); // ตั้งค่าเริ่มต้นการใช้งาน SharedPreferences ไว้เก็บข้อมูล
_checkExactAlarmPermission(); // ส่วนของการตรวจสอบสิทธิ์
}
// ฟังก์ชั่นการกำหนดลงทะเบียน port และกำหนดการทำงาน ที่รับค่ามาจาก background
void _initializePort() {
IsolateNameServer.registerPortWithName(
port.sendPort, // ใช้ port ส่งข้อมูล
isolateName, // กำหนดชื่อ port ใช้จากที่เรากำหนดด้านบน
);
// คอยรับค่าจากการทำงานเบื้องหลัง แล้วไปอัปเดทหน้า UI ถ้าต้องการ
// อย่างในที่เราทำการเพิ่มค่าการจำนวนการตั้งเวลา
// ในตัวอย่าง เรามีการส่งข้อมูลมา เลยรอรับค่าจากตัวแปร message
port.listen((dynamic message) async {
print("Update UI here");
await _incrementCounter(); // เรียกฟังก์ชั่นเพิ่มข้อมูล
print('Alarm triggered: $message');
});
}
// ฟังก์ชั่นคาเริ่มต้นการกำหนดการเก็บข้อมูลด้วย SharedPreferences
Future<void> _initPrefs() async {
// ตัวแปรอ้างอิงการใช้งาน SharedPreferences
_prefs = await SharedPreferences.getInstance();
// ตรวจสอบว่า เคยมี key ชื่อตามที่กำหนดหือไม่ ถ้าไมมีให้สร้างและมีค่าเเริ่มต้นเป็น 0
// นั่นคือค่าตัวไว้เก็บจำนวนการตั้งเวลา
if (!_prefs.containsKey(countKey)) {
await _prefs.setInt(countKey, 0);
}
setState(() {
_isPrefsLoaded = true; // อัปเดตสถานะเมื่อการโหลดเสร็จสิ้น
});
}
// ส่วนของการเรียกใช้งาน AndroidAlarmManager เริ่มต้น
void _initAlarm() async {
await AndroidAlarmManager.initialize();
}
// ฟักง์ชั่นตรวจสอบสถานะการอนุญาตตั้งเวลาหรือไม่
void _checkExactAlarmPermission() async {
final currentStatus = await Permission.scheduleExactAlarm.status;
setState(() {
// อัปเดทสถานะค่าปัจจุบัน
_exactAlarmPermissionStatus = currentStatus;
});
}
// ฟังก์ชั่นสำหรับการเพิ่มค่าจำนวนการตั้งเวลา
Future<void> _incrementCounter() async {
// โหลดข้อมูลจาก SharedPreferences เป็นค่าล่าสุดที่บันทึกไว้
await _prefs.reload();
setState(() {
// อัปเดทค่าในหน้าปัจจุบัน
_counter++;
});
}
// ส่วนของ port ที่ทำงานเบื้องหลัง
static SendPort? uiSendPort;
// ฟังก์ชั่น static ทำงานเบื้องหลัง เช่น อัปเดทข้อมูล SharedPreferences
@pragma('vm:entry-point')
static Future<void> callback() async {
print("debug: run");
print(DateTime.now().toIso8601String()); // เอาไว้ทดสอบดูเวลาการทำางน
// ดึงข้อมูลจาก SharedPreferences ล่าสุดมา แล้วอัปเดทค่า โดยเพิ่มจำนวนการตั้งเวลา
final prefs = await SharedPreferences.getInstance();
final currentCount = prefs.getInt(countKey) ?? 0; // ตรวจสอบค่าเดิมที่บันทึก
await prefs.setInt(countKey, currentCount + 1); // กำหนดค่าเป็นค่าใหม่
// มี port พร้อมทำงานเบื้องหลังหรือไม่ ถ้ามีคือไม่ใช่ null
// เป็น port ชื่อเดียวกับที่เรากำหนดไว้
uiSendPort ??= IsolateNameServer.lookupPortByName(isolateName);
if (uiSendPort != null) { // ถ้ามี port เพิ่มทำงาน
print("debug: SendPort found, sending message");
String message = "Alarm triggered!";
uiSendPort?.send(message); // ส่งข้อความไปแสดงหรืออัปเดทหน้า UI
} else { // ถ้าไม่มี port พร้อมใช้งาน
print("debug: SendPort not found!");
}
}
// ฟังก์ชั่นยกเลิกการตั้งเวลา ถ้ามีการส่ง id ของ alarmId มาก็ยกเลิกเฉพาะค่านั้น
// แต่ถ้าไม่มี เราจะใช้ค่าที่เคยเก็บไว้ใน ลิสตอนสร้าง มาวลูปล้างค่าทั้งหมด
void cancelAlarm([int? id]) async {
if(id!=null){ // มี id ส่งมา ยกเลิกเฉพาะไอดี
await AndroidAlarmManager.cancel(id);
print("debug: cancel alarmID = $id");
}else{ // ไม่ id ส่งมา วนลูปยกเลิกทั้งหมด
print("debug: cancel All alarmID");
for (int id in alarmIds) {
await AndroidAlarmManager.cancel(id);
}
}
}
@override
Widget build(BuildContext context) {
// แสดง loading จนกว่า _prefs จะถูกโหลดเสร็จ เพื่อป้องกัน error
if (!_isPrefsLoaded) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
appBar: AppBar(
title: Text('Alarm Simple'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'มีการตั้งเวลาขณะใช้งานแอปจำนวน: $_counter',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
'มีการตั้งเวลาตั้งแต่ติดตั้งแอป: ${_prefs.getInt(countKey).toString()} ',
textAlign: TextAlign.center,
),
// แสดงสถานะการอนุญาตตั้งเวลาหรือไม่
if (_exactAlarmPermissionStatus.isDenied) // ถ้วยังไม่ได้อนุญาต หรือได้รับสิทธิ์
Text(
'SCHEDULE_EXACT_ALARM is denied\n\nAlarms scheduling is not available',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
)
else // ถ้าได้รับสิทธิ์แล้ว
Text(
'SCHEDULE_EXACT_ALARM is granted\n\nAlarms scheduling is available',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 32),
// ปุ่มขอสิทธิ์การอนุญาตตั้งเวลา
ElevatedButton(
onPressed: _exactAlarmPermissionStatus.isDenied // ถ้ายังไม่อนุญาต
? () async {
// กดปุ่มเพื่อขอสิทธิ์การตั้งเวลา หากได้รับสิทธิ์ให้อัปเดทค่าสถานะ
await Permission.scheduleExactAlarm
.onGrantedCallback(() => setState(() {
_exactAlarmPermissionStatus =
PermissionStatus.granted;
}))
.request();
}
: null, // อนุญาตแล้ว กดไม่ได้ ไม่มีผล
child: const Text('ขอสิทธิ์การตั้งเวลา alarm permission'),
),
const SizedBox(height: 32),
// ส่วนของปุ่มทดสอบการตั้งเวลาแบบต่างๆ
ElevatedButton(
onPressed: _exactAlarmPermissionStatus.isGranted
? () async {
print("debug: 4");
// กำนหด alarmID แบบใช้ค่า randorm เพื่อให้มั่นใจว่าค่าจะไม่ซ้ำกัน
int alarmId = Random().nextInt(pow(2, 31) as int);
print('Set time: ${DateTime.now().toIso8601String()}'); // เอาไว้ทดสอบดูเวลาการทำางน
// การตั้งเวลาด้วย oneShot คือทำงานครั้งเดียว ในช่วงเวลาที่กำหนด
// เช่นตัวอย่าางด้านล่าง หลัง 5 วินาทีจากการตั้งเวลา ให้ทำงาน
/*
bool isAlarmSet = await AndroidAlarmManager.oneShot(
const Duration(seconds: 5), // ระบุระยะเวลาที่ต้องการให้ Alarm ทำงานหลังจากตั้งค่า Alarm
alarmId, // สร้าง ID แบบสุ่ม
callback, // ฟังก์ชันที่เรียกเมื่อ Alarm ทำงาน
exact: true, // ให้ทำงานตรงเวลาแน่นอน
wakeup: true, // ปลุกอุปกรณ์หากอยู่ในโหมด sleep
);
*/
/* กรณีรองรับการทำงานต่อแม้ปิดแอปไปแล้ว ใช้งาน allowWhileIdle และ rescheduleOnReboot
bool isAlarmSet = await AndroidAlarmManager.oneShot(
const Duration(seconds: 10),
alarmId, // สร้าง ID แบบสุ่ม
callback, // ฟังก์ชันที่เรียกเมื่อ Alarm ทำงาน
exact: true, // ให้ทำงานตรงเวลาแน่นอน
wakeup: true, // ปลุกอุปกรณ์หากอยู่ในโหมด sleep
allowWhileIdle: true, // อนุญาตให้ทำงานขณะอุปกรณ์ idle
rescheduleOnReboot: true, // ตั้งค่าใหม่เมื่อรีบูตเครื่อง
);
*/
// การตั้งเวลาด้วย oneShotAt ใช้ำกำหนด ณ วันที่หรือเวลาที่เจาะจง
// ตัวอย่างเช่น Schedule at 10:30 AM on October 25, 2024
/*
DateTime scheduledTime = DateTime(2024, 10, 25, 10, 30, 0);
bool isAlarmSet = await AndroidAlarmManager.oneShotAt(
scheduledTime, // ใช้เวลาที่เจาะจง
alarmId, // สร้าง ID แบบสุ่ม
callback, // ฟังก์ชันที่เรียกเมื่อ Alarm ทำงาน
exact: true, // ให้ทำงานตรงเวลาแน่นอน
wakeup: true, // ปลุกอุปกรณ์หากอยู่ในโหมด sleep
allowWhileIdle: true, // อนุญาตให้ทำงานขณะอุปกรณ์ idle
rescheduleOnReboot: true, // ตั้งค่าใหม่เมื่อรีบูตเครื่อง
);
*/
// การตั้งเวลาด้วย periodic เป็นการตั้งเวลาให้ทำซ้ำ ทุกๆ เวลาที่กำหนด ค่าน้อยสุด
// ตั้งแต่ 1 นาทีขึ้นไป ตั้งน้อยกว่า 1 นาทีไม่ได้
// เวลาที่เราทำงานครั้งแรก จะไม่สามารถระบุแน่นอนได้
// แต่หลังจากครั้งแรกทำงาน ครั้งต่อไปก็จะห่วงจากครั้งแรกทุกๆ 1 นาที หรือทุกค่าที่กำหนด
/*
bool isAlarmSet = await AndroidAlarmManager.periodic(
const Duration(minutes: 1), // ทุกๆ 1 นาที
alarmId, // สร้าง ID แบบสุ่ม
callback, // ฟังก์ชันที่เรียกเมื่อ Alarm ทำงาน
exact: true, // ให้ทำงานตรงเวลาแน่นอน
wakeup: true, // ปลุกอุปกรณ์หากอยู่ในโหมด sleep
allowWhileIdle: true, // อนุญาตให้ทำงานขณะอุปกรณ์ idle
rescheduleOnReboot: true, // ตั้งค่าใหม่เมื่อรีบูตเครื่อง
);
*/
// แต่ถ้ากำหนดร่วมกับ startAt หรือกำหนดเลาเริ่มต้น ใช้งานร่วมกับ periodic
// เวลาจะเริ่มนับทันทีหลังจากตั้งเวลา แต่เหมือนตัวตั้งเวลาก็ยังมี bug อยู่
// คือครั้งแรก จะเร็วกว่าปกติ 10 วินาที
DateTime now = DateTime.now(); // เริ่มจากเวลาปัจจุบัน
// ดังนั้นเราสามารถกำหนด เพิ่มไปอีก 10 วินาทีกับค่า startAt ได้ เช่น
DateTime startAt = DateTime(
now.year, now.month, now.day,
now.hour, now.minute + 1 ,10);
print("StartAt: ${startAt.toIso8601String()}");
bool isAlarmSet = await AndroidAlarmManager.periodic(
const Duration(minutes: 2), // ทุกๆ 1 นาที
alarmId, // สร้าง ID แบบสุ่ม
callback, // ฟังก์ชันที่เรียกเมื่อ Alarm ทำงาน
startAt: startAt, // เริ่มนับจากเวลาปัจจุบัน now หรือใช้ค่า startAt ที่เพิ่มอีก 10 วินาที
exact: true, // ให้ทำงานตรงเวลาแน่นอน
wakeup: true, // ปลุกอุปกรณ์หากอยู่ในโหมด sleep
allowWhileIdle: true, // อนุญาตให้ทำงานขณะอุปกรณ์ idle
rescheduleOnReboot: true, // ตั้งค่าใหม่เมื่อรีบูตเครื่อง
);
// ตรวจสอบผลลัพธ์การกำหนดการตั้งเวลา ว่าตั้งได้หรือไม่
if (isAlarmSet) {
print("Alarm ตั้งค่าเรียบร้อยแล้ว");
// เพิ่มค่ารายการ alarmId ที่ได้ตั้งแล้ว
alarmIds.add(alarmId);
} else {
print("การตั้งค่า Alarm ล้มเหลว");
}
}
: null, // ถ้าไม่มีสิทธิ์ตั้งเวลา จะกดปุ่มไม่ได้ ไม่มีการทำงานใดๆ
child: const Text('Set Alarm'),
),
// ปุ่มสำหรับล้างค่าการตั้วเวลาทั้งหมด สำหรับทดสอบเราตั้งให้ยกเลิกทั้งหมด
ElevatedButton(
onPressed: () async {
cancelAlarm(); // เรียกฟังก์ชันยกเลิก แบบไม่ส่ง id ไป
},
child: const Text('Clear All Alarm'),
),
const SizedBox(height: 32),
],
)
),
);
}
}
ผลลัพธ์ที่ได้

ก่อนอื่นให้เข้าใจว่า การตั้งเวลาด้วย alarm manager plus ไม่ใช่การตั้งเวลาปลุกในเมือถือ
เป็นการตั้งเวลาการทำงานอย่างใดอย่างหนึ่งที่เราต้องการ ดังนั้น ในตัวอย่างผลลัพธ์จึงไม่มีอะไรให้ดู
หรือสังเกตเป็นพิเศษ เราต้องนำไปปรับประยุกต์เพิ่มเติม เช่น ใช้ร่วมกับ flutter local notification
ทำระบบแจ้งเตือน หรือใช้ร่วมกับ flutter_tts ให้อ่านออกเสียงเวลาแจ้งเตือนแทนข้อความ ก็ได้
สำหรับการตั้งเวลาทำซ้ำๆ สมมติเรากำหนดให้ทำซ้ำๆ ทุกๆ 30 นาที นั้นไม่ได้หมายความว่า ทุกๆ
8.30 9.00 9.30 ไม่ใช่ในลักษณะนี้ แต่เป็นการนับจากเวลาที่เรากำหนดการตั้งค่า สมมติเราตั้งไปที่เวลา
8.20 ดังนั้นถ้าให้ทำทุกๆ 30 นาที ครั้งต่อไปก็จะเป็น 8.50 แบบนี้เป็นต้น อย่างไรก็ดี อาจจะไม่ตรงในระดับ
วินาทีในบางครั้งขึ้นกับหลายๆ ปัจจัย
หวังว่าแนวทางนี้จะสามารถนำไปปรัยประยุกต์ใช้งานต่อไปไม่มากก็น้อย