การใช้งาน Theme และรู้จักกับ InheritedWidget ใน Flutter

เขียนเมื่อ 4 ปีก่อน โดย Ninenik Narkdee
themedata theme inheritedwidget flutter

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

ดูแล้ว 10,823 ครั้ง




ก่อนที่เราจะไปประยุกต์การใช้งาน Theme กับโปรเจ็คจาก
เนื้อหาบทความของตอนที่ผ่านมา จะพาไปรู้จักกับ Inherit
Widget กันก่อนว่า คืออะไร และมีรุปแบบ การกำหนดและ
ใช้งานอย่างไร
    ทบทวนโปรเจ็คเนื้อหาตอนที่แล้วได้ที่บทความ
    การใช้งาน Navigator และ Routing ใน Flutter เบื้องต้น http://niik.in/958
 
 

Inherited Widget คืออะไร

    เป็น widget พื้นฐานที่สามารถกระจายหรือส่งต่อข้อมูลลงไปในโครงสร้างลำดับขั้น
ของ widget อย่างมีประสิทธิภาพ เราพอทราบอยู่แล้วว่า widget ที่อยู่ในระดับขั้นที่ต่่ำลงไป สามารถ
ใช้ข้อมูลจาก widget ที่อยู่เหนือกว่า หรือที่อยู่ระดับสูงกว่า โดยส่งผ่านค่า parameter contructor 
ต่อกันมาเรื่อยๆ ซึ่งถ้าโครงสร้างลำดับขั้นไม่ได้ซับซ้อน ก็อาจจะสามารถใช้การกำหนด และส่งค่าด้วยวิธี
ดังกล่าวได้ แต่ถ้าเป็นโครงสร้างที่ซับซ้อน มีหลายชั้น วิธีดังกล่าวก็ดูยุ่งยาก และไม่มีประสิทธิภาพ ดังนั้น
Inherited Widget จึงเป็นอีกวิธี ที่ทำให้เราสามารถใช้งานข้อมูลได้จาก widget ที่อยู่ลำดับชั้นที่ต่ำกว่า
โดยข้ามลำดับชั้นอื่นๆ ขึ้นมายัง Inherited widget เพื่อเรียกใช้งานข้อมูลที่ต้องการได้
    ดูโครงสร้างลำดับขั้นด้านล่าง เราสาามารถใช้งาน Inherited widget ที่อยูด้านบนสุดได้โดยตรงจาก
    ภายใน widget ที่อยู่ลำดับขั้นที่ต่ำกว่าลงมาได้
 
 

 
 
    Inherited widget จึงเหมาะสำหรับเป็นตัวจัดหาหรือกระจายข้อมูล ให้กับ widget ย่อยต่างๆ ใน App โดยอาจจะ
เป็นข้อมูล Data ทั่วไป หรืออาจจะเป็น Service หรืออย่างอื่นๆ ตามการประยุกต์ใช้งาน ตัวอย่าง widget ที่เราผ่าน
ตาจากตอนที่แล้ว ที่มีรูปแบบการใช้งาน Inherited Widget ก็ เช่น Scaffold ที่สำหรับแสดงหน้า App หรือหน้า Route
แบบเต็มพื้นที่ ถึงแม้ Scaffold จะเป็น StatefulWidget แต่ Scaffold ก็มีการใช้งาน State และใน State ก็เรียกใช้งาน
InheritedWidget อีกที ทำให้เราสามารถเรียกใช้ showSnackBar() ที่อยู่ใน Scaffold State ได้  และอีกตัวที่ผ่านตา
จากตอนที่แล้ว ก็คือ ModalRoute ที่เราใช้รับค่า arguments ที่ส่งมากับคำสั่ง Navigator.pushNamed() หรือ 
Navigator.push() 
    Inherited widget เป็น widget ที่เมื่อสร้างแล้วจะไม่สามารถแก้ไขได้ เช่น ไม่สามารถแก้ไข property ซึ่งส่วนใหญ่มักจะ
กำหนดเป็น final หรือ const เป็นค่าที่ไม่สามารถแก้ไขเปลี่ยนแปลงได้ นั่นคือเมื่อมีการกำหนดค่านั้นแล้ว ค่าดังกล่าวจะไม่สามารถ
แก้ไขเป็นค่าอื่นๆ ได้ สมมติเช่น กำหนด color เป็น "red" เมื่อรัน App ค่า color จะเปลี่ยนเป็นค่่าอื่นไม่ได้ จนกว่าจะปิดและเปิด 
App ขึ้นมาใหม่  อย่าสับสนกับคำว่าเปลี่ยนแปลงค่าไม่ได้ กับกำหนดค่าใหม่  การกำหนดค่าใหม่นั่นหมายถึง เราเริ่มต้นกำหนด color 
เมื่อเริ่มต้นใหม่ สมมติเปลี่ยนเป็นสี "blue" นั่นคือค่า color ก็จะเป็น "blue" จนกว่าจะกำหนดค่าใหม่เมื่อเริ่มต้นอีกครั้ง ระหว่างนั้น เราไม่
สามารถเปลี่ยนค่า color หลังจากกำหนดไปแล้ว

 
 

