Scaffold.of () được gọi với bối cảnh không chứa Scaffold


183

Như bạn có thể thấy nút của tôi ở trong cơ thể của Scaffold. Nhưng tôi nhận được ngoại lệ này:

Scaffold.of () được gọi với bối cảnh không chứa Scaffold.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SnackBar Playground'),
      ),
      body: Center(
        child: RaisedButton(
          color: Colors.pink,
          textColor: Colors.white,
          onPressed: _displaySnackBar(context),
          child: Text('Display SnackBar'),
        ),
      ),
    );
  }
}

_displaySnackBar(BuildContext context) {
  final snackBar = SnackBar(content: Text('Are you talkin\' to me?'));
  Scaffold.of(context).showSnackBar(snackBar);
}

BIÊN TẬP:

Tôi tìm thấy một giải pháp khác cho vấn đề này. Nếu chúng tôi cung cấp cho Scaffold một khóa là GlobalKey, chúng tôi có thể hiển thị SnackBar như sau mà không cần phải bọc cơ thể bằng tiện ích Builder. Các widget trả về Scaffold phải là widget Stateful mặc dù:

 _scaffoldKey.currentState.showSnackBar(snackbar); 

Tôi đã tìm thấy một hướng dẫn rất hay, nơi anh ấy tạo ra một trình bao bọc để anh ấy có thể dễ dàng thay đổi hoặc kiểm soát bối cảnh của toàn bộ ứng dụng: noobieprogrammer.blogspot.com/2020/06/ Lỗi
doppelgunner

Câu trả lời:


245

Ngoại lệ này xảy ra bởi vì bạn đang sử dụng contexttiện ích con khởi tạo Scaffold. Không phải contextcủa một đứa trẻ Scaffold.

Bạn có thể giải quyết điều này bằng cách chỉ sử dụng một bối cảnh khác:

Scaffold(
    appBar: AppBar(
        title: Text('SnackBar Playground'),
    ),
    body: Builder(
        builder: (context) => 
            Center(
            child: RaisedButton(
            color: Colors.pink,
            textColor: Colors.white,
            onPressed: () => _displaySnackBar(context),
            child: Text('Display SnackBar'),
            ),
        ),
    ),
);

Lưu ý rằng trong khi chúng tôi đang sử dụng Builderở đây, đây không phải là cách duy nhất để có được sự khác biệt BuildContext.

Cũng có thể trích xuất cây con thành một thứ khác Widget(thường sử dụng extract widgetrefactor)


Tôi nhận được ngoại lệ này với điều này: setState () hoặc markNeedBuild () được gọi trong quá trình xây dựng. I / flutter (21754): Tiện ích Scaffold này không thể được đánh dấu là cần xây dựng vì khung đã có trong I / flutter (21754): quá trình xây dựng các widget.
Figen Güngor

1
Các onPressedbạn đã chuyển đến RaisedButtonkhông phải là một chức năng. Đổi nó thành() => _displaySnackBar(context)
Rémi Rousselet

Gotcha: onPression: () {_displaySnackBar (bối cảnh);}, Cảm ơn =)
Figen Güngor

1
Tôi đã nghĩ với .of (bối cảnh) nó được cho là đi lên hệ thống phân cấp widget cho đến khi nó gặp một trong các loại đã cho, trong trường hợp này, Scaffold, nó sẽ gặp phải trước khi nó đạt đến "Widget build (..". Nó không hoạt động Cách này?
CodeGrue

@ RémiRousselet, Flutter có bao nhiêu loại bối cảnh? Giống như bạn đã nói bối cảnh của widget, bối cảnh của một đứa trẻ của Scaffold.
CopsOnRoad

121

Bạn có thể sử dụng một GlobalKey. Nhược điểm duy nhất là sử dụng GlobalKey có thể không phải là cách hiệu quả nhất để làm việc này.

Một điều tốt về điều này là bạn cũng có thể chuyển khóa này cho lớp widget tùy chỉnh khác không chứa bất kỳ giàn giáo nào. Xem ( ở đây )

class HomePage extends StatelessWidget {
  final _scaffoldKey = GlobalKey<ScaffoldState>(); \\ new line
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,                           \\ new line
      appBar: AppBar(
        title: Text('SnackBar Playground'),
      ),
      body: Center(
        child: RaisedButton(
          color: Colors.pink,
          textColor: Colors.white,
          onPressed: _displaySnackBar(context),
          child: Text('Display SnackBar'),
        ),
      ),
    );
  }
  _displaySnackBar(BuildContext context) {
    final snackBar = SnackBar(content: Text('Are you talkin\' to me?'));
    _scaffoldKey.currentState.showSnackBar(snackBar);   \\ edited line
  }
}

