簡體   English   中英

flutter:未處理的異常:錯誤 state:調用關閉后無法添加新事件

[英]flutter: Unhandled Exception: Bad state: Cannot add new events after calling close

我正在嘗試使用 bloc 模式來管理來自 API 的數據並將它們顯示在我的小部件中。 我能夠從 API 獲取數據並對其進行處理並顯示它,但我使用的是底部導航欄,當我將選項卡和 go 更改為上一個選項卡時,它返回此錯誤:

未處理的異常:錯誤 state:調用關閉后無法添加新事件。

我知道這是因為我正在關閉 stream 然后嘗試添加它,但我不知道如何修復它,因為不處理發布主題將導致publishsubject memory leak 這是我的用戶界面代碼:

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  @override
  void initState() {
    serviceBloc.getAllServices();
    super.initState();
  }

  @override
  void dispose() {
    serviceBloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: serviceBloc.allServices,
      builder: (context, AsyncSnapshot<ServiceModel> snapshot) {
        if (snapshot.hasData) {
          return _homeBody(context, snapshot);
        }
        if (snapshot.hasError) {
          return Center(
            child: Text('Failed to load data'),
          );
        }
        return CircularProgressIndicator();
      },
    );
  }
}

_homeBody(BuildContext context, AsyncSnapshot<ServiceModel> snapshot) {
  return Stack(
      Padding(
          padding: EdgeInsets.only(top: screenAwareSize(400, context)),
          child: _buildCategories(context, snapshot))
    ],
  );
}

_buildCategories(BuildContext context, AsyncSnapshot<ServiceModel> snapshot) {
  return Padding(
    padding: EdgeInsets.symmetric(vertical: 20),
    child: GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3, crossAxisSpacing: 3.0),
      itemCount: snapshot.data.result.length,
      itemBuilder: (BuildContext context, int index) {
        return InkWell(
          child: CategoryWidget(
            title: snapshot.data.result[index].name,
            icon: Icons.phone_iphone,
          ),
          onTap: () {},
        );
      },
    ),
  );
}

這是我的集團代碼:

class ServiceBloc extends MainBloc {
  final _repo = new Repo();
  final PublishSubject<ServiceModel> _serviceController =
      new PublishSubject<ServiceModel>();
  Observable<ServiceModel> get allServices => _serviceController.stream;
  getAllServices() async {
    appIsLoading();
    ServiceModel movieItem = await _repo.getAllServices();
    _serviceController.sink.add(movieItem);
    appIsNotLoading();
  }

  void dispose() {
    _serviceController.close();
  }
}

ServiceBloc serviceBloc = new ServiceBloc();

我沒有包含 repo 和 API 代碼,因為它不在此錯誤的主題中。

使用StreamController.isClosed檢查控制器是否關閉,如果未關閉,則向其添加數據。

if (!_controller.isClosed) 
  _controller.sink.add(...); // safe to add data as _controller isn't closed yet

從文檔:

流控制器是否關閉以添加更多事件。

控制器通過調用 close 方法關閉。 無法通過調用 add 或 addError 將新事件添加到關閉的控制器。

如果控制器關閉了,“done”事件可能還沒有傳遞,但是已經被調度了,再添加更多的事件已經來不及了。

如果錯誤實際上是由您發布的代碼引起的,我只需添加一個檢查以確保在調用dispose()后沒有添加新事件。

class ServiceBloc extends MainBloc {
  final _repo = new Repo();
  final PublishSubject<ServiceModel> _serviceController =
      new PublishSubject<ServiceModel>();
  Observable<ServiceModel> get allServices => _serviceController.stream;
  getAllServices() async {
    // do nothing if already disposed
    if(_isDisposed) {
      return;
    }
    appIsLoading();
    ServiceModel movieItem = await _repo.getAllServices();
    _serviceController.sink.add(movieItem);
    appIsNotLoading();
  }

  bool _isDisposed = false;
  void dispose() {
    _serviceController.close();
    _isDisposed = true;
  }
}

ServiceBloc serviceBloc = new ServiceBloc();

我遇到了同樣的錯誤,並注意到如果您檢查 isClosed,則屏幕不會更新。 在您的代碼中,您必須從 Bloc 文件中刪除最后一行:

ServiceBloc serviceBloc = new ServiceBloc();

並將這一行放在CategoryPage 中initState() 之前。 這樣,您的小部件正在創建和處置該集團。 以前,小部件只處理 bloc,但在重新創建小部件時永遠不會重新創建它。

除了提供的解決方案之外,我認為您還應該通過以下方式排出 ServiceBloc 中使用的流allServices

@override
void dispose() {
      ...
      allServices?.drain(); 
}

@cwhisperer 是絕對正確的。 如下所示,在小部件中初始化和處理您的塊。

final ServiceBloc serviceBloc = new ServiceBloc();

  @override
  void initState() {
    serviceBloc.getAllServices();
    super.initState();
  }

  @override
  void dispose() {
    serviceBloc.dispose();
    super.dispose();
  }

並刪除ServiceBloc serviceBloc = new ServiceBloc(); 從你的class ServiceBloc

ServiceBloc serviceBloc = new ServiceBloc();

// 刪除此代碼 // 不要在同一頁面中初始化會導致錯誤狀態的類。

我在生產中也遇到了這個問題,我意識到我們應該在釋放 Widget 時釋放 BehaviorSubject(或任何其他 StreamController),或者在添加新值之前檢查 Stream 是否關閉。

這是一個很好的擴展來完成所有工作:

extension BehaviorSubjectExtensions <T> on BehaviorSubject<T> {
  set safeValue(T newValue) => isClosed == false ? add(newValue) : () {};
}

你可以像這樣使用它:

class MyBloc {
  final _data = BehaviorSubject<String>();
  
  void fetchData() {
    // get your data from wherever it is located
    _data.safeValue = 'Safe to add data';
  }

  void dispose() {
    _data.close();
  }
}

如何在 Widget 中配置:

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  late MyBloc bloc;

  @override
  void initState() {
    bloc = MyBloc();
    bloc.fetchData();
    super.initState();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  // Other part of your Widget
}

更好的是,如果您不確定在處理后不會重用流:

在關閉流之前調用流上的 drain() 函數。

dispose() async{
await _coinDataFetcher.drain();
_coinDataFetcher.close();
_isdisposed = true;

}

使用 flutter_bloc 時不必擔心內存泄漏,因為使用 bloc 時,如果您已使用 bloc 提供程序注入 bloc,則無需手動關閉 bloc。 如 flutter_bloc 文檔中所述,Bloc Providers 會為您開箱即用地處理這些問題。

BlocProvider 負責創建 bloc,它會自動處理關閉 bloc

您可以在您的應用程序中對此進行測試。 嘗試在 bloc 的close()覆蓋上打印。

如果從導航堆棧中刪除了提供該塊的屏幕,則該給定塊的close()方法將被立即調用。

檢查 bloc/cubit 是否由isClosed變量關閉。 將此 if 條件包裝到那些拋出異常的狀態。

示例代碼

class LandingCubit extends Cubit<LandingState> {
  LandingCubit(this.repository) : super(LandingInitial());
  final CoreRepository repository;

  // Fetches image urls that needs to shown in landing page
  void getLandingImages() async {
    emit(LandingImagesLoading());
    try {
      List<File> landingImages = await repository.landingImages();
      if (!isClosed) {
        emit(LandingImagesSuccess(landingImages));
      }
    } catch (e) {
      if (!isClosed) {
        emit(LandingImagesFetchError(e.toString()));
      }
    }
  }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM