การกำหนดและใช้งาน PopupMenuButton ใน Flutter

เขียนเมื่อ 3 ปีก่อน โดย Ninenik Narkdee
flutter popupmenubutton enum type

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

ดูแล้ว 4,725 ครั้ง




เนื้อหาตอนต่อไปนี้จะมาดู Widget เล็กๆ ที่มีรูปแบบการใช้งาน
ง่ายๆ ที่เรียกว่า PopupMenuButton เป็นปุ่มเมนูเพิ่มเติมที่แสดงมา
ให้เราเลือกใช้งาน หรือกำหนดการทำคำสั่งที่ต้องการทำงานเพิ่มเติม
จะใช้เนื้อหาจากตอนที่แล้ว จะจัดการเฉพาะในไฟล์ article.dart
ทบทวนตอนที่แล้วได้ที่บทความ
    การใช้งาน WebView แสดงเว็บไซต์ ใน Flutter http://niik.in/1043
 
    *เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/961
 
 
 

การใช้งาน PopupMenuButton

    ตัว PopupMenuButton เมื่อกำหนดหรือเรียกใช้งาน จะแสดงเป็นปุ่มไอคอน จุด 3 จุดใน
แนวตั้งหรือชื่อไอคอนว่า Icons.more_vert เป็นการสื่อว่ามีเพิ่มเติม เมื่อเรากดที่ปุ่มนี้ก็จะแสดง
ลิสราายการปุ่มต่างๆ ให้เราเลือก ถ้าเราเลือกปุ่มรายการใดๆ ก็จะใช้ค่าปุ่มรายการนั้นๆ เป็นตัว
กำหนดเงื่อนไขการทำงานอีกที ถ้าเราไม่ต้องการเลือกรายการที่แสดง ก็สามารถกดไปที่พื้นที่ว่าง
นอกรายการเพื่อปิดปุ่มนั้นๆ ไป  ในการสร้างปุ่ม PopupMenuButton จะต้องมีการกำหนด itemBuilder
เพื่อสร้างรายการของปุ่ม 
 

    รูปแบบการใช้งาน PopupMenuButton

 
PopupMenuButton<T>(
  onSelected: (T result) { },
  itemBuilder: (BuildContext context) => <PopupMenuEntry<T>>[
    const PopupMenuItem<T>(
      value: T.value,
      child: Text('Menu 1'),
    ),
    const PopupMenuItem<T>(
      value: T.value,
      child: Text('Menu 2'),
    ),    
  ],
)
 
    สัญลักษณ์ T คือข้อมูลประเภท Type หรือก็คือ class ดูตัวอย่าง type ในภาษา Dart 
 
// ข้อมูล type ColorOption
enum ColorOption { red, green, blue }
// ข้อมูล type Option
class Option{
  int a = 0;
}
 
    ทั้ง ColorOption และ Option เป็นรูปแบบหนึ่งของ class โดยตัว ColorOption จะใช้คำว่า enum เป็นคำ
keyword เป็น class พิเศษเฉพาะที่กำหนดจำนวนของค่าคงที่ ที่เรียกว่า enum type ข้อมูลที่มีการระบุแจกแจง
ค่าไว้อย่างชัดเจน ค่าของ Enum จะอ้างอิงผ่าน property ที่ชื่อ values
 
print(ColorOption.values); // แสดงข้อมูลของ enum type
 
    ก็จะได้เป็น List<ColorOption> มีค่าเป็น
 
[ColorOption.red, ColorOption.green, ColorOption.blue]
// ColorOption.values[0] = ColorOption.red
// ColorOption.values[1] = ColorOption.green
// ColorOption.values[2] = ColorOption.blue
 
    เราจะใช้ช้อมูล Enum type สำหรับกำหนดรายการให้กับ PopupMenuButton ยกตัวอย่างเช่นข้อมูล
 
enum ColorOption { red, green, blue }
 
    สามารถกำหนดใช้งานใน PopupMenuButton เป็นดังนี้
 
PopupMenuButton<ColorOption>(
  onSelected: (ColorOption result) { },
  itemBuilder: (BuildContext context) => <PopupMenuEntry<ColorOption>>[
    const PopupMenuItem<ColorOption>(
      value: ColorOption.red,
      child: Text('Menu 1 Red'),
    ),
    const PopupMenuItem<ColorOption>(
      value: ColorOption.green,
      child: Text('Menu 2 Green'),
    ),    
    const PopupMenuItem<ColorOption>(
      value: ColorOption.blue,
      child: Text('Menu 3 Blue'),
    ),        
  ],
)
 
    หรือกรณีเราใช้เป็นข้อมูล String type ก็จะเป็นประมาณนี้ 
 
PopupMenuButton<String>(
  onSelected: (String result) { },
  itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
    const PopupMenuItem<String>(
      value: '1',
      child: Text('Menu 1 Red'),
    ),
    const PopupMenuItem<String>(
      value: '2',
      child: Text('Menu 2 Green'),
    ),    
    const PopupMenuItem<String>(
      value: '3',
      child: Text('Menu 3 Blue'),
    ),        
  ],
)
 
    หรือกรณีเราใช้เป็นข้อมูล boolean type ก็จะเป็นประมาณนี้ 
 
PopupMenuButton<bool>(
  onSelected: (bool result) { },
  itemBuilder: (BuildContext context) => <PopupMenuEntry<bool>>[
    const PopupMenuItem<bool>(
      value: true,
      child: Text('Menu 1 Red'),
    ),
    const PopupMenuItem<bool>(
      value: false,
      child: Text('Menu 2 Green'),
    ),    
  ],
)
 
    เราสามารถสร้างลิสรายการจากข้อมูลอาเรย์หรือ List ได้ง่ายเพื่อลดขึ้นตอนการกำหนดแต่ละรายการ
 
// สร้างตัวแปร ลืสรายการเมนูที่เป็น String
var myMenuItems = <String>[
  'Home',
  'Profile',
  'Setting',
];    
 
    จากนั้นทำการวนลูปแสดงใน PopupMenuItem ดังนี้
 
PopupMenuButton<String>(
  onSelected: (String result) { },
  itemBuilder: (BuildContext context) {
    return myMenuItems.map((String choice) {
      return PopupMenuItem<String>(
        child: Text(choice),
        value: choice,
      );
    }).toList();
  }
)
 
    ผลลัพธ์ที่ได้
 


 
 
    กรณีประยุกต์กับ Map Type เพิ่ม FontAwesome ไอคอนเข้าไป
 
 
// สร้างตัวแปร ลืสรายการเมนูที่เป็น Map<dynamic, dynamic>
var myMenuItems = <Map>[
  {'icon':FontAwesomeIcons.home,'value':'home','label':'Home'},
  {'icon':FontAwesomeIcons.userAlt,'value':'profile','label':'Profile'},
  {'icon':FontAwesomeIcons.cog,'value':'setting','label':'Setting'}
];   
 
    จากนั้นทำการวนลูปแสดงใน PopupMenuItem ดังนี้
 
PopupMenuButton<Map>(
  onSelected: (Map result) { },
  itemBuilder: (BuildContext context) {
    return myMenuItems.map((Map choice) {
      return PopupMenuItem<Map>(
        child: ListTile(
          leading: Icon(choice['icon']),
          title: Text(choice['label'], style: Theme.of(context).textTheme.bodyText1),
        ),
        value: choice,
      );
    }).toList();
  }
)
 
    ผลลัพธ์ที่ได้
 


 
 
    ตอนนี้เราได้รู้จักแนวทางการประยุกต์การสร้างลิสรายการในรูปแบบต่างๆ ให้สังเกตให้ค่า value ของ
PopupMenuItem คือเมื่อเราแตะเลือกที่รายการใด ค่า value นี้จะถูกส่งเข้าไปใน callback ฟังก์ชั่นของ
onSelected ดังนั้นในการกำหนดเงื่อนไขการทำงาน ก็จะไปกำหนดในค่าที่เลือกว่าเป็นค่าใด และให้ทำงาน
อย่างเรา ยกตัวอย่างรูปแบบกรณีล่าสุด ก็จะเป็น
 
PopupMenuButton<Map>(
  onSelected: (Map result) {
    result = Map<String, dynamic>.from(result); // แปลงค่ากลับ
    switch (result['value']) { // ตรวจสอบค่าที่จะใช้งานเป็นเงื่อนไข
      case 'home':
        print('Home clicked');
        break;
      case 'profile':
        print('Profile clicked');
        break;
      case 'setting':
        print('Setting clicked');
        break;
    }
  },
  itemBuilder: (BuildContext context) {
    return myMenuItems.map((Map choice) {
      return PopupMenuItem<Map>(
        child: ListTile(
          leading: Icon(choice['icon']),
          title: Text(choice['label'], style: Theme.of(context).textTheme.bodyText1),
        ),
        value: choice,
      );
    }).toList();
  }
)
 
    เนื่องจากค่าจาก Map type เป็นข้อมูลที่มีความซับซ้อนดังนั้น จึงมีการแปลงกลับมาในรูปแบบที่สามารถ
อ้างอิงการใช้งานได้ก่อน แต่ถ้าเป็นค่าอื่นๆ เช่น boolean Sring Int Enum เหล่านี้ สามารถนำค่าไปตรวจ
สอบเป็นเงื่อนไขได้เลย 
    ในตัวอย่างการทำคำสั่งเมื่อเข้าเงื่อนไข จะใช้เป็นการเรียกใช้ฟังก์ชั่นอีกที เพราะถ้าเขียนการทำงานในนี้
ก็จะยาวเกินไป ข้างต้นเราแค่ทดสอบแสดงข้อความเท่านั้น
 

    ไฟล์ 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);    
    // ส่วนของตัวแปรจัดการ cookies
    final WebViewCookieManager _cookieManager = WebViewCookieManager();   
    // ส่วนของตัวแปร กำหนดให้ตรวจสอบว่าโหลด url แล้วหรือไม่เพื่อเรียกใช้งานเพียงครั้งเดียวที่เปิดขึ้นมา
    bool _isUrlLoaded = 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; // ถ้าเป็นลิ้งค์อื่นๆ เข้าไปปกติ
          },
        ),
      // เพิ่มส่วนนี้เพื่อ สร้าง JavascriptChannel สำหรับรับค่าข้อมูลที่ส่งผ่านทาง JavaScript  
      )..addJavaScriptChannel(
        'Toaster', // กำหนดชื่อสำหรับเรียกใช้งาน
        onMessageReceived: (JavaScriptMessage message) {
          print(message.message);
          // ในที่นี้เมื่อได้ค่ามาแล้ว จะแสดงข้อความด้วย SnackBar
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(message.message)),
          );          
        },
      );       

    }


    @override
    Widget build(BuildContext context) {
        // เนื่องจาการใช้งาน PopupMenuButton จะมีการ rebuild widget ทุกครั้งที่กด
        // ดังนั้นเพื่อไม่ให้มีการโหลดหน้าเพจ เมื่อกดที่ปุ่มเมนูนี้ เราต้องกำหนดเงื่อนไขว่า 
        // โหลดหน้าเพจเฉพาะครั้งแรกเท่าานั้น
        if (!_isUrlLoaded) {
        // รับค่า url ที่ส่งมาใน arguments          
          final url = ModalRoute.of(context)!.settings.arguments as String;
          _controller.loadRequest(Uri.parse(url));
          _isUrlLoaded = true;
        }

        return Scaffold(
            appBar: AppBar(
                title: Text('Articles'),
                actions: <Widget>[
                  NavigationControls( // เมนูส่วนของการใช้งาน NavigationControls
                    controller: _controller, 
                    canGoBack: _canGoBack, 
                    canGoForward: 
                    _canGoForward
                  ),          
                  SampleMenu( // เมนูส่วนของการใชงาน PopupMenuButton
                    controller: _controller,
                    cookieManager: _cookieManager),
                ],                
            ),
            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(),
        ),
      ],
    );
  }
}


// กำหนด Enum Type สำหรับเป็นลิสรายการของ PopupMenuButton
enum MenuOptions {
  showUserAgent,
  listCookies,
  clearCookies,
  addToCache,
  listCache,
  clearCache,
}
 
// สร้าง widget สำหรับทำปุ่มควบคุม เพิ่มเติมแบบ PopupMenuButton
class SampleMenu extends StatelessWidget {

  // กำหนด class constructor รับค่าที่จำเป็น
  SampleMenu({
    required this.controller,
    required this.cookieManager,
    Key? key,
  }) : super(key: key);

  final WebViewController controller; // ใช้งาน WebViewController
  final WebViewCookieManager cookieManager; // ใช้งาน CookieManager
 
