Flutter: Làm thế nào để sử dụng đúng một Widget Kế thừa?


103

Cách chính xác để sử dụng InheritedWidget là gì? Cho đến nay tôi đã hiểu rằng nó cho bạn cơ hội truyền dữ liệu xuống cây Widget. Đặc biệt, nếu bạn đặt là RootWidget, nó sẽ có thể truy cập được từ tất cả các Widget trong cây trên tất cả các Routes, điều này tốt vì bằng cách nào đó tôi phải làm cho ViewModel / Model của mình có thể truy cập được cho các Widget của tôi mà không cần phải sử dụng đến các hình cầu hoặc Singleton.

NHƯNG InheritedWidget là bất biến, vậy làm cách nào để cập nhật nó? Và quan trọng hơn, làm thế nào mà các Stateful Widget của tôi được kích hoạt để xây dựng lại các cây con của chúng?

Thật không may, tài liệu ở đây rất không rõ ràng và sau khi thảo luận với rất nhiều người dường như không ai thực sự biết cách sử dụng chính xác nó.

Tôi thêm một trích dẫn từ Brian Egan:

Vâng, tôi xem đó là một cách để truyền dữ liệu xuống cây. Điều tôi thấy khó hiểu, từ tài liệu API:

"Các widget kế thừa, khi được tham chiếu theo cách này, sẽ khiến người tiêu dùng xây dựng lại khi chính widget kế thừa thay đổi trạng thái."

Khi tôi lần đầu tiên đọc nó, tôi đã nghĩ:

Tôi có thể nhồi một số dữ liệu vào InheritedWidget và sửa đổi nó sau. Khi đột biến đó xảy ra, nó sẽ xây dựng lại tất cả các Widget tham chiếu đến InheritedWidget của tôi Những gì tôi tìm thấy:

Để thay đổi trạng thái của một Inhe inheritWidget, bạn cần phải bọc nó trong một StatefulWidget Sau đó, bạn thực sự biến đổi trạng thái của StatefulWidget và chuyển dữ liệu này xuống cho InheritedWidget, nơi sẽ chuyển dữ liệu xuống cho tất cả các con của nó. Tuy nhiên, trong trường hợp đó, nó dường như xây dựng lại toàn bộ cây bên dưới StatefulWidget, chứ không chỉ các Widget tham chiếu đến InheritedWidget. Đúng không? Hay bằng cách nào đó nó sẽ biết cách bỏ qua các Widget tham chiếu đến InheritedWidget nếu updateShouldNotify trả về false?

Câu trả lời:


107

Vấn đề đến từ câu trích dẫn của bạn không chính xác.

Như bạn đã nói, InheritedWidgets, giống như các widget khác, là bất biến. Do đó họ không cập nhật . Chúng được tạo ra một lần nữa.

Vấn đề là: InheritedWidget chỉ là một widget đơn giản không làm gì khác ngoài việc lưu trữ dữ liệu . Nó không có bất kỳ logic cập nhật hay bất kỳ thứ gì. Nhưng, giống như bất kỳ widget nào khác, nó được liên kết với một Element. Và đoán xem? Thứ này có thể thay đổi được và nó sẽ sử dụng lại nó bất cứ khi nào có thể!

Trích dẫn được sửa chữa sẽ là:

InheritedWidget, khi được tham chiếu theo cách này, sẽ khiến người tiêu dùng xây dựng lại khi InheritedWidget liên kết với một InheritedElement thay đổi.

Có một cuộc nói chuyện tuyệt vời về cách các widget / phần tử / hộp kết xuất được kết hợp với nhau . Nhưng tóm lại, chúng như thế này (bên trái là tiện ích con điển hình của bạn, ở giữa là 'phần tử' và bên phải là 'hộp kết xuất'):

nhập mô tả hình ảnh ở đây

Vấn đề là: Khi bạn khởi tạo một widget mới; rung sẽ so sánh nó với cái cũ. Sử dụng lại nó là "Phần tử", trỏ đến một RenderBox. Và thay đổi các thuộc tính RenderBox.


Okey, nhưng điều này trả lời câu hỏi của tôi như thế nào?

Khi khởi tạo một InheritedWidget, và sau đó gọi context.inheritedWidgetOfExactType(hoặc MyClass.ofvề cơ bản là giống nhau); những gì ngụ ý là nó sẽ lắng nghe Elementliên kết với của bạn InheritedWidget. Và bất cứ khi nào Elementcó một widget mới, nó sẽ buộc làm mới bất kỳ widget nào được gọi là phương thức trước đó.

Nói tóm lại, khi bạn thay thế một cái hiện có InheritedWidgetbằng một cái mới; rung động sẽ thấy rằng nó đã thay đổi. Và sẽ thông báo cho các vật dụng bị ràng buộc về một sửa đổi tiềm năng.

Nếu bạn hiểu mọi thứ, bạn hẳn đã đoán được giải pháp:

Bọc InheritedWidgetbên trong của bạn một StatefulWidgetcái sẽ tạo ra một thương hiệu mới InheritedWidgetbất cứ khi nào có gì đó thay đổi!

Kết quả cuối cùng trong mã thực tế sẽ là:

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;
  }
}

Nhưng không phải tạo một InheritedWidget mới sẽ xây dựng lại toàn bộ cây?

Không, nó sẽ không nhất thiết. Vì InheritedWidget mới của bạn có khả năng có con giống hệt như trước đây. Và chính xác, ý tôi là cùng một trường hợp. Các widget có cùng phiên bản mà chúng đã có trước đây sẽ không xây dựng lại.

Và trong hầu hết các tình huống (Có một Tiện ích kế thừa ở gốc ứng dụng của bạn), tiện ích con được kế thừa là không đổi . Vì vậy, không có xây dựng lại không cần thiết.


1
Nhưng không phải tạo một InheritedWidget mới sẽ xây dựng lại toàn bộ cây? Tại sao lại cần Người nghe?
Thomas

1
Đối với nhận xét đầu tiên của bạn, tôi đã thêm phần thứ ba vào câu trả lời của mình. Về việc tẻ nhạt: Tôi không đồng ý. Một đoạn mã có thể tạo ra điều này khá dễ dàng. Và việc truy cập dữ liệu cũng đơn giản như gọi điện MyInherited.of(context).
Rémi Rousselet

3
Không chắc bạn có quan tâm hay không, nhưng đã cập nhật Mẫu bằng kỹ thuật này: github.com/brianegan/flutter_architecture_samples/tree/master/… Chắc chắn bây giờ sẽ bớt trùng lặp hơn một chút! Nếu bạn có bất kỳ đề xuất nào khác cho việc triển khai đó, rất thích xem lại mã nếu bạn có một chút thời gian rảnh rỗi :) Vẫn đang cố gắng tìm ra cách tốt nhất để chia sẻ nền tảng logic chéo này (Flutter và Web) và đảm bảo rằng nó có thể kiểm tra được ( đặc biệt là những thứ không đồng bộ).
brianegan

4
updateShouldNotifythử nghiệm luôn tham chiếu đến cùng một MyInheritedStatetrường hợp, nó sẽ không luôn trả về false? Chắc chắn rằng buildphương pháp của MyInheritedStatelà tạo các _MyInheritedphiên bản mới , nhưng datatrường này luôn luôn tham chiếu thiskhông? Tôi đang gặp sự cố ... Hoạt động nếu tôi chỉ viết mã cứng true.
cdock

2
@cdock Vâng, tệ của tôi. Không nhớ tại sao tôi làm điều đó vì nó rõ ràng sẽ không hoạt động. Đã sửa bằng cách chỉnh sửa thành true, cảm ơn.
Rémi Rousselet

20

TL; DR

Không sử dụng tính toán nặng bên trong phương thức updateShouldNotify và sử dụng const thay vì new khi tạo widget


Trước hết, chúng ta nên hiểu các đối tượng Widget, Element và Render là gì.

  1. Các đối tượng kết xuất là những gì thực sự được hiển thị trên màn hình. Chúng có thể thay đổi , chứa bức tranh và logic bố cục. Cây kết xuất rất giống với Mô hình đối tượng tài liệu (DOM) trên web và bạn có thể xem một đối tượng kết xuất như một nút DOM trong cây này
  2. Widget - là một mô tả về những gì sẽ được hiển thị. Chúng là bất biến và rẻ tiền. Vì vậy, nếu một Widget trả lời câu hỏi “Cái gì?” (Cách tiếp cận so sánh) thì một đối tượng Kết xuất trả lời câu hỏi “Làm thế nào?” (Cách tiếp cận mệnh lệnh). Một ví dụ từ web là "DOM ảo".
  3. Element / BuildContext - là một proxy giữa các đối tượng WidgetRender . Nó chứa thông tin về vị trí của một widget trong cây * và cách cập nhật đối tượng Render khi một widget tương ứng bị thay đổi.

Bây giờ chúng ta đã sẵn sàng đi sâu vào phương thức của InheritedWidget và BuildContext inheritFromWidgetOfExactType .