การกำหนด Inherited Widget

    รูปแบบการกำหนด Inherited widget เริ่มต้น จะมีสองส่วนหลัก คือ กำหนด parameter constructor โดยอย่างน้อยต้องมี 
Widget child เป็น parameter และ ส่งต่อ child ไปยัง parent contrcutor ด้วยรูปแบบ : super(child: child) ตัวอย่างเช่น
 
    MyWidget({required Widget child}) : super(child: child);
    ทบทวนการใช้งาน Constructor ในภาษา Dart
    ทบทวน การสืบทอด Inheritance class ในภาษา Dart
 
    เข้าใจอย่างง่าย เวลาเราสร้าง widget ก็เหมือนเราสร้างกล่องขึ้นมาสำหรับเก็บของบางอย่าง และในกล่องก็สามารถใส่กล่องย่อยๆ
เพิ่มเข้าไปได้ ช่องสำหรับใส่กล่องย่อยๆ ก็คือ child  ตัวอย่างข้างต้น ถ้าเรานำไปใช้งาน สมมติเรา นำไปใส่ Container widget อีกที
ก็จะเรียกใช้งานเป็นดังนี้
 
MyWidget()
    child: Container(),
)
    Container Wdiget ก็จะเป็น argument ของ parameter child ที่เรากำหนดให้กับ MyWidget() และถูกส่งต่อไปยัง Parent ที่สืบทอดมา ผ่านการ
กำหนด : super(child: child); นั่นคือ ถ้า MyWidget ถูกครบอด้วย Wdiget อื่น เช่น Center Widget ดังนั้น Container ก็จะถูกส่งใช้งาน
Center widget ผ่าน super keyword ดูตัวอย่าง เมื่อมี Center widget ครอบอีกที
 
Center(
    child : MyWidget(
        child: Container(),
    ),
)
    นอกจาก parameter ที่จำเป็นอย่าง child แล้ว อาจจะกำหนด Key ด้วยหรือไม่ก็ได้ ซึ่ง key เป็นตัวที่ใช้กำหนด ความเป็น unique 
เข้าใจคร่าวๆ ได้ว่าเหมือนกับ ID ของ widget นั้น ซึ่งถ้ากำหนดแล้ว มักจะใช้กับการจัดการ widget ที่อาจจะต้องมีการปรับเปลี่ยนตำแหน่ง
ในการ rebuild แต่ละครั้ง จึงจำเป็นต้องระบุให้ได้ว่าเป็น widget ตัวใด รูปแบบถ้ากำหนด จะเป็น
 
    MyWidget({Key?: key, required Widget child}) : super(key: key,child: child);
    ส่วนการกำหนดที่สองของ Inherited widget คือการ override "updateShouldNotify" method คืนค่าเป็น boolean ค่า true
หรือ false  ลักษณะก็เหมือนกับเรากด subscribe ช่อง youtube จะเตือนเราเมื่อมีอัพเดทวิดีโอใหม่ หลักการคล้ายๆ กัน ใช้ true
เมื่อต้องการอัพเดทหากมีการเปลี่ยนแปลง หรือ false หากไม่ได้สนใจการเปลี่ยนแปลง 
    ตัวอย่างการ override แต่ละแบบ
 
    @override
    bool updateShouldNotify(data){
        return true;
    }
//     bool updateShouldNotify(data) => true; // แบบย่อ
    หรือจะใช้แบบ ให้ค่าเป็น true หรือ false โดยเปรียบเทียบจากการเปลี่ยนแปลงของข้อมูล เป็น
 
    @override
    bool updateShouldNotify(oldData){
        return data != oldData;
    }
//     bool updateShouldNotify(data) => data != oldData;
   หากเรากำหนดครบทั้งสองส่วนนี้ เราก็จะได้ Inherited widget แล้ว แต่ก็ยังถือว่า ไม่ได้มีประโยชน์อะไร เหมือนแค่สร้างตัว widget
มาครอบ widget อื่น อีกที ดังนั้น ส่วนที่เรา ต้องมีเพิ่มเข้ามาคือ property ข้อมูลที่เราจะกระจายไปใช้งานในโครงสร้างลำดับขั้นของ
widget โดยเป็นค่าที่ไม่สามารถแก้ไขได้ กำหนดเป็น final สมมติเช่น เรากำหนดเป็น สี Color object ใช้ตัวแปรเป็น color และให้
สามารถกำหนดค่าเริ่มต้นผ่าน parameter contructor จะได้ Inherited widget ของเราเป็นดังนี้
 
class MyWidget extends InheritedWidget {
    final Color color; // property
 
    // constructor
    const MyWidget({
        Key? key,
        required this.color,
        required Widget child,
    }) : super(child: child);
 
