Flutter如何使用mvi? bloc结合自定义http库的实现


前言

提示:本篇并不算严谨的科普文章,仅仅只是记录使用bloc的思路
最近对kotlin的mvi使用比较娴熟,但是关于flutter架构相关的比较少,之前也有看过provider这些框架总觉得没那么好使而且还挺麻烦的,现在也有大佬研究getx的mvvm,这里我就不展开了,我的本意是想使用getx作为路由管理框架,将它的状态管理使用bloc替代,别问为什么这样考虑,getx虽然提供了很强大的状态管理,但是总有些缺点,具体的没有去深入研究真假暂不确定,可能企业级使用bloc会多一点,新版本dart提供了一些新功能,怎么说呢依旧感觉没有kotlin好使,本篇文章的目的是为了记录bloc使用的示例,可能会比较依赖multiple_result这个库,但是使用Result返回参数这个概念感觉还不错,dart提供了类似模式匹配的简化版,搭配Result还是挺不错的,本篇文章和Android mvi 三这篇文章思路是一致的,可惜dart对sealed class的支持比较薄弱,可以当作是换了关键字的抽象类,下面就是最终的简单效果,点击按钮请求网络出现加载动画,拿到数据后显示数据,效果图如下:

在这里插入图片描述


一、先看看如何使用bloc吧

1. 定义页面需要的数据

代码如下:

import '../../lib_base/index.dart';
import '../../models/banner_model.dart';

class MyFromState {
  MyFromState({required this.banner});

/// 可以不写这段代码,将这段代码放在bloc类里面写是可以的
  factory MyFromState.init() {
    return MyFromState(
      banner: InitState()
    );
  }

  final BannerState banner;

  MyFromState copyWith({
    BannerState? banner,
  }) {
    return MyFromState(
      banner: banner ?? this.banner,
    );
  }
}

/// 定义ui状态
sealed class BannerState {}
/// 用于初始化状态实例
class InitState extends BannerState {}
/// 成功返回 - 有数据
class OnSuccess extends BannerState {
  final List<BannerModel> body;
  OnSuccess(this.body);
}
/// 成功返回 - 无数据
class OnNoData extends BannerState {}
/// 加载状态
class OnLoading extends BannerState {
  final LoadingEffect loading;
  OnLoading(this.loading);
}

2. 定义通用加载状态

代码如下:

/// 通用响应事件
sealed class LoadingEffect{}

/// 开启或关闭加载动画, true表示开启加载动画,false表示请求已经结束关闭加载动画
final class Loading extends LoadingEffect {
  final bool isShow;
  Loading(this.isShow);
}

/// 用于处理401需要鉴权的情况
final class OnAuthority extends LoadingEffect {}

3. 定义事件

代码如下:

sealed class MyFromEvent {}

/// 需要发送的事件
class BannerEvent extends MyFromEvent {}

4. 定义bloc

代码如下:

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:multiple_result/multiple_result.dart';

import '../../lib_base/index.dart';
import '../../lib_http/index.dart';
import '../repository/banner_repository.dart';
import 'event.dart';
import 'state.dart';

class MyFromBloc extends Bloc<MyFromEvent, MyFromState> {
  // 负责数据请求
  final BannerRepository _repository = BannerRepository();

  // 这里和mvi的思路一致,初始化state,并绑定对应事件
  MyFromBloc() : super(MyFromState.init()) {
    // 绑定事件,当使用add时就会执行对应事件
    on<BannerEvent>(_bannerEvent);
  }

  /// 对应事件的处理
  void _bannerEvent(BannerEvent event, Emitter<MyFromState> emit) async {
    // http请求
    final response = await _repository.getBanner(
        onLoading: (isShow) =>
            // 发送加载状态
            emit(state.copyWith(banner: OnLoading(Loading(isShow)))));
     // 判断当前请求结果
    switch (response) {
      case Success():
        // 返回加载成功的数据
        emit(state.copyWith(banner: OnSuccess(response.success)));
        break;
      case Error():
        final error = response.error;
        if (error case NoData()) {
          // 返回无数据
          emit(state.copyWith(banner: OnNoData()));
        }
        if (error case RequestFailure()) {
          // 在下面使用中有对这个函数的定义,这个函数对应于具体的业务项目的业务逻辑处理
          requestFailureError(error.code, error.msg,
              () => emit(state.copyWith(banner: OnLoading(OnAuthority()))));
        }
        break;
    }
  }
}

5. 定义UI

代码如下:

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

import '../../lib_base/index.dart';
import '../../models/index.dart';
import 'bloc.dart';
import 'event.dart';
import 'state.dart';

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

  
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => MyFromBloc(),
      child: const MyFromView(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body:  BlocBuilder<MyFromBloc, MyFromState>(
        builder: body,
      ),
        floatingActionButton: FloatingActionButton(
          onPressed: _requestBanner(context.read<MyFromBloc>()),
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        )
    );
  }

  Widget body(BuildContext context, MyFromState state) {
    // 拿对应状态
    final banner = state.banner;
    // 如果当前状态是OnLoading则进入判断
    if (banner case OnLoading()) {
      final loading = banner.loading;
      switch(loading) {
        case Loading():
          if (loading.isShow) {
            return _buildLoadingView();
          }
          break;
        case OnAuthority():
          // 跳转登录页
          break;
      }
    }
    if (banner case OnSuccess()) {
      // 成功后显示的布局
      return _listView(banner.body);
    }

    // 这里只会在没有数据的时候触发,如果请求在页面初始化后就已经发起了,这个布局是一个无用布局
    return const Center();
  }

  /// 加载中,加载动画可自行替换
  Widget _buildLoadingView() {
    return const SizedBox(
      width: double.maxFinite,
      height: double.maxFinite,
      child: Center(
        child: SizedBox(
          height: 22,
          width: 22,
          child: CircularProgressIndicator(
            strokeWidth: 2,
            // valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryBgBlue),
          ),
        ),
      ),
    );
  }

  Widget _listView(List<BannerModel> dataArray) {
    return ListView.builder(
      itemCount: dataArray.length,
        itemBuilder: (context, index) {
          final data = dataArray[index];
          return Column(
            children: [
              Image.network(data.imagePath),
              Text(data.title, style: const TextStyle(fontSize: 30)),
              Text(data.desc, style: const TextStyle(fontSize: 20)),
            ],
          );
        }
    );
  }

  /// 发起网络请求
  void Function() _requestBanner(MyFromBloc bloc) => () {
    bloc.add(BannerEvent());
  };
}

6. 使用

都已经这么详细了就不贴代码了。

二、lib_http

使用适配器模式的http上层封装

1. request定义

代码如下:

import 'rock_net_adapter.dart';

/// http 请求方式
enum HttpMethod { get, post, put, delete, patch }

/// 请求参数配置
abstract class RockRequest {
  /// 请求路径
  final String url;

  /// 规范子类, 必须要传递的参数
  RockRequest(this.url, [this._method = HttpMethod.get]);

  /// 请求路径
  HttpMethod _method;
  HttpMethod get method => _method;

  //region 请求头参数
  /// 请求头参数
  final Map<String, String> _headers = {};

  /// 提供可访问的 header对象, 注意该对象只能用于访问修改该对象无法影响到实际关联对象的修改
  Map<String, String> get headers => {}..addAll(_headers);

  //endregion

  //region 请求参数
  /// 请求参数
  final Map<String, dynamic> _params = {};

  /// 提供参数的访问
  Map<String, dynamic> get params => {}..addAll(_params);

  //endregion

  //region query参数
  // url参数
  final Map<String, String> _queryParams = {};

  /// 提供参数的访问
  Map<String, String> get queryParams => {}..addAll(_queryParams);

  //endregion

  /// 单独设置适配器
  IRockNetAdapter? _adapter;

  IRockNetAdapter? get adapter => _adapter;

  /// 添加请求头
  RockRequest addHeader(String key, String value);

  /// 添加url query参数
  RockRequest addQuery(String key, dynamic value);

  /// 添加请求参数
  RockRequest addParam(String key, dynamic value);

  /// 指定适配器
  void setAdapter(IRockNetAdapter adapter);

  /// 创建一个新对象
  RockRequest setMethod(HttpMethod method);
}

/// 具体实现
class RockRequestBuilder extends RockRequest {
  RockRequestBuilder(super.url);

  
  RockRequest addHeader(String key, String value) {
    _headers[key] = value;
    return this;
  }

  
  RockRequest addParam(String key, value) {
    _params[key] = value;
    return this;
  }

  
  RockRequest addQuery(String key, value) {
    _queryParams[key] = value;
    return this;
  }

  
  void setAdapter(IRockNetAdapter adapter) {
    _adapter = adapter;
  }

  
  RockRequest setMethod(HttpMethod method) {
    _method = method;
    return this;
  }
}

2. response定义

代码如下:


/// 用于封装请求相关参数与处理
final class RockResponse<T> {
  /// 请求状态码
  final int statusCode;
  /// 返回数据
  final T? data;
  /// 异常或消息
  final String? message;
  /// 任意数据
  dynamic extra;

  RockResponse(this.statusCode, {this.data, this.message, this.extra});

  RockResponse copyWith({
    int? statusCode,
    T? data,
    String? message,
    dynamic extra,
  }) {
    return RockResponse(
      statusCode ?? this.statusCode,
      data: data ?? this.data,
      message: message ?? this.message,
      extra: extra ?? this.extra,
    );
  }
}

3. 适配器接口

代码如下:

import 'package:multiple_result/multiple_result.dart';

import 'rock_adapter_engine.dart';
import 'rock_error.dart';
import 'rock_response.dart';

/// 适配器接口
abstract class IRockNetAdapter {
  Future<Result<RockResponse<T>, RockNetException>> send<T>(RockAdapterEngine config);
}

4. 构建adapter需要的数据

代码如下:

import 'rock_request.dart';

/// 构建adapter需要的数据
final class RockAdapterEngine {
  /// base url
  final String _baseUrl;

  /// 请求数据
  final RockRequest request;

  RockAdapterEngine(this._baseUrl, this.request);

  /// 生成url
  String url() {
    // http 和 https的切换
    final (isHttp, authority) = _authority(_baseUrl);
    final uri = isHttp
        ? Uri.https(authority, request.url,
            request.queryParams.isNotEmpty ? request.queryParams : null)
        : Uri.http(authority, request.url,
            request.queryParams.isNotEmpty ? request.queryParams : null);

    return uri.toString();
  }

  /// 获取域名
  (bool, String) _authority(String url) {
    var urlArray = url.split('//');
    if (urlArray.length <= 1) {
      return (false, '');
    }
    if (url.startsWith('https')) {
      return (true, urlArray[1]);
    } else {
      return (false, urlArray[1]);
    }
  }
}

5. 网络异常统一封装

代码如下:

/// 网络异常统一封装
sealed class RockNetException implements Exception {
  final String _message;
  final int _code;

  RockNetException(this._code, this._message);

  /// 请求状态
  int get code => _code;

  /// 获取异常信息
  String get message => _message;
}

/// 通用错误处理
class RockNetError extends RockNetException {
  RockNetError(super.code, super.message);
}

/// 需要登录异常
class RockNeedLogin extends RockNetError {
  RockNeedLogin() : super(401, '请先登录');
}

/// 无权限访问异常
class RockNeedAuth extends RockNetError {
  RockNeedAuth() : super(403, '无权限访问');
}

/// 404
class NotFoundError extends RockNetError {
  NotFoundError() : super(404, '请求路径不正确');
}

/// 500
class InternalServerError extends RockNetError {
  InternalServerError() : super(500, '服务器内部错误');
}

/// 连接异常
class RockNoNetwork extends RockNetError {
  RockNoNetwork() : super(-1, '当前网络连接异常,请检查网络配置');
}

6. 核心请求类

代码如下:

import 'package:multiple_result/multiple_result.dart';
import '../../lib_utils/log_util.dart';
import 'rock_adapter_engine.dart';
import 'rock_error.dart';
import 'rock_net_adapter.dart';
import 'rock_request.dart';

/// json 类型的数据
typedef JSONData = dynamic;

/// 发起请求
final class RockNet {
  /// 发送请求, dynamic表示为json类型
  Future<Result<JSONData, RockNetException>> send<T>(
      String baseUrl,
      RockRequest request,
      IRockNetAdapter adapter,
      void Function(RockRequest)? block) async {
    // 创建request
    final adapterRequest = RockAdapterEngine(baseUrl, request);
    // 注: 使用时必须要指定adapter
    final newAdapter = request.adapter ?? adapter;
    // 使用拦截器
    block?.call(request);
    // 打印请求参数
    _printRequest(adapterRequest);

    // 开始请求
    final result = await newAdapter.send(adapterRequest);
    switch (result) {
      case Success():
        _log('http send result = ${result.success.data}');
        return Success(result.success.data);
      case Error():
        _log("${result.error}");
        return Error(result.error);
    }
  }

  /// 打印请求数据
  void _printRequest(RockAdapterEngine engine) {
    _log('url = ${engine.url()} ${engine.request.method}');
    _log('headers = ${engine.request.headers}');
    _log('params = ${engine.request.params}');
  }

  /// 打印函数
  void _log(dynamic msg) {
    LogUtil.debug(msg);
  }
}

7. 提供网络访问配置

代码如下:

import 'package:multiple_result/multiple_result.dart';

import '../core/rock_error.dart';
import '../core/rock_net.dart';
import '../core/rock_net_adapter.dart';
import '../core/rock_request.dart';

/// 提供网络访问配置
final class RockNetUtil {
  RockNetUtil._();

  static RockNetUtil get instance => _getInstance();
  static RockNetUtil? _instance;

  static RockNetUtil _getInstance() {
    _instance ??= RockNetUtil._();
    return _instance!;
  }

  /// 网络请求 url
  String? _baseUrl;

  /// 适配器, 一定要指定
  IRockNetAdapter? _adapter;

  /// 拦截器, 用于自定义配置
  void Function(RockRequest request)? _interceptor;

  /// 设置 base url
  RockNetUtil setBaseUrl(String url) {
    _baseUrl = url;
    return this;
  }

  /// 设置适配器
  RockNetUtil setAdapter(IRockNetAdapter adapter) {
    _adapter = adapter;
    return this;
  }

  /// 添加拦截器
  void setInterceptor(void Function(RockRequest request) block) {
    _interceptor = block;
  }

  /// 请求统一封装
  Future<Result<JSONData, RockNetException>> _send<T>(RockRequest request, void Function() callback) async {
    final rockNet = RockNet();
    // 回调设置不同属性
    callback();
    return await rockNet.send(_baseUrl!, request, _adapter!, _interceptor);
  }

  /// 发起get请求
  Future<Result<JSONData, RockNetException>> get<T>(RockRequest request) async {
    return _send<T>(request, () {
      request.setMethod(HttpMethod.get);
    });
  }

  /// post请求
  Future<Result<JSONData, RockNetException>> post<T>(RockRequest request) async {
    return _send<T>(request, () {
      request.setMethod(HttpMethod.post);
    });
  }

  /// put请求
  Future<Result<JSONData, RockNetException>> put<T>(RockRequest request) async {
    return _send<T>(request, () {
      request.setMethod(HttpMethod.put);
    });
  }

  /// delete 请求
  Future<Result<JSONData, RockNetException>> delete<T>(RockRequest request) async {
    return _send<T>(request, () {
      request.setMethod(HttpMethod.delete);
    });
  }

  /// patch 请求
  Future<Result<JSONData, RockNetException>> patch<T>(RockRequest request) async {
    return _send<T>(request, () {
      request.setMethod(HttpMethod.patch);
    });
  }
}

8. dio适配器

代码如下:

import 'dart:io';

import 'package:dio/dio.dart';
import 'package:multiple_result/multiple_result.dart';

import '../../lib_utils/log_util.dart';
import '../core/rock_adapter_engine.dart';
import '../core/rock_error.dart';
import '../core/rock_net_adapter.dart';
import '../core/rock_request.dart';
import '../core/rock_response.dart';

/// dio 适配器
class DioAdapter implements IRockNetAdapter {
  
  Future<Result<RockResponse<T>, RockNetException>> send<T>(
      RockAdapterEngine config) async {
    // 提前调用
    var url = config.url();
    // dio 配置
    var options = Options(
      headers: config.request.headers,
      sendTimeout: const Duration(seconds: 60),
    );

    try {
      final response = await _sendHandle(config, url, options);
      return Success(RockResponse(response?.statusCode ?? -1,
          data: response?.data, message: response?.statusMessage));
    } on DioException catch (e) {
      var response = e.response;
      // 输出当前抛出异常的url
      LogUtil.error('url = ${response?.realUri}');

      // 每一种对应错误都需要写出来
      switch (response?.statusCode) {
        case 401:
          return Error(RockNeedLogin());
        case 403:
          return Error(RockNeedAuth());
        case 404:
          return Error(NotFoundError());
        case 500:
          return Error(InternalServerError());
        default:
          if (e.error is SocketException) {
            return Error(RockNoNetwork());
          }

          // 这里的错误一般不是http请求错误不用特殊处理, ui直接弹出提示即可
          return Error(RockNetError(-1, e.toString()));
      }
    }
  }

  /// 用于处理请求的实际实现
  Future<Response?> _sendHandle(
      RockAdapterEngine config, String url, Options options) async {
    switch (config.request.method) {
      case HttpMethod.get:
        return await Dio().get(url, options: options);
      case HttpMethod.post:
        return await Dio()
            .post(config.url(), data: config.request.params, options: options);
      case HttpMethod.put:
        return await Dio()
            .put(config.url(), data: config.request.params, options: options);
      case HttpMethod.delete:
        return await Dio().delete(config.url(),
            data: config.request.params, options: options);
      case HttpMethod.patch:
        return await Dio()
            .patch(config.url(), data: config.request.params, options: options);
    }
  }
}

9. 抽象数据类型

代码如下:

/// 抽象数据接收类
abstract class BaseResult<T> {
  /// 当前请求是否成功
  bool isSuccess();

  /// 实际要返回的数据
  T? getData();

  /// 可以是业务错误, 也可以是http状态码
  int errCode();

  /// 请求成功但返回失败
  String errMsg();
}

10. HttpBaseRepository

下面中的log打印需要替换成弹窗提示,由于当前只是示例需要各位自行引入替换

import 'package:multiple_result/multiple_result.dart';

import '../../lib_utils/log_util.dart';
import '../core/rock_error.dart';
import '../core/rock_net.dart';
import 'base_result.dart';

/// 请求失败
sealed class ResponseStateError {
  final String? _msg;
  final int _code;

  ResponseStateError(this._code, this._msg);

  /// 请求状态
  int get code => _code;

  /// 获取异常信息
  String? get msg => _msg;
}

/// 没有数据返回
class NoData extends ResponseStateError {
  NoData() : super(0, null);
}

/// 忽略改错误,该错误只用于返回
class DefError extends ResponseStateError {
  DefError() : super(0x0, null);
}

/// 请求失败
class RequestFailure extends ResponseStateError {
  RequestFailure(super.code, super.msg);
}

/// Repository 专属返回类型
typedef RepositoryResult<T> = Result<T, ResponseStateError>;

abstract class HttpBaseRepository {
  /// 帮助请求
  /// onLoading - 当前是否加载
  /// request - 具体请求 RockNetUtil.x
  /// onDataConversion - 将请求数据转换成对应类型
  Future<RepositoryResult<T>> baseRequest<T>({
    /// 当前是否加载
    void Function(bool isShow)? onLoading,

    /// 具体请求
    required Future<Result<JSONData, RockNetException>> Function() request,

    /// 数据转换
    required BaseResult<T?> Function(JSONData response) onDataConversion,
  }) async {
    // 开始加载
    onLoading?.call(true);
    // 开始请求并获取结果
    final result = await request();
    // 关闭加载动画
    onLoading?.call(false);

    // 请求成功
    switch (result) {
      case Success():
        final response = result.success;
        final baseModel = onDataConversion(response);
        if (baseModel.isSuccess()) {
          final data = baseModel.getData();
          return data != null ? Success(data) : Error(NoData());
        } else {
          return Error(RequestFailure(baseModel.errCode(), baseModel.errMsg()));
        }
      case Error():
        final err = result.error;
        switch (err) {
          case RockNeedLogin():
            return Error(RequestFailure(err.code, err.message));
          case RockNeedAuth():
            LogUtil.error('RockNeedAuth = ${err.code}: ${err.message}');
            break;
          case NotFoundError():
            LogUtil.error('NotFoundError = ${err.code}: ${err.message}');
            break;
          case InternalServerError():
            LogUtil.error('InternalServerError = ${err.code}: ${err.message}');
            break;
          case RockNoNetwork():
            LogUtil.error('RockNoNetwork = ${err.code}: ${err.message}');
            break;
          case RockNetError():
            LogUtil.error('RockNetError = ${err.code}: ${err.message}');
            break;
        }
        return Error(DefError());
    }
  }
}

11. 使用片段

代码如下:

/// 请求失败处理, 独立个体方便使用, 上面代码也使用到了这个,这里的封装需要根据对应项目业务来展开
/// 这里只提供一个处理的思路
void requestFailureError(int errCode, String? errMsg, void Function()? onAuth) {
  if (errCode == 401 || errCode == -1001) {
    onAuth?.call();
  }
}
final class TestRepository extends HttpBaseRepository {
  /// 获取轮播图
  Future<RepositoryResult<List<BannerModel>>> getBanner({
    // 请求结束,可用于关闭加载动画
    void Function(bool isShow)? onLoading,
  }) async {
    final request = RockRequestBuilder('/banner/json');
    final response = await baseRequest(
        onLoading: onLoading,
        request: () => RockNetUtil.instance.get(request),
        onDataConversion: (jsonData) => BaseModel.fromJson(
            jsonData, (json) => BannerModel.fromJsonArray(json)));
    return response;
	}
}

void main() {
  // 初始化网络配置
  initHttp();
  runApp(const MyApp());
}
void initHttp() {
  RockNetUtil.instance
  		// 设置base url
      .setBaseUrl('https://www.wanandroid.com')
      // .setBaseUrl("http://192.168.190.128:3000")
      // 设置适配器
      .setAdapter(DioAdapter())
      // 设置拦截器
      .setInterceptor((request) {
    request.addHeader('token', 'test');
  });
}

// 发起请求测试
void _incrementCounter() async {
    // TODO 测试 发起请求
    final response = await repository.getBanner(
        onLoading: (isShow) => LogUtil.error('开启加载状态: $isShow'));
    if (response case Success()) {
      LogUtil.error(response.success);
    }
    if (response case Error()) {
      final error = response.error;
      if (error case NoData()) {
        LogUtil.error('body 为空');
      }
      if (error case RequestFailure()) {
        requestFailureError(error.code, error.msg, () => LogUtil.error('需要登录'));
      }
    }
}

总结

// 使用到的库
# 状态管理
flutter_bloc: ^8.1.3
# 网络框架
dio: ^5.1.2
# json序列化注解
json_annotation: ^4.8.0
# 日志
logger: ^1.3.0
# result的一种实现
multiple_result: ^5.0.0

以上就是本篇的全部代码,如果感觉思路不太清晰可以先去了解mvi架构图来对照看,bloc的demo示例比较少,官方demo感觉不太能理解其意,所以将mvi思路照搬一下就解释得通了,http库中使用到了multiple_result作为数据返回核心,这样不需要使用try catch来对异常捕获,很好的解决了数据返回异常的特点,如有其他思路欢迎交流讨论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值