Flutter: Bagaimana cara menggunakan Widget Waris dengan benar?

103

Apa cara yang benar untuk menggunakan InheritedWidget? Sejauh ini saya mengerti bahwa ini memberi Anda kesempatan untuk menyebarkan data ke pohon Widget. Secara ekstrim jika Anda meletakkannya sebagai RootWidget, itu akan dapat diakses dari semua Widget di pohon di semua Rute, yang tidak masalah karena entah bagaimana saya harus membuat ViewModel / Model saya dapat diakses untuk Widget saya tanpa harus menggunakan global atau Singletons.

TAPI InheritedWidget tidak dapat diubah, jadi bagaimana cara memperbaruinya? Dan yang lebih penting bagaimana Stateful Widgets saya dipicu untuk membangun kembali subpohonnya?

Sayangnya dokumentasinya ada di sini sangat tidak jelas dan setelah diskusi dengan banyak orang sepertinya tidak ada yang benar-benar tahu apa cara yang benar untuk menggunakannya.

Saya menambahkan kutipan dari Brian Egan:

Ya, saya melihatnya sebagai cara untuk menyebarkan data ke bawah pohon. Yang menurut saya membingungkan, dari dokumen API:

"Widget yang diwariskan, jika direferensikan dengan cara ini, akan menyebabkan konsumen membuat ulang saat widget yang diwariskan itu sendiri berubah status."

Ketika saya pertama kali membaca ini, saya berpikir:

Saya bisa memasukkan beberapa data di InheritedWidget dan memutasinya nanti. Ketika mutasi itu terjadi, itu akan membangun kembali semua Widget yang mereferensikan InheritedWidget saya Apa yang saya temukan:

Untuk memutasi State of an InheritedWidget, Anda perlu membungkusnya dalam StatefulWidget. Anda kemudian benar-benar mengubah status StatefulWidget dan meneruskan data ini ke InheritedWidget, yang menyerahkan data ke semua turunannya. Namun, dalam kasus ini, tampaknya membangun kembali seluruh pohon di bawah StatefulWidget, bukan hanya Widget yang mereferensikan InheritedWidget. Apakah itu benar? Atau apakah itu akan tahu cara melewati Widget yang mereferensikan InheritedWidget jika updateShouldNotify mengembalikan false?

Thomas
sumber

Jawaban:

108

Masalahnya berasal dari kutipan Anda, yang salah.

Seperti yang Anda katakan, InheritedWidgets, seperti widget lainnya, tidak dapat diubah. Oleh karena itu mereka tidak memperbarui . Mereka diciptakan kembali.

Masalahnya adalah: InheritedWidget hanyalah widget sederhana yang tidak melakukan apa pun selain menyimpan data . Itu tidak memiliki logika pembaruan atau apa pun. Tapi, seperti widget lainnya, ini terkait dengan file Element. Dan coba tebak? Benda ini bisa berubah dan flutter akan menggunakannya kembali bila memungkinkan!

Kutipan yang dikoreksi adalah:

InheritedWidget, jika direferensikan dengan cara ini, akan menyebabkan konsumen membangun kembali saat InheritedWidget yang terkait dengan InheritedElement berubah.

Ada pembicaraan bagus tentang bagaimana widget / elemen / renderbox dihubungkan bersama . Tapi singkatnya, mereka seperti ini (kiri adalah widget khas Anda, tengah adalah 'elemen', dan kanan adalah 'kotak render'):

masukkan deskripsi gambar di sini

Masalahnya adalah: Saat Anda membuat widget baru; flutter akan membandingkannya dengan yang lama. Gunakan kembali "Elemen", yang menunjuk ke RenderBox. Dan bermutasi sifat RenderBox.


Oke, tapi bagaimana ini menjawab pertanyaan saya?

Saat membuat instance InheritedWidget, lalu memanggil context.inheritedWidgetOfExactType(atau MyClass.ofyang pada dasarnya sama); apa yang tersirat adalah bahwa itu akan mendengarkan yang Elementterkait dengan Anda InheritedWidget. Dan setiap kali Elementmendapat widget baru, itu akan memaksa refresh widget apa pun yang memanggil metode sebelumnya.

Singkatnya, saat Anda mengganti yang sudah ada InheritedWidgetdengan yang baru; flutter akan melihat bahwa itu berubah. Dan akan memberi tahu widget terikat tentang kemungkinan modifikasi.

Jika Anda memahami segalanya, Anda seharusnya sudah menebak solusinya:

Bungkus InheritedWidgetbagian dalam Anda StatefulWidgetyang akan menciptakan merek baru InheritedWidgetsetiap kali ada sesuatu yang berubah!

