จะจัดการกับวิดเจ็ตที่ไม่ต้องการได้อย่างไร?


143

ด้วยเหตุผลหลายประการบางครั้งbuildเมธอดของวิดเจ็ตของฉันถูกเรียกอีกครั้ง

ฉันรู้ว่ามันเกิดขึ้นเพราะผู้ปกครองอัปเดต แต่สิ่งนี้ทำให้เกิดผลกระทบที่ไม่ต้องการ สถานการณ์ทั่วไปที่ทำให้เกิดปัญหาคือเมื่อใช้FutureBuilderวิธีนี้:

@override
Widget build(BuildContext context) {
  return FutureBuilder(
    future: httpCall(),
    builder: (context, snapshot) {
      // create some layout here
    },
  );
}

ในตัวอย่างนี้หากจะเรียกใช้วิธีการสร้างอีกครั้งก็จะเรียกใช้คำขอ http อื่น ซึ่งไม่เป็นที่ต้องการ

เมื่อพิจารณาถึงสิ่งนี้จะจัดการกับงานสร้างที่ไม่ต้องการได้อย่างไร? มีวิธีใดบ้างที่จะป้องกันการสร้างการโทร?


1
โพสต์นี้อาจช่วยคุณได้ .. /programming/53223469/flutter-statelesswidget-build-called-multiple-times/55626839#55626839
bunny

4
ในเอกสารของผู้ให้บริการคุณเชื่อมโยงที่นี่โดยระบุว่า "ดูคำตอบ stackoverflow นี้ซึ่งจะอธิบายในรายละเอียดเพิ่มเติมว่าเหตุใดการใช้ตัวสร้าง. value เพื่อสร้างค่าจึงไม่เป็นที่ต้องการ" อย่างไรก็ตามคุณไม่ได้กล่าวถึงตัวสร้างค่าที่นี่หรือในคำตอบของคุณ คุณต้องการเชื่อมโยงที่อื่นหรือไม่?
Suragch

คำตอบ:


226

สร้างวิธีการออกแบบในลักษณะดังกล่าวว่ามันควรจะเป็น/ บริสุทธิ์ไม่มีผลข้างเคียง เนื่องจากปัจจัยภายนอกหลายอย่างสามารถทริกเกอร์การสร้างวิดเจ็ตใหม่เช่น:

  • เส้นทางป๊อป / พุช
  • การปรับขนาดหน้าจอมักเกิดจากลักษณะแป้นพิมพ์หรือการเปลี่ยนทิศทาง
  • วิดเจ็ตหลักสร้างลูกใหม่
  • InheritedWidget วิดเจ็ตขึ้นอยู่กับClass.of(context)การเปลี่ยนแปลง( รูปแบบ)

ซึ่งหมายความว่าbuildวิธีการที่ควรจะไม่ก่อให้เกิดการเรียกร้อง http หรือปรับเปลี่ยนใด ๆ ของรัฐ


เกี่ยวข้องกับคำถามนี้อย่างไร?

ปัญหาที่คุณกำลังเผชิญคือวิธีการสร้างของคุณมีผลข้างเคียง / ไม่บริสุทธิ์ทำให้การสร้างการโทรภายนอกเป็นเรื่องยุ่งยาก

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

ในกรณีของตัวอย่างของคุณที่คุณต้องการเปลี่ยนวิดเจ็ตของคุณให้เป็นStatefulWidgetแล้วดึงที่โทร HTTP ไปinitStateของคุณState:

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  Future<int> future;

  @override
  void initState() {
    future = Future.value(42);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: future,
      builder: (context, snapshot) {
        // create some layout here
      },
    );
  }
}

ฉันรู้เรื่องนี้แล้ว ฉันมาที่นี่เพราะต้องการเพิ่มประสิทธิภาพการสร้างใหม่จริงๆ

นอกจากนี้ยังเป็นไปได้ที่จะสร้างวิดเจ็ตที่สามารถสร้างใหม่โดยไม่ต้องบังคับให้ลูก ๆ สร้างด้วย

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

วิธีที่ง่ายที่สุดคือใช้ตัวconstสร้างโผ:

@override
Widget build(BuildContext context) {
  return const DecoratedBox(
    decoration: BoxDecoration(),
    child: Text("Hello World"),
  );
}

ด้วยconstคีย์เวิร์ดนั้นอินสแตนซ์ของDecoratedBoxจะยังคงเหมือนเดิมแม้ว่าบิลด์จะถูกเรียกหลายร้อยครั้งก็ตาม

แต่คุณสามารถบรรลุผลลัพธ์เดียวกันได้ด้วยตนเอง:

@override
Widget build(BuildContext context) {
  final subtree = MyWidget(
    child: Text("Hello World")
  );

  return StreamBuilder<String>(
    stream: stream,
    initialData: "Foo",
    builder: (context, snapshot) {
      return Column(
        children: <Widget>[
          Text(snapshot.data),
          subtree,
        ],
      );
    },
  );
}

