บทความนี้ต่อยอดจากตอนที่แล้ว ที่เราได้รู้จักกับการสร้าง qrcode
เพื่อใช้งาน แชร์ หรือส่งต่อไปยังแอปอื่นหรือบันทึกลงไว้ในเครื่องไปแล้ว
เนื้อหาตอนนี้เราจะมาสร้างส่วนที่เราใช้สแกนหรือการตรวจจับ qrcode
เพื่ออ่านค่า และนำค่าที่ได้ไปใช้งาน โดยเราจะใช้ plugin ที่ชื่อว่า
mobile_scanner ในการจัดการ
ทบทวนบทความตอนที่แล้วได้ที่
สร้างคิวอาร์โค้ดในแอป Flutter ด้วย qr flutter อย่างง่าย http://niik.in/1120
ติดตั้ง package ที่จำเป็นเพิ่มเติม ตามรายการด้านล่าง
แพ็กเก็จที่จำเป็นต้องติดตั้งเพิ่มเติม สำหรับการทำงานมีดังนี้
mobile_scanner: ^5.2.3 flutter_ringtone_player: ^4.0.0+3 url_launcher: ^6.3.0
สำหรับ flutter_ringtone_player และ url_launcher ที่เพิ่มเข้ามา ก็เพื่อให้เราสามารถจัดการ
บางอย่างได้มากขึ้น โดย flutter_ringtone_player เราจะใช้สำหรับเล่นไฟล์เสียงจากเครื่อง เพื่อให้
รู้ว่ามีการสแกนและส่งผลลัพธ์กลับมาแล้ว ทำให้เรารับรู้และสังเกตได้ง่าย ส่วน url_launcher เราจะ
ใช้สำหรับจัดการกับรูปแบบข้อความที่สแกนได้เพื่อใช้งานต่อ เช่น ถ้าเป็นลิ้งค์ หรือข้อความ sms หรือ
ค่าอื่นๆ ตามรูปแบบข้อมูล ก็สามารถส่งต่อไปใช้งานได้เลย แทนที่จะแสดงข้อความธรรมดา โดยตัว
url_launcher จะจัดการกับลิ้งค์หรือข้อความนั้นๆ แล้วส่งต่อไปจัดการได้
การกำหนดการขอสิทธิ์เข้าถึงการใช้งานข้อมูล
จริงๆ เนื้อหานี้เป็นบทความต่อเนื่อง อย่างไรก็ดี เมื่อมีการใช้กล้อง เราควรเพิ่มการขอสิทธิ์การ
ใช้งานกล้องเข้าไปด้วย ด้านล่างเป็นสิทธิ์ทั้งหมดในโค้ดตัวอย่าง ดาวน์โหลดท้ายบทความ
ไฟล์ android > app > src > main > AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
<!-- For Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<application ....
....
</manifest>
การสแกน Qr Code และ Barcode ด้วย mobile_scanner
ความสามารถของ mobile_scanner นอกจากจะสแกนคิวอาร์โค้ดแล้ว ยังรองรับการสแกน
Barcode ได้อีกด้วย อย่างไรก็ตามการใช้งาน plugin ตัวนี้จะมีทั้งรูปแบบที่รวม MLKit
Barcode-scanning for Android และแบบไม่รวม ซึ่งหากไม่ต้องการตั้งค่าอะไร จะเป็นแบบ
รวมเข้ามาแล้ว แต่ก็จะมีส่วนให้ขนาดไฟล์ของแอปเราเพิ่มขึ้น 3-10 MB โดยประมาณ ขึ้นกับการ
ใช้งาน ในที่นี้เราจะใช้แบบรวมมาแล้ว เพื่อให้รองรับกับ android หรือมือถือที่อาจจะไม่มี Google
Play ได้ และด้วยความสามารถที่น่าพอใจก็ดีพอที่จะใช้งานในรูปแบบนี้
ไฟล์ scanner.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';
class Scanners extends StatefulWidget {
static const routeName = '/scanner';
const Scanners({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _ScannersState();
}
}
class _ScannersState extends State<Scanners> with WidgetsBindingObserver {
// กำหนดส่วนควบคุม scanner ปรับค่าต่างๆ
final MobileScannerController controller = MobileScannerController(
autoStart: false, // ไม่เริ่มอัตโนมัติ
torchEnabled: false, // ไม่เปิดแฟลชทันที
useNewCameraSelector: true, // ใช้การเรียกใช้กล้องแบบ API ใหม่
);
// กำหนดค่าเก็บผลลัพธ์ ในที่นี้เราจะมีอยู่ 2 ค่า
// _barcode ค่าที่ได้จากการสแกน และ _result ค่าไว้เก็บชั่วคราวไปส่งต่อ หรือปรับแต่ง
Barcode? _result;
Barcode? _barcode;
// ตัวแปรตรวจจับข้อมูลที่มีการส่งมาอยางต่อเนื่อง เช่น ข้อมูลเซ็นเซอร์ หรือข้อมูลจากกล้อง
StreamSubscription<Object?>? _subscription;
// ตัวจัดการควบคุมข้อมูล String ที่ได้จากการสแกน
TextEditingController _textController = TextEditingController();
// ฟังก์ชั่นจัดการข้อมูล barcode ถ้าไม่มีให้มีค่าเป็น null
void _handleBarcode(BarcodeCapture barcodes) {
if (mounted) {
setState(() {
_barcode = barcodes.barcodes.firstOrNull;
});
}
}
@override
void initState() {
super.initState();
// ตรวจจับสถานะของแอป เมื่อใช้ร่วมกับ mixin WidgetsBindingObserver
WidgetsBinding.instance.addObserver(this);
// ตรวจจับค่า stream การเปลี่ยนแปลงข้อมูลที่ต่อเนื่อง
_subscription = controller.barcodes.listen(_handleBarcode);
// เรียกใช้ controller.start() โดยไม่ต้องรอให้ทำงานเสร็จ
unawaited(controller.start());
}
// ตรวจจับการเปลี่ยนแปลงของสถานะแอปต่างๆ แล้วกำหนดการทำงาน
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!controller.value.isInitialized) {
return;
}
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
return;
// แอปกลับมาทำงานต่อ
case AppLifecycleState.resumed:
// ตรวจจับค่าข้อมูล
_subscription = controller.barcodes.listen(_handleBarcode);
unawaited(controller.start());
case AppLifecycleState.inactive:
// เมื่อไม่ active หรือเปิดแอปอื่นขึ้นมาใช้งานอยู่
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(controller.stop());
}
}
// ฟังก์ชั่นสำหรับเปิดหรือเรียกใช้ค่าจาก รูปแบบข้อมูล
Future<void> _launchURL(String url) async {
try {
// ตรวจสอบว่ามันเป็น URL หรือไม่
final Uri? parsedUrl = Uri.tryParse(url);
if (parsedUrl != null && (parsedUrl.isAbsolute || url.startsWith('geo:'))) {
// ถ้าเป็น URL ที่ถูกต้องหรือ geo link
if (await canLaunchUrl(parsedUrl)) {
await launchUrl(parsedUrl);
} else {
throw 'xdebug: Could not launch $url';
}
} else {
String finalUrl = url;
// ถ้าไม่ใช่ URL ให้ใช้ canLaunchUrlString
if (await canLaunchUrlString(finalUrl)) {
await launchUrlString(finalUrl);
} else {
throw 'xdebug: Could not launch $url';
}
}
} catch (e) {
print('Error launching URL: $e');
// สามารถแสดงข้อความแสดงข้อผิดพลาดให้ผู้ใช้ได้ที่นี่ เช่น:
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error launching URL: $e')));
}
}
// การล้างค่าต่างๆ เมื่อไม่มีการใช้งาน
@override
Future<void> dispose() async {
_textController.dispose();
WidgetsBinding.instance.removeObserver(this);
unawaited(_subscription?.cancel());
_subscription = null;
super.dispose();
await controller.dispose();
}
// ฟังก์ชั่นสำหรับคัดลองข้อความที่สแกน
Future<void> _copyToClipboard() async {
await Clipboard.setData(ClipboardData(text: _textController.text));
// แสดงข้อความเมื่อคัดลอกข้อความแล้ว
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('คัดลอกข้อมูลเรียบร้อยแล้ว')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('สแกน บาร์โค้ด / คิวอาร์โค้ด'),
),
body: SingleChildScrollView(
child: Column(
// mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
height: MediaQuery.of(context).size.height * 0.60, // กำหนดความสูงที่ต้องการ
child: _buildScanner(context),
),
// Expanded(flex: 3, child: _buildScanner(context)),
Container(
child: Column(
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
// ส่วนของการแสดงข้อมูล เมื่อสแกนแล้ว ไม่เป็นค่า null
if (_barcode != null)
Builder(builder: (context) {
// print("xdebug: ${_barcode!.rawValue!}");
// ถ้าผลลลัพธ์ชั่วคราวเป้น null หรือ ค่าเดิมไม่เหมือนค่าใหม่ที่สแกน
if (_result == null ||
(_result!.displayValue! != _barcode!.displayValue!)) {
// เก็บค่าชั่วคราว จากค่าที่เพิ่งสแกนมา
_result = _barcode;
// เล่นไฟล์เสียงแจ้งเตือนใช้ค่าเริ่มต้นของระบบ
FlutterRingtonePlayer().playNotification();
}
// กำหนดค่า string ข้อความที่สแกนมาได้
/* _textController.value =
TextEditingValue(text: _barcode!.displayValue!); */
// กรณีเป็นค่าที่ไม่ใช่ข้อความ เช่น พวก vCard WIFI Event ใช้ค่า rawValue
_textController.value =
TextEditingValue(text: _barcode!.rawValue!);
// กำหนดรูปแบบข้อมูลที่รองรับการเปิด้วย url_launcher
// ในที่นี้จะไม่รองรับการเปิด WIFI VCard และ Event Calendar
final canLaunchList = ['http:','https:','tel:','sms:', 'mailto:', 'geo:'];
bool showLaunch = canLaunchList.any((field) => _textController.text.toString().contains(field));
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 300.0,
child: TextFormField(
controller: _textController,
decoration: InputDecoration(
fillColor: Color(0xFFfeeeee), // สีพื้นหลัง
filled: true, // กำหนดถ้ามีใส่สีพื้นหลัง fillColor
prefixIcon: IconButton(
onPressed: _copyToClipboard, // คัดลอกข้อความ
icon: const FaIcon(FontAwesomeIcons.copy)),
// ตรวจสอบรูปแบบข้อความ เพื่อกำหนดปุ่มด้านหลัง สามารถเรียกใช้งานหรือไม่
suffixIcon: showLaunch
? IconButton(
onPressed: () {
// ถ้าเป็นลิ้งค์ เรียกใช้งานฟังก์ชั่น _launchURL
_launchURL(_textController.text);
},
icon: Builder(builder: (context) {
// ตรวจสอบว่าเป็นลิ้งค์ประเภทไหน เช่น ไลน์ อีเมล sms เบอร์โทร หรืออื่นๆ
// แล้วกำหนดรูปแบบไอคอนให้สอดคล้อง
if (_textController.text
.toString()
.indexOf(':\/\/line') >
-1) {
return const FaIcon(
FontAwesomeIcons.line);
} else if (_textController.text
.toString()
.indexOf('mailto:') >
-1) {
return const FaIcon(
FontAwesomeIcons.envelope);
} else if (_textController.text
.toString()
.indexOf('sms:') >
-1) {
return const FaIcon(
FontAwesomeIcons.commentSms);
} else if (_textController.text
.toString()
.indexOf('tel:') >
-1) {
return const FaIcon(
FontAwesomeIcons.mobileScreenButton);
} else if (_textController.text
.toString()
.indexOf('geo:') >
-1) {
return const FaIcon(
FontAwesomeIcons.map);
} else if (_textController.text
.toString()
.indexOf('youtu.be') >
-1 ||
_textController.text
.toString()
.indexOf('youtube.com') >
-1) {
return const FaIcon(
FontAwesomeIcons.youtube);
} else if (_textController.text
.toString()
.indexOf('facebook.com') >
-1 ||
_textController.text
.toString()
.indexOf('fb:\/\/') >
-1) {
return const FaIcon(
FontAwesomeIcons.facebook);
} else if (_textController.text
.toString()
.indexOf('tiktok.com') >
-1) {
return const FaIcon(
FontAwesomeIcons.tiktok);
} else {
return const FaIcon(FontAwesomeIcons
.upRightFromSquare);
}
}),
)
: null,
),
maxLines: null, // ทำให้ TextField ขยายได้ไม่จำกัด
minLines: 1, // จำนวนแถวขั้นต่ำ
readOnly: true, // ทำให้เป็นแบบอ่านอย่างเดียว
),
),
);
}),
// แสดงข้อความแนะนำ หรือคำอธิบายการใช้งาน
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: const Text(
'สแกนบาร์โค้ด หรือคิวอาร์โค้ด',
style: TextStyle(
fontSize: 20.0,
color: Colors.black54,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// ส่วนของการกำหนดการใช้งานแฟลช
ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
// ถ้ากล้องไม่พร้อมใช้งานหรือยังไม่ทำงน แสดง
// สร้าง widget กล่องที่มีขนาดเล็กที่สุดเท่าที่จะเป็นไปได้ ขนาด 0
// หรือ widget ที่ "ว่างเปล่า"
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
// เงื่อนไขของการแสดงปุ่มเปิดแฟลช หรือปิดแฟลช หรือแบบ auto
switch (state.torchState) {
case TorchState.auto:
return ElevatedButton(
style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFFf8a8ab),
),
onPressed: () async {
await controller.toggleTorch();
setState(() {});
},
child: const Icon(
Icons.flash_auto,
color: Color(0xFF404040),
),
);
case TorchState.off:
return ElevatedButton(
style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFFf8a8ab),
),
onPressed: () async {
await controller.toggleTorch();
setState(() {});
},
child: const Icon(
Icons.flash_off,
color: Color(0xFF404040),
),
);
case TorchState.on:
return ElevatedButton(
style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFFf8a8ab),
),
onPressed: () async {
await controller.toggleTorch();
setState(() {});
},
child: const Icon(
Icons.flash_on,
color: Color(0xFF404040),
),
);
case TorchState.unavailable:
return ElevatedButton(
style: ElevatedButton.styleFrom(
// backgroundColor: ui.Color.fromARGB(255, 226, 221, 222),
),
onPressed: () async {
setState(() {});
},
child: const Icon(
Icons.no_flash,
color: Colors.grey,
),
);
}
},
),
SizedBox(
width: 10.0,
),
// ส่วนของการกำหนดกล้องหน้า กล้องหลัง ถ้ามี ตรวจสอบจาก controller
ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
// ถ้ากล้องไม่พร้อมใช้งานหรือยังไม่ทำงน แสดง
// สร้าง widget กล่องที่มีขนาดเล็กที่สุดเท่าที่จะเป็นไปได้ ขนาด 0
// หรือ widget ที่ "ว่างเปล่า"
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
// ตรวจสอบค่าสถานะจากกล้อง
final int? availableCameras = state.availableCameras;
// print("xdebug: ${availableCameras}");
// ตรวจสอบค่าสถานะจำนวนกล้องที่รองรับ
// ถ้ามีแค่กล้องเดียว ไม่ต้องแสดงอะไร โขว์ widget ที่ "ว่างเปล่า" แทน
// เพราะไม่จำเป็นต้องสลับกล้อง
if (availableCameras != null &&
availableCameras < 2) {
return const SizedBox.shrink();
}
// กำหนด widget สำหรับไอคอนแสดงกล้อง
final Widget icon;
// กรณีมีมากกว่า 1 กล้อง
// ใช้กล้องไหนอยู่แสดงไอคอนตามกล้องนั้น
switch (state.cameraDirection) {
case CameraFacing.front:
icon = const Icon(Icons.camera_front,
color: Color(0xFF404040));
case CameraFacing.back:
icon = const Icon(Icons.camera_rear,
color: Color(0xFF404040));
}
// แสดงปุ่มสลับกล้อง
return ElevatedButton(
style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFFf8a8ab),
// elevation: 0.0,
),
onPressed: () async {
// เรียกใช้คำสั่งสลับกล้อง
await controller.switchCamera();
setState(() {});
},
child: icon,
);
},
),
SizedBox(
width: 10.0,
),
// ส่วนปุ่มใช้งาน และหยุดชั่วคราว ให้แสดงขึ้นกับค่า controller
ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
// ถ้ากล้องหยุดชั่วคราวอยู่ หรือยังไม่ทำงาน แสดงปุ่มใช้งานต่อ
if (!state.isInitialized || !state.isRunning) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
),
onPressed: () async {
// เรียรกใช้คำสั่งใช้งานกล้องต่อ
await controller.start();
setState(() {});
},
child: const Text('ใช้งานต่อ',
style: TextStyle(
fontSize: 20,
color:
Color(0xFFf92D050))),
);
}
// ถ้ากล้องใช้งานอยู่ แสดงปุม หยุดชั่วคราว
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
),
onPressed: () async {
// เรียกใช้คำส่งหยุดกล้องชั่วคราว
await controller.stop();
setState(() {});
},
child: const Text('หยุดชั่วคราว',
style: TextStyle(
fontSize: 20,
color: Color(0xFFf92D050))),
);
},
),
],
),
],
),
),
],
),
),
);
}
// ฟังก์ชั่นสร้างส่วนของช่องสแกน
Widget _buildScanner(BuildContext context) {
// กำหนดความกว้าง ความสูง ตามต้องการ
final double overlayHeight = 250.0; // Set custom height
final double overlayWidth = 300.0; // Set custom width
return Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: Container(
color: Colors.black, // Background overlay
),
),
// วางตำแหน่งของสแกนเนอร์
Center(
child: Container(
width: overlayWidth,
height: overlayHeight,
child: ClipRRect(
borderRadius: BorderRadius.circular(
16.0), // กำหนดความมนของกรอบ
child: MobileScanner(
fit: BoxFit.cover,
controller: controller,
),
),
),
),
// ปรับแต่งพื้นที่สแกนเพิ่มเติม เช่นใส่เส้นขอบ
Center(
child: Container(
width: overlayWidth,
height: overlayHeight,
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFf92D050),
width: 3.0,
),
borderRadius: BorderRadius.circular(16.0),
),
),
),
],
);
}
}
ผลลัพธ์ที่ได้

เราสามารถนำไปสแกนรูปแบบ qrcode จากเนื้อหาตอนที่แล้วทั้ง 10 รูปแบบได้ โดยในโค้ดตัวอย่าง
จะรองรับ ข้อความทั่วไป ลิ้งค์เว็บไซต์ทั่วไป sms เบอร์โทร อีเมล พิกัดในแผนที่ โดยจะแสดงทั้งข้อมูล
และสามารถกดลิ้งค์ด้านหลังเพื่อเปิดไปยังแอปที่เกี่ยวข้องได้ เช่น เบอร์โทร กดเข้าไปก็จะไปหน้าโทรให้
อีเมล ก็จะไปหน้าส่งอีเมลให้ แบบนี้เป็นต้น ส่วนอีก 3 รูปแบบที่เหลือ คือ WIFI VCard และ Calendar
Event จะไม่รองรับการเรียกใช้งานด้วย url_launcher ดังนั้น เราจึงแสดงแค่ปุ่มด้านหน้า ให้สามารถ
คัดลอกข้อความได้เท่านั้น ตัวสแกนเนอร์นี้ รองรับการสแกน Barcode ด้วย
เพิ่มเติมส่วนของ url_launcher
ในตัวอย่างจะเห็นว่าเรามีการเปิดไปยังแอปต่างๆ ที่เกี่ยวข้องกับรูปแบบข้อมูลของ qrcode ซึ่งในส่วนนี้
เราจำเป็นต้องกำหนดเพิ่มเติมในไฟล์ AndroidManifest.xml ส่วนของค่าการ <queries> หรือการ
บอกให้แอปนั้นสามารถรองรับการทำงานบางสิ่งบางอย่าง หรือการใช้งานร่วมกับแอปที่เกี่ยวข้องได้
ไฟล์ android > app > src > main > AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
<!-- For Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<application
android:label="Demo App"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
..........
</application>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="sms" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
</manifest>
การกำหนดค่าภายในแท็ก <queries> ในไฟล์ AndroidManifest.xml เป็นการแจ้งว่าแอป
ของเราต้องการค้นหาและสามารถทำงานร่วมกับแอปพลิเคชันอื่นๆ ที่สามารถตอบสนองต่อ intent
ที่กำหนดไว้ได้ โดยเฉพาะการทำงานร่วมกับข้อมูลหรือบริการต่างๆ ที่ระบุโดย scheme เช่น ข้อความ
(SMS), การโทร (tel), แผนที่ (geo), เว็บไซต์ (http, https), อีเมล (mailto) เป็นต้น
หวังว่าเนื้อหาตอนนี้จะเป็นประโยชน์ไม่มากก็น้อยในการนำปรับไปประยุกต์ใช้งานต่อไป สำหรับ
บทความในตอนหน้าจะเป็นอะไร รอติดตาม
