Flutter安全状态管理插件safe_bloc的使用

发布于 1周前 作者 bupafengyu 来自 Flutter

Flutter安全状态管理插件safe_bloc的使用

概述

safe_bloc 是对 bloc 状态管理库的扩展,用于处理代码中的意外异常,并将这些异常以可自定义的用户友好错误消息的形式展示给用户。该库通过 UnexpectErrorHandler 小部件来统一处理异常,并且可以根据需要自定义错误对话框和屏幕,以匹配应用程序的设计。此外,异常处理过程是可定制的,可以显示、记录或忽略异常。safe_bloc 还提供了 onUnexpectedError 回调函数,每次发生异常时都会被调用,特别适合用于异常日志记录。

该库区分了两种类型的错误:

  1. 错误状态:仅在初始屏幕加载时发生。如果屏幕加载失败,没有数据可以显示给用户,UnexpectErrorHandler 会显示一个由 errorScreen 参数表示的错误屏幕。
  2. 错误动作:通常发生在用户触发某个操作(例如点击按钮)时。此时我们不希望显示错误屏幕并清除已加载的数据,而是显示一个错误对话框,告知用户当前操作不可用,确保屏幕的现有数据仍然可用。

使用方法

1. 创建 UnexpectedError 状态

首先,创建一个在发生异常时发出的状态,该状态必须实现 UnexpectedErrorBase 接口。

sealed class MyAppState {}

final class MyAppErrorState extends MyAppState implements UnexpectedErrorBase {
  [@override](/user/override)
  final UnexpectedError error;

  MyAppErrorState(this.error);
}
2. 使用 SafeBlocSafeCubit

根据你使用的 BlocCubit,分别继承 SafeBlocSafeCubit 类,并重写 errorState getter。

Bloc

如果你使用的是 Bloc,继承 SafeBloc 并重写 errorState getter:

class MyAppBloc extends SafeBloc<MyAppEvent, MyAppState> {
  // body

  [@override](/user/override)
  MyAppState Function(UnexpectedError error) get errorState => MyAppErrorState.new;
}

然后,在注册事件处理器时,使用 onSafe<EVENT> 代替标准的 on<EVENT>

onSafe<MyBlocEvent>(event, emit, {required trackingId}) async {
  // do something
}
Cubit

如果你使用的是 Cubit,继承 SafeCubit 并重写 errorState getter。然后将所有公共的 Cubit 方法包装在 safeCall 中:

class MyAppCubit extends SafeCubit<MyAppState> {
  MyAppCubit(super.initialState);

  FutureOr<void> someMethod() => safeCall((trackingId) {
    // do something
  });

  [@override](/user/override)
  MyAppState Function(UnexpectedError error) get errorState => MyAppErrorState.new;
}

对于同步方法,可以使用 safeCallSync

void someSyncMethod() => safeCallSync((trackingId) {
  // do something
});

每次发生异常时,父类会捕获异常并发出 MyAppErrorState,该状态包含一个 UnexpectedError 对象,其中包含有关异常的详细信息。

onSafesafeCall 提供了一个唯一的 trackingId,用于跟踪用户操作。它们还提供了一些额外的参数:

  • devErrorMessage(可选):传递给 UnexpectedError 对象的字符串消息,可用于日志记录。
  • isAction:布尔值,指示该方法是错误动作还是错误状态。如果设置为 trueUnexpectedErrorHandler 会显示错误对话框或调用 onErrorAction 回调。默认为 false,即显示由 errorScreen 参数指定的错误屏幕。
  • ignoreError:布尔值,指示是否忽略异常。如果设置为 true,异常会被捕获,但不会发出 MyAppErrorState
  • onIgnoreError(可选):如果发生异常且 ignoreError 设置为 true,则调用此回调。
  • errorMapper(可选):将个别异常映射到 BlocCubit 状态的函数。如果为 null,所有异常都将映射到 MyAppErrorState

此外,SafeBlocSafeCubit 还允许重写 onUnexpectedError 方法,该方法在每次抛出异常时被调用,可用于异常日志记录。

3. 在 UI 中展示错误

使用 UnexpectedErrorHandler 小部件来显示错误:

UnexpectedErrorHandler<MyAppBloc, MyAppState>(
  errorScreen: (context, error) => Text(error.toString()),
  onErrorAction: (context, error) {
    return showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(title: Text(error.toString()));
      },
    );
  },
  child: MyWidget(),
)

UnexpectedErrorHandler 提供了 errorScreen 参数用于在初始屏幕加载时显示错误,以及 onErrorAction 参数用于处理错误动作。如果 onSafesafeCallisAction 参数设置为 true,则会调用 onErrorAction 回调。

4. 使用呈现事件

safe_bloc 使用 bloc_presentation 库来处理用户错误动作事件。bloc_presentationBlocCubit 添加了一个流,用于向用户呈现一次性事件(例如对话框或 Snackbar)。safe_bloc 使用 BaseEffect 作为呈现事件,并继承 UnexpectedErrorEffect 类用于异常处理。如果你有特定的呈现事件,可以在 SafeBlocWithPresentationSafeCubitWithPresentation 中实现自己的呈现事件。

class MyAppCubit extends SafeCubitWithPresentation<MyAppState, MyAppPresentationEvent> {
  MyAppCubit(super.initialState);

  FutureOr<void> someMethod() => safeCall((trackingId) {
    // do something
  });

  [@override](/user/override)
  MyAppState Function(UnexpectedError error) get errorState => MyAppErrorState.new;

  [@override](/user/override)
  MyAppPresentationEvent Function(UnexpectedError error) get errorEffect => MyAppErrorPresentationEvent.new;
}

完整示例 Demo

以下是一个完整的示例,展示了如何在计数器应用中使用 safe_bloc

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:safe_bloc/safe_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(const MainApp());
}

/// Flutter Counter app that stores the count to shared preferences and loads it back at the app start.
class MainApp extends StatelessWidget {
  const MainApp({super.key});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: BlocProvider<CounterBloc>(
          create: (_) => CounterBloc()..add(LoadCounter()),
          child: const CounterView(),
        ),
      ),
    );
  }
}

class CounterView extends StatelessWidget {
  const CounterView({super.key});

  [@override](/user/override)
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: UnexpectedErrorHandler<CounterBloc, CounterState>(
          child: BlocBuilder<CounterBloc, CounterState>(
            builder: (context, state) {
              if (state is! CounterLoaded) return const CircularProgressIndicator();

              return Text(state.count.toString(), style: textTheme.displayMedium);
            },
          ),
        ),
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            child: const Icon(Icons.add),
            onPressed: () => context.read<CounterBloc>().add(IncrementPressed()),
          ),
        ],
      ),
    );
  }
}

sealed class CounterEvent {}

final class IncrementPressed extends CounterEvent {}

final class LoadCounter extends CounterEvent {}

sealed class CounterState {}

final class CounterLoading extends CounterState {}

final class CounterLoaded extends CounterState {
  final int count;

  CounterLoaded({required this.count});
}

final class CounterBlocError extends CounterState implements UnexpectedErrorBase {
  [@override](/user/override)
  final UnexpectedError error;

  CounterBlocError(this.error);
}

class CounterBloc extends SafeBloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterLoading()) {
    // `isAction` parameter is set to false for initial data loading
    onSafe<LoadCounter>(_loadCounter, isAction: false);

    // `isAction` parameter is set to true for user action
    onSafe<IncrementPressed>(_increment, isAction: true);
  }

  Future<void> _increment(
    IncrementPressed event,
    SafeEmitter<CounterState> emit, {
    required String trackingId,
  }) async {
    if (state case final CounterLoaded loaded) {
      final incrementedCount = loaded.count + 1;

      final preferences = await SharedPreferences.getInstance();
      await preferences.setInt('count_key', incrementedCount);

      // Uncomment the exception throwing to test the error dialog
      // throw Exception("Something wrong happened");

      emit(CounterLoaded(count: incrementedCount));
    }
  }

  Future<void> _loadCounter(LoadCounter event, SafeEmitter<CounterState> emit, {required String trackingId}) async {
    final preferences = await SharedPreferences.getInstance();
    final count = preferences.getInt('count_key');

    // Uncomment the exception throwing to test the error screen
    // throw Exception("Something wrong happened when counter loading");

    emit(CounterLoaded(count: count ?? 0));
  }

  [@override](/user/override)
  CounterState Function(UnexpectedError error) get errorState => CounterBlocError.new;

  [@override](/user/override)
  Future<void> onUnexpectedError(Object? error, StackTrace stackTrace, String trackingId) async {
    if (kDebugMode) {
      print('Exception: $error, $stackTrace');
    }
  }
}

更多关于Flutter安全状态管理插件safe_bloc的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter安全状态管理插件safe_bloc的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter项目中使用safe_bloc进行安全状态管理的代码示例。safe_bloc是一个增强版的Bloc库,旨在提供更安全、更简洁的状态管理解决方案。

首先,确保你已经在pubspec.yaml文件中添加了safe_bloc依赖:

dependencies:
  flutter:
    sdk: flutter
  safe_bloc: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

接下来,让我们创建一个简单的示例,展示如何使用safe_bloc来管理计数器应用的状态。

1. 创建CounterEvent类

首先,定义事件类,这些事件将触发Bloc中的状态更改。

// counter_event.dart
import 'package:equatable/equatable.dart';

abstract class CounterEvent extends Equatable {
  const CounterEvent();

  @override
  List<Object> get props => [];
}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

2. 创建CounterState类

接下来,定义状态类,这些状态将表示Bloc的当前状态。

// counter_state.dart
import 'package:equatable/equatable.dart';

class CounterState extends Equatable {
  final int count;

  const CounterState({required this.count});

  @override
  List<Object> get props => [count];
}

3. 创建CounterBloc类

然后,定义Bloc类,它将处理事件并发出新的状态。

// counter_bloc.dart
import 'package:bloc/bloc.dart';
import 'package:safe_bloc/safe_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> with SafeBloc {
  CounterBloc() : super(CounterState(count: 0)) {
    on<IncrementEvent>((event, emit) {
      emit(state.copyWith(count: state.count + 1));
    });

    on<DecrementEvent>((event, emit) {
      emit(state.copyWith(count: state.count - 1));
    });
  }
}

4. 使用CounterBloc在UI中

最后,在Flutter的UI组件中使用CounterBloc

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_bloc.dart';
import 'counter_event.dart';

void main() {
  runApp(BlocProvider(
    create: (_) => CounterBloc(),
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Safe Bloc Counter')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              BlocBuilder<CounterBloc, CounterState>(
                builder: (context, state) {
                  return Text(
                    '${state.count}',
                    style: Theme.of(context).textTheme.headline4,
                  );
                },
              ),
            ],
          ),
        ),
        floatingActionButton: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: <Widget>[
            FloatingActionButton(
              onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
              tooltip: 'Increment',
              child: Icon(Icons.add),
            ),
            SizedBox(height: 10),
            FloatingActionButton(
              onPressed: () => context.read<CounterBloc>().add(DecrementEvent()),
              tooltip: 'Decrement',
              child: Icon(Icons.remove),
            ),
          ],
        ),
      ),
    );
  }
}

在这个示例中,我们创建了一个简单的计数器应用,使用safe_bloc进行状态管理。我们定义了事件和状态类,创建了一个处理这些事件的Bloc类,并在UI组件中使用了这个Bloc来显示和更新计数器的值。

这个示例展示了safe_bloc的基本用法,你可以根据需要扩展和修改这个示例以适应更复杂的应用场景。

回到顶部