Cảm ơn Lebohang Mbele
Nhà phát triển

1
Tôi có thể hỏi làm thế nào để sao chép cái này khi cây của bạn bao gồm nhiều widget được định nghĩa trong nhiều tệp không? _scaffoldKeylà riêng tư do gạch dưới hàng đầu. Nhưng ngay cả khi nó không có, nó vẫn ở trong HomePagelớp và do đó không có sẵn mà không khởi tạo HomePage mới ở đâu đó trên cây, dường như tạo ra một tham chiếu vòng tròn.
ExactaBox

1
để trả lời câu hỏi của riêng tôi: tạo một lớp đơn với một thuộc GlobalKey<ScaffoldState>tính, chỉnh sửa hàm tạo của Widget bằng giàn giáo để đặt thuộc tính khóa của singleton, sau đó trong cây của widget con, chúng ta có thể gọi một cái gì đó như mySingleton.instance.key.currentState.showSnackBar()...
ExactaBox

Tại sao điều này không hiệu quả?
dùng2233706

@ user2233706 Điều này được đề cập trong tài liệu của GlobalKey tại đây . Ngoài ra bài này có thể làm sáng tỏ hơn.
Lebohang Mbele

43

Hai cách để giải quyết vấn đề này

1) Sử dụng tiện ích Builder

Scaffold(
    appBar: AppBar(
        title: Text('My Profile'),
    ),
    body: Builder(
        builder: (ctx) => RaisedButton(
            textColor: Colors.red,
            child: Text('Submit'),
            onPressed: () {
                 Scaffold.of(ctx).showSnackBar(SnackBar(content: Text('Profile Save'),),);
            }               
        ),
    ),
);

2) Sử dụng GlobalKey

class HomePage extends StatelessWidget {

  final globalKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
     return Scaffold(
       key: globalKey,
       appBar: AppBar(
          title: Text('My Profile'),
       ),
       body:  RaisedButton(
          textColor: Colors.red,
          child: Text('Submit'),
          onPressed: (){
               final snackBar = SnackBar(content: Text('Profile saved'));
               globalKey.currentState.showSnackBar(snackBar);
          },
        ),
     );
   }
}

16

Kiểm tra điều này từ tài liệu của phương pháp:

Khi Scaffold thực sự được tạo trong cùng một chức năng xây dựng, đối số ngữ cảnh của chức năng xây dựng không thể được sử dụng để tìm Giàn giáo (vì nó "ở trên" tiện ích được trả về). Trong các trường hợp như vậy, kỹ thuật sau đây với Trình tạo có thể được sử dụng để cung cấp phạm vi mới với BuildContext "bên dưới" Giàn giáo:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Demo')
    ),
    body: Builder(
      // Create an inner BuildContext so that the onPressed methods
      // can refer to the Scaffold with Scaffold.of().
      builder: (BuildContext context) {
        return Center(
          child: RaisedButton(
            child: Text('SHOW A SNACKBAR'),
            onPressed: () {
              Scaffold.of(context).showSnackBar(SnackBar(
                content: Text('Hello!'),
              ));
            },
          ),
        );
      },
    ),
  );
}

Bạn có thể kiểm tra mô tả từ các tài liệu phương thức


13

Cách đơn giản để giải quyết vấn đề này sẽ là tạo một khóa cho giàn giáo của bạn như trận chung kết này với đoạn mã sau:

Đầu tiên: GlobalKey<ScaffoldState>() _scaffoldKey = GlobalKey<ScaffoldState> ();

Scecond: Gán chìa khóa cho Giàn giáo của bạn key: _scaffoldKey

Thứ ba: Gọi Snackbar bằng _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text("Welcome")));


3

Chính hành vi bạn đang gặp phải thậm chí còn được gọi là "trường hợp khó khăn" trong tài liệu Flutter .

Làm thế nào để khắc phục

Vấn đề được khắc phục theo những cách khác nhau như bạn có thể thấy từ các câu trả lời khác được đăng ở đây. Ví dụ, phần tài liệu tôi đề cập để giải quyết vấn đề bằng cách sử dụng một tài liệu Buildertạo

một bên trong BuildContextđể các onPressedphương thức có thể tham chiếu đến Scaffoldvới Scaffold.of().

Vì vậy, một cách để gọi showSnackBartừ Scaffold sẽ là

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Demo')),
    body: Builder(
      builder: (BuildContext innerContext) {
        return FlatButton(
          child: Text('BUTTON'),
          onPressed: () {
            Scaffold.of(innerContext).showSnackBar(SnackBar(
              content: Text('Hello.')
            ));
          }
        );
      }
    )
  );
}

Bây giờ một số chi tiết cho người đọc tò mò

Bản thân tôi thấy khá hữu ích khi khám phá tài liệu Flutter bằng cách đơn giản ( Android Studio ) đặt con trỏ trên một đoạn mã ( lớp Flutter , phương thức, v.v.) và nhấn ctrl + B để hiển thị tài liệu cho đoạn cụ thể đó.

Vấn đề cụ thể mà bạn đang gặp phải được đề cập trong tài liệu về BuildContext , nơi có thể đọc được

Mỗi tiện ích có BuildContext riêng , trở thành cha mẹ của tiện ích được trả về bởi hàm [...]. Build.

Vì vậy, điều này có nghĩa là trong trường hợp bối cảnh của chúng tôi sẽ là cha mẹ của tiện ích Giàn giáo của chúng tôi khi nó được tạo (!). Hơn nữa, docu cho Scaffold.of nói rằng nó trả về

Trạng thái từ thể hiện [ Giàn giáo ] gần nhất của lớp này bao quanh bối cảnh đã cho.

Nhưng trong trường hợp của chúng tôi, bối cảnh không bao gồm (chưa) một Giàn giáo (nó chưa được xây dựng). Có nơi Builder bắt đầu hoạt động!

Một lần nữa, docu chiếu sáng chúng ta. Ở đó chúng ta có thể đọc

[Lớp Builder, chỉ đơn giản là] Một widget plonic gọi một bao đóng để lấy widget con của nó.

Này, đợi một lát, cái gì!? Ok, tôi thừa nhận: điều đó không giúp được gì nhiều ... Nhưng nó đủ để nói (theo một chủ đề SO khác ) rằng

Mục đích của lớp Builder chỉ đơn giản là xây dựng và trả về các widget con.

Vì vậy, bây giờ tất cả trở nên rõ ràng! Bằng cách gọi Builder bên đài chúng tôi đang xây dựng các đài để có thể có được bối cảnh của riêng mình, và trang bị mà innerContext chúng tôi cuối cùng có thể gọi Scaffold.of (innerContext)

Một phiên bản chú thích của mã ở trên như sau

@override
Widget build(BuildContext context) {
  // here, Scaffold.of(context) returns null
  return Scaffold(
    appBar: AppBar(title: Text('Demo')),
    body: Builder(
      builder: (BuildContext innerContext) {
        return FlatButton(
          child: Text('BUTTON'),
          onPressed: () {
            // here, Scaffold.of(innerContext) returns the locally created Scaffold
            Scaffold.of(innerContext).showSnackBar(SnackBar(
              content: Text('Hello.')
            ));
          }
        );
      }
    )
  );
}

1

Tôi sẽ không bận tâm khi sử dụng thanh snack mặc định, bởi vì bạn có thể nhập gói thanh cuộn, cho phép khả năng tùy biến cao hơn:

https://pub.dev/packages/flushbar

Ví dụ:

Flushbar(
                  title:  "Hey Ninja",
                  message:  "Lorem Ipsum is simply dummy text of the printing and typesetting industry",
                  duration:  Duration(seconds: 3),              
                )..show(context);

0

Một giải pháp hiệu quả hơn là chia chức năng xây dựng của bạn thành nhiều widget. Điều này giới thiệu một 'bối cảnh mới', từ đó bạn có thể có được Giàn giáo

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Scaffold.of example.')),
        body: MyScaffoldBody(),
      ),
    );
  }
}

class MyScaffoldBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: RaisedButton(
          child: Text('Show a snackBar'),
          onPressed: () {
            Scaffold.of(context).showSnackBar(
              SnackBar(
                content: Text('Have a Snack'),
              ),
            );
          }),
    );
  }
}

0

Trích xuất widget nút của bạn sẽ hiển thị thanh snack.

class UsellesslyNestedButton extends StatelessWidget {
  const UsellesslyNestedButton({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: (){
        showDefaultSnackbar(context);
                    },
                    color: Colors.blue,
                    child: Text('Show about'),
                  );
  }
}

Làm thế nào để cách tiếp cận của bạn so với các câu trả lời hiện có? Có một lý do để chọn phương pháp này hơn, nói, câu trả lời được chấp nhận?
Jeremy Caney

0

ở đây chúng tôi sử dụng một trình xây dựng để bọc trong một widget khác nơi chúng tôi cần snackbar

Builder(builder: (context) => GestureDetector(
    onTap: () {
        Scaffold.of(context).showSnackBar(SnackBar(
            content: Text('Your Services have been successfully created Snackbar'),
        ));
        
    },
    child: Container(...)))
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.