使用 Getx 和 MVVM 发布 API
POST API:https://reqres.in/api/login
电子邮件:[电子邮件受保护]
密码:cityslicka
第一步:创建抽象类BaseApiService
abstract class BaseApiService { Future<dynamic> getApi(String url); Future<dynamic> postApi(dynamic data, String url);}
第2步:创建类AppExceptions并实现Exception
class AppExceptions implements Exception { final _message; final _prefix; AppExceptions([this._message, this._prefix]); String toString() { return '$_prefix$_message'; }}
第 3 步:创建 InternetException 类并扩展 AppExceptions
class InternetException extends AppExceptions { InternetException([String? message]) : super(message, "No Internet");}
第 4 步:创建类 RequestTimeOut 并扩展 AppExceptions
class RequestTimeOut extends AppExceptions { RequestTimeOut([String? message]) : super(message, 'Request Time out');}
第 5 步:创建 ServerException 类并扩展 AppExceptions
class ServerException extends AppExceptions { ServerException([String? message]) : super(message, 'Internal server error');}
第 6 步:创建类 InvalidUrlException 并扩展 AppExceptions
class InvalidUrlException extends AppExceptions { InvalidUrlException([String? message]) : super(message, 'Invalid Url');}
第 7 步:创建类 FetchDataException 并扩展 AppExceptions
class FetchDataException extends AppExceptions { FetchDataException([String? message]) : super(message, '');}
第8步:创建NetworkApiService类并扩展BaseApiService
class NetworkApiService extends BaseApiService { @override Future<dynamic> getApi(String url) async { if (kDebugMode) { print(url); } dynamic responseJson; try { final response = await http.get(Uri.parse(url)).timeout(const Duration(minutes: 3)); responseJson = returnResponse(response); } on SocketException { throw InternetException(""); } on RequestTimeOut { throw RequestTimeOut(""); } print(responseJson); return responseJson; } @override Future<dynamic> postApi(var data, String url) async { if (kDebugMode) { print(url); print(data); } dynamic responseJson; try { final response = await http.post(Uri.parse(url), body: data).timeout(const Duration(minutes: 3)); responseJson = returnResponse(response); } on SocketException { throw InternetException(""); } on RequestTimeOut { throw RequestTimeOut(""); } if (kDebugMode) { print(responseJson); } return responseJson; } dynamic returnResponse(http.Response response) { switch (response.statusCode) { case 200: dynamic responseJson = jsonDecode(response.body); return responseJson; case 400: throw FetchDataException("Bad Request: ${response.body}"); default: throw FetchDataException(AppString.errorInServer + response.statusCode.toString()); } }}
第9步:创建类AppUrl
class AppUrl { static const String baseUrl = 'https://reqres.in' ; static const String loginApi = '$baseUrl/api/login' ;}
第10步:创建类LoginRepository
class LoginRepository { final _apiService = NetworkApiService(); Future<dynamic> loginApi(var data) async { try { dynamic response = await _apiService.postApi(data, AppUrl.loginApi); return response; } catch (error) { rethrow; } }}
第11步:创建UserModel类
class UserModel { String? token; bool? isLogin ; UserModel({this.token , this.isLogin}); UserModel.fromJson(Map<String, dynamic> json) { token = json['token']; isLogin = json['isLogin']; } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['token'] = this.token; data['isLogin'] = this.token; return data; }}
第 12 步:创建 UserPreference 类
class UserPreference { Future<bool> saveUser(UserModel responseModel) async { SharedPreferences sp = await SharedPreferences.getInstance(); sp.setString('token', responseModel.token.toString()); sp.setBool('isLogin', responseModel.isLogin!); return true; } Future<UserModel> getUser() async { SharedPreferences sp = await SharedPreferences.getInstance(); String? token = sp.getString('token'); bool? islogin = sp.getBool('isLogin'); return UserModel(token: token, isLogin: islogin); } Future<bool> removeUser() async { SharedPreferences sp = await SharedPreferences.getInstance(); sp.clear(); return true; }}
第13步:制作类Utils
class Utils { static void fieldFocusNode(BuildContext context, FocusNode current, FocusNode nextFocus) { current.unfocus(); FocusScope.of(context).requestFocus(nextFocus); } static toastMessage(String message) { Fluttertoast.showToast( msg: message, backgroundColor: AppColors.blackColor, textColor: AppColors.whiteColor, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_LONG); } static snackBar(String title, String message, Color backgroundColor, Color colorText) { Get.snackbar(title, message, backgroundColor: backgroundColor, colorText: colorText); }}
第14步:创建类LoginViewModel并扩展GetxController
class LoginViewModel extends GetxController { final _api = LoginRepository(); UserPreference userPreference = UserPreference(); final emailController = TextEditingController().obs; final passwordController = TextEditingController().obs; final emailFocusNode = FocusNode().obs; final passwordFocusNode = FocusNode().obs; RxBool loading = false.obs; void loginApi() { loading.value = true; Map data = {"email": emailController.value.text, "password": passwordController.value.text}; _api.loginApi(data).then((value) { loading.value = false; if (value["error"] == "User not found") { Utils.snackBar("Login", value["error"], AppColors.redLightColor, AppColors.redColor); } else { UserModel userModel = UserModel(token: value["token"], isLogin: true); userPreference.saveUser(userModel).then((value) { Get.delete<LoginViewModel>(); Get.toNamed(RouteName.homeScreen)!.then((value) {}); Utils.snackBar("Login", "Login successfully", AppColors.greenLightColor, AppColors.greenColor); }).onError((error, stackTrace) { Utils.snackBar("Error", "Failed to save user data", AppColors.redLightColor, AppColors.redColor); }); } }).onError((error, stackTrace) { loading.value = false; if (error is FetchDataException && error.toString().contains("Bad Request")) { Utils.snackBar("Login", "User not found or bad request", AppColors.redLightColor, AppColors.redColor); } else { Utils.snackBar("Error", error.toString(), AppColors.redLightColor, AppColors.redColor); } }); }}
第15步:创建类LoginView并扩展StatelessWidget
class LoginView extends StatelessWidget { LoginView({super.key}); final loginVM = Get.put(LoginViewModel()); final _formKey = GlobalKey<FormState>(); final RxBool _obscureText = true.obs; @override Widget build(BuildContext context) { var height = MediaQuery.of(context).size.height * 1.0; var width = MediaQuery.of(context).size.width * 1.0; return Scaffold( body: SafeArea( top: true, bottom: true, child: Center( child: Container( height: height, width: width, child: Padding( padding: EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ CustomText(text: AppString.loginText, fontSize: 30.0, color: AppColors.primeryColor, fontWeight: FontWeight.bold), SizedBox( height: 35, ), Form( key: _formKey, child: Column( children: [ CustomTextField( keyboardType: TextInputType.emailAddress, prefixIcon: Icons.email, labelText: CustomText( text: AppString.userNameLable, color: AppColors.primeryColor, ), hintText: CustomText( text: AppString.userNameHint, color: AppColors.primeryLightColor, ), controller: loginVM.emailController.value, focusNode: loginVM.emailFocusNode.value, validator: (value) { if (value!.isEmpty) { Utils.snackBar("Email", "Enter email", AppColors.redLightColor, AppColors.redColor); } else if (!RegExp(r'^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$').hasMatch(value)) { return 'emailVadilate'; } return null; }, onFieldSubmitted: (value) { Utils.fieldFocusNodeChange(context, loginVM.emailFocusNode.value, loginVM.passwordFocusNode.value); }, ), SizedBox( height: 10, ), Obx( () => CustomTextField( labelText: CustomText( text: AppString.passwordLable, color: AppColors.primeryColor, ), hintText: CustomText( text: AppString.passwordHint, color: AppColors.primeryLightColor, ), controller: loginVM.passwordController.value, focusNode: loginVM.passwordFocusNode.value, validator: (value) { if (value!.isEmpty) { Utils.snackBar("Password", "Enter Password", AppColors.redLightColor, AppColors.redColor); } }, keyboardType: TextInputType.text, prefixIcon: Icons.lock, suffixIcon: _obscureText.value ? Icons.visibility : Icons.visibility_off, onSuffixIconPressed: () { _obscureText.value = !_obscureText.value; }, obscureText: _obscureText, obscuringCharacter: "*", ), ), ], )), SizedBox( height: 25, ), Obx( () => CustomButton( title: CustomText( text: AppString.loginButton, color: AppColors.whiteColor, fontWeight: FontWeight.bold, ), loading: loginVM.loading.value, onPressed: () { if (_formKey.currentState!.validate()) { loginVM.loginApi(); } }), ) ], ))), ))); }}
第 16 步:创建 AppColors 类
class AppColors { static const Color hintTextColor = Color(0xB2A4A4A4); static const Color whiteColor = Color(0xffffffff); static const Color blackColor = Color(0xff000000); static const Color primeryColor = Color(0xff7f65cb); static const Color primeryLightColor = Color(0xffc5b2fd); static const Color redColor = Color(0xffd42020); static const Color redLightColor = Color(0xfff5dfdf); static const Color greenLightColor = Color(0xffe0f1dd); static const Color greenColor = Color(0xff0e5900);}