Để làm ví dụ, tôi khuyên chúng ta nên xem xét ví dụ này từ tài liệu của Flutter về 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 - chỉ là một widget triển khai một phương pháp quan trọng trong trường hợp của chúng ta - updateShouldNotify . updateShouldNotify - một hàm chấp nhận một tham số oldWidget và trả về một giá trị boolean: true hoặc false.

Giống như bất kỳ widget nào, InheritedWidget có một đối tượng Element tương ứng. Đó là Thừa kế . InheritedElement gọi updateShouldNotify trên widget mỗi khi chúng ta tạo một widget mới (gọi setState trên tổ tiên). Khi updateShouldNotify trả về true, Inhe inheritElement sẽ lặp lại các gói phụ thuộc (?) Và gọi phương thức didChangeDependencies trên đó.

Nơi Inhe inheritElement nhận được các phụ thuộc ? Ở đây chúng ta nên xem xét phương thức inheritFromWidgetOfExactType .

inheritFromWidgetOfExactType - Phương thức này được định nghĩa trong BuildContext và mọi Phần tử triển khai giao diện BuildContext (Phần tử == BuildContext). Vì vậy, mọi Element đều có phương thức này.

Hãy xem mã của inheritFromWidgetOfExactType:

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

Ở đây, chúng tôi cố gắng tìm tổ tiên trong _inheritedWidgets được ánh xạ theo loại. Nếu tổ tiên được tìm thấy, thì chúng ta gọi inheritFromElement .

Mã cho inheritFromElement :

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. Chúng tôi thêm tổ tiên dưới dạng phụ thuộc của phần tử hiện tại (_dependencies.add (tổ tiên))
  2. Chúng tôi thêm phần tử hiện tại vào các phụ thuộc của tổ tiên (parent.updateDependencies (this, khía cạnh))
  3. Chúng tôi trả về tiện ích của tổ tiên là kết quả của inheritFromWidgetOfExactType (trả lại tổ tiên.widget )

Vì vậy, bây giờ chúng ta biết Inhe inheritElement lấy các phụ thuộc của nó ở đâu.

Bây giờ chúng ta hãy xem xét phương thức didChangeDependencies . Mọi phần tử đều có phương thức này:

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

Như chúng ta có thể thấy, phương pháp này chỉ đánh dấu một phần tử là bẩn và phần tử này sẽ được xây dựng lại trên khung tiếp theo. Rebuild nghĩa là xây dựng phương thức gọi dựa trên phần tử widget tương ứng.

Nhưng còn "Toàn bộ cây con được xây dựng lại khi tôi xây dựng lại InheritedWidget thì sao?". Ở đây chúng ta nên nhớ rằng Widget là bất biến và nếu bạn tạo widget mới thì Flutter sẽ xây dựng lại cây con. Làm thế nào chúng ta có thể sửa chữa nó?

  1. Cache widget bằng tay (thủ công)
  2. Sử dụng const vì const tạo ra một trường hợp duy nhất của giá trị / lớp

1
giải thích tuyệt vời maksimr. Điều khiến tôi bối rối nhất là nếu toàn bộ cây con vẫn được xây dựng lại khi inheritWidget bị thay thế, thì điểm của updateShouldNotify () là gì?
Panda World

3

Từ các tài liệu :

[BuildContext.inheritFromWidgetOfExactType] lấy tiện ích con gần nhất của loại đã cho, phải là loại của một lớp con InheritedWidget cụ thể và đăng ký ngữ cảnh xây dựng này với tiện ích đó sao cho khi tiện ích đó thay đổi (hoặc một tiện ích mới của loại đó được giới thiệu, hoặc tiện ích biến mất), ngữ cảnh xây dựng này được xây dựng lại để nó có thể nhận các giá trị mới từ tiện ích đó.

Điều này thường được gọi ngầm từ các phương thức tĩnh của (), ví dụ: Theme.of.

Như OP đã lưu ý, một InheritedWidgetcá thể không thay đổi ... nhưng nó có thể được thay thế bằng một cá thể mới tại cùng một vị trí trong cây tiện ích. Khi điều đó xảy ra, có thể các widget đã đăng ký cần được xây dựng lại. Các InheritedWidget.updateShouldNotifyphương pháp làm cho quyết định này. (Xem: tài liệu )

Vì vậy, làm thế nào một trường hợp có thể được thay thế? Một InheritedWidgetthể hiện có thể được chứa bởi một StatefulWidget, có thể thay thế một thể hiện cũ bằng một thể hiện mới.


-1

InheritedWidget quản lý dữ liệu tập trung của ứng dụng và chuyển nó cho trẻ, Giống như chúng tôi có thể lưu trữ ở đây số lượng giỏ hàng như được giải thích ở đây :

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.