    @override
    bool updateShouldNotify(data) => true;
 
}
    เท่านี้เราก็ได้ Inherited widget อย่างง่ายไว้ใช้งาน สมมติเราต้องการที่จะให้สามารถใช้งานข้อมูล color ใน App ของเรา เราก็แค่
ทำการครอบ MyWidget ไว้ตำแหน่งบนสุด สมมมติครอบ MaterialApp widget ก็จะได้เป็นดังนี้
 
// ส่วนของ Stateless widget
class MyApp extends StatelessWidget{
    @override
    Widget build(BuildContext context) {
        return MyWidget(
            color: Colors.red,
            child:  MaterialApp(},
        );
    }
}
    
    จะเห็นว่า เรียกใช้งาน MyWidget โดยกำหนด parameter สองค่าคือ color และ child โดย color คือ ค่าสีที่เราส่งค่าไปกำหนดให้กับ
color property ของ MyWidget ซึ่งเป็น Inherited widget ในตัวอย่างเรากำหนดสีแดง ถ้า MaterialApp widget มี child ย่ยอๆ 
หลายลำดับขั้น สมมติมี 4 ระดับ widget ในระดับที่ 4 ก็สามารถใช้งานค่า color property ของ MyWidget 
    สมมติให้ widget ในระดับที่ 4 ชื่อ LevelFour เป็น Stateless widget และต้องการใช้งาน color property ของ MyWidget ที่อยู่
ด้านบนสุดของโครงสร้างลำดับขั้น 
 
// ส่วนของ Stateless widget
class LevelFour extends StatelessWidget{
    
    @override
    Widget build(BuildContext context) {
        final color = context.dependOnInheritedWidgetOfExactType<MyWidget>()!.color;
        return Text("Level four",style: TextStyle(color: color),);
    }
}
    ถึงแม้เราจะอยู่ในระดับขั้นที่ 4 แต่เราก็สามารถเรียกใช้งาน color ของ MyWidget ได้โดยใช้คำสั่ง dependOnInheritedWidgetOfExactType
คล้ายกับบอก context ว่าให้ไปที่ MyWidget แล้วใช้ค่า color property มาใช้งาน ดังตัวอย่าง เรานำค่าที่ได้มากำหนดในตัวแปร color
และเรียกใช้กำหนดสีให้กับข้อความ เวลาเรียกใช้งาน เราต้องกำหนดใน build() method เพราะจำเป็นต้องใช้งาน context
 
    ในข้างต้น เราเรียกใช้งานผ่านคำสั่ง context.dependOnInheritedWidgetOfExactType()!.color ซึ่งถ้าจะต้องใช้แบบนี้บ่อยๆ
คงไม่สะดวกแน่ ดังนั้น เราสามารถที่จะกำหนด การเรียกคำสั่งใหม่ หรือก็คือตั้งชื่อเรียกใช้งานใหม่ โดยใช้ "of" method เป็นแบบ static
โดยเพิ่มเข้าไปใน MyWidget ดังนี้
 
class MyWidget extends InheritedWidget {
    final Color color; // property
 
    // constructor
    const MyWidget({
        Key? key,
        required this.color,
        required Widget child,
    }) : super(child: child);

    static MyWidget of(BuildContext context) {
        return context.dependOnInheritedWidgetOfExactType<MyWidget>()!;
    }    
 
    @override
    bool updateShouldNotify(data) => true;
 
}
    เรากำหนด static method ที่ชื่อ "of" โดยเรียกใช้ผ่านชื่อ widget
ทำให้เราสามารถเรียกใช้คำสั้ง context.dependOnInheritedWidgetOfExactType()
โดยเปลี่ยนป็น MyWidget.of(context) จะได้เป็นดังนี้
 
 
final color = MyWidget.of(context).color;
 
    ทำให้เราสามารถเรียกใช้งานข้อมูลจาก Inherited widget ได้ง่ายและสะดวกมากขึ้น
 
    การกำหนด และเรียกใช้งาน Inherited widget โดยตรงข้างต้น มีข้อจำกัดตรงที่เราไม่สามารถแก้ไข ข้อมูลใน Inherited widget
ได้ ดังนั้น หากเราต้องการใช้งาน Inherited widget ที่รองรับการแก้ไขข้อมูลได้ เราต้องทำการเรียกใช้งาน Inherited widget เป้น
private จาก Stateful widget อีกที เหมือนรูปแบบ ที่ Scaffold widget ใช้งาน
    ให้มองภาพอย่างนี้ คือ เมื่อเราสร้าง Stateful widget เราจะมี State widget เพิ่มเข้ามา ใน State widget เราสามารถกำหนด 
property ต่างๆ และสามารถแก้ไข proprerty เหล่านั้นได้ ดังนั้น ถ้าเราเรียกใช้งาน Inherited widget ภายใน State ของ Stateful
widget เราก็สามารถที่ใช้ property ของ State widget ผ่าน Inherited widget ได้นั่นเอง อาจมองภาพไม่ออก  เราจะลองปรับ
MyWidget แบบเดิมเป็นแบบใหม่ โดยใช้เป็น StatefulWidget ดังนี้
 
class MyWidget extends StatefulWidget {
   final Widget child;
   final Color color; // property
 
    const MyWidget({
        Key? key,
        required this.color,
        required this.child,
    }) : super(key: key);
 
    static _MyWidgetState of(BuildContext context) {
        return context.findAncestorStateOfType<_MyWidgetState>()!; 
    }  

    @override
    _MyWidgetState createState() => _MyWidgetState();
}
 
class _MyWidgetState extends State<MyWidget> {
    Color color = Colors.green;

    @override
    void initState() {
      super.initState();
    }
 
     void setRed(){
         setState(() {
          color = Colors.red;
         });
     }
 
     void setBlue(){
         setState(() {
            color = Colors.blue;
         });
     }
 
     void setColor(_color){
         setState(() {
            color = _color;
         });
     }
 
    @override
    Widget build(BuildContext context) {
        return _MyWidget(color: color, child: widget.child);
    }
}

// _MyWidget เป็น private 
class _MyWidget extends InheritedWidget {
    final Color color; // property
 
    // constructor
    const _MyWidget({
        Key? key,
        required this.color,
        required Widget child,
    }) : super(child: child); 
 
    @override
    bool updateShouldNotify(data) => true;
 
}
    จากโค้ด MyWidget คือ StatefulWidget โดยสร้าง _MyWidgetState State ที่สามาระเปลี่ยนแปลงข้อมูลได้ เป็นแบบ private
(* สังเกตว่าเราใช้ _ เพื่อกำหนดเป็น private ) โดยจะเรียกใช้งานเฉพาะในไฟล์ library หรือ package นี้เท่านั้น 
    ใน _MyWidgetState State เราเรียกใช้งาน Inherited widget โดยส่ง property ของ State เป็น ข้อมูลหรือ data ของ
Inherited widget อีกที และค่า เป็น property ของ State ไม่ว่าจะเป็น color และ method การกำหนด
ค่าสีอย่าง setRed() setBlue() และ setColor() ล้วนเป็น data ของ Inherited widget ที่สามารถเรียกใช้งาน และแก้ไขได้
    ขออธิบายแยกเป็นส่วนๆ เพิ่มเติมดังนี้
    ส่วนแรก เป็นของ Inherited widget เรากำหนดรูปแบบคล้ายเดิม
 
// _MyWidget เป็น private 
class _MyWidget extends InheritedWidget {
    final Color color; // property
 
    // constructor
    const _MyWidget({
        Key? key,
        required this.color,
        required Widget child,
    }) : super(child: child); 
 
    @override
    bool updateShouldNotify(data) => true;
 
}
    ในที่นี้เราเพิ่ม Key เข้าไปใน parameter เพื่อให้เกิดการ unique มากขึ้น สำหรับไว้อ้างอิงเมื่อมีการใช้งานที่ซับซ้อน สังเกตว่า
ส่วนของ property เราใช้ตัวแปร color เก็บชนิดข้อมูลเป็น Color ซึ่งเป็นส่วนของ Stateful ที่สามารถเปลี่ยนแปลงได้
เนื่่องจากกรณีนี้ Inherited widget ของเราไม่ใช้ widget สุดท้ายที่เราเรียกใช้งาน เหมือนในกรณีแรก ที่เป็นแบบเปลี่ยนแปลงไม่ได้
เราใช้ widget โดยเรียกใช้งานโดยตรง แต่กรณีนี้ เราจะเรียกใช้ widget นี้ใน State อีกทีดังนั้น เรากำหนด widget นี้เป็นแบบ private
และเราไม่ได้กำหนด การใช้งาน "of" static method ในนี้ แต่จะไปกำหนดใน Stateful แทน เพราะเป็นตัวที่จะถูกเรียกใช้งาน หรือเป็น
ตัวที่ถูกส่งออกเป็น widget ไปใช้งานโดยตรงแทน
    ต่อไปดูส่วนของ State
 
class _MyWidgetState extends State<MyWidget> {
    Color color = Colors.green;

    @override
    void initState() {
      super.initState();
    }
 
     void setRed(){
         setState(() {
          color = Colors.red;
         });
     }
 
     void setBlue(){
         setState(() {
            color = Colors.blue;
         });
     }
 
     void setColor(_color){
         setState(() {
            color = _color;
         });
     }
 
    @override
    Widget build(BuildContext context) {
        return _MyWidget(color: color, child: widget.child);
    }
}
    ใน State เรากำหนด property และ method ไว้ใช้งาน โดยสามารถเรียกใช้งาน หรือเปลี่ยนแปลงค่าได้ ทั้งค่า color, setColor(), 
setBlue() และ setRed() ล้วนเป็นข้อมูลของ _MyWidgetState เรากำหนดค่าสีเริ่มต้นเป็นสีเขียว และในส่วนของ 
build() method เราก็เรียกใช้งาน Inherited widget อีกที โดยกำหนด parameter แรกตามรูปแบบ contructor ของ Inherited widget 
มี color เป็น ข้อมูลชนิด Color นั่นก็คือเราใช้ค่าจาก "color" ส่วนค่าที่สองเป็น child ซึ่งจะต้อง
เป็นค่าที่ส่งมาจาก constuctor ของ Stateful ดังนั้นเรา จะอ้างอิงค่าใน Stateful ด้วยคำว่า widget จะได้ child เป็น widget.child 
    เข้าใจอย่างง่าย Color ที่เราส่งไปโดยใช้ color ก็คือค่าใน State ถูกส่งเข้าไปเป็นข้อมูลของ Inherited widget ให้สามารถเรียก
ใช้งาน และแก้ไขได้
 
    ส่วนสุดท้ายก็คือส่วนของ Stateful ส่วนนี้คือส่วนที่จะถูกส่งออกไปเป็น widget ไว้ใช้งาน เนื่องจากเราจะมีการส่ง child เข้าไปใน State
ดังนั้น เราต้องกำหนด ตัวแปร child เป็น widget ด้วย
 
class MyWidget extends StatefulWidget {
   final Widget child;
   final Color color; // property
 
    const MyWidget({
        Key? key,
        required this.color,
        required this.child,
    }) : super(key: key);
 
    static _MyWidgetState of(BuildContext context) {
        return context.findAncestorStateOfType<_MyWidgetState>()!; 
    }  

    @override
    _MyWidgetState createState() => _MyWidgetState();
}
    กล่าวคือ เมื่อเรียกใช้งาน MyWidget ตัว Widget child จะถูกส่งเข้าไปใน State ผ่านการอ้างอิงตัวแปร widget.child และถูกส่งต่อ
ไปยัง Inherited widget อีกที ส่วนที่เพิ่มเข้ามาใน Stateful คือ การกำหนดชื่ออ้างอิงการทำงานโดยใช้ "of" static
method เพื่อให้เรียกใช้คำสั่งได้ง่ายและสะดวกขึ้น โดยฟังก์ชั่นนี้จะคืนค่าเป็น _MyWidgetState หรือก็คือข้อมูล data property
ที่เรากำหนดให้กับ Inherited widget ดังนั้น Type ใน <> จึงต้องเป็น _MyWidgetState class เพื่ออ้างอิงไปที่ _MyWidget และใช้
ข้อมูลที่เป็น data 
    เมื่อเข้าใจเบื้องต้นแล้ว ก็มาดูต่อว่าเราสามารถใช้งาน Inherited widget ที่สามารถแก้ไขข้อมูล ได้อย่างไร
    
    รูปแบบการเรียกใช้งานก็คล้ายวิธีเดิม เรากำหนดไว้ในส่วนบนสุดของ App 
 
// ส่วนของ Stateless widget
class MyApp extends StatelessWidget{
    @override
    Widget build(BuildContext context) {
        return MyWidget(
		color: Colors.green, 
            child:  MaterialApp(},
        );
    }
}
    จะเห็นว่า MyWidget จะเป็น StatefulWidget ที่ส่ง MaterialApp เป็น widget child เข้าไปใน State และส่งต่อไปยัง Inherited widget
เท่านี้ เราก็สามารถใช้ข้อมูลใน State ผ่าน Inherited widget ได้แล้ว ดังนี้
 
// ส่วนของ Stateless widget
class LevelFour extends StatelessWidget{

    @override
    Widget build(BuildContext context) {
        final state = MyWidget.of(context);
        final color = state.color;
        return Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                    Text("Level four",style: TextStyle(color: color),),
                    Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: ElevatedButton(
                            onPressed: state.setBlue,
                            child: Text('SetBlue'),
                        ),
                    ),
                    Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: ElevatedButton(
                            onPressed: state.setRed,
                            child: Text('SetRed'),
                        ),
                    ),
                ],
            )
        );
    }
}
    เรากำหนดตัวแปร final state เพื่ออ้างอิง inherited widget ด้วยคำสั่ง MyWidget.of(context) เราก็ได้ property ของ _MyWidgetState
ทั้งหมดมาใช้งาน เช่น ใช้ color โดยกำหนดเป็น state.color หรือจะเรียรกใช้ method เพื่อกำหนดค่า สีใหม่เป็น state.setRed หรือ 
state.setBlue อย่างในตัวอย่างด้านบน ที่เรากำหนดให้เรียกใช้ method ให้กับปุ่ม เพื่อเปลี่ยนสีข้อความ
 
 
 
 

ประยุกต์ใช้งาน Inherited Widget

    ก่อนที่จะไปเนื้อหาการใช้งาน Theme เราจะมาลองประยุกต์ใช้งาน Inherited widget โดยจะใช้รูปแบบเดียวกับตัวอย่างที่อธิบายไปใน
ตอนต้น เราจะสร้าง Inherited Widget ไว้ใน package หรือ โฟลเดอร์ชื่อ "providers" ใช้ชื่อไฟล์เป็น mycolor.dart ดังนี้
 
    ไฟล์ mycolor.dart
import 'package:flutter/material.dart';

class MyColor extends StatefulWidget {
   final Widget child;
   final Color color; // property
 
    const MyColor({
        Key? key,
        required this.color,
        required this.child,
    }) : super(key: key);
 
    static _MyColorState of(BuildContext context) {
        return context.findAncestorStateOfType<_MyColorState>()!; 
    }  

    @override
    _MyColorState createState() => _MyColorState();
}
 
class _MyColorState extends State<MyColor> {
    Color color = Colors.green;

    @override
    void initState() {
      super.initState();
    }
 
     void setRed(){
         setState(() {
          color = Colors.red;
         });
     }
 
     void setBlue(){
         setState(() {
            color = Colors.blue;
         });
     }
 
     void setColor(_color){
         setState(() {
            color = _color;
         });
     }
 
    @override
    Widget build(BuildContext context) {
        return _MyColor(color: color, child: widget.child);
    }
}

// _MyColor เป็น private 
class _MyColor extends InheritedWidget {
    final Color color; // property
 
    // constructor
    const _MyColor({
        Key? key,
        required this.color,
        required Widget child,
    }) : super(child: child); 
 
    @override
    bool updateShouldNotify(data) => true;
 
}
    และเราก็สร้างไฟล์ fourth_screen.dart ไว้ใน app_screen package สำหรับสร้างหน้าตั้งค่าการกำหนดสีที่ต้องการ ดังนี้
 
    ไฟล์ fourth_screen.dart 
import 'package:flutter/material.dart';
import '../providers/mycolor.dart';
 
class FourthScreen extends StatefulWidget {
    static const routeName = '/fourth';

    @override
    State<StatefulWidget> createState() {
        return _FourthScreen();
    }
}
 
class _FourthScreen extends State<FourthScreen> {
 
    @override
    Widget build(BuildContext context) {
       final state = MyColor.of(context); 
        final color = state.color;
        return Scaffold(
            appBar: AppBar(
                title: Text('Fourth Screen'),
                backgroundColor: color,
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('Fourth Screen'),
                        Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: ElevatedButton(
                                onPressed: () => state.setColor(Colors.green),
                                child: Text('Default'),
                            ),
                        ),
                        Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: ElevatedButton(
                                onPressed: state.setBlue,
                                child: Text('SetBlue'),
                            ),
                        ),
                        Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: ElevatedButton(
                                onPressed: state.setRed,
                                child: Text('SetRed'),
                            ),
                        ),
                    ],
                )
            ),
        );
    }
}

    เราจะนำค่าสีที่ได้ไปกำหนดให้กับ backgroundColor ของ AppBar
    จะได้โครงสร้างไฟล์ในโปรเจ็คทดสอบของเราเป็นดังนี้
 
 

 
 
    ให้ทำการ import mycolor.dart มาใช้งานในทุกหน้า และเรียกใช้กำหนด color ให้กับ AppBar ดูตัวอย่างการกำหนด สังเกตโค้ด
บรรทัดที่เรากำหนดจุดวงกลมสีแดงไว้ด้านหน้า คือส่วนที่เราเพิ่มเข้าไปในทุกหน้า สังเกตว่า เราเรียกใช้งาน MyColor.of(context) ใน
build() method 
 
 

 
 
    ในไฟล์ first_screen.dart นอกจากในส่วนของ AppBar แล้ว เรายังมีปุ่ม FloatingActionButton ที่กำหนดสีเข้าไปด้วย
 
 

 
 
    และเพิ่มปุ่ม setting เข้าไปใน AppBar เพื่อเปิดหน้า ตั้งค่าการกำหนดสี
 
 
IconButton(
    icon: const Icon(Icons.settings),
    tooltip: 'Settings',
    onPressed: () {
      Navigator.pushNamed(
          context,
          FourthScreen.routeName
      );                                  
    }
),
 
 

 
 
    ต่อไปก็กำหนดการเรียกใช้ widget ในไฟล์ main.dart ดังนี้
 
 

 
import 'package:flutter/material.dart';
import './app_screen/first_screen.dart';
import './app_screen/second_screen.dart';
import './app_screen/third_screen.dart';
import './app_screen/fourth_screen.dart';
import './providers/mycolor.dart';
 
void main(){
    runApp(MyApp());
}
 
// ส่วนของ Stateless widget
class MyApp extends StatelessWidget{
    @override
    Widget build(BuildContext context) {
        return MyColor(
          color: Colors.red, 
          child: MaterialApp(
              title: 'First Flutter App',
  //            home: FirstScreen(),
              initialRoute: '/', // สามารถใช้ home แทนได้
              routes: {
                  '/': (context) => FirstScreen(),
                  '/second': (context) => SecondScreen(),
                  ThirdScreen.routeName: (context) => ThirdScreen(),
                  FourthScreen.routeName: (context) => FourthScreen(),
             },
          ),
        );      
    }
}
 
    จะเห็นว่าเราครอบ MyColor ซึ่งเป็น StatefulWdiget ที่เรียกใช้งาน Inherited widget อีกที ทำให้เราสามารถกระจายค่า property
ต่างๆ ไปใช้งานในลำดับขั้นที่ต่ำกว่าภายใน App ของเราได้ ดูผลลัพธ์ การทำงาน
 
 

 
 
    เราสามารถเข้าไปแก้ไขสีที่เป็น property ของ _MyColorState ที่ถูกกระจายข้อมูลลงมาในระดับขั้นที่ต่ำกว่าใน App ของเราโดยใช้
_MyColor ซึ่งเป็น Inherited widget  แนวทางแบบนี้สามารถนำไปประยุกต์ใช้งานกับข้อมูลอื่นๆ ได้ ไม่ว่าจะเป็นการใช้สำหรับสร้าง 
provider service เช่น service สำหรับดึงข้อมูลไปใช้งานในส่วนต่างๆ ของ App เป็นต้น
 
 
 

การประยุกต์ใช้งาน Theme

    เราสามารถกำหนดรูปแบบของสีและข้อความภายใน App โดยใช้งาน theme  ซึ่งอาจจะใช้ Theme widget สำหรับกำหนดในบาง
ส่วนของ App ที่ต้องการ  หรือจะกำหนดทั้ง App ผ่าน theme property ของ MaterialApp widget  ที่ Root ของ App ก็ได้เหมือนกัน
เมื่อมีการกำหนดการใช้งาน Theme แล้ว Material widget ต่างๆ จะมีการใช้งานค่าสีและรูปแบบของตัวอักษร ปรับเปลี่ยนไปตามค่า
ที่กำหนด เช่น ใน AppBar หรือปุ่ม Button ต่างๆ เหล่านี้เป็นต้น
    การกำหนดข้อมูล Theme ต่างๆ จะใช้ ThemeData widget เราสามารถกำหนดค่าสีตาม property ต่างๆ เพื่อไว้เรียกใช้งาน เช่น
primaryColor สีหลักของ theme, accentColor สีที่ต้องการเน้น, textTheme รูปแบบข้อความ สามารถดู property ต่างๆ เพิ่มเติม
ได้ที่ ThemeData constuctor ใน ThemeData Widget API
    
 
   * เราจะปิดการใช้งานการกำหนดสีจากตัวอย่างการใช้งาน MyColor ที่ผ่านมาก่อน โดย comment ปิดการกำหนดสีให้กับส่วน
ต่างใน App ไปก่อนตามแนวทางดังนี้
 
 

 

 

    รูปแบบการกำหนด ThemeData ใน MaterialApp

    เราจะลองกำหนดข้อมูล theme โดยใช้ ThemeData ใน MaterialApp ดังนี้
 
 
// ส่วนของ Stateless widget
class MyApp extends StatelessWidget{
    @override
    Widget build(BuildContext context) {
        return MyColor(
          color: Colors.red, 
          child: MaterialApp(
              theme: ThemeData(
                colorScheme: ColorScheme.fromSwatch(
                  primarySwatch: Colors.pink,
                ).copyWith(
                  secondary: Colors.purple,
                ),
                textTheme: TextTheme(displayMedium: TextStyle(color: Colors.red))
              ),
              title: 'First Flutter App',
  //            home: FirstScreen(),
              initialRoute: '/', // สามารถใช้ home แทนได้
              routes: {
                  '/': (context) => FirstScreen(),
                  '/second': (context) => SecondScreen(),
                  ThirdScreen.routeName: (context) => ThirdScreen(),
                  FourthScreen.routeName: (context) => FourthScreen(),
             },
          ),
        );      
    }
}
 
    กำหนดสีหลักเป็นสีชมพู สีสำหรับเน้นเป็นสีม่วง และสีข้อความเป็นสีแดง สังเกตผลลัพธ์ที่เกิดขึ้น
 
 

 
 
    ในส่วนของ AppBar สีจะเปลี่ยนไปตามสีหลักหรือ primarySwatch สีของ FloatingActionButton จะเปลี่ยนไปตามสีเน้น หรือ
secondary และสีข้อความใน body จะเปลี่ยนเป็นสีแดงจากรูปแบบ textTheme
 
    เราสามารถใช้งาน ThemeData แต่เลือกใช้สีตามต้องการกับ widget ได้ โดยใช้คำสั่ง Theme.of() เป็นรูปแบบการใช้งานที่เราอธิบาย
ไปแล้วเกี่ยวกับ Inherited widget โดยจะสร้างตัวแปร แล้วมากำหนดค่า หรือจะกำหนดโดยตรงใน widget ที่ต้องการก็ได้ สมมติเราต้องการ
สีปุ่มของ FloatingActionButton เป็นสีหลัก ก็ใช้เป็น
 
 
floatingActionButton: FloatingActionButton(
    backgroundColor: Theme.of(context).colorScheme.primary,
    onPressed: _addRandomWord,
    child: Icon(Icons.add),
),
 
    ผลลัพธ๊ที่ได้ ก็จะเป็นสีตาม property ที่เราเรียกใช้งาน โดยใช้ค่าจาก ThemeData ผ่าน Theme.of() mthoed ซึ่งเราเลือกเป็นสีหลัก
เป็นสีชมดู ปุ่มที่ได้จึงเป็นสีชมพูด้วย
 
 

 
 
    เราสามารถกำหนด Dark Mode โดยใช้ brightness: Brightness.dark 
 
 
child: MaterialApp(
	  theme: ThemeData(
		colorScheme: ColorScheme.fromSwatch(
		  primarySwatch: Colors.pink,
		).copyWith(
		  secondary: Colors.purple,
		),
		textTheme: TextTheme(displayMedium: TextStyle(color: Colors.red))
	  ),
    darkTheme: ThemeData(
      brightness: Brightness.dark,
      /* dark theme settings */
    ),
    themeMode: ThemeMode.dark,   
 
    ผลลัพธ์ที่ได
 
 

 
 
 

    รูปแบบการกำหนด Theme ด้วย Theme Widget

    นอกจากเราจะสามารถกำหนด ThemeData ให้กับ MaterialApp widget เพื่อใช้งาน รูปแบบ Theme เดียวทั้ง App แล้ว เรายัง
สามารถใช้ Theme Widget เพื่อกำหนด ThemeData ให้กับบางส่วนเพิ่มอีกได้ เช่น สมมติว่าทุกหน้าใช้ Theme หลักที่กำหนดใน 
MaterialApp แต่เฉพาะหน้าที่ 3 เราต้องการอีกรูปแบบ Theme หนึ่งเฉพาะหน้านี้ เช่นอาจจะเน้นไปทางโทนสีส้ม เราก็สามารถใช้
Theme widget ครอบทับและจัดการได้ดังนี้
 
 
return Theme(
  data: ThemeData(
    colorScheme: ColorScheme.fromSwatch(
          primarySwatch: Colors.orange,
        ).copyWith(
          secondary: Colors.purple,
    ),
    textTheme: TextTheme(displayMedium: TextStyle(color: Colors.white))
  ),
  child: Scaffold(
    appBar: AppBar(
        title: Text('Third Screen'),
    ),
 
    จะเห็นว่าเราใช้ Theme widget ครอบ Scaffold widget และกำหนด ThemeData ใหม่เฉพาะสำหรับหน้านี้ สีหลัก และสีเน้นใช้
โทนสีส้ม และกำหนดรูปแบบของข้อความใน textTheme พร้อมกับเรียกใช้งานในส่วนของการแสดงข้อความ โดยใช้คำสั่ง 
    
Theme.of(context).textTheme.title
    ผลลัพธ์ที่ได้
 
 

 
 
    ในกรณีที่เราต้องการปรับแต่งเฉพาะบางค่าจาก Theme หลัก เราสามารถใช้คำสั่ง copyWith() เพื่อดึงรูปแบบการกำหนดใน Theme
หลักมาใช้ และปรับเฉพาะบางค่าที่ต้องการ สมมติเช่น หน้านี้ เราต้องการเปลี่ยนเฉพาะสีพื้นหลัง
จาก theme หลัก ก็จะได้เป็น
 
 
return Theme(
  data: Theme.of(context).copyWith(
    scaffoldBackgroundColor: Colors.blue,
  ),
  child: Scaffold(
    appBar: AppBar(
        title: Text('Third Screen'),
    ),
 
    ผลลัพธ์ที่ได้
 
 

 
 
    จะเห็นว่าเฉพาะส่วนของสีที่เป็นพื้นหลัง มีการเปลี่ยนแปลงจากค่าเดิม แต่ส่วนของ ข้อความก็ยังเป็นสีแดิม ซึ่งเป็นค่าจาก Theme หลัก
วิธีนี้เหมาะสำหรับเราต้องการใช้ Theme หลัก แต่อยากปรับเฉพาะบางจุดเล็กน้อย โดยไม่ต้องสร้าง ThemeData ใหม่ทั้งหมด
 
    แนวทางการประยุกต์แนะนำเพิ่มเติม เช่น เราสามารถใช้ Inherited widget สำหรับกำหนดค่า ให้ผู้ใช้เลือกรูปแบบ Theme ที่ต้องการ
การใช้ เช่น เปิด-ปิด ระบบ Dark Mode เป็นต้น
 
    ตอนนี้ App โปรเจ็คของเราเริ่มมีหลายหน้ามากขึ้น เราอยากจะมี Drawer เมนูสำหรับจัดการหน้าเพิ่มเติม แทนการเพิ่มปุ่มใน AppBar 
ในตอนหน้าเราจะเพิ่มในส่วนนี้กัน และอาจจะการประยุกต์เพิ่มเติม รอติดตาม


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


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

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


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



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



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









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






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

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

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

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



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




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





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

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


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


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







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