เนื้อหานี้จะเป็นแนวทางการใช้งานการตั้งเวลาการทำงาน โดยเฉพาะ
การทำงานในขณะปิดแอปอยู่ หรือไม่ได้ใช้งานแอป ในกรณีต้องการ
ใช้ความสามารถนี้กับงานพิเศษบางอย่าง เช่น การแจ้งเตือน การตั้งปลุก
และอื่นๆ ลักษณะการทำงานนี้ เราอาจจะคุ้นมาแล้ว ในบทความเกี่ยวกับ
การใช้งาน 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 แบบนี้เป็นต้น อย่างไรก็ดี อาจจะไม่ตรงในระดับ
วินาทีในบางครั้งขึ้นกับหลายๆ ปัจจัย
หวังว่าแนวทางนี้จะสามารถนำไปปรัยประยุกต์ใช้งานต่อไปไม่มากก็น้อย