Hasil akhir dalam kode sebenarnya adalah:

class MyInherited extends StatefulWidget {
  static MyInheritedData of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(MyInheritedData) as MyInheritedData;

  const MyInherited({Key key, this.child}) : super(key: key);

  final Widget child;

  @override
  _MyInheritedState createState() => _MyInheritedState();
}

class _MyInheritedState extends State<MyInherited> {
  String myField;

  void onMyFieldChange(String newValue) {
    setState(() {
      myField = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MyInheritedData(
      myField: myField,
      onMyFieldChange: onMyFieldChange,
      child: widget.child,
    );
  }
}

class MyInheritedData extends InheritedWidget {
  final String myField;
  final ValueChanged<String> onMyFieldChange;

  MyInheritedData({
    Key key,
    this.myField,
    this.onMyFieldChange,
    Widget child,
  }) : super(key: key, child: child);

  static MyInheritedData of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedData>();
  }

  @override
  bool updateShouldNotify(MyInheritedData oldWidget) {
    return oldWidget.myField != myField ||
        oldWidget.onMyFieldChange != onMyFieldChange;
  }
}

Tetapi bukankah membuat InheritedWidget baru akan membangun kembali seluruh pohon?

Tidak, belum tentu. Karena InheritedWidget baru Anda berpotensi memiliki anak yang sama persis seperti sebelumnya. Dan tepatnya, maksud saya contoh yang sama. Widget yang memiliki contoh yang sama dengan sebelumnya tidak dibangun kembali.

Dan pada sebagian besar situasi (Memiliki inheritedWidget di root aplikasi Anda), widget yang diwariskan bersifat konstan . Jadi tidak perlu membangun kembali yang tidak perlu.

Rémi Rousselet
sumber
1
Tetapi bukankah membuat InheritedWidget baru akan membangun kembali seluruh pohon? Lalu mengapa perlunya Pendengar?
Thomas
1
Untuk komentar pertama Anda, saya menambahkan bagian ketiga pada jawaban saya. Adapun membosankan: Saya tidak setuju. Potongan kode dapat menghasilkan ini dengan cukup mudah. Dan mengakses data itu semudah menelepon MyInherited.of(context).
Rémi Rousselet
3
Tidak yakin apakah Anda tertarik, tetapi perbarui Sampelnya dengan teknik ini: github.com/brianegan/flutter_architecture_samples/tree/master/… Sedikit duplikasi sekarang pasti! Jika Anda memiliki saran lain untuk penerapan itu, akan sangat menyukai tinjauan kode jika Anda punya waktu luang :) Masih mencoba mencari cara terbaik untuk berbagi logika lintas platform ini (Flutter dan Web) dan memastikannya dapat diuji ( terutama yang asinkron).
Brianegan
4
Karena updateShouldNotifypengujian selalu mengacu pada MyInheritedStatecontoh yang sama , bukankah akan selalu kembali false? Tentu saja buildmetode MyInheritedStatemembuat _MyInheritedinstance baru , tetapi kolom dataselalu merujuk thisno? Saya mengalami masalah ... Berfungsi jika saya hanya membuat kode keras true.
cdock
2
@cdock Ya saya buruk. Tidak ingat mengapa saya melakukan itu karena jelas tidak akan berhasil. Diperbaiki dengan mengedit ke true, terima kasih.
Rémi Rousselet
20

TL; DR

Jangan gunakan komputasi berat di dalam metode updateShouldNotify dan gunakan const sebagai ganti yang baru saat membuat widget


Pertama-tama, kita harus memahami apa itu widget, Element, dan objek Render.

  1. Objek render adalah apa yang sebenarnya ditampilkan di layar. Mereka bisa berubah , mengandung logika lukisan dan tata letak. Pohon Render sangat mirip dengan Model Objek Dokumen (DOM) di web dan Anda dapat melihat objek render sebagai simpul DOM di pohon ini
  2. Widget - adalah deskripsi tentang apa yang harus dirender. Mereka tidak bisa diubah dan murah. Jadi jika Widget menjawab pertanyaan "Apa?" (Pendekatan deklaratif) maka objek Render menjawab pertanyaan "Bagaimana?" (Pendekatan imperatif). Sebuah analogi dari web adalah "DOM Virtual".
  3. Element / BuildContext - adalah proxy antara Widget dan objek Render . Ini berisi informasi tentang posisi widget di pohon * dan cara memperbarui objek Render ketika widget terkait diubah.

Sekarang kita siap untuk menyelami metode InheritedWidget dan BuildContext inheritFromWidgetOfExactType .

Sebagai contoh, saya sarankan kami mempertimbangkan contoh ini dari dokumentasi Flutter tentang InheritedWidget:

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  })  : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(FrogColor);
  }

  @override
  bool updateShouldNotify(FrogColor old) {
    return color != old.color;
  }
}

InheritedWidget - hanya sebuah widget yang diimplementasikan dalam kasus kami satu metode penting - updateShouldNotify . updateShouldNotify - fungsi yang menerima satu parameter oldWidget dan mengembalikan nilai boolean: true atau false.

Seperti widget lainnya, InheritedWidget memiliki objek Elemen yang sesuai. Itu adalah InheritedElement . Panggilan InheritedElement updateShouldNotify pada widget setiap kali kita membuat widget baru (panggil setState pada leluhur). Ketika updateShouldNotify mengembalikan True InheritedElement melakukan iterasi melalui dependensi (?) Dan memanggil metode didChangeDependencies padanya.

Di mana InheritedElement mendapatkan dependensi ? Di sini kita harus melihat metode inheritFromWidgetOfExactType .

inheritFromWidgetOfExactType - Metode ini ditentukan dalam BuildContext dan setiap Elemen mengimplementasikan antarmuka BuildContext (Element == BuildContext). Jadi setiap Elemen memiliki metode ini.

Mari kita lihat kode inheritFromWidgetOfExactType:

final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
  assert(ancestor is InheritedElement);
  return inheritFromElement(ancestor, aspect: aspect);
}

Di sini kami mencoba menemukan leluhur di _inheritedWidgets yang dipetakan berdasarkan jenis. Jika leluhur ditemukan, kita kemudian memanggil inheritFromElement .

Kode untuk inheritFromElement :

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. Kami menambahkan leluhur sebagai ketergantungan elemen saat ini (_dependencies.add (leluhur))
  2. Kami menambahkan elemen saat ini ke dependensi leluhur (leluhur.updateDependencies (ini, aspek))
  3. Kami mengembalikan widget leluhur sebagai hasil dari inheritFromWidgetOfExactType (mengembalikan leluhur.widget )

Jadi sekarang kita tahu dari mana InheritedElement mendapatkan dependensinya.

Sekarang mari kita lihat metode didChangeDependencies . Setiap Elemen memiliki metode ini:

  void didChangeDependencies() {
    assert(_active); // otherwise markNeedsBuild is a no-op
    assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
    markNeedsBuild();
  }

Seperti yang kita lihat, metode ini hanya menandai elemen sebagai kotor dan elemen ini harus dibangun kembali pada bingkai berikutnya. Rebuild berarti metode panggilan yang dibangun di atas elemen widget yang sesuai.

Tapi bagaimana dengan "Seluruh sub-pohon membangun kembali ketika saya membangun kembali InheritedWidget?". Di sini kita harus ingat bahwa Widget tidak dapat diubah dan jika Anda membuat widget baru Flutter akan membangun kembali sub-pohon. Bagaimana cara memperbaikinya?

  1. Widget cache dengan tangan (secara manual)
  2. Gunakan const karena const membuat satu-satunya instance dari nilai / kelas
maksimr
sumber
1
penjelasan yang bagus maksimr. Hal yang paling membingungkan saya adalah jika seluruh sub tree tetap dibangun kembali ketika inheritedWidget diganti, apa gunanya updateShouldNotify ()?
Panda World
3

Dari dokumen :

[BuildContext.inheritFromWidgetOfExactType] mendapatkan widget terdekat dari jenis yang diberikan, yang harus merupakan jenis subkelas InheritedWidget konkret, dan mendaftarkan konteks build ini dengan widget tersebut sehingga saat widget tersebut berubah (atau widget baru dari jenis tersebut diperkenalkan, atau widget menghilang), konteks build ini dibuat ulang sehingga dapat memperoleh nilai baru dari widget itu.

Ini biasanya dipanggil secara implisit dari () metode statis, misalnya Theme.of.

Seperti yang dicatat OP, sebuah InheritedWidgetinstance tidak berubah ... tetapi dapat diganti dengan instance baru di lokasi yang sama di pohon widget. Jika itu terjadi, mungkin saja widget yang terdaftar perlu dibangun kembali. The InheritedWidget.updateShouldNotifyMetode membuat penentuan ini. (Lihat: dokumen )

Jadi bagaimana sebuah contoh bisa diganti? Sebuah InheritedWidgetcontoh mungkin berisi oleh StatefulWidget, yang dapat menggantikan contoh lama dengan yang baru.

kkurian
sumber
-1

InheritedWidget mengelola data aplikasi yang terpusat dan meneruskannya ke anak, Seperti yang bisa kita simpan di sini jumlah keranjang seperti yang dijelaskan di sini :

Sunil
sumber