分享我在flutter中使用的MVVM框架 - 2

前言

嗨,各位好,天气逐渐转凉,记得多保重身体。

在上一篇中,介绍了我自己想的MVVM框架,只是在实务上有时候并不是那么的好理解,因此在我研究许久以后,写了第二个版本,这次也加上如何call api,让各位可以看看成果!

由於這次更改的地方著重在viewModel以及api的部分,因此這篇只會介紹這兩個地方。

代碼解釋

api.dart

dart 复制代码
class CallApi{}

首先我会建立一个api Basic,专门拿来给所有的api提供一个通用的接口,方便管理。

dart 复制代码
    if (kDebugMode) {
      // 在调试模式下使用的基本 URL
      return 'https://api.open-meteo.com';
    } else {
      // 在 release 模式下使用的基本 URL
      return 'https://api.open-meteo.com';
    }
  }

在上面我会先写一个 getBaseUrl() ,这个主要是在调试的过程中,避免掉每一次修改baseUrl会造成部署上的错误

dart 复制代码
 //共用 API 請求處理邏輯
  Future<Response> handleApiCall(
    Future<Response> Function() apiCall, {
    Function()? onSuccess,
    Function()? onError,
  }) async {
    try {
      // onSuccess?.call();
      final response = await apiCall();
      onSuccess?.call();
      return response;
    } catch (e) {
      print('API 呼叫錯誤: $e');
      onError?.call();
      rethrow;
    }
  }

再来我会建立一个handle的function,提供每一个api能够使用get/post与成功或错误时会执行的function

dart 复制代码
Future<Response> get(String endpoint, Map<String, dynamic> queryParams,
    Map<String, String> header) async {
  final String baseUrl = getBaseUrl();
  print('GET 請求: $endpoint');
  Uri uri =
      Uri.parse(baseUrl + endpoint).replace(queryParameters: queryParams);
  return _handleRequest(() => http.get(uri, headers: header));
}

Future<Response> post(
    String endpoint, dynamic reqBody, Map<String, String> header) async {
  final String baseUrl = getBaseUrl();
  print('POST 請求: $endpoint');
  Uri uri = Uri.parse(baseUrl + endpoint);
  final body = jsonEncode(reqBody);
  return _handleRequest(() => http.post(uri, headers: header, body: body));
}

Future<Response> _handleRequest(
    Future<http.Response> Function() request) async {
  try {
    final httpResponse = await request();
    log('HTTP 回傳狀態: ${httpResponse.statusCode}');

    if (httpResponse.statusCode == 200) {
      return Response.fromJson(jsonDecode(httpResponse.body));
    } else {
      throw Exception('Server Error: ${httpResponse.statusCode}');
    }
  } catch (e) {
    log('HTTP 請求失敗: $e');
    rethrow;
  }
}

最后我会写get与post的function,并且把get与post后,回传回来的request去做一个判断,并且把资料回传回去

service/home.dart

再来是建立一个get的function,用于get这次的资料:天气资料。

dart 复制代码
class HomeService {
  final CallApi api;

  HomeService() : api = CallApi(); // 直接初始化

  Future<HourlyWeather> hourlyWeather(
      {required Map<String, dynamic> queryParams,
      required Map<String, String> header}) async {
    final res = await api.handleApiCall(
      () => api.get("/v1/forecast", queryParams, header),
      onError: () => print("取得失敗"),
    );

    final hourlyWeather =
        HourlyWeather.fromJson(res.hourly as Map<String, dynamic>);

    return hourlyWeather;
  }
}

这里会需要传入query、header,但因为这次不需要header,因此我会在更原头的地方传空字串。

会把service当成class写,最主要的原因是因为要整合,也很方便扩增需求

(毕竟面对客户,增加需求是常有的事情)

这样也单独对service做单元测试,在做call 金流方面的api时,单元测试是很重要的!

在能够成功回传资料以后,这边的功能也就完成啦。

viewModel/home.dart

dart 复制代码
class HomeViewModel extends ChangeNotifier {
  final HomeService homeService = HomeService();
  HourlyWeather hourlyWeather = HourlyWeather();
  Future<HourlyWeather?>? weatherFuture;

  Future<void> callGetWeather({
    required Map<String, String> queryParams,
    required Map<String, String> header,
  }) async {
    weatherFuture = homeService
        .hourlyWeather(queryParams: queryParams, header: header)
        .then((data) {
      hourlyWeather = data;
      notifyListeners();
    }).catchError((e) => throw e);
  }
}

在这里会建立一个service,再建立一个model,用于储存资料并显示在画面上。这里的query与header仍然是要传入的,因此可以让所有变数集中在一个地方,也是方便管理!

page/home.dart

dart 复制代码
      homeViewModel.callGetWeather(queryParams: {
        "latitude": "52.52",
        "longitude": "13.41",
        "hourly": "temperature_2m",
        "models": "cma_grapes_global",
      }, header: globalViewModel.headers);
    });

这里会把需要的query与header给api,并且层层传下去,确保了变数的一致性。

这里的globalViewModel.plantHeaders如下:

dart 复制代码
  Map<String, String> get headers {
    return headers().setHeader("", token: "");
  }

会这样子写也是为了要确保header一致,因此使用一个通用的header。

最后我们在view中,用FutureBuilder把资料显示出来,这样就完成啦!

dart 复制代码
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (_) => homeViewModel,
        child: Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: const Text("首頁"),
            centerTitle: true,
          ),
          body: Center(
            child: Consumer<HomeViewModel>(
              builder: (context, viewModel, child) {
                return FutureBuilder<HourlyWeather?>(
                  future: viewModel.weatherFuture,
                  builder: (context, snapshot) {
                    if (snapshot.connectionState == ConnectionState.waiting ||
                        snapshot.connectionState == ConnectionState.none) {
                      return const CircularProgressIndicator();
                    }
                    if (snapshot.hasError) {
                      return Text('Error: ${snapshot.error}');
                    }

                    log(viewModel.hourlyWeather.time!.length.toString());
                    return ListView.builder(
                      itemCount: viewModel.hourlyWeather.time!.length,
                      itemBuilder: (context, index) {
                        return ListTile(
                          title: Text(viewModel.hourlyWeather.time![index]),
                          subtitle: Text(viewModel
                              .hourlyWeather.temperature_2m![index]
                              .toString()),
                        );
                      },
                    );
                  },
                );
              },
            ),
          ),
        ));
  }

演示效果画面如下:

有点丑,因为想说是范例,因此便着重在介绍架构而非画面😝

结语

那这就是这次对于我的MVVM中,我所改进的地方,以及介绍了如何使用Api的部分!

这次的改进主要也是受到了前端网页的影响,想要把一些东西分得更开,除了方便做单元测试以外,也能让后进能够快速地看懂代码的构造,不会因为viewModel把所有东西挤在一起而看得更久。

感谢看到这里的你,希望今后的你也是顺顺利利的喔!

相关推荐
hackeroink33 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css