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

文章目录

  • 前言
  • 一、先看看如何使用bloc吧
    • 1. 定义页面需要的数据
    • 2. 定义通用加载状态
    • 3. 定义事件
    • 4. 定义bloc
    • 5. 定义UI
    • 6. 使用
  • 二、lib_http
    • 1. request定义
    • 2. response定义
    • 3. 适配器接口
    • 4. 构建adapter需要的数据
    • 5. 网络异常统一封装
    • 6. 核心请求类
    • 7. 提供网络访问配置
    • 8. dio适配器
    • 9. 抽象数据类型
    • 10. HttpBaseRepository
    • 11. 使用片段
  • 总结


前言

提示:本篇并不算严谨的科普文章,仅仅只是记录使用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});@overrideWidget build(BuildContext context) {return BlocProvider(create: (_) => MyFromBloc(),child: const MyFromView(),);}
}class MyFromView extends StatelessWidget {const MyFromView({super.key});@overrideWidget 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);@overrideRockRequest addHeader(String key, String value) {_headers[key] = value;return this;}@overrideRockRequest addParam(String key, value) {_params[key] = value;return this;}@overrideRockRequest addQuery(String key, value) {_queryParams[key] = value;return this;}@overridevoid setAdapter(IRockNetAdapter adapter) {_adapter = adapter;}@overrideRockRequest 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 urlfinal String _baseUrl;/// 请求数据final RockRequest request;RockAdapterEngine(this._baseUrl, this.request);/// 生成urlString 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 {// 创建requestfinal adapterRequest = RockAdapterEngine(baseUrl, request);// 注: 使用时必须要指定adapterfinal 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!;}/// 网络请求 urlString? _baseUrl;/// 适配器, 一定要指定IRockNetAdapter? _adapter;/// 拦截器, 用于自定义配置void Function(RockRequest request)? _interceptor;/// 设置 base urlRockNetUtil 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 {@overrideFuture<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;// 输出当前抛出异常的urlLogUtil.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

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来对异常捕获,很好的解决了数据返回异常的特点,如有其他思路欢迎交流讨论。

本文链接:https://my.lmcjl.com/post/1964.html

展开阅读全文

4 评论

留下您的评论.