เนื้อหานี้จะมาดูเกี่ยวกับการใช้งาน WebView widget ซึ่งจะเป็น
package ที่เราจะต้องติดตั้งเพิ่ม เพื่อใช้งานใน flutter ใช้สำหรับ
แสดงหน้าเว็บเพจใน flutter ถ้าเข้าใจอย่างง่ายก็คือเป็นเหมือนมี
บราวเซอร์เล็กๆ ใน app ของเรา สามารถลิ้งค์ไปยังหน้าเพจต่างๆ ได้
เช่น ใช้สำหรับนำเสนอข้อมูลหรือหน้าเพจบางอย่าง อย่างเช่นหน้า นโยบาย
ข้อมูลความเป้นส่วนตัว policy หรือหน้าเพจอื่นๆ ที่ต้องการ
เนื้อหานี้จะใช้รูปแบบเริ่มต้นจากลิ้งค์บทความด้านล่างเป็นแนวทาง
จัดการข้อมูล Model และแนวทางการนำมาใช้งาน ใน Flutter http://niik.in/1041
https://www.ninenik.com/content.php?arti_id=1041 via @ninenik
*เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/961
เตรียมข้อมูลสำหรับใช้งาน WebView
ส่วนนี้จะยังไม่เกี่ยวกับการใช้งาน webview แต่จะเป็นการเตรียมข้อมูลสำหรับใช้งาน
ร่วมกับเนื้อหาในบทความ เราจะใช้ข้อมูลจากบทความในเว็บไซต์ ninenik.com โดยให้สร้าง
Data Model ไว้ในโฟลเดดร์ models ในชื่อไฟล์ article_model.dart และมีรูปแบบดังนี้
lib > models > article_model.dart
ไฟล์ article_model.dart
// Data models class Article { final String id; final String topic; final String description; final String date; final String? image; final String url; final String view; final String? lastvisited; Article({ required this.id, required this.topic, required this.description, required this.date, this.image, required this.url, required this.view, this.lastvisited, }); // ส่วนของ name constructor ที่จะแปลง json string มาเป็น Article object factory Article.fromJson(Map<String, dynamic> json) { return Article( id: json['id'], topic: json['topic'], description: json['description'], date: json['date'], image: json['img'], url: json['url'], view: json['view'], lastvisited: json['lastvisited'], ); } }
ต่อไปสร้างไฟล์ที่จแสดงข้อมูลหรือใช้งาน webview ในโฟลเดอร์ screen ใช้ชื่อไฟล์เป็น article.dart
lib > screen > article.dart
ไฟล์ article.dart
import 'package:flutter/material.dart'; class Articles extends StatefulWidget { static const routeName = '/articles'; const Articles({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _ArticlesState(); } } class _ArticlesState extends State<Articles> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Articles'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Articles Screen'), ], ) ), ); } }
ต่อไปส่วนของไฟล์ทดสอบหน้า home.dart จะเป็นดังนี้
ไฟล์ home.dart
import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'article.dart'; import '../models/article_model.dart'; class Home extends StatefulWidget { static const routeName = '/'; const Home({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _HomeState(); } } class _HomeState extends State<Home> { // กำนหดตัวแปรข้อมูล articles late Future<List<Article>> articles; // ตัว ScrollController สำหรับจัดการการ scroll ใน ListView final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); articles = fetchArticle(); } Future<void> _refresh() async { setState(() { articles = fetchArticle(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Home'), ), body: Center( child: FutureBuilder<List<Article>>( // ชนิดของข้อมูล future: articles, // ข้อมูล Future builder: (context, snapshot) { // มีข้อมูล และต้องเป็น done ถึงจะแสดงข้อมูล ถ้าไม่ใช่ ก็แสดงตัว loading if (snapshot.hasData) { bool _visible = false; // กำหนดสถานะการแสดง หรือมองเห็น เป็นไม่แสดง if(snapshot.connectionState == ConnectionState.waiting){ // เมื่อกำลังรอข้อมูล _visible = true; // เปลี่ยนสถานะเป็นแสดง } if(_scrollController.hasClients){ //เช็คว่ามีตัว widget ที่ scroll ได้หรือไม่ ถ้ามี // เลื่อน scroll มาด้านบนสุด _scrollController.animateTo(0, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); } return Column( children: [ Visibility( child: const LinearProgressIndicator(), visible: _visible, ), Container( // สร้างส่วน header ของลิสรายการ padding: const EdgeInsets.all(5.0), decoration: BoxDecoration( color: Colors.orange.withAlpha(100), ), child: Row( children: [ Text('Total ${snapshot.data!.length} items'), // แสดงจำนวนรายการ ], ), ), Expanded( // ส่วนของลิสรายการ child: snapshot.data!.isNotEmpty // กำหนดเงื่อนไขตรงนี้ ? RefreshIndicator( onRefresh: _refresh, child: ListView.separated( // กรณีมีรายการ แสดงปกติ controller: _scrollController, // กำนหนด controller ที่จะใช้งานร่วม itemCount: snapshot.data!.length, itemBuilder: (context, index) { Article article = snapshot.data![index]; Widget card; // สร้างเป็นตัวแปร card = Card( margin: const EdgeInsets.all(5.0), // การเยื้องขอบ child: Column( children: [ ListTile( leading: Image.network(article.image!), title: Text(article.topic, maxLines: 2, overflow: TextOverflow.ellipsis, ), subtitle: Text('View: ${article.view}'), trailing: Icon(Icons.more_vert), onTap: (){ Navigator.push( context, MaterialPageRoute(builder: (context) => Articles(), settings: RouteSettings( arguments: article.url // ส่งค่าไปใน arguments ), ), ); }, ), ], ) ); return card; }, separatorBuilder: (BuildContext context, int index) => const SizedBox(), ), ) : const Center(child: Text('No items')), // กรณีไม่มีรายการ ), ], ); } else if (snapshot.hasError) { // กรณี error return Text('${snapshot.error}'); } // กรณีสถานะเป็น waiting ยังไม่มีข้อมูล แสดงตัว loading return const RefreshProgressIndicator(); }, ), ), ); } } // สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล Future ประเภท List ของ Article Future<List<Article>> fetchArticle() async { // ทำการดึงข้อมูลจาก server ตาม url ที่กำหนด final response = await http .get(Uri.parse('https://www.ninenik.com/demo/article_api.php')); // เมื่อมีข้อมูลกลับมา if (response.statusCode == 200) { // ส่งข้อมูลที่เป็น JSON String data ไปทำการแปลง เป็นข้อมูล List<Article // โดยใช้คำสั่ง compute ทำงานเบื้องหลัง เรียกใช้ฟังก์ชั่นชื่อ parseArticles // ส่งข้อมูล JSON String data ผ่านตัวแปร response.body return compute(parseArticles, response.body); } else { // กรณี error throw Exception('Failed to load article'); } } // ฟังก์ชั่นแปลงข้อมูล JSON String data เป็น เป็นข้อมูล List<Article> List<Article> parseArticles(String responseBody) { final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>(); return parsed.map<Article>((json) => Article.fromJson(json)).toList(); }
ผลลัพธ์ที่ได้
การทำงานก็คือแสดงรายการบทความล่าสุด และเมื่อกดไปหน้าบทความใดๆ ก็จะเปิดไปหน้า
article เป็นหน้าแสดงรายละเอียดหรือหน้าที่จะเปิดหน้าเพจ ของ ลิ้งค์ url ที่ส่งไปมาแสดง
เกี่ยวกับโค้ดไฟล์ home.dart เป็นเนื้อหาเดิมทั้งหมดเปลี่ยนแค่ข้อมูล ทบทวนได้ที่บทความ
ผ่านๆ มา เราจะสนใจที่ไฟล์ article.dart ซึ่งจะเป็นไฟล์ที่เราจะใช้งาน webview เพื่อแสดงหน้าเพจ
เตรียมพร้อมก่อนใช้งาน WebView
มาต่อในส่วนของการใช้งาน WebView ก่อนอื่นเราจะต้องทำการติดตั้ง package ที่ชื่อว่า
webview_flutter พยายามใช้ให้เป็นเวอร์ชั่นปัจจุบันที่สุด
การติดตั้ง WebView package
ในส่วนของไฟล์ pubspec.yaml ให้เราเพิ่มการเรียกใช้งาน package เข้าไปดังนี้
dependencies: webview_flutter: ^4.8.0
ตัวอย่างการเพิ่ม
เนื่องจากการใช้งาน WebView จะมีการกำหนดในเรื่องของ API level ของ android ต่ำสุดที่รองรับ
เราจะต้องกำหนดเป็น 19 หรือ 20 ขึ้นไป ในที่นี้จะกำหนดเป็น 19 โดยให้ไปแก้ไขที่ไฟล์ build.gradle
android > app > build.gradle
กำหนด minSdkVersion เป็น 19 ตามรูป ถ้าเราไม่กำหนด จะไม่สามารถ build ผ่านได้
สำหรับหน้าทื่จะใช้งาน WebView เราก็ import package เข้ามาใช้งาน ดังนี้
import 'package:webview_flutter/webview_flutter.dart';
การกำหนดและใช้งาน WebView
เราจะมาลงรายละเอียดในการใช้งาน webview ในไฟล์ article.dart ซึ่งนอกจาก webview package แล้ว
เรายังต้องมีการใช้งาน async และ io library ของ dart ร่วมด้วย ทั้งนี้เพราะ ในการแสดงข้อมูลและโหลด
ข้อมูลจะมีเรื่องของเวลาที่ต้องรอเข้ามาเกี่ยวข้อง รวมกึงการใช้งานส่วนของ io ที่จัดการเกี่ยวกับ input ouput
โดยเฉพาะ keyboard ให้สามารถใช้งานในหน้าเพจที่แสดงได้
การกำหนดไฟล์ article.dart เพื่อแสดงเว็บเพจเบื้องต้น
ไฟล์ article.dart
import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class Articles extends StatefulWidget { static const routeName = '/articles'; const Articles({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _ArticlesState(); } } class _ArticlesState extends State<Articles> { // กำหนดตัวแปร controler สำหรับควบคุมการทำงาน final WebViewController _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { // Update loading bar. }, onPageStarted: (String url) {}, onPageFinished: (String url) {}, onHttpError: (HttpResponseError error) {}, onWebResourceError: (WebResourceError error) {}, onNavigationRequest: (NavigationRequest request) { // ส่วนของการกำหนดเงื่อนไขการทำงาน เช่น ป้องกันไม่ให้เข้าเว็บยูทูป หรืออื่นๆ ถ้ามีเพิ่มเติม if (request.url.startsWith('https://www.youtube.com/')) { return NavigationDecision.prevent; } return NavigationDecision.navigate; }, ), ); @override void initState() { super.initState(); } @override Widget build(BuildContext context) { // รับค่า url ที่ส่งมาใน arguments final url = ModalRoute.of(context)!.settings.arguments as String; _controller.loadRequest(Uri.parse(url)); return Scaffold( appBar: AppBar( title: Text('Articles'), ), body: WebViewWidget(controller: _controller), ); } }
ผลลัพธ์ที่ได้
เรามีการใช้งาน Library และ Package ที่เกี่ยวข้อง
import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart';
จากนั้นกำหนด controller สำหรับใช้งาน
// กำหนดตัวแปร controler สำหรับควบคุมการทำงาน final WebViewController _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { // Update loading bar. }, onPageStarted: (String url) {}, onPageFinished: (String url) {}, onHttpError: (HttpResponseError error) {}, onWebResourceError: (WebResourceError error) {}, onNavigationRequest: (NavigationRequest request) { // ส่วนของการกำหนดเงื่อนไขการทำงาน เช่น ป้องกันไม่ให้เข้าเว็บยูทูป หรืออื่นๆ ถ้ามีเพิ่มเติม if (request.url.startsWith('https://www.youtube.com/')) { return NavigationDecision.prevent; } return NavigationDecision.navigate; }, ), );
ส่วนของการรับค่า url ที่ส่งเข้ามา และนำไปเรียกใช้งาน
// รับค่า url ที่ส่งมาใน arguments final url = ModalRoute.of(context)!.settings.arguments as String; _controller.loadRequest(Uri.parse(url));
สุดท้ายเรียกใช้งาน WebView widget
return Scaffold( appBar: AppBar( title: Text('Articles'), ), body: WebViewWidget(controller: _controller), );
ในการใช้งานร่วมกับ WebView การกำหนด controller ถือว่าเป็นสิ่งสำคัญมาก ทั้งนี้ก็เพราะว่าข้อมูล
ที่เกี่ยวกับข้องจะเป็นข้อมูลที่มีเรื่องของเวลาที่ต้องรอ หรือเป็นข้อมูลแบบ async การสร้าง widget ต่างๆ
มาใช้งานร่วมกับข้อมูลของเว็บเพจ จึงต้องมีข้อมูล Future มาเกี่ยวข้อง
การตั้งค่าเพิ่มเติมใน WebView
final WebViewController _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { print("WebView is loading (progress : $progress%)"); // Update loading bar. }, onPageStarted: (String url) { // กำหนดการทำงานเมื่อมีการโหลดเว็บเพจ print('Page started loading: $url'); }, onPageFinished: (String url) { // กำหนดการทำงานเมื่อโหลดเว็บเพจเสร็จสิ้น 100% print('Page finished loading: $url'); }, onHttpError: (HttpResponseError error) {}, onWebResourceError: (WebResourceError error) {}, onNavigationRequest: (NavigationRequest request) {// กำหนดการทำงานเมื่อคลิกลิ้งค์ในเว็บเพจ // เช่นการตรวจ url และ block ไม่ให้ใช้้งาน url ที่กำหนด if (request.url.startsWith('https://www.youtube.com/')) { print('blocking navigation to $request}'); return NavigationDecision.prevent; // ถ้าเป็นจากลิ้งค์ youtube ให้ block } print('allowing navigation to $request'); return NavigationDecision.navigate; // ถ้าเป็นลิ้งค์อื่นๆ เข้าไปปกติ }, ), );
การ block ลิ้งค์ตามรูปแบบเงื่อนไขที่กำหนด หน้าเพจจะไม่มีการเปลี่ยนแปลงเมื่อคลิกไปยังลิ้งค์
ที่ถูก block
การสร้างปุ่มควบคุมใน WebView และจัดการด้วย controller
เราต้องการให้มีปุ่มควบคุมพื้นฐานใน appbar เช่น ปุ่มย้อนกลับ ปุ่มไปข้างหน้า และปุ่ม รีเฟรช เพื่อจัดการกับ
webview ที่กำลังใช้งานอยู่ โดยจะทำการสร้าง widget มาใช้งานดังนี้
// สร้าง widget สำหรับทำปุ่มควบคุม เช่น ก่อนหน้า ย้อนหลัง รีเฟรช class NavigationControls extends StatelessWidget { // กำหนด class constructor รับค่าที่จำเป็น const NavigationControls({ required this.controller, required this.canGoBack, required this.canGoForward, Key? key, }) : super(key: key); // กำหนดตัวแปรที่เกี่ยวข้อง /* ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management library ที่ซับซ้อน เช่น Provider หรือ Bloc */ final WebViewController controller; final ValueNotifier<bool> canGoBack; final ValueNotifier<bool> canGoForward; /* ValueListenableBuilder เป็น widget ที่ใช้ในการสร้าง UI ที่ฟังการเปลี่ยนแปลงค่าของ ValueNotifier และทำการ rebuild UI เมื่อค่าของ ValueNotifier มีการเปลี่ยนแปลง */ @override Widget build(BuildContext context) { return Row( children: <Widget>[ ValueListenableBuilder<bool>( valueListenable: canGoBack, builder: (context, value, child) { return IconButton( icon: const Icon(Icons.arrow_back), onPressed: value ? () => controller.goBack() : null, ); }, ), ValueListenableBuilder<bool>( valueListenable: canGoForward, builder: (context, value, child) { return IconButton( icon: const Icon(Icons.arrow_forward), onPressed: value ? () => controller.goForward() : null, ); }, ), IconButton( icon: const Icon(Icons.refresh), onPressed: () => controller.reload(), ), ], ); } }
เพื่อควบคุมการทำงานของ webview เราจึงส่งค่า WebViewController เป็น paramter เข้ามา
ใช้งานใน widget นี้ หลักการทำงานก็คือสร้าง ปุ่ม ก่อหน้า ย้อนหลัง และรีเฟรช แล้วควบคุมด้วย /div>
ผลลัพธ์ที่ได้
ผลลัพธ์ที่ได้
controller ที่เราส่งเข้ามา เช่น
controller.goBack(); // ย้อนหลังไปหน้าก่อนหน้า controller.goForward(); // ไปหน้าถัดไป controller!.reload(); // โหลดหน้าเว็บเพจใหม่อีกครั้ง
ปรับแต่งไฟล์ artcile.dart ใหม่เป็นดังนี้
import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class Articles extends StatefulWidget { static const routeName = '/articles'; const Articles({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _ArticlesState(); } } class _ArticlesState extends State<Articles> { // แก้ไขตัวแปรสำหรับ contrller ใหม่ ให้เป็นชนิดข้อมูล late late final WebViewController _controller; /* ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management library ที่ซับซ้อน เช่น Provider หรือ Bloc */ // กำหนดค่าเริ่มต้นเป็น false final ValueNotifier<bool> _canGoBack = ValueNotifier<bool>(false); final ValueNotifier<bool> _canGoForward = ValueNotifier<bool>(false); @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { print("WebView is loading (progress : $progress%)"); // Update loading bar. }, onPageStarted: (String url) async { _canGoBack.value = await _controller.canGoBack(); _canGoForward.value = await _controller.canGoForward(); }, onPageFinished: (String url) async { _canGoBack.value = await _controller.canGoBack(); _canGoForward.value = await _controller.canGoForward(); }, onHttpError: (HttpResponseError error) {}, onWebResourceError: (WebResourceError error) {}, onNavigationRequest: (NavigationRequest request) {// กำหนดการทำงานเมื่อคลิกลิ้งค์ในเว็บเพจ // เช่นการตรวจ url และ block ไม่ให้ใช้้งาน url ที่กำหนด if (request.url.startsWith('https://www.youtube.com/')) { print('blocking navigation to $request}'); return NavigationDecision.prevent; // ถ้าเป็นจากลิ้งค์ youtube ให้ block } print('allowing navigation to $request'); return NavigationDecision.navigate; // ถ้าเป็นลิ้งค์อื่นๆ เข้าไปปกติ }, ), ); } @override Widget build(BuildContext context) { // รับค่า url ที่ส่งมาใน arguments final url = ModalRoute.of(context)!.settings.arguments as String; _controller.loadRequest(Uri.parse(url)); return Scaffold( appBar: AppBar( title: Text('Articles'), actions: <Widget>[ NavigationControls( controller: _controller, canGoBack: _canGoBack, canGoForward: _canGoForward ), ], ), body: WebViewWidget(controller: _controller), ); } } // สร้าง widget สำหรับทำปุ่มควบคุม เช่น ก่อนหน้า ย้อนหลัง รีเฟรช class NavigationControls extends StatelessWidget { // กำหนด class constructor รับค่าที่จำเป็น const NavigationControls({ required this.controller, required this.canGoBack, required this.canGoForward, Key? key, }) : super(key: key); // กำหนดตัวแปรที่เกี่ยวข้อง /* ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management library ที่ซับซ้อน เช่น Provider หรือ Bloc */ final WebViewController controller; final ValueNotifier<bool> canGoBack; final ValueNotifier<bool> canGoForward; /* ValueListenableBuilder เป็น widget ที่ใช้ในการสร้าง UI ที่ฟังการเปลี่ยนแปลงค่าของ ValueNotifier และทำการ rebuild UI เมื่อค่าของ ValueNotifier มีการเปลี่ยนแปลง */ @override Widget build(BuildContext context) { return Row( children: <Widget>[ ValueListenableBuilder<bool>( valueListenable: canGoBack, builder: (context, value, child) { return IconButton( icon: const Icon(Icons.arrow_back), onPressed: value ? () => controller.goBack() : null, ); }, ), ValueListenableBuilder<bool>( valueListenable: canGoForward, builder: (context, value, child) { return IconButton( icon: const Icon(Icons.arrow_forward), onPressed: value ? () => controller.goForward() : null, ); }, ), IconButton( icon: const Icon(Icons.refresh), onPressed: () => controller.reload(), ), ], ); } }
ผลลัพธ์ที่ได้
เท่านี้ เราก็มีเมนูควบคุม webview พื้นฐานให้ใช้งาน
นอกจากการสร้างเป็น widget class แล้วส่ง controller เข้าไปเรียกใช้งานแล้ว เรายังสามารถสร้างเป็น
ฟังก์ชั่น ภายในแทน เพื่อสร้าง widget โดยไม่ต้องส่งค่า controller เหมือนวิธีก่อนหน้า แต่สามารถเรียกใช้งาน
ได้เลย เช่น สมมติเราจะใช้ปุ่ม floatingActionButton ทำปุ่มสำหรับ เพิ่ม หน้าเพจที่เป็นอยู่นั้นไว้ใน favorite
จะได้เป็นดังนี้
class _ArticlesState extends State<Articles> { ..... ..... @override Widget build(BuildContext context) { ....... .... }), floatingActionButton: favoriteButton(), // เรียกใช้ปุ่มจากฟังก์ชั่น ); } // สร้างฟังก์ชั่น คืนค่าเป็น widget Widget favoriteButton() { return FloatingActionButton( // คืนค่าเป็นปุ่มรูปหัวใจ onPressed: () async { // ถ้ากด print('Favorited'); String fav_url = (await _controller.currentUrl())!; fav_url = Uri.decodeFull(fav_url); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Favorited $fav_url')), ); }, child: const Icon(Icons.favorite), ); } }
ผลลัพธ์ที่ได้
หรือจะประยุกต์สร้างฟังก์ชั่นเลื่อนหน้าเพจไปด้านบนสุด แบบนี้ได้
class _ArticlesState extends State<Articles> { ..... ..... @override Widget build(BuildContext context) { ....... .... }), floatingActionButton: scrollTopButton(), // เรียกใช้ปุ่มจากฟังก์ชั่น ); } // สร้างฟังก์ชั่น คืนค่าเป็น widget Widget scrollTopButton() { return FloatingActionButton( // คืนค่าเป็นปุ่มรูปหัวใจ onPressed: () async { // ถ้ากด // เรียกคำสั่ง javascript เลื่อน scroll ไปด้านบนสุด await _controller.runJavaScript('window.scrollTo(0, 0);'); }, child: const Icon(Icons.arrow_upward), ); } }
ผลลัพธ์ที่ได้
เมื่อกดเลื่อนที่ลูกศร หน้าเพจก็เลื่อนไปด้านบนสุด ตัว controller จะทำคำสั่ง runJavascript
เพื่อทำงานในคำสั่ง JavaScript ที่กำหนด
มาดูโค้ดเต็มของไฟล์ article.dart ของบทความนี้
ไฟล์ article.dart
import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class Articles extends StatefulWidget { static const routeName = '/articles'; const Articles({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _ArticlesState(); } } class _ArticlesState extends State<Articles> { // แก้ไขตัวแปรสำหรับ contrller ใหม่ ให้เป็นชนิดข้อมูล late late final WebViewController _controller; /* ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management library ที่ซับซ้อน เช่น Provider หรือ Bloc */ // กำหนดค่าเริ่มต้นเป็น false final ValueNotifier<bool> _canGoBack = ValueNotifier<bool>(false); final ValueNotifier<bool> _canGoForward = ValueNotifier<bool>(false); @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { print("WebView is loading (progress : $progress%)"); // Update loading bar. }, onPageStarted: (String url) async { _canGoBack.value = await _controller.canGoBack(); _canGoForward.value = await _controller.canGoForward(); }, onPageFinished: (String url) async { _canGoBack.value = await _controller.canGoBack(); _canGoForward.value = await _controller.canGoForward(); }, onHttpError: (HttpResponseError error) {}, onWebResourceError: (WebResourceError error) {}, onNavigationRequest: (NavigationRequest request) {// กำหนดการทำงานเมื่อคลิกลิ้งค์ในเว็บเพจ // เช่นการตรวจ url และ block ไม่ให้ใช้้งาน url ที่กำหนด if (request.url.startsWith('https://www.youtube.com/')) { print('blocking navigation to $request}'); return NavigationDecision.prevent; // ถ้าเป็นจากลิ้งค์ youtube ให้ block } print('allowing navigation to $request'); return NavigationDecision.navigate; // ถ้าเป็นลิ้งค์อื่นๆ เข้าไปปกติ }, ), ); } @override Widget build(BuildContext context) { // รับค่า url ที่ส่งมาใน arguments final url = ModalRoute.of(context)!.settings.arguments as String; _controller.loadRequest(Uri.parse(url)); return Scaffold( appBar: AppBar( title: Text('Articles'), actions: <Widget>[ NavigationControls( controller: _controller, canGoBack: _canGoBack, canGoForward: _canGoForward ), ], ), body: WebViewWidget(controller: _controller), floatingActionButton: scrollTopButton(), // เรียกใช้ปุ่มจากฟังก์ชั่น ); } // สร้างฟังก์ชั่น คืนค่าเป็น widget Widget scrollTopButton() { return FloatingActionButton( // คืนค่าเป็นปุ่มรูปหัวใจ onPressed: () async { // ถ้ากด // เรียกคำสั่ง javascript เลื่อน scroll ไปด้านบนสุด await _controller.runJavaScript('window.scrollTo(0, 0);'); }, child: const Icon(Icons.arrow_upward), ); } } // สร้าง widget สำหรับทำปุ่มควบคุม เช่น ก่อนหน้า ย้อนหลัง รีเฟรช class NavigationControls extends StatelessWidget { // กำหนด class constructor รับค่าที่จำเป็น const NavigationControls({ required this.controller, required this.canGoBack, required this.canGoForward, Key? key, }) : super(key: key); // กำหนดตัวแปรที่เกี่ยวข้อง /* ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management library ที่ซับซ้อน เช่น Provider หรือ Bloc */ final WebViewController controller; final ValueNotifier<bool> canGoBack; final ValueNotifier<bool> canGoForward; /* ValueListenableBuilder เป็น widget ที่ใช้ในการสร้าง UI ที่ฟังการเปลี่ยนแปลงค่าของ ValueNotifier และทำการ rebuild UI เมื่อค่าของ ValueNotifier มีการเปลี่ยนแปลง */ @override Widget build(BuildContext context) { return Row( children: <Widget>[ ValueListenableBuilder<bool>( valueListenable: canGoBack, builder: (context, value, child) { return IconButton( icon: const Icon(Icons.arrow_back), onPressed: value ? () => controller.goBack() : null, ); }, ), ValueListenableBuilder<bool>( valueListenable: canGoForward, builder: (context, value, child) { return IconButton( icon: const Icon(Icons.arrow_forward), onPressed: value ? () => controller.goForward() : null, ); }, ), IconButton( icon: const Icon(Icons.refresh), onPressed: () => controller.reload(), ), ], ); } }
เกี่ยวกับการใช้งาน WebView เบื้องต้น ก็ขอจบเพียงเท่านี้ ยังมีส่วนที่ยังไม่กล่าวถึง อาจจะได้มานำ
เสนอในตอนต่อๆ ไป เนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม