การกำหนด และใช้งาน ListView Widget ใน Flutter เบื้องต้น

เขียนเมื่อ 4 ปีก่อน โดย Ninenik Narkdee
listtile listview flutter itembuilder

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

ดูแล้ว 18,890 ครั้ง




ต่อจากตอนที่แล้ว ที่เราได้ใช้งาน Package จากภายนอก
สำหรับสร้าง random word มาใช้งานในโปรเจ็ค App และ
จำนำมาต่อยอดในเนื้อหาของตอนนี้ ซึ่งเป็นการใช้งาน ListView
    ดังนั้น ก่อนที่จะเข้าสู่เนื้อหาการประยุกต์ใช้งาน เราจะทำความ
รู้จักกับ ListView Widget เบื้องต้นกันก่อน ว่ามีรูปแบบการกำหนด
และการใช้งานอย่างไร 
    ทบทวนเนื้อหาตอนที่แล้วได้ที่บทความด้านล่าง
    การติดตั้งและใช้งาน Package ภายนอก ใน Flutter เบื้องต้น http://niik.in/956 

 
 

การใช้งาน ListView Widget

    เป็น widget ที่ใช้ในการสร้างลิสรายการที่สามารถเลือนได้ โดยเรียงต่อกันเป็นแนว สามารถกำหนดลิสรายการเป็น widget ต่างๆ
ส่วนใหญ่แล้วเราจะพบเห็นใช้บ่อยในการสร้างเป็นลิสรายการข้อความ  widget ยอ่ยหรือลิสรายการแต่ละรายการจะเรียงต่อหลังกันไป
เรื่อยๆ ตามทิศทางการเลื่อน scroll ซึ่งเป็นได้ทั้งในแนวตั้งและแนวนอน ขึ้นอยู่กับการกำหนด
    สามารถสร้าง ListView ได้จากวิธีต่างๆ 4 วิธี ดังนี้
 

    1. การใช้งาน ListView()

    ใช้ default constructor เป็น ListView() แล้วกำหนด children เป็น List<Widget> หรืออาเรย์ widget ต่างๆ ที่ต้องการ ซึ่ง
โดยทั่วไปจะใช้เป็น ListTile ที่จะมีรูปแบบที่สามารถกำหนด ส่วนของ leading, title, subtitle และ trailing เหล่านี้เป็นต้น
ในตัวอย่างด้านล่าง กำหนดโดยใช้ ListTile สามรายการแรก และรายการที่ 6 ส่วนรายการที่ 4 และ 5 กำหนดโดยใช้ Text และ Icon
widget ตามลำดับ
 
 
body: ListView(
    children: <Widget>[
      ListTile(
        onTap: (){},
        leading: Icon(Icons.map),
        title: Text('Map'),
      ),
      ListTile(
        onTap: (){},
        leading: Icon(Icons.photo_album),
        title: Text('Albumn'),
      ),
      ListTile(
        onTap: (){},
        leading: Icon(Icons.phone),
        title: Text('Phone'),
      ),
      Text('This is only text item'),
      Icon(Icons.favorite),
      ListTile(
        onTap: (){},
        leading: Icon(Icons.photo),
        title: Text('Photo'),
        subtitle: Text('Subtitle text'),
        trailing: Icon(Icons.delete,color: Colors.red,),
      ),
    ],
),            
 
    ผลลัพธ์ที่ได้
 
 

 
 
    การใช้งาน ListView รูปแบบนี้ เหมาะสำหรับรายการที่มีจำนวนไม่มาก เช่น 4 - 10 รายการหรือไม่ควรเกินขอบเขต ที่สามารถมอง
เห็นได้ เช่นมีจำนวนเกินความสูงของหน้าจอทำให้มีบางส่วนถูกซ่อนไป และอีกสาเหตุที่ไม่ควรมีจำนวนมาก เพราะว่า เราจะต้องจัดการ
แต่ละ ลิสรายการ เช่นกำหนดการเรียกใช้คำสั่ง เมื่อเกิด onTap หรือแตะที่รายการนั้น โดยจะทำได้กับรายการที่สามารถมองเห็นได้เท่านั้น


 

    2. การใช้งาน ListView.builder()

    ใช้ named constructor เป็น ListView.builder() แล้ว child ลิสรายการ ให้กับ itemBuilder ด้วยการเรียกใช้งานฟังก์ชั่น IndexedWidgetBuilder()
เมื่อเลื่อน scroll เพื่อแสดงรายการโดยพิจารณาจากค่า index   ซึ่งลิสรายการจะไม่ถูกเรียกมาแสดงทั้งหมด
ในครั้งเดียว เหมือนกับการใช้งานในวิธีแรก แต่จะแสดงเฉพาะบางส่วนให้เต็มพื้นที่ที่มองเห็น เช่น หน้าจอแสดงได้ 10 รายการ ก็อาจจะแสดง
มาสัก 15-20 กว่ารายการ เป็นต้น และเมื่อทำการเลื่อน scroll ลงไปเพื่อแสดงรายการที่เหลือ ฟังก์ชั่น IndexedWidgetBuilder() ก็จะทำการเพิ่มรายการ
เข้ามาเรื่อยๆ ให้เต็มพื้นที่ที่สามารถมองเห็นได้ จนกว่ารายการจะแสดงครบ 
หรือบางทีก็เป็นรายการแบบ infinite ที่เพิ่มรายการได้ไม่สิ้นสุด ซึ่งวิธีที่สองนี้เหมาะกับการใช้งานการแสดงลิสรายการจำนวนมากๆ
    สมมติเราจำลองสร้าง รายการทั้งหมด 1000 รายการโดย generate ค่าไว้ในตัวแปร items ด้วยคำสั่ง
 
final items = List<String>.generate(10000, (i) => "Item $i");
    จากนั้นเรียกใช้งาน ListView.builder() จะได้เป็นดังนี้
 
 
body:ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index){
    return ListTile(
      title: Text('${items[index]}'),
      onTap: (){},
    );
  }
),        
 
    ในบรรทัดที่ 3 itemBuilder จะสร้างรายการโดยเรียกใช้งานฟังก์ชั่น IndexedWidgetBuilder() สังเกตจะเห็นว่าฟังก์ชั่นนี้ ไม่มีการ
กำหนดชื่อฟังก์ชั่น เราเรียกว่า Typedef ในภาษา Dart เหมือนเป็นฟังก์ชั่นต้นแบบหรือ prototype โดยตัวฟังก์ชั่น IndexedWidgetBuilder
นี้จะวนลูป List หรืออาเรย์ของ context (ในที่นี้คือตัวแปร items ที่เราทำการสร้างรายการสมมติมา 1000 รายการ) แสดงรายการให้เต็ม
พื้นที่ที่สามารถมองเห็น และเมื่อเลื่อน scroll ลงไปเพื่อแสดงรายกาเพิ่มเติม ก็จะทำการวนลูปแสดงรายการจาก index ที่เหลือต่อไปเรื่อยๆ
    บรรทัดที่ 2 อย่าลืมกำหนด itemCount ไม่อย่างงั้นจะเกิด Range error นั่นคือไปวนลูปเพิ่มรายการเกินขอบเขตหรือเกินจำนวน
ที่มีจริง ในที่นี้คือระบุ items.length
 
    ผลลัพธ์ที่ได้
 
 

 
 
    จะเห็นว่าเมื่อแสดงครั้งแรก จะแสดงลิสรายการที่ index 0 -10 และพอเราเลื่อนลงไป ก็จะแสดงรายการเพิ่มเข้ามาเรือยๆ ตามค่า index
ที่เปลี่ยนแปลง


 

    3. การใช้งาน ListView.separated()

    ใช้ named constructor เป็น ListView.separated() รูปแบบวิธีการนี้ จะเหมือนกับวิธีที่สอง แต่ที่เพิ่มเข้ามา คือมีส่วนของตัวที่กำหนด
ตัวแบ่งคั่นเพิ่มเข้ามา นั่นคือในวิธีที่ 2 มีเฉพาะ itemBuilder ส่วนวิธีที่ 3 จะมี separatorBuilder ที่ทำงานคล้ายกันเพิ่มเข้ามา เข้าใจอย่าง
ง่าย คือเมื่อวนลูปสร้างรายการใหม่แต่ละครั้ง ก็จะวนลูปสร้างตัวแบ่งเพิ่มเข้ามาด้วย ซึ่งตัวแบ่ง เราอาจจะใช้เป็น Divider widget ที่เป็นเส้น
คั่น 1px หรือจะใช้เป็น ลิสแบ่งหัวข้อรายการลิสอีกทีก็ได้ ขึ้นอยู่กับการประยุกต์ใช้งาน  
    วิธีการนี้ จริงๆ แล้วเราสามารถใช้วิธีที่ 2 แล้วสร้างเงือนไข ในการสร้าง wdiget ได้ โดยพิจารณาใช้จากค่า index ที่เริ่มต้นจาก 0 เสมอ
แล้วเพิ่มค่าไปเรื่อยๆ เช่น index (0) แรกเป็น ลิสรายการ ข้อความ index (1) ถัดไปเป็นลิสรายการ ตัวแบ่ง และเมื่อลำดับ index เพิ่มข้ึน
ในรูปแบบ 0, 1, 2, 3, 4, ..... นั่นคือที่ตำแหน่ง 1, 3 ซึ่งเป็นเลขคี่ เราก็ใช้เป็นเงื่อนไขตัวแบ่งได้ดังนี้
 
 
body:ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index){
    if(index.isOdd) return Divider();
    return ListTile(
      title: Text('${items[index]}'),
      onTap: (){},
    );
  }
),   
 
    ในบรรทัดที่ เราเพิ่ม if (index.isOdd) return Divider(); เข้าไป เพื่อให้สร้าง Divider wdiget เข้าไปแทนใน index ที่เป็นเลขคี่
แต่ผลที่ตามมาคือ เราใช้การนับจำนวน itemCount จากจำนวนของข้อมูล ดังนั้นจำนวนข้อมูลที่แสดงจริงจะไม่ครบ ดูตัวอย่าง เรากำหนด
ลิสรายการ 5 รายการ ซึ่งจะมี index เป็น 0,1,2,3,4 และตำแหน่งที่ 1,3 เป็นของ Divider ทำให้ข้อมูลแสดงแค่ตำแหน่งที่ 0,2 และ 4
ซึ่งข้อมูลเราควรมี 5 รายการ ดูผลลัพธ์ ดังรูป ที่แสดงรายการไม่ครบ เพราะมีตัวแบ่งเพิ่มเข้ามา
 
 

 
 
    วิธีแก้ปัญหาที่เหมาะสม คือ สร้างฟังก์ชั่นที่ทำการ return ค่า Widget ที่มีการจัดรูปแบบแล้วกลับออกมาเเป็น context แทนการกำหนด
การใช้งาน widget เดียว นั้นคือ เราสร้างฟังก์ชั่น ที่รวมเอา ListTile และ Divider รวมกัน แล้ว return ออกมาเป็น Widget เดียว ดังนี้
 
 
            body:ListView.builder(
              itemCount: items.length,
              itemBuilder: (context, index){
                return _buildRow(context, index);
              }
            ),               
            floatingActionButton: FloatingActionButton(
                backgroundColor: Colors.green,
                onPressed: _generateWord,
                child: Icon(Icons.add),
            ),
        );
    }

    Widget _buildRow(context, index){
      return Container(
        child: Column(
            children: <Widget>[
              ListTile(
                title: Text('${items[index]}'),
                onTap: (){},
              ),
              const Divider(),
            ],
          ),
      );
    }
}
 
    บรรทัดที่ 4 แทนที่เราจะ return ListTile หรือ Divider ตามเงื่อนไข index อย่างใดอย่างหนึ่ง เราก็สามารถเลือกที่จะ return ทั้งสอง
ออกมาพร้อมกันโดย จากการใช้ฟังก์ชั่น _buildRow() โดยส่ง context และ index เป็น paramter เข้าไปใช้งานอีกที บรรทัดที่ 15 - 27
เป็นส่วนที่เราสร้าง Widget ใหม่ โดยวิธีที้ เราสามารถกำหนดรูปแบบหรือจัดเรียง widget ได้ตามต้องการ แต่ถ้าเราต้องการใช้งาน ListTile
ซึ่งทั่วไปแล้วมักกำหนดใช้งานกับ ListView และจะสามารถกำหนดได้ในบาง widget เช่น  Column , Drawer  และ Card เป็นต้น
 
    ผลลัพธ์ที่ได้
 
 

 
 
    และอีกวิธี ก็คือใช้วิธีที 3 แทนดังนี้
 
 
body: ListView.separated(
  itemCount: items.length,
  itemBuilder: (context, index){
    return ListTile(
      title: Text('${items[index]}'),
      onTap: (){},
    );
  },
  separatorBuilder: (context, index){
    return const Divider();
  },
),      
 
    วิธีการนี้ เราใช้ตัวแบ่ง โดยเรียกฟังก์ชั่น IndexedWidgetBuilder แยก จึงไม่มีผลกับการแสดงข้อมูล เพราะใช้ index จาก Context 
คนละตัวกัน เราสามารถใช้รูปแบบ Fat Arrow ให้กับฟังก์ชั่น ที่มีการทำงานแค่บรรทัดเดียว สามารถใช้เป็น
 
separatorBuilder: (context, index) => const Divider(),
    ผลลัพธ์ที่ได้
 
 

 

 

    4. การใช้งาน ListView.custom()

    ใช้ named constructor เป็น ListView.custom() จะมีการใช้งาน SliverChildDelegate class เป็นตัวแทนสร้าง ลิสรายการให้กับ
childrenDelegate โดยสามารถกำหนดการจัดการหรือรูปแบบเพิ่มเติมให้กับ child ได้ เช่น SliverChildDelegate สามารถประมาณการ
ขนาดของ child ที่ยังไม่ได้แสดงได้ ส่วนนี้จำเป็นต้องเข้าใจส่วนอื่นเพิ่มเติม ดังนั้นขอ ข้ามรายละเอียดไปก่อน อาจจะนำมาอธิบายเพิ่มเติม
ภายหลังหากมีเนื้อหาที่สัมพันธ์กับการใช้งาน
 
 
    นอกจากแนวทางการสร้าง ListView แล้ว เรายังจัดรูปแบบการใช้งานเพิ่มเติมได้ เช่น การแสดงในแนวนอนดังนี้ 
 
 
            body: Container(
              margin: EdgeInsets.symmetric(vertical: 20.0),
              height: 200.0,
              child: ListView.builder(
                scrollDirection: Axis.horizontal,
                itemCount: items.length,
                itemBuilder: (context, index){
                  return _buildRow(context, index);
                },
              ),
            ),            
            floatingActionButton: FloatingActionButton(
                backgroundColor: Colors.green,
                onPressed: _generateWord,
                child: Icon(Icons.add),
            ),
        );
    }

    Widget _buildRow(context, index){
      return Container(
        width: 160.0,
        child: Column(
            children: <Widget>[
              ListTile(
                title: Text('${items[index]}'),
                onTap: (){},
              ),
              const Divider(),
            ],
          ),
      );
    }
}
 
    ผลลัพธ์ที่ได้
 
 

 
 
    การปรับแต่ง และกำหนด property ต่างเพิ่มเติม สามารถดูได้ที่ ListView Widget API
     
 
    เมื่อเราพอเข้าใจแนวทางการใช้งานเบื้องต้นเกี่ยวกับ ListView แล้ว เราจะกลับมาที่เนื้อหาจากตอนที่แล้วของเรา และจะมาประยุกต์
การใช้งาน ดังนี้คือ ส่วนของ _FirstScreen state class จากตอนที่แล้ว ในไฟล์ first_screen.dart เป็นดังนี้
 
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
 
// ส่วนของ Stateful widget
class FirstScreen extends StatefulWidget{
    const FirstScreen({super.key});

    @override
    State<StatefulWidget> createState() {
        return _FirstScreen();
    }
}
class _FirstScreen extends State<FirstScreen>{
    String _randomWord = WordPair.random().asPascalCase;
    final _biggerFont = const TextStyle(color: Colors.black,  fontSize: 20.0);
 
    void _generateWord(){
        setState(() {
            _randomWord = WordPair.random().asPascalCase;
        });
    }
 
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text('Welcome to Flutter'),
                backgroundColor: Colors.green
            ),
            body: ListTile(
                title: Text(
                    _randomWord,
                    style: _biggerFont,
                ),
            ),
            floatingActionButton: FloatingActionButton(
                backgroundColor: Colors.green,
                onPressed: _generateWord,
                child: Icon(Icons.add),
            ),
        );
    }
}
    จากโค้ดข้างต้น เดิมเราใช้งานตัวแปร _randomWord เป็น final String ข้อความที่ได้จากการ Random โดยการใช้งาน "english_word" 
package และใช้ปุ่ม "Renew" สำหรับแสดงข้อความใหม่ที่ได้จากการ Random


 
 

ประยุกต์เพิ่มรายการใน ListView  

    ในเนื้อหาตอนนี้ เราจะให้ตัวแปร _randomWord เป็น List รายการ ของ WordPair หรือก็คือ List<WordPair> เพื่อเก็บรายการ Random
ทั้งหมด โดยกำหนดโดยใช้ var แทน final เนื่องจากว่า ค่าจะต้องมีการเปลี่ยนแปลงได้ แล้วเปลี่ยนปุ่ม เป็นปุ่ม "Add New" โดยทำการเพิ่มรายการจากที่ได้ทำการ Random มาทีละ 3 รายการ เข้าไปใน List จากนั้นนำไป แสดงใน ListView โดยเรียกใช้งานฟังก์ชั่น _buildRow
เพื่อสร้างลิสรายการสำหรับ ListView อีกที นอกจากทำการเพิ่มรายการได้แล้ว เรายังเพิ่ม action เป็น IconButton เขาไปใน AppBar 
เป็นปุ่มสำหรับล้างค่ารายการ _randomWord ทั้งหมด เพื่อเริ่มการเพิ่มรายการใหม่
    สรูปก็คือ เราจะได้ฟังก์ชั่น ทั้งหมด 3 ฟังก์ชั่น สำหรับเรียรกใช้งาน คือ ฟังก์ชั่นเพิ่มรายการทีละ 3 รายการชื่อว่า _addRandomWord
ต่อด้วยฟังก์ชั่น ล้างค่ารายการทั้งหมด ชื่อว่า _clearRandomWord และสุดท้ายฟังก์ชั่นสำหรับสร้าง widget จากตัวแปร _randomWord
เพื่อเพิ่มเข้าไปใน ListView ชื่อฟังก์ชั่นว่า _buildRow
    จะได้ไฟล์ first_screen.dart เป็นดังนี้
 
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
 
// ส่วนของ Stateful widget
class FirstScreen extends StatefulWidget{
    const FirstScreen({super.key});

    @override
    State<StatefulWidget> createState() {
        return _FirstScreen();
    }
}
class _FirstScreen extends State<FirstScreen>{
    var _randomWord = <WordPair>[];
    final _biggerFont = const TextStyle(color: Colors.black,  fontSize: 20.0);
 
    void _addRandomWord(){
        setState(() {
          _randomWord.addAll(generateWordPairs().take(3).toList());
        });
    }
 
    void _clearRandomWord(){
        setState(() {
            _randomWord.clear();
        });
    }
 
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text('Welcome to Flutter'),
                backgroundColor: Colors.green,
                actions: <Widget>[
                    IconButton(
                        icon: const Icon(Icons.clear_all),
                        tooltip: 'Clear List',
                        onPressed: _clearRandomWord,
                    ),
                ],
            ),
            body: Container(
                child: ListView.builder(
                        itemCount: _randomWord.length,
                        itemBuilder: (context, index) {
                            return _buildRow(_randomWord, index);
                        },
                ),
            ),
            floatingActionButton: FloatingActionButton(
                backgroundColor: Colors.green,
                onPressed: _addRandomWord,
                child: Icon(Icons.add),
            ),
        );
    }
 
    Widget _buildRow(randomWord, index) {
        return Container(
            child: Column(
                children: <Widget>[
                    ListTile(
                      // title: Text('${randomWord[index]}'),
                        title: Text('${randomWord[index].asPascalCase}'),
                        onTap: (){},
                    ),
                    const Divider(),
                ],
            ),
        );
    }
}
    ดูตัวอย่างผลลัพธ์ที่ได้  ตามลำดับดังนี้
    1. เมื่อเปิด App เริ่มต้นขึ้นมา จะไม่มีรายการใดๆ แสดง เพราะตัวแปร _randomWord ยังไม่มีข้อมูลใดๆ เป็นค่าว่าง
    2. เมื่อเราคลิกที่ปุ่ม "Add New" ก็จะเพิ่มรายการเข้าไปใน _randomWord ทั้งหมด 3 รายการ เกิดการ setState และทำการ
       สร้างลิสรายการ 3 รายการขึ้นมา
    3. กดเพิ่มอีกครั้งเป็น 6 รายการ (สามารถกดเพิ่มไปได้เรื่อยๆ ในที่นี้จะสมมติกดไป แค่ 2 ครั้ง)
    4. ทำการล้างค่า 6 รายการที่ได้เพิ่มไป โดยกดที่ปุ่ม "Clear All" ที่ปุ่ม action ตรง AppBar
 
 

 
 
    เนื้อหาในตอนนี้เราได้แนวทางการใช้งาน ListView ไปพอสมควร ตอนหน้า เรายังจะประยุกต์ต่อการใช้งาน "english_word" package
ร่วมกับการใช้งาน ListView โดยจะเพิ่มเติมเนื้อหาการจัดการรายการในหน้า Screen ใหม่ รอติดตาม


   เพิ่มเติมเนื้อหา ครั้งที่ 1 วันที่ 16-10-2021


การจัดการกรณีเงื่อนไขไม่มีรายการ

    เราสามารถกำหนดรูปแบบการจัดการเงื่อนไขกรณีไม่มีรายการโดยใช้การตรวจสอบจำนวน
ของรายการว่ามากกว่า 0 หรือไม่ ถ้ามากกว่า 0 ก็เข้าเงื่อนไขการทำงานปกติ แต่ถ้าเท่ากับ 0
ก็ให้ทำอีกเงื่อนไข ตัวอย่างด้านล่าง กรณีเท่ากับ 0 หรือเป็นรายการว่าง ก็จะให้แสดงข้อความ
ตรงกลางคำว่า "No items" เป็นรุปแบบอย่างง่าย
 
body: Container(
    child: items.length > 0 // กำหนดเงื่อนไขตรงนี้
    ? ListView.separated( // กรณีมีรายการ แสดงปกติ
            itemCount: items.length,
            itemBuilder: (context, index) {
                return ListTile(
                  title: Text('${items[index]}'),
                );
            },
            separatorBuilder: (BuildContext context, int index) => const Divider(),                    
      )
    : const Center(child: Text('No items')), // กรณีไม่มีรายการ
 
ถ้ามองจากรูปแบบการใช้งาน ก็จะเป็นในรูปแบบ (condition) ? true : false


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



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



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









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









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





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

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


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


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







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