  @override
  Widget build(BuildContext context) {
        return PopupMenuButton<MenuOptions>(
          onSelected: (MenuOptions value) {
            switch (value) { // ใช้เงื่อนไขค่าที่เลือก ทำฟังก์ชั่นที่ต้องการ
              case MenuOptions.showUserAgent:
                _onShowUserAgent(controller, context);
                break;
              case MenuOptions.listCookies:
                _onListCookies(controller, context);
                break;
              case MenuOptions.clearCookies:
                _onClearCookies(context);
                break;
              case MenuOptions.addToCache:
                _onAddToCache(controller, context);
                break;
              case MenuOptions.listCache:
                _onListCache(controller, context);
                break;
              case MenuOptions.clearCache:
                _onClearCache(controller, context);
                break;
            }
          },
          itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
            PopupMenuItem<MenuOptions>(
              value: MenuOptions.showUserAgent,
              child: const Text('Show user agent'),
              // enabled: controller.!,
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.listCookies,
              child: Text('List cookies'),
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.clearCookies,
              child: Text('Clear cookies'),
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.addToCache,
              child: Text('Add to cache'),
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.listCache,
              child: Text('List cache'),
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.clearCache,
              child: Text('Clear cache'),
            ),
          ],
        );
  }
 
  // ส่วนของฟังก์ชั่นการทำงานต่างๆ 
 
  // ฟังก์ชั่นแสดง UserAgent ของ WebView
  void _onShowUserAgent(
      WebViewController controller, BuildContext context) async {
    await controller.runJavaScript(
        'Toaster.postMessage("User Agent: " + navigator.userAgent);');
  }
 
  // ฟังก์ชั่นแสดงรายการ cookie
  void _onListCookies(
      WebViewController controller, BuildContext context) async {
      final String cookies = await controller
      .runJavaScriptReturningResult('document.cookie')
      .then((value) => value.toString());
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          const Text('Cookies:'),
          _getCookieList(cookies),
        ],
      ),
    ));
  }
 
  // ฟังก์ชั่นเพิ่มรายการ cache
  void _onAddToCache(WebViewController controller, BuildContext context) async {
    await controller.runJavaScript(
        'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";');
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
      content: Text('Added a test entry to cache.'),
    ));
  }
 
  // ฟังก์ชั่นแสดงรายการ cache
  void _onListCache(WebViewController controller, BuildContext context) async {
    await controller.runJavaScript('caches.keys()'
        '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))'
        '.then((caches) => Toaster.postMessage(caches))');
  }
 
  // ฟังก์ชั่นล้างค่า cache
  void _onClearCache(WebViewController controller, BuildContext context) async {
    await controller.clearCache();
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
      content: Text("Cache cleared."),
    ));
  }
 
  // ฟังก์ชั่นล้างค่า cookie
  void _onClearCookies(BuildContext context) async {
    final bool hadCookies = await cookieManager.clearCookies();
    String message = 'There were cookies. Now, they are gone!';
    if (!hadCookies) {
      message = 'There are no cookies.';
    }
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text(message),
    ));
  }
 
  // ฟังก์ชั่นแสดงรายการ cookie
  Widget _getCookieList(String cookies) {
    if (cookies.isEmpty || cookies == '""') {
      return Container();
    }
    final List<String> cookieList = cookies.split(';');
    final Iterable<Text> cookieWidgets =
        cookieList.map((String cookie) => Text(cookie));
    return Column(
      mainAxisAlignment: MainAxisAlignment.end,
      mainAxisSize: MainAxisSize.min,
      children: cookieWidgets.toList(),
    );
  }
}
 
    ผลลัพธ์ที่ได้
 


 
 
    เนื้อหานี้จะเน้นไปที่การใช้งาน PopupMenuButton รายละเอียดโค้ดอื่นๆ ที่เสริมเข้ามามีรูปแบบ
การใช้งานเหมือนบทความตอนที่แล้ว คำอธิบายแสดงในโค้ด
    หวังว่าเนื้อหานี้จะทำให้เราสามารถประยุกต์การใช้งาน PopupMenuButton เพื่อกำหนดคำสั่งเพิ่ม
เติมที่ต้องการได้ เนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม


   เพิ่มเติมเนื้อหา ครั้งที่ 1 วันที่ 28-07-2024


ดาวน์โหลดโค้ดตัวอย่าง สามารถนำไปประยุกต์ หรือ run ทดสอบได้

http://niik.in/download/flutter/demo_018_28072024_source.rar


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



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



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









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









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





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

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


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


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







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