การดาวน์โหลดไฟล์ด้วย Dio ใน Flutter อย่างง่าย

บทความ เมื่อไม่กี่สัปดาห์ โดย Ninenik Narkdee
flutter inappwebview dio

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ flutter_inappwebview dio

ดูแล้ว 140 ครั้ง


เนื้อหาตอนต่อไปนี้ เราจะมาดูต่อจากตอนที่แล้ว ซี่งมีความเกี่ยวเนื่องกัน
ในกรณีที่เราจำเป็นจะต้องมีการดาวน์ไฟล์จากหน้าเว็บที่ต้องการ ซึ่งใน
flutter_inappwebview มีความสามารถในส่วนนี้อยู่แล้ว เราแค่เพียง
เรียกใช้งาน อย่างไรก็ดี เราจำเป้นต้องใช้งานเกี่ยวกับการจัดการ path ไฟล์
และการขอสิทธิ์เข้าถึงการเขียนไฟล์เข้ามาเกี่ยวข้อง ก็จะมีการใช้งานในส่วนของ
path_provider และ permission_handler เพิ่มเติมเข้ามาด้วย
 
ทบทวนบทความตอนที่แล้วได้ที่
เปิดเว็บไซต์ภายในแอปด้วย InappWebview ใน Flutter http://niik.in/1122
 
อย่างไรก็ดี ถึงแม้ว่า InappWebview จะรองรับในส่วนของการดาวน์โหลดไฟล์ในเบื้องต้นแล้ว โดยไม่
จำเป็นต้องใช้ตัวช่วยเพิ่มเติม แต่ในที่นี้ เราจะแนะนำเพิ่มในส่วนของการใช้งาน Dio ซึ่งเป็นไลบรารี
ที่ช่วยให้การทำงานกับ HTTP ใน Dart และ Flutter เป็นไปได้อย่างรวดเร็วและสะดวก มีฟีเจอร์
ที่ครบครัน  เหมาะสำหรับการพัฒนาแอปที่มีการเชื่อมต่อกับเซิร์ฟเวอร์และต้องการการจัดการข้อมูล
ที่มีประสิทธิภาพ ถ้าเราคุ้นเคยกับการใช้งาน http มาแล้ว ตัวนี้ Dio ก็คือตัวที่ทำได้มากกว่า รองรับ
การทำงานหลายอย่างมากกว่า ในขณะที่ตัว http เหมาะสำหรับการใช้งานพื้นฐานเช่นการทำ GET,
POST, PUT, DELETE ไม่ต้องการฟีเจอร์ขั้นสูงมากนัก
 

ติดตั้ง package ที่จำเป็นเพิ่มเติม ตามรายการด้านล่าง

    แพ็กเก็จที่จำเป็นต้องติดตั้งเพิ่มเติม สำหรับการทำงานมีดังนี้
 
  url_launcher: ^6.3.0
  flutter_inappwebview: ^6.1.3
  path_provider: ^2.1.4
  permission_handler: ^11.3.1
  dio: ^5.7.0
 

การกำหนดการขอสิทธิ์เข้าถึงการใช้งานข้อมูล

    ส่วนนี้จริงๆ อธิบายไปในหลายบทความแล้ว เลยเอามาแปะไว้ เตือนความจำว่า เราต้องกำหนด
การเข้าถึงการเขียนไฟล์ อ่านไฟล์สำหรับ EXTERNAL_STORAGE เพิ่มเข้าไปด้วย
ไฟล์ 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>
 
 

การใช้งาน InappWebview ในการดาวน์โหลดไฟล์จาก Server

    ตามที่ได้เกริ่นไปก่อนหน้าว่า ก่อนที่จะไปในส่วนของการใช้งาน Dio เราจะใช้รูปแบบในกรณีที่ไม่ใช้
งาน Dio ก่อน โดยจะใช้แค่ InappWebview ในการจัดการ ซึ่งโค้ดก็ไม่ยุ่งยากอะไร เพิ่มมาจาก
บทความตอนที่แล้วเล็กน้อย คำอธิบายแสดงในโค้ด
 

ไฟล์ inappwebview.dart

import 'dart:async';
import 'dart:collection';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

class InappWeb extends StatefulWidget {
  static const routeName = '/inappweb';

  const InappWeb({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _InappWebState();
  }
}

class _InappWebState extends State<InappWeb> {
  // กำหนด key สำหรับ InAppWebView กรณีต้องการไม่ให้ซ้ำหรืออ้างอิง
  final GlobalKey webViewKey = GlobalKey();

  // ตัวควบคุม webview
  InAppWebViewController? webViewController;
  // กำหนดการต้้งค่าเริ่มต้นต่างๆ 
  InAppWebViewSettings settings = InAppWebViewSettings(
      isInspectable: kDebugMode, // รองรับการตรวจสอบ (inspect) ค่า true หรือ false
      mediaPlaybackRequiresUserGesture: false, // ผู้ใช้ต้องกดเพื่อเล่น หรือเล่นอัตโนมัติ
      allowsInlineMediaPlayback: true, //  ให้เล่นมีเดียแบบ inline
      iframeAllow: "camera; microphone", //  iframe สามารถขอเข้าถึงกล้องและไมโครโฟน
      iframeAllowFullscreen: true // ให้ iframe แสดงผลแบบเต็มหน้าจอ
  );

  // ส่วนสำหรับควบคุมการปัดลงเพื่อรีเฟรช หรือ Pull to refresh
  PullToRefreshController? pullToRefreshController;

  // กำกนดตัวเก็บค่าสถานะการโหลดข้อมูล
  final ValueNotifier<double> _progressNotifier = ValueNotifier<double>(0);

  // กำหนดค่าเริ่มต้นเป็น false สถานะการเลื่อนในเมนู ว่าไปหน้าเพจก่อนหน้า ไปหน้าถัดไป
  final ValueNotifier<bool> _canGoBack = ValueNotifier<bool>(false);
  final ValueNotifier<bool> _canGoForward = ValueNotifier<bool>(false);

  // ส่วนสำหรับกำหนดเมนูคลิกขวา หรือ การกดค้างแล้วแสดงเมนู
  late ContextMenu contextMenu;
  String url = ""; // สำหรับเก็บ url
  double progress = 0; // สถานะการโหลด
  // สำหรับเก็บค่า ข้อความ อาจจะใช้เก็บ url ได้
  final urlController = TextEditingController();

  @override
  void initState() {
    super.initState();

    // ตั้งค่าเิริ่มต้นเมื่อใช้งาน สำหรับการสร้างปุมเมนู กำหนดเอง
    // ในที่นี้เพิมแค่เมนูเดียวเป็นตัวอย่าง ชื่อว่าเมนู Special
    contextMenu = ContextMenu(
        menuItems: [
          ContextMenuItem(
              id: 1,
              title: "Special",
              action: () async {
                // กำหนด คำสั่งตามต้องการ ถ้าเลือกเมนูนี้
                // ในตัวอย่าง แสดงข้อความที่เลือก ถ้ามี จากนั้นให้ปล่อยการโฟกัส
                print("xdebug: Menu item Special clicked!");
                print("xdebug: ${await webViewController?.getSelectedText()}");
                await webViewController?.clearFocus();
              })
        ],
        // ซ่อนเมนูของรบบหรือไม่ ถ้า true จะไม่แสดงเมนูอื่นๆ เช่น เมนูคัดลอก จะแสดงแค่เมนูที่เราสร้างขึ้น
        settings: ContextMenuSettings(hideDefaultSystemContextMenuItems: true),
        // คำสั่งเมื่อกดค้าง เพื่อแสดงเมนู ทำงานทันทีที่เรียกใช้
        onCreateContextMenu: (hitTestResult) async {
          print("xdebug: onCreateContextMenu");
          print("xdebug: ${hitTestResult.extra}");
          print("xdebug: ${await webViewController?.getSelectedText()}");
        },
        // เมื่อยกเลิกการเลือก หรือเมนูที่ขึ้นแสดง ซ่อนไป
        onHideContextMenu: () {
          print("xdebug: onHideContextMenu");
        },
        // เมื่อคลิกที่เมนู ที่แสดงขื้นมา
        onContextMenuActionItemClicked: (contextMenuItemClicked) async {
          var id = contextMenuItemClicked.id;
          print("xdebug: onContextMenuActionItemClicked: " +
              id.toString() +
              " " +
              contextMenuItemClicked.title);
        });

        // ส่วนของการตั้งค่าการกดปัดลงเพื่อรีเฟรช
        // ตรวจสอบให้ทำงานถ้าเป็น platform ที่รองรับ
        pullToRefreshController = kIsWeb ||
            ![TargetPlatform.iOS, TargetPlatform.android]
                .contains(defaultTargetPlatform)
        ? null // ถ้าไม่รองรับ ไม่ต้องแสดงอะไร
        : PullToRefreshController( // ถ้ารองรับก็ใช้งาน PullToRefreshController widget
            settings: PullToRefreshSettings(
              color: Colors.blue,
            ),
            onRefresh: () async {
              if (defaultTargetPlatform == TargetPlatform.android) {
                webViewController?.reload();
              } else if (defaultTargetPlatform == TargetPlatform.iOS) {
                webViewController?.loadUrl(
                    urlRequest:
                        URLRequest(url: await webViewController?.getUrl()));
              }
            },
        );

        // กำหนดการร้องขอสิทธิืเข้าถึงการเขียนและอ่านไฟล์ storage
        requestPermissions();
  }


  // ขอ permission สำหรับจัดการ storage
  void requestPermissions() async {
    await Permission.storage.request();
  }  

  // ฟังก์ชั่นสำหรับเรียกใช้งานการแทรก CSS และ JavaScript
  void _injectCSSAndJavaScript() async {
    print("xdebug: inject css run");
    if (webViewController != null) {
      print("xdebug: inject css run and controller work");

      // ข้อความ css code
      String css = """
        .header-layout-side.morenews-header,
        #secondary,
        .af-breadcrumbs,
        footer.site-footer {
          display:none;
        }
      """;

      // ข้อความ javascript code
      String script = """
        var style = document.createElement('style');
        style.type = 'text/css';
        style.appendChild(document.createTextNode(`$css`));
        document.head.appendChild(style);
      """;

      // ตัวอย่างการแทรก JavaScript (Inject JavaScript)
      await webViewController!.evaluateJavascript(source: script);

      // ตัวอย่างถ้าต้องการแทรก css (Inject CSS)
/*       await webViewController!.injectCSSCode(source: """
.header-layout-side.morenews-header,
#secondary,
.af-breadcrumbs,
footer.site-footer {
  display:none;
}
      """); */
    }
  }

  // การยกเลิกและล้างค่าต่างๆ เมื่อได้ใช้งาน เพื่อคืนหน่วยความจำ
  @override
  void dispose() {
    //pullToRefreshController?.dispose();
    _progressNotifier.dispose();
    urlController.dispose();
    _canGoBack.dispose();
    _canGoForward.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // รับค่า url ที่ส่งมาใน arguments
    // const url = ModalRoute.of(context)!.settings.arguments as String;
    // กำหนด url เริมต้น
    const url = "https://ninenik.com/demo/example_download.html";
    // const url = "https://www.ninenik.com";
    // เก็บ url ไว้จัดการ ถ้าต้องการควบคุมผ่าน  TextEditingController 
    urlController.text = url;

    return Scaffold(
        appBar: AppBar(
          backgroundColor: Color.fromRGBO(64, 64, 64, 1),
          foregroundColor: Colors.white,
          title: Text(
            'WebView',
            style: TextStyle(color: Colors.white),
          ),
          actions: <Widget>[
            // ส่วนของเมนูควบคุมการท่องเว็บ สร้างมาจาก อีก คลาส 
            // เพื่อจะได้จัดการง่าย ส่งคาที่จำเป็นไปตามรูปแบบที่กำหนด
            NavigationControls(
              controller: webViewController,
              canGoBack: _canGoBack,
              canGoForward: _canGoForward,
              initialUrl: url,
            ),
          ],
        ),
        body: SafeArea( // ใช้ให้แสดงเฉพาะส่วนที่ไม่เกี่ยวกับแถบสถานะแบตด้านบน
            child: Column(children: <Widget>[
          Expanded(
            child: Stack( // จัดเรียงแบบซ้อนทับด้วย Stack
              children: [
                // ส่วนของการแสดง webview อยู่ล่างสุด
                InAppWebView(
                  key: webViewKey,
                  initialUrlRequest: URLRequest(url: WebUri(url)), // เรียก url เริ่มต้น
                  // ส่วนเรียกให้ทำงานก่อน เมื่อมีการ render เฟรมแรก
                  onPageCommitVisible: (controller, url) async {
                    // ในที่นี้เราเอาไว้แทรก css และ javascript 
                    _injectCSSAndJavaScript();
                  },
                  // การแทรกสริปเมื่อเริ่มค้น โดยไว้กำหนด script ที่ไม่ให้แก้ไขได้ ในที่นี้เป้นค่าว่าง
                  initialUserScripts: UnmodifiableListView<UserScript>([]),
                  initialSettings: settings, // ใช้การตั้งค่าที่กำหนดด้านบน
                  contextMenu: contextMenu, // ส่วนแสดงเมนูคลิกขวา หรือกดค้าง
                  pullToRefreshController: pullToRefreshController, // pull to refresh
                  onWebViewCreated: (controller) async {
                    // เมื่อเริ่มการสร้าง webview กำหนดค่า controller ไว้ใช้งาน
                    webViewController = controller;
                  },
                  // ส่วนจัดการเมื่อเริ่มมีการดาวน์โฆลดไฟล์
                  onDownloadStartRequest: (controller, url) async {
                    // เมื่อเริ่มการดาวน์โหลดไฟล์ เก็บชื่อไฟล์
                    String fileName = url.url.toString().split('/').last;
                    // สมมติในที่นี้ชื่อ package เป็น com.example.demo_ap  ดูเพิ่มเกี่ยวกับ path http://niik.in/1066
                    // กรณีเก็บในโฟลเดอร์ /storage/emulated/0/Android/data/com.example.demo_app/files/
                    // เปิดดูและจัดการได้จากโปรแกรมจัดการไฟล์ หรือคอมพิวเตอร์
                    /*
                    Directory? externalDir = await getExternalStorageDirectory();
                    String filePath = "${externalDir?.path}/$fileName";
                    */

                    // กรณีเก็บในโฟลเดอร์ /data/user/0/com.example.demo_app/app_flutter/
                    // ดูและจัดการได้เฉพาะในแอปเท่านั้น
                    /*
                    Directory? externalDir = await getApplicationDocumentsDirectory();
                    String filePath = "${externalDir?.path}/$fileName";
                    */
                    
                    // กรณีเก็บในโฟลเดอร์ /storage/emulated/0/Android/data/com.example.demo_app/files/downloads/
                    /*
                    List<Directory>?  externalDir = await getExternalStorageDirectories(type: StorageDirectory.downloads);
                    String filePath = '';
                    if (externalDir != null && externalDir.isNotEmpty) {
                      filePath = '${externalDir.first.path}/$fileName'; // Save in Downloads folder
                    } 
                    */

                    // กรณีต้องการบันทึกแบบสา๔ารณะ /storage/emulated/0/Download
                    // แอปในเครื่องจัดการได้ ถ้ามีสิทธิ์เข้าถึงส่วนดาวน์โหลดหลักของเครื่อง
                    Directory downloadsDirectory = Directory('/storage/emulated/0/Download');
                    String filePath = "${downloadsDirectory?.path}/$fileName";

                    try {
                      // สร้างไฟล์ใหม่
                      File file = File(filePath);
                      // เชื่อมต่อไปยังไฟล์ในฝั่ง server ผ่านลิ้งค์หรือ url ที่กำหนด แล้วเรียกดูไฟล์หรือดาวน์โหลด
                      HttpClient httpClient = HttpClient();
                      var request = await httpClient.getUrl(url.url!);
                      var response = await request.close();
                      // เรียกใช้ฟังก์ชั่นสำหรับทำหน้าที่ในการรวมข้อมูลที่ถูกส่งกลับจาก HttpClientResponse 
                      // เป็นอาร์เรย์ของไบต์ (byte array) 
                      var bytes = await consolidateHttpClientResponseBytes(response);
                      await file.writeAsBytes(bytes);

                      // ส่วนนี้ใช้ตรวจสอบว่ามีไฟล์หรือไม่ หลังจากเขียนแล้ว จะใช้หรือไม่ใช้ ก็ได้
                      if (await file.exists()) {
                        print('File downloaded and exists at: $filePath');
                      } else {
                        print('File download failed or file does not exist.');
                      }                      

                      // ดาวน์โหลดสำเร็จ แจ้งตำแหน่งที่บันทึก
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text("File downloaded to $filePath")),
                      );
                    } catch (e) {
                      // เกิดข้อผิดพลาด ดาวน์ดหลดไม่สำเร็จ
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text("Download failed: $e")),
                      );
                    }
                  },                  
                  onLoadStart: (controller, url) async { // เมื่อเริ่มโหลดหน้าเว็บ
                    _progressNotifier.value = 0; // สถานะการโหลด
                    setState(() {
                      // กำหนดค่าเริมต้นต่างๆ 
                      this.url = url.toString();
                      urlController.text = this.url;
                    });
                  },
                  // เมื่อเว็บไซต์นั้นๆ  ร้องขอสิทธิ์การเข้าถึงส่วนต่างๆ
                  onPermissionRequest: (controller, request) async {
                    return PermissionResponse(
                        resources: request.resources,
                        action: PermissionResponseAction.GRANT);
                  },
                  // การทำงานเมื่อมีการกดลิ้งค์ในเว็บไซต์
                  shouldOverrideUrlLoading:
                      (controller, navigationAction) async {
                        // ตรวจสอบค่าจาก url
                    var uri = navigationAction.request.url!;

                    // ถ้าไม่ใช่ค่าตามที่ระบบไว้ ให้เรียกใช้ฟังก์ชั่นการเปิดจาก url_launcher
                    if (![
                      "http",
                      "https",
                      "file",
                      "chrome",
                      "data",
                      "javascript",
                      "about"
                    ].contains(uri.scheme)) {
                       // เปลี่ยนลิ้งค์ เป็น url ที่สามารถเข้าได้ เช่น intent:// เปลี่ยนเป็น https:// 
                      String newUrl = '';
                      newUrl  = uri.toString().replaceFirst('${uri.scheme}://', 'https://'); // แปลงเป็น https URL
                      print("xdebug: not in allow ${newUrl}");
                      WebUri newUri = WebUri(newUrl);
                      if (await canLaunchUrl(newUri)) {
                        // เปิดจากแอป หรือส่วนอื่นที่ผ่านหน้าเว็บ
                        await launchUrl(
                          newUri,
                        );
                        // ถ้าไม่สามาถเปิดผ่านส่วนจัดการใดๆ ได้
                        return NavigationActionPolicy.CANCEL;
                      }
                    }
                    // กรณีเป็น url ตามลิสรายการที่กำหนด ให้อนุญาตเข้าใช้งานได้
                    return NavigationActionPolicy.ALLOW;
                  },
                  // เมื่อโหลดหน้าเพจเสร็จ
                  onLoadStop: (controller, url) async {
                    // หยุดการทำงานส่วนควบคุม pull to refresh
                    pullToRefreshController?.endRefreshing();
                    // เก็บค่าสถานะการลิ้งค์ไปหน้าต่างๆ ว่าทำได้หรือม่
                    _canGoBack.value =
                        await webViewController?.canGoBack() ?? false;
                    _canGoForward.value =
                        await webViewController?.canGoForward() ?? false;
                    setState(() {
                      this.url = url.toString();
                      urlController.text = this.url;
                    });
                  },
                  // กรณีการโหลดเกิด error 
                  onReceivedError: (controller, request, error) {
                    pullToRefreshController?.endRefreshing();
                  },
                  // การดำเนินการระหว่างกำลังโหลดหน้าเพจ
                  onProgressChanged: (controller, progress) {
                    // เมื่อโหลดเสร็จ 100 %
                    if (progress == 100) {
                      pullToRefreshController?.endRefreshing();
                    }
                    // ในระหว่างการโหลด เก็บค่า percent ไว้แสดง
                    _progressNotifier.value = progress / 100;
                    setState(() {
                      this.progress = progress / 100;
                      urlController.text = this.url;
                    });
                  },
                  // เมื่อมีการรีโหลดหน้านั้นๆ
                  onUpdateVisitedHistory: (controller, url, isReload) {
                    setState(() {
                      this.url = url.toString();
                      urlController.text = this.url;
                    });
                  },
                  // เมื่อมีการแสดงข้อความจาก console.log
                  onConsoleMessage: (controller, consoleMessage) {
                    print(consoleMessage);
                  },
                ),
                // ส่วนของการแสดงสถานะขณะกำลังโหลด
                // ส่วนนี้จะซ้อนอยู่ด้านบนก่อนที่เว็บจะโหลดเสร็จก็จะหายไป
                ValueListenableBuilder<double>(
                  valueListenable: _progressNotifier,
                  builder: (context, progress, child) {
                    if (progress == 1.0) {
                      return SizedBox
                          .shrink(); // ไม่แสดงอะไรเมื่อโหลดเสร็จ
                    } else {
                      // ขณะกำลังโหลด เราสามารถสร้างตัวคลุมไม่ให้เห็นส่วนของเว็บกำลังโหลดได้
                      // อย่างในที่นี้ เราสร้างตัวคลุมพื้นหลังสีขาว กั้นไว้ ให้ css หรือ javascript ทำงาน
                      // ถ้าอยากให้เห็นหน้าเพจกำลังโหลด สามารถกำหนดสีพ้ืนหลังเป็น Colors.transparent
                      return Container(
                        width: double.infinity, // Full width
                        height: double.infinity, // Full height
                        // color: Colors.white, // White background
                        child: Center(
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              CircularProgressIndicator(
                                color: Color.fromRGBO(64, 64, 64, 1),
                                value: progress,
                              ),
                              SizedBox(height: 10),
                              Text('${(progress * 100).round()}%'),
                            ],
                          ),
                        ),
                      );
                    }
                  },
                )
              ],
            ),
          ),
        ])));
  }
}

// ฟังก์ชั่นสำหรับทำหน้าที่ในการรวมข้อมูลที่ถูกส่งกลับจาก HttpClientResponse 
// เป็นอาร์เรย์ของไบต์ (byte array) 
Future<List<int>> consolidateHttpClientResponseBytes(
    HttpClientResponse response) {
  final completer = Completer<List<int>>();
  final contents = <int>[];
  response.listen( // เพื่อฟังข้อมูลที่ถูกส่งมา
    contents.addAll, // เพิ่มข้อมูลที่ได้รับเข้าลิสต์ contents
    // เมื่อการรับข้อมูลเสร็จสิ้น (done), จะเรียกใช้ completer.complete(contents)
    // เพื่อส่งคืนลิสต์ที่รวบรวมข้อมูลทั้งหมด
    onDone: () => completer.complete(contents),
    //  ถ้ามีข้อผิดพลาดเกิดขึ้นในการรับข้อมูล, จะเรียกใช้ completer.completeError เพื่อส่งคืนข้อผิดพลาด
    onError: completer.completeError,
    // จะทำให้หยุดการฟังข้อมูลเมื่อเกิดข้อผิดพลาด
    cancelOnError: true,
  );
  return completer.future;
}

// สร้าง widget สำหรับทำปุ่มควบคุม เช่น ก่อนหน้า ย้อนหลัง รีเฟรช
class NavigationControls extends StatelessWidget {
  // กำหนด class constructor รับค่าที่จำเป็น
  const NavigationControls({
    required this.controller,
    required this.canGoBack,
    required this.canGoForward,
    required this.initialUrl,
    Key? key,
  }) : super(key: key);

  // กำหนดตัวแปรที่เกี่ยวข้อง
  /*
   ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier 
   ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง 
   ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management 
   library ที่ซับซ้อน เช่น Provider หรือ Bloc
  */
  final InAppWebViewController? controller;
  final ValueNotifier<bool> canGoBack;
  final ValueNotifier<bool> canGoForward;
  final String initialUrl;

  /*
  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: Icon(
                Icons.home,
                color: value ? Colors.white : Colors.white60,
              ),
              onPressed: value
                  ? () => controller?.loadUrl(
                      urlRequest: URLRequest(url: WebUri(initialUrl)))
                  : null,
            );
          },
        ),
        ValueListenableBuilder<bool>(
          valueListenable: canGoBack,
          builder: (context, value, child) {
            return IconButton(
              icon: Icon(
                Icons.arrow_back,
                color: value ? Colors.white : Colors.white60,
              ),
              onPressed: value ? () => controller?.goBack() : null,
            );
          },
        ),
        ValueListenableBuilder<bool>(
          valueListenable: canGoForward,
          builder: (context, value, child) {
            return IconButton(
              icon: Icon(
                Icons.arrow_forward,
                color: value ? Colors.white : Colors.white60,
              ),
              onPressed: value ? () => controller?.goForward() : null,
            );
          },
        ),
        IconButton(
          icon: const Icon(Icons.refresh),
          onPressed: () => controller?.reload(),
        ),
      ],
    );
  }
}
 

ผลลัพธ์ที่ได้

กรณีบันทึกไว้ใน Download โฟลเดอร์ ใช้งานได้จากทุกแอป
 



 
กรณีบันทึกไว้ใน app_flutter ดูได้เฉพาะในแอป

 


 
ในตัวอย่างเราจะลิ้งค์ไปหน้าดาวน์โหลดไฟล์ตัวอย่าง ถ้าเรากำหนดให้ดาวน์โหลดไปยังโฟลเดอร์
ดาวน์โหลดของระบบ ก็จะสามารถเข้าใช้งานได้จากแอปต่างๆ  และอีกรูปตัวอย่าง ถ้าเรากำหนดให้
บันทึกไว้ใน app_flutter ที่ดูได้เฉพาะในแอป ในตัวอย่างเราดาวน์โหลดไฟล์ simple.mp3 และ
 simple.mp4 ตามรูป  
    การทำงานของส่วนดาวน์โหลดนี้ เราจะไม่รู้เลยว่ามีการดาวน์โหลดหรือยัง จนกว่าทำรายการสำเร็จ
ก็จะขึ้นแจ้งเตือน ไม่มีสถานะติดตามหรือโปรเกรสใดๆ ซึ่งเป็นข้อจำกัดในส่วนนี้ แต่ก็มีข้อดีคือใช้งานง่าย
ถึงเราจะเปิดแอปอื่นขึ้นมา หากระบบไม่ปิดการทำงานกรณีไฟล์ใหญ่เกินไป การดาวน์โหลดก็จะทำงาน
ไปจนเสร็จ ถือว่าเหมาะสำหรับการดาวน์โหลดไฟล์ขนาดไม่ใหญ่มาก


 

การใช้งาน Dio ดาวน์โหลดไฟล์จาก Server

    การใช้งานก็ไม่ยุ่งยาก ใช้ง่าย ปรับแต่งเล็กน้อยจากโค้ดส่วนที่แล้ว คือเมื่อมีการเริ่มดาวน์โหลด เราก็จะ
เรียกใช้งานฟังก์ชั่นที่เราใช้ Dio มาช่วย ซึ่งจะมีสถานะการดาวน์โหลดให้เราใช้งานด้วย สิ่งที่เรามีเพิ่มเข้า
มาก็คือ กำหนด
 
  // เพิ่มส่วนสำหรับเก็บสถานะการดาวนโหลดไว้
  double downloadProgress = 0.0;
 
ฟังก์ชั่นการดาวน์โหลด โดยแค่ส่ง url ลิ้งค์ของไฟล์มาใช้งาน
 
  // ฟังก์ชั่นเรียกใช้งาน Dio เพื่อดาวน์โหลดไฟล์
  Future<void> downloadFile(Uri url) async {
    Dio dio = Dio();
    // เก็บชื่อไฟล์
    String fileName = url.toString().split('/').last;

    // หาพาธสำหรับจัดเก็บไฟล์
    Directory? downloadsDirectory = Directory('/storage/emulated/0/Download');
    if (!downloadsDirectory.existsSync()) { // ถ้าไม่มีให้ใช้เป็น 
      // สมมติในที่นี้ชื่อ package เป็น com.example.demo_ap  ดูเพิ่มเกี่ยวกับ path http://niik.in/1066
      // กรณีเก็บในโฟลเดอร์ /storage/emulated/0/Android/data/com.example.demo_app/files/
      // เปิดดูและจัดการได้จากโปรแกรมจัดการไฟล์ หรือคอมพิวเตอร์    
      downloadsDirectory = await getExternalStorageDirectory();
    }

    // กำหนดพาธไฟล์เต็ม
    String filePath = "${downloadsDirectory!.path}/$fileName";
    
    // ดาวน์โหลดไฟล์พร้อมติดตามความคืบหน้า
    try {
      await dio.download(
        url.toString(),
        filePath,
        onReceiveProgress: (receivedBytes, totalBytes) {
          // อัปเดทสถานะการดำเนินการดาวน์โหลด
          if (totalBytes != -1) {
            setState(() {
              downloadProgress = receivedBytes / totalBytes;
            });
            print("Download progress: ${(downloadProgress * 100).toStringAsFixed(0)}%");
          }
        },
      );

      // ตั้งเวลารอ 3 วินาที เพื่อรีเซ็ตค่าการโหลด เพื่อเมื่อค่าเท่า 0 ตัวแสดงสถานะจะได้ซ่อนไป
      Timer(Duration(seconds: 3), () {
        // หลังจาก 3 วินาทีให้กำหนดค่า downloadProgress เป็น 0
        setState(() {
          downloadProgress = 0.0;
        });
        print('Download progress reset to $downloadProgress');
      });      
      // ดาวน์โหลดสำเร็จ แจ้งตำแหน่งที่บันทึก
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("File downloaded to $filePath")),
      );      
    } catch (e) {
      // เกิดข้อผิดพลาด ดาวน์ดหลดไม่สำเร็จ
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("Error downloading file: $e")),
      );      
    }
  }
 
มีการแสดงแถบสถานะการดาวน์โหลด แสดงเมื่อมีค่ามากกว่า 0 และหยุดแสดงหลังจาก ดาวน์โหลด
เสร็จเรียบร้อยแล้ว
 
// ถ้าเริ่มดาวน์โหลด 
if (downloadProgress > 0)
LinearProgressIndicator(
  value: downloadProgress,
  backgroundColor: Colors.grey,
  valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
),
 
และสุดท้าย ส่วนของการทำงาน ให้เรียกใช้คำสั่งดาวน์โหลด เมื่อผู้ใช้คลิกที่ลิ้งค์เพื่อดาวน์โหลดผ่าน
WebView แทรกโค้ดเล็กน้อยนี้เข้าไป
 
// ส่วนจัดการเมื่อเริ่มมีการดาวน์โฆลดไฟล์
onDownloadStartRequest: (controller, url) async {

    print("Download started: ${url.url}");

    // ขอสิทธิ์การเข้าถึง storage
    if (await Permission.storage.request().isGranted) {
      // เรียกฟังก์ชันดาวน์โหลดไฟล์
      await downloadFile(url.url!);
    }
}
 
เราสามารถประยุกต์เพิ่มเติมได้มากกว่านี้ถ้าต้องการ ในที่นี้ถือว่าครอบคลุมการใช้งาน
 

ไฟล์ inappwebview.dart (ใช้งาน Dio)

import 'dart:async';
import 'dart:collection';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:dio/dio.dart';

class InappWeb extends StatefulWidget {
  static const routeName = '/inappweb';

  const InappWeb({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _InappWebState();
  }
}

class _InappWebState extends State<InappWeb> {
  // กำหนด key สำหรับ InAppWebView กรณีต้องการไม่ให้ซ้ำหรืออ้างอิง
  final GlobalKey webViewKey = GlobalKey();

  // ตัวควบคุม webview
  InAppWebViewController? webViewController;
  // กำหนดการต้้งค่าเริ่มต้นต่างๆ 
  InAppWebViewSettings settings = InAppWebViewSettings(
      isInspectable: kDebugMode, // รองรับการตรวจสอบ (inspect) ค่า true หรือ false
      mediaPlaybackRequiresUserGesture: false, // ผู้ใช้ต้องกดเพื่อเล่น หรือเล่นอัตโนมัติ
      allowsInlineMediaPlayback: true, //  ให้เล่นมีเดียแบบ inline
      iframeAllow: "camera; microphone", //  iframe สามารถขอเข้าถึงกล้องและไมโครโฟน
      iframeAllowFullscreen: true // ให้ iframe แสดงผลแบบเต็มหน้าจอ
  );

  // ส่วนสำหรับควบคุมการปัดลงเพื่อรีเฟรช หรือ Pull to refresh
  PullToRefreshController? pullToRefreshController;

  // กำกนดตัวเก็บค่าสถานะการโหลดข้อมูล
  final ValueNotifier<double> _progressNotifier = ValueNotifier<double>(0);

  // กำหนดค่าเริ่มต้นเป็น false สถานะการเลื่อนในเมนู ว่าไปหน้าเพจก่อนหน้า ไปหน้าถัดไป
  final ValueNotifier<bool> _canGoBack = ValueNotifier<bool>(false);
  final ValueNotifier<bool> _canGoForward = ValueNotifier<bool>(false);

  // ส่วนสำหรับกำหนดเมนูคลิกขวา หรือ การกดค้างแล้วแสดงเมนู
  late ContextMenu contextMenu;
  String url = ""; // สำหรับเก็บ url
  double progress = 0; // สถานะการโหลด
  // สำหรับเก็บค่า ข้อความ อาจจะใช้เก็บ url ได้
  final urlController = TextEditingController();

  // เพิ่มส่วนสำหรับเก็บสถานะการดาวนโหลดไว้
  double downloadProgress = 0.0;

  @override
  void initState() {
    super.initState();

    // ตั้งค่าเิริ่มต้นเมื่อใช้งาน สำหรับการสร้างปุมเมนู กำหนดเอง
    // ในที่นี้เพิมแค่เมนูเดียวเป็นตัวอย่าง ชื่อว่าเมนู Special
    contextMenu = ContextMenu(
        menuItems: [
          ContextMenuItem(
              id: 1,
              title: "Special",
              action: () async {
                // กำหนด คำสั่งตามต้องการ ถ้าเลือกเมนูนี้
                // ในตัวอย่าง แสดงข้อความที่เลือก ถ้ามี จากนั้นให้ปล่อยการโฟกัส
                print("xdebug: Menu item Special clicked!");
                print("xdebug: ${await webViewController?.getSelectedText()}");
                await webViewController?.clearFocus();
              })
        ],
        // ซ่อนเมนูของรบบหรือไม่ ถ้า true จะไม่แสดงเมนูอื่นๆ เช่น เมนูคัดลอก จะแสดงแค่เมนูที่เราสร้างขึ้น
        settings: ContextMenuSettings(hideDefaultSystemContextMenuItems: true),
        // คำสั่งเมื่อกดค้าง เพื่อแสดงเมนู ทำงานทันทีที่เรียกใช้
        onCreateContextMenu: (hitTestResult) async {
          print("xdebug: onCreateContextMenu");
          print("xdebug: ${hitTestResult.extra}");
          print("xdebug: ${await webViewController?.getSelectedText()}");
        },
        // เมื่อยกเลิกการเลือก หรือเมนูที่ขึ้นแสดง ซ่อนไป
        onHideContextMenu: () {
          print("xdebug: onHideContextMenu");
        },
        // เมื่อคลิกที่เมนู ที่แสดงขื้นมา
        onContextMenuActionItemClicked: (contextMenuItemClicked) async {
          var id = contextMenuItemClicked.id;
          print("xdebug: onContextMenuActionItemClicked: " +
              id.toString() +
              " " +
              contextMenuItemClicked.title);
        });

        // ส่วนของการตั้งค่าการกดปัดลงเพื่อรีเฟรช
        // ตรวจสอบให้ทำงานถ้าเป็น platform ที่รองรับ
        pullToRefreshController = kIsWeb ||
            ![TargetPlatform.iOS, TargetPlatform.android]
                .contains(defaultTargetPlatform)
        ? null // ถ้าไม่รองรับ ไม่ต้องแสดงอะไร
        : PullToRefreshController( // ถ้ารองรับก็ใช้งาน PullToRefreshController widget
            settings: PullToRefreshSettings(
              color: Colors.blue,
            ),
            onRefresh: () async {
              if (defaultTargetPlatform == TargetPlatform.android) {
                webViewController?.reload();
              } else if (defaultTargetPlatform == TargetPlatform.iOS) {
                webViewController?.loadUrl(
                    urlRequest:
                        URLRequest(url: await webViewController?.getUrl()));
              }
            },
        );

        // กำหนดการร้องขอสิทธิืเข้าถึงการเขียนและอ่านไฟล์ storage
        requestPermissions();
  }


  // ขอ permission สำหรับจัดการ storage
  void requestPermissions() async {
    await Permission.storage.request();
  }  

  // ฟังก์ชั่นสำหรับเรียกใช้งานการแทรก CSS และ JavaScript
  void _injectCSSAndJavaScript() async {
    print("xdebug: inject css run");
    if (webViewController != null) {
      print("xdebug: inject css run and controller work");

      // ข้อความ css code
      String css = """
        .header-layout-side.morenews-header,
        #secondary,
        .af-breadcrumbs,
        footer.site-footer {
          display:none;
        }
      """;

      // ข้อความ javascript code
      String script = """
        var style = document.createElement('style');
        style.type = 'text/css';
        style.appendChild(document.createTextNode(`$css`));
        document.head.appendChild(style);
      """;

      // ตัวอย่างการแทรก JavaScript (Inject JavaScript)
      await webViewController!.evaluateJavascript(source: script);

      // ตัวอย่างถ้าต้องการแทรก css (Inject CSS)
/*       await webViewController!.injectCSSCode(source: """
.header-layout-side.morenews-header,
#secondary,
.af-breadcrumbs,
footer.site-footer {
  display:none;
}
      """); */
    }
  }


  // ฟังก์ชั่นเรียกใช้งาน Dio เพื่อดาวน์โหลดไฟล์
  Future<void> downloadFile(Uri url) async {
    Dio dio = Dio();
    // เก็บชื่อไฟล์
    String fileName = url.toString().split('/').last;

    // หาพาธสำหรับจัดเก็บไฟล์
    Directory? downloadsDirectory = Directory('/storage/emulated/0/Download');
    if (!downloadsDirectory.existsSync()) { // ถ้าไม่มีให้ใช้เป็น 
      // สมมติในที่นี้ชื่อ package เป็น com.example.demo_ap  ดูเพิ่มเกี่ยวกับ path http://niik.in/1066
      // กรณีเก็บในโฟลเดอร์ /storage/emulated/0/Android/data/com.example.demo_app/files/
      // เปิดดูและจัดการได้จากโปรแกรมจัดการไฟล์ หรือคอมพิวเตอร์    
      downloadsDirectory = await getExternalStorageDirectory();
    }

    // กำหนดพาธไฟล์เต็ม
    String filePath = "${downloadsDirectory!.path}/$fileName";
    
    // ดาวน์โหลดไฟล์พร้อมติดตามความคืบหน้า
    try {
      await dio.download(
        url.toString(),
        filePath,
        onReceiveProgress: (receivedBytes, totalBytes) {
          // อัปเดทสถานะการดำเนินการดาวน์โหลด
          if (totalBytes != -1) {
            setState(() {
              downloadProgress = receivedBytes / totalBytes;
            });
            print("Download progress: ${(downloadProgress * 100).toStringAsFixed(0)}%");
          }
        },
      );

      // ตั้งเวลารอ 3 วินาที เพื่อรีเซ็ตค่าการโหลด เพื่อเมื่อค่าเท่า 0 ตัวแสดงสถานะจะได้ซ่อนไป
      Timer(Duration(seconds: 3), () {
        // หลังจาก 3 วินาทีให้กำหนดค่า downloadProgress เป็น 0
        setState(() {
          downloadProgress = 0.0;
        });
        print('Download progress reset to $downloadProgress');
      });      
      // ดาวน์โหลดสำเร็จ แจ้งตำแหน่งที่บันทึก
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("File downloaded to $filePath")),
      );      
    } catch (e) {
      // เกิดข้อผิดพลาด ดาวน์ดหลดไม่สำเร็จ
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("Error downloading file: $e")),
      );      
    }
  }

  // การยกเลิกและล้างค่าต่างๆ เมื่อได้ใช้งาน เพื่อคืนหน่วยความจำ
  @override
  void dispose() {
    //pullToRefreshController?.dispose();
    _progressNotifier.dispose();
    urlController.dispose();
    _canGoBack.dispose();
    _canGoForward.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // รับค่า url ที่ส่งมาใน arguments
    // const url = ModalRoute.of(context)!.settings.arguments as String;
    // กำหนด url เริมต้น
    const url = "https://ninenik.com/demo/example_download.html";
    // const url = "https://www.ninenik.com";
    // เก็บ url ไว้จัดการ ถ้าต้องการควบคุมผ่าน  TextEditingController 
    urlController.text = url;

    return Scaffold(
        appBar: AppBar(
          backgroundColor: Color.fromRGBO(64, 64, 64, 1),
          foregroundColor: Colors.white,
          title: Text(
            'WebView',
            style: TextStyle(color: Colors.white),
          ),
          actions: <Widget>[
            // ส่วนของเมนูควบคุมการท่องเว็บ สร้างมาจาก อีก คลาส 
            // เพื่อจะได้จัดการง่าย ส่งคาที่จำเป็นไปตามรูปแบบที่กำหนด
            NavigationControls(
              controller: webViewController,
              canGoBack: _canGoBack,
              canGoForward: _canGoForward,
              initialUrl: url,
            ),
          ],
        ),
        body: SafeArea( // ใช้ให้แสดงเฉพาะส่วนที่ไม่เกี่ยวกับแถบสถานะแบตด้านบน
            child: Column(children: <Widget>[
          Expanded(
            child: Stack( // จัดเรียงแบบซ้อนทับด้วย Stack
              children: [
                // ส่วนของการแสดง webview อยู่ล่างสุด
                InAppWebView(
                  key: webViewKey,
                  initialUrlRequest: URLRequest(url: WebUri(url)), // เรียก url เริ่มต้น
                  // ส่วนเรียกให้ทำงานก่อน เมื่อมีการ render เฟรมแรก
                  onPageCommitVisible: (controller, url) async {
                    // ในที่นี้เราเอาไว้แทรก css และ javascript 
                    _injectCSSAndJavaScript();
                  },
                  // การแทรกสริปเมื่อเริ่มค้น โดยไว้กำหนด script ที่ไม่ให้แก้ไขได้ ในที่นี้เป้นค่าว่าง
                  initialUserScripts: UnmodifiableListView<UserScript>([]),
                  initialSettings: settings, // ใช้การตั้งค่าที่กำหนดด้านบน
                  contextMenu: contextMenu, // ส่วนแสดงเมนูคลิกขวา หรือกดค้าง
                  pullToRefreshController: pullToRefreshController, // pull to refresh
                  onWebViewCreated: (controller) async {
                    // เมื่อเริ่มการสร้าง webview กำหนดค่า controller ไว้ใช้งาน
                    webViewController = controller;
                  },
                  // ส่วนจัดการเมื่อเริ่มมีการดาวน์โฆลดไฟล์
                  onDownloadStartRequest: (controller, url) async {

                      print("Download started: ${url.url}");
                      // ขอสิทธิ์การเข้าถึง storage
                      if (await Permission.storage.request().isGranted) {
                        // เรียกฟังก์ชันดาวน์โหลดไฟล์
                        await downloadFile(url.url);
                      }

                  },                  
                  onLoadStart: (controller, url) async { // เมื่อเริ่มโหลดหน้าเว็บ
                    _progressNotifier.value = 0; // สถานะการโหลด
                    setState(() {
                      // กำหนดค่าเริมต้นต่างๆ 
                      this.url = url.toString();
                      urlController.text = this.url;
                    });
                  },
                  // เมื่อเว็บไซต์นั้นๆ  ร้องขอสิทธิ์การเข้าถึงส่วนต่างๆ
                  onPermissionRequest: (controller, request) async {
                    return PermissionResponse(
                        resources: request.resources,
                        action: PermissionResponseAction.GRANT);
                  },
                  // การทำงานเมื่อมีการกดลิ้งค์ในเว็บไซต์
                  shouldOverrideUrlLoading:
                      (controller, navigationAction) async {
                        // ตรวจสอบค่าจาก url
                    var uri = navigationAction.request.url!;

                    // ถ้าไม่ใช่ค่าตามที่ระบบไว้ ให้เรียกใช้ฟังก์ชั่นการเปิดจาก url_launcher
                    if (![
                      "http",
                      "https",
                      "file",
                      "chrome",
                      "data",
                      "javascript",
                      "about"
                    ].contains(uri.scheme)) {
                       // เปลี่ยนลิ้งค์ เป็น url ที่สามารถเข้าได้ เช่น intent:// เปลี่ยนเป็น https:// 
                      String newUrl = '';
                      newUrl  = uri.toString().replaceFirst('${uri.scheme}://', 'https://'); // แปลงเป็น https URL
                      print("xdebug: not in allow ${newUrl}");
                      WebUri newUri = WebUri(newUrl);
                      if (await canLaunchUrl(newUri)) {
                        // เปิดจากแอป หรือส่วนอื่นที่ผ่านหน้าเว็บ
                        await launchUrl(
                          newUri,
                        );
                        // ถ้าไม่สามาถเปิดผ่านส่วนจัดการใดๆ ได้
                        return NavigationActionPolicy.CANCEL;
                      }
                    }
                    // กรณีเป็น url ตามลิสรายการที่กำหนด ให้อนุญาตเข้าใช้งานได้
                    return NavigationActionPolicy.ALLOW;
                  },
                  // เมื่อโหลดหน้าเพจเสร็จ
                  onLoadStop: (controller, url) async {
                    // หยุดการทำงานส่วนควบคุม pull to refresh
                    pullToRefreshController?.endRefreshing();
                    // เก็บค่าสถานะการลิ้งค์ไปหน้าต่างๆ ว่าทำได้หรือม่
                    _canGoBack.value =
                        await webViewController?.canGoBack() ?? false;
                    _canGoForward.value =
                        await webViewController?.canGoForward() ?? false;
                    setState(() {
                      this.url = url.toString();
                      urlController.text = this.url;
                    });
                  },
                  // กรณีการโหลดเกิด error 
                  onReceivedError: (controller, request, error) {
                    pullToRefreshController?.endRefreshing();
                  },
                  // การดำเนินการระหว่างกำลังโหลดหน้าเพจ
                  onProgressChanged: (controller, progress) {
                    // เมื่อโหลดเสร็จ 100 %
                    if (progress == 100) {
                      pullToRefreshController?.endRefreshing();
                    }
                    // ในระหว่างการโหลด เก็บค่า percent ไว้แสดง
                    _progressNotifier.value = progress / 100;
                    setState(() {
                      this.progress = progress / 100;
                      urlController.text = this.url;
                    });
                  },
                  // เมื่อมีการรีโหลดหน้านั้นๆ
                  onUpdateVisitedHistory: (controller, url, isReload) {
                    setState(() {
                      this.url = url.toString();
                      urlController.text = this.url;
                    });
                  },
                  // เมื่อมีการแสดงข้อความจาก console.log
                  onConsoleMessage: (controller, consoleMessage) {
                    print(consoleMessage);
                  },
                ),
                // ส่วนของการแสดงสถานะขณะกำลังโหลด
                // ส่วนนี้จะซ้อนอยู่ด้านบนก่อนที่เว็บจะโหลดเสร็จก็จะหายไป
                ValueListenableBuilder<double>(
                  valueListenable: _progressNotifier,
                  builder: (context, progress, child) {
                    if (progress == 1.0) {
                      return SizedBox
                          .shrink(); // ไม่แสดงอะไรเมื่อโหลดเสร็จ
                    } else {
                      // ขณะกำลังโหลด เราสามารถสร้างตัวคลุมไม่ให้เห็นส่วนของเว็บกำลังโหลดได้
                      // อย่างในที่นี้ เราสร้างตัวคลุมพื้นหลังสีขาว กั้นไว้ ให้ css หรือ javascript ทำงาน
                      // ถ้าอยากให้เห็นหน้าเพจกำลังโหลด สามารถกำหนดสีพ้ืนหลังเป็น Colors.transparent
                      return Container(
                        width: double.infinity, // Full width
                        height: double.infinity, // Full height
                        // color: Colors.white, // White background
                        child: Center(
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              CircularProgressIndicator(
                                color: Color.fromRGBO(64, 64, 64, 1),
                                value: progress,
                              ),
                              SizedBox(height: 10),
                              Text('${(progress * 100).round()}%'),
                            ],
                          ),
                        ),
                      );
                    }
                  },
                ),
                // ถ้าเริ่มดาวน์โหลด 
                if (downloadProgress > 0)
                LinearProgressIndicator(
                  value: downloadProgress,
                  backgroundColor: Colors.grey,
                  valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
                ),
              ],
            ),
          ),
        ])));
  }
}


// สร้าง widget สำหรับทำปุ่มควบคุม เช่น ก่อนหน้า ย้อนหลัง รีเฟรช
class NavigationControls extends StatelessWidget {
  // กำหนด class constructor รับค่าที่จำเป็น
  const NavigationControls({
    required this.controller,
    required this.canGoBack,
    required this.canGoForward,
    required this.initialUrl,
    Key? key,
  }) : super(key: key);

  // กำหนดตัวแปรที่เกี่ยวข้อง
  /*
   ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier 
   ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง 
   ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management 
   library ที่ซับซ้อน เช่น Provider หรือ Bloc
  */
  final InAppWebViewController? controller;
  final ValueNotifier<bool> canGoBack;
  final ValueNotifier<bool> canGoForward;
  final String initialUrl;

  /*
  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: Icon(
                Icons.home,
                color: value ? Colors.white : Colors.white60,
              ),
              onPressed: value
                  ? () => controller?.loadUrl(
                      urlRequest: URLRequest(url: WebUri(initialUrl)))
                  : null,
            );
          },
        ),
        ValueListenableBuilder<bool>(
          valueListenable: canGoBack,
          builder: (context, value, child) {
            return IconButton(
              icon: Icon(
                Icons.arrow_back,
                color: value ? Colors.white : Colors.white60,
              ),
              onPressed: value ? () => controller?.goBack() : null,
            );
          },
        ),
        ValueListenableBuilder<bool>(
          valueListenable: canGoForward,
          builder: (context, value, child) {
            return IconButton(
              icon: Icon(
                Icons.arrow_forward,
                color: value ? Colors.white : Colors.white60,
              ),
              onPressed: value ? () => controller?.goForward() : null,
            );
          },
        ),
        IconButton(
          icon: const Icon(Icons.refresh),
          onPressed: () => controller?.reload(),
        ),
      ],
    );
  }
}
 

ผลลัพธ์ที่ได้

 
จะเห็นว่าการใช้งาน Dio เราสามารถรู้สถานะการดาวน์โหลด การติดตามความก้าวหน้าการดาวน์โหลด
ข้อมูลได้ จะเห็นผลกรณีเป็นไฟล์ที่มีขนาดใหญ่ เราจะได้รู้ว่าดาวน์โหลดไปถึงไหนแล้ว ซึ่งสามารถนำค่า
ความก้าวหน้าการดาวน์โหลดไปปรับแต่ง UI แจ้งสถานะได้ตามต้องการ ในตัวอย่าง ใช้เป็นแบบแถบ
สถานะด้านบน ถ้าโหลดเสร็จก็จะขึ้นสีเขียวเต็มและรอ 3 วินาทีก็จะหายไปเพื่อรีเซ็ตค่ารอการดาวน์โหลด
ไฟล์ใหม่ถ้ามีอีกครั้ง
 
 

การอัปโหลดไฟล์ด้วย Dio 

    โค้ดส่วนนี้เป็นแค่แนวทางตัวอย่างเพิ่มเติมเท่านั้น อาจจะมีบทความเพิ่มเติม รอติดตาม เป็นส่วนของ
ตัวอย่างกรณีต้องการใช้งาน Dio เพื่ออัปโหลดไฟล์ขึ้นไปยัง server ตัวอย่างโค้ดด้านล่าง แค่ให้เห็น
แนวทางกระบวนการทำงานเท่านั้น
 
import 'package:dio/dio.dart';
import 'dart:io';

void uploadFile() async {
  Dio dio = Dio();

  // กำหนด path ของไฟล์ที่ต้องการอัปโหลด
  String filePath = '/path/to/your/file.txt'; // แทนที่ path นี้ด้วย path จริงของไฟล์ที่ต้องการอัปโหลด
  File file = File(filePath);

  try {
    // สร้าง FormData เพื่อใส่ไฟล์ที่ต้องการอัปโหลด
    FormData formData = FormData.fromMap({
      'file': await MultipartFile.fromFile(filePath, filename: 'file.txt'), // กำหนดชื่อไฟล์บนเซิร์ฟเวอร์
    });

    // ส่ง request อัปโหลดไฟล์ไปยังเซิร์ฟเวอร์
    Response response = await dio.post(
      'https://example.com/upload', // เปลี่ยน URL เป็น endpoint ของเซิร์ฟเวอร์ที่ต้องการอัปโหลดไฟล์
      data: formData,
      options: Options(
        headers: {
          // ถ้าจำเป็นต้องมี Authorization หรือ headers เพิ่มเติม
          'Authorization': 'Bearer your_token',
        },
      ),
      // ฟังก์ชันเพื่อติดตามความก้าวหน้าของการอัปโหลด
      onSendProgress: (int sent, int total) {
        print('Progress: ${(sent / total * 100).toStringAsFixed(0)}%');
      },
    );

    // ตรวจสอบการตอบกลับจากเซิร์ฟเวอร์
    if (response.statusCode == 200) {
      print('File uploaded successfully');
    } else {
      print('Failed to upload file');
    }
  } catch (e) {
    print('Error uploading file: $e');
  }
}

// เรียกใช้งาน
uploadFile();
สำหรับเนื้อหาเกี่ยวกับการจัดการดาวน์โหลดไฟล์ผ่าน inappwebview โดยทั้งแบบใช้งาน Dio และ
ไม่ใช้งานก็มีประมาณนี้ เป็นแนวทางในการประยุกต์ต่อไป


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ



ทบทวนบทความที่แล้ว









เนื้อหาที่เกี่ยวข้อง






เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน เพื่ออ่านเนื้อหาบทความ

ยังไม่เป็นสมาชิก

สมาชิกล็อกอิน



( หรือ เข้าใช้งานผ่าน Social Login )




URL สำหรับอ้างอิง





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


    ( หรือ เข้าใช้งานผ่าน Social Login )







เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