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

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

相关推荐
Pedantic34 分钟前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘1 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆1 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师2 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆2 小时前
VSCode自动格式化三要素
前端
爱勇宝3 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen3 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518136 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode6 小时前
Redis 在生产项目的使用
前端·后端