分享我在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把所有东西挤在一起而看得更久。

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

相关推荐
Boilermaker19921 小时前
【Java EE】SpringIoC
前端·数据库·spring
中微子1 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁2 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry2 小时前
Fetch 笔记
前端·javascript
拾光拾趣录2 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟2 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan2 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson2 小时前
青苔漫染待客迟
前端·设计模式·架构