ในตัวอย่างนี้เมื่อ StreamBuilder ได้รับแจ้งค่าใหม่subtreeจะไม่สร้างใหม่แม้ว่า StreamBuilder / Column จะทำก็ตาม มันเกิดขึ้นเนื่องจากการปิดตัวอย่างMyWidgetไม่เปลี่ยนแปลง

รูปแบบนี้ถูกใช้มากในแอนิเมชั่น การใช้งานทั่วไปคือAnimatedBuilderและการเปลี่ยนทั้งหมดเช่นAlignTransition.

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


2
คุณช่วยอธิบายได้ไหมว่าทำไมการจัดเก็บsubtreeในฟิลด์คลาสจึงทำให้การโหลดซ้ำแบบร้อน
mFeinstein

4
ปัญหาที่ฉันพบStreamBuilderคือเมื่อแป้นพิมพ์ปรากฏขึ้นหน้าจอจะเปลี่ยนไปดังนั้นจึงต้องสร้างเส้นทางใหม่ จึงStreamBuilderถูกสร้างขึ้นใหม่และมีการสร้างใหม่StreamBuilderและสมัครเป็นสมาชิกไฟล์stream. เมื่อStreamBuilderสมัครไปstream, snapshot.connectionStateจะกลายเป็นConnectionState.waitingซึ่งจะทำให้รหัสของฉันกลับมาCircularProgressIndicatorแล้วsnapshot.connectionStateมีการเปลี่ยนแปลงเมื่อมีข้อมูลและรหัสของฉันจะกลับมาเป็นเครื่องมือที่แตกต่างกันซึ่งจะทำให้การสั่นไหวหน้าจอที่มีสิ่งที่แตกต่างกัน
mFeinstein

1
ฉันตัดสินใจที่จะทำให้การStatefulWidgetสมัครไปstreamบนinitState()และตั้งค่าcurrentWidgetด้วยsetState()ขณะที่streamส่งข้อมูลใหม่ผ่านcurrentWidgetไปที่build()วิธีการ มันมีทางออกที่ดีกว่านี้ไหม?
mFeinstein

1
ฉันสับสนเล็กน้อย คุณกำลังตอบคำถามของคุณเองอย่างไรก็ตามจากเนื้อหาดูเหมือนจะไม่เป็นเช่นนั้น
sgon00

8
เอ่อการบอกว่าการสร้างไม่ควรเรียกเมธอด HTTP นั้นเอาชนะตัวอย่างที่ใช้งานได้จริงของไฟล์FutureBuilder.
TheGeekZn

6

คุณสามารถป้องกันการโทรแบบบิวด์ที่ไม่ต้องการได้โดยใช้วิธีเหล่านี้

1) สร้างคลาส Statefull ลูกสำหรับส่วนเล็ก ๆ ของ UI

2) ใช้ไลบรารีของผู้ให้บริการดังนั้นเมื่อใช้คุณสามารถหยุดการเรียกวิธีการสร้างที่ไม่ต้องการได้

ในการเรียกวิธีการสร้างสถานการณ์ด้านล่างนี้

  • หลังจากเรียกinitState
  • หลังจากเรียกdidUpdateWidget
  • เมื่อsetState ()ถูกเรียกใช้
  • เมื่อแป้นพิมพ์เปิดอยู่
  • เมื่อการวางแนวหน้าจอเปลี่ยนไป
  • วิดเจ็ตหลักถูกสร้างจากนั้นวิดเจ็ตลูกยังสร้างใหม่

0

ValueListenableBuilder<T> class กระพือนอกจากนี้ยังมี ช่วยให้คุณสร้างเฉพาะบางวิดเจ็ตที่จำเป็นสำหรับวัตถุประสงค์ของคุณและข้ามวิดเจ็ตราคาแพง

คุณสามารถดูเอกสารได้ที่นี่ValueListenableBuilder flutter docs
หรือโค้ดตัวอย่างด้านล่าง:

  return Scaffold(
  appBar: AppBar(
    title: Text(widget.title)
  ),
  body: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text('You have pushed the button this many times:'),
        ValueListenableBuilder(
          builder: (BuildContext context, int value, Widget child) {
            // This builder will only get called when the _counter
            // is updated.
            return Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                Text('$value'),
                child,
              ],
            );
          },
          valueListenable: _counter,
          // The child parameter is most helpful if the child is
          // expensive to build and does not depend on the value from
          // the notifier.
          child: goodJob,
        )
      ],
    ),
  ),
  floatingActionButton: FloatingActionButton(
    child: Icon(Icons.plus_one),
    onPressed: () => _counter.value += 1,
  ),
);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.