Flutter GetX 骨架

Flutter Getx 骨架项目,使项目创建过程快速简便。

简介

当我们想开始一个新项目时,我们都会面临同样的问题,我们需要处理一些可重复的事情,例如:

  • 主题(浅色/深色)和在共享偏好中存储当前主题?
  • 本地化和在共享偏好中存储当前区域设置?️
  • Firebase 消息?
  • 通知设置?
  • 安全的 API 请求和错误处理?
  • 在 API 调用期间更改小部件(加载中、成功、失败等)?
  • Snackbar、Toasts 和应用内通知?
  • 使应用程序更具响应性并停止字体缩放⚖️ 此项目将处理所有这些可重复的事情,以便您可以在几个步骤内启动您的项目,并且所有提到的点都已设置好并准备好使用?

致谢

项目是使用 get_cli 创建的,这是一个非常有用的工具,可以帮助您(启动项目、创建屏幕/控制器、处理 DI)等,我们将列出其他帮助创建此骨架的包

克隆并启动项目

在浏览文件夹之前,让我们先执行一些操作,使项目准备好启动

  • 首先运行此命令,它将生成 hive 类型适配器(用于我们要本地存储的自定义类)

    flutter packages pub run build_runner build --delete-conflicting-outputs
    

    如果您不想使用 hive,请注释掉 main.dart 中的这行

    await MyHive.init(adapters: [UserModelAdapter()]);
  • 为了使您的应用程序响应并与您的 (xd,figma..等) 设计完全一致,您需要在 main.dart 中为 flutter_ScreenUtil 设置画布尺寸

    ScreenUtilInit(
      designSize: const Size(375, 812), // change this to your xd artboard size
  • FCM 和 Awesome Notifications 结合在同一个类中,所以每当您将应用程序连接到 firebase 时,您的应用程序将准备好接收通知,您所要做的就是通过实现 FcmHelper 类中的 sendFcmTokenToServer 方法来将 fcm 通知发送到您的 API?

    static _sendFcmTokenToServer(){
        var token = MySharedPref.getFcmToken();
        // TODO SEND FCM TOKEN TO SERVER
    }
  • 更改应用程序包名

    flutter pub run change_app_package_name:main com.new.package.name
    
  • 更改应用程序名称

    flutter pub run rename_app:main all="My App Name"
    
  • 更改应用程序启动图标(将 assets/images/app_icon.png 替换为您的应用程序图标),然后运行此命令

    flutter pub run flutter_launcher_icons:main
    
  • FCM:firebase 最近添加了“添加 flutter 应用”,这将使将我们的 flutter (android/ios) 应用添加到 firebase 只需 2 个步骤?但首先您需要下载 Firebase CLI 并在终端中执行

    dart pub global activate flutterfire_cli
    

    然后按照 Firebase 的指南进行操作,您将得到一个类似于此的命令

    flutterfire configure --project=flutter-firebase-YOUR_PROJECT_ID
    

    就是这样!您的项目现在已连接到 firebase,fcm 已准备就绪,可以接收通知

快速入门

  • 响应式应用程序:为了使您的应用程序具有响应性,您需要利用 flutter_ScreenUtil,因此,与其使用普通的 double 值来设置高度、宽度、半径等,不如像这样使用它:

  • 200.w // adapted to screen width
    100.h // /Adapted to screen height
    25.sp // adapter font size
    10.r // adapter radius
    // Example
    Container(
        height: 100.h,
        width: 200.w,
        child: Text("Hello",style: TextStyle(fontSize: 20.sp,))
    )
  • 主题

    • 更改主题

      MyTheme.changeTheme();
    • 检查当前主题

      bool isThemeLight = MyTheme.getThemeIsLight();
  • 本地化

    • 更改应用程序区域设置

      LocalizationService.updateLanguage('en');
    • 获取当前区域设置

      LocalizationService.getCurrentLocal();
    • 使用翻译

      Text(Strings.hello.tr)
  • 安全的 API 调用

    • 逻辑代码(在控制器中)

          // api call status
        ApiCallStatus apiCallStatus = ApiCallStatus.holding;
      
        // getting data from api simulating
        getData() async {
          // *) indicate loading state
          apiCallStatus = ApiCallStatus.loading;
          update();
          // *) perform api call
          await BaseClient.get(
            Constants.todosApiUrl, // url
            onSuccess: (response){ // api done successfully
              data = List.from(response.data);
              // -) indicate success state
              apiCallStatus = ApiCallStatus.success;
              update(); // update ui
            },
            // if you dont pass this method base client
            // will automaticly handle error and show message
            onError: (error){
              // show error message to user
              BaseClient.handleApiError(error);
              // -) indicate error status
              apiCallStatus = ApiCallStatus.error;
              update(); // update ui
            }, // error while performing request
          );
        }
    • UI:MyWidgetsAnimator 将根据当前的 API 调用状态在小部件之间进行动画

      GetBuilder<HomeController>(
      builder: (controller){
        LocalizationService.updateLanguage('en');
        LocalizationService.getCurrentLocal();
        return MyWidgetsAnimator(
            apiCallStatus: controller.apiCallStatus,
            loadingWidget: () => const Center(child: CircularProgressIndicator(),),
            errorWidget: ()=> const Center(child: Text('Something went worng!'),),
            successWidget: () =>
               ListView.separated(
                itemCount: controller.data!.length,
                separatorBuilder: (_,__) => SizedBox(height: 10.h,),
                itemBuilder: (ctx,index) => ListTile(
                    title: Text(controller.data![index]['userId'].toString()),
                    subtitle: Text(controller.data![index]['title']),
                  ),
              ),
      
        );
      },
      )
  • Snackbars(应用内通知)

    CustomSnackBar.showCustomSnackBar(title: 'Done successfully!', message: 'item added to wishlist');
    CustomSnackBar.showCustomErrorSnackBar(title: 'Failed!', message: 'failed to load data');
    CustomSnackBar.showCustomToast(message: 'added to card');
    CustomSnackBar.showCustomErrorToast(message: 'added to card');

          

探索项目

设置好所有必需项后,现在让我们谈谈文件夹结构,该结构主要基于 Getx 模式,并包含一些个人意见。如果您打开 lib 文件夹,您会找到这些文件夹:

.
└── lib
    ├── app
    │   ├── components
    │   ├── data
    │   │   ├── local
    │   │   └── models
    │   ├── modules
    │   │   └── home
    │   ├── routes
    │   └── services
    ├── config
    │   ├── theme
    │   └── translation
    └── utils
  • app:将包含我们所有核心的应用程序逻辑
    • components:将包含所有共享的 UI 小部件
    • data:将包含我们的模型和本地数据源(本地数据库和共享偏好)
    • modules:应用程序屏幕
    • routes:由 get_cli 生成,并将包含我们的导航路由
    • services:包含所有用于进行安全且干净的 API 调用的逻辑
  • config:将包含应用程序配置,例如主题、本地化服务
  • utils:用于我们的辅助类

功能

  • Theme:如果您打开 theme 包,您会看到这些文件

    └── theme
        ├── dark_theme_colors.dart
        ├── light_theme_colors.dart
        ├── my_fonts.dart
        ├── my_styles.dart
        └── my_theme.dart
    
    

    您只需要更改应用程序颜色(light/dark_theme_colors),如果您想更改应用程序字体大小和系列,只需修改 my_fonts.dart,就这样,您无需担心样式和主题,您只需要编辑 my_syles.dart,如果您想更改某些元素的 theme data(padding、border 等),并且如果您想更改主题,只需使用此代码:

    // change theme and save current theme state to shared pref
    MyTheme.changeTheme();

    如果您想检查主题是深色/浅色,只需使用:

    bool themeIsLight = MyTheme.getThemeIsLight();
    // OR
    bool themeIsLight = MySharedPref.getThemeIsLight();
  • 本地化/翻译我们将使用 getx 本地化系统,在正常情况下代码看起来会是这样的:

    class LocalizationService extends Translations {
        @override
        Map<String, Map<String, String>> get keys => {
            'en_US': { 'hello' : 'Hello' },
            'ar_AR': { 'hello' : 'مرحباً' },
        };
    }
    
    Text('hello'.tr); // translated text 

    但是因为我们有很多单词需要翻译,我们将把键文件(strings_enum.dart)和语言映射分离到不同的类中,所以代码将变成这样:

    class LocalizationService extends Translations {
          @override
          Map<String, Map<String, String>> get keys => {
              'en_US': enUs,
              'ar_AR': arAR,
          };
      }
    // keys
    class Strings {
        static const String hello = 'hello';
    }
    // english words
    const Map<String, String> enUs = {
        Strings.hello : 'Hello',
    }
    // arabic translate
    final Map<String, String> arAR = {
        Strings.hello : 'مرحبا',
    }
    //result
    Text(Strings.hello.tr)

    这解释了为什么我们的翻译包中有这个文件结构

       └── translations
           ├── ar_Ar
           │   └── ar_ar_translation.dart
           ├── en_US
           │   └── en_us_translation.dart
           ├── localization_service.dart
           └── strings_enum.dart
    

    要更改语言,您将使用:

    LocalizationService.updateLanguage('en');

    要获取当前区域设置/语言,您可以使用:

    LocalizationService.getCurrentLocal();
    // OR
    MySharedPref.getCurrentLocal();
  • 安全的 API 调用:在 lib/app/services 包下,您会找到 3 个文件

    • api_call_status.dart:包含我们 API 调用所有可能的阶段(加载中、成功、错误等)
    • api_exception.dart:自定义异常类,使错误处理更具信息量
    • base_client.dart:包含我们的安全 API 调用函数,用于正确执行 API 请求,您可以这样做:

class HomeController extends GetxController {
  // hold data
  List<dynamic>? data;
  // api call status
  ApiCallStatus apiCallStatus = ApiCallStatus.holding;

  // getting data from api simulating
  getData() async {
    // *) indicate loading state
    apiCallStatus = ApiCallStatus.loading;
    update();
    // *) perform api call
    await BaseClient.get(
      Constants.todosApiUrl, // url
      onSuccess: (response){ // api done successfully
        data = List.from(response.data);
        // -) indicate success state
        apiCallStatus = ApiCallStatus.success;
        update(); // update ui
      },
      // if you dont pass this method base client
      // will automaticly handle error and show message
      onError: (error){
        // show error message to user
        BaseClient.handleApiError(error);
        // -) indicate error status
        apiCallStatus = ApiCallStatus.error;
        update(); // update ui
      }, // error while performing request
    );
  }

  @override
  void onInit() {
    getData();
    super.onInit();
  }
}

base client 将捕获所有可能的错误,如果您没有传递 onError 函数,它将自动在 UI 端捕获错误,代码将是:

GetBuilder<HomeController>(
        builder: (_){
          return MyWidgetsAnimator(
              apiCallStatus: controller.apiCallStatus,
              loadingWidget: () => const Center(child: CircularProgressIndicator(),),
              errorWidget: ()=> const Center(child: Text('Something went worng!'),),
              successWidget: () =>
                 ListView.separated(
                  itemCount: controller.data!.length,
                  separatorBuilder: (_,__) => SizedBox(height: 10.h,),
                  itemBuilder: (ctx,index) => ListTile(
                      title: Text(controller.data![index]['userId'].toString()),
                      subtitle: Text(controller.data![index]['title']),
                    ),
                ),

          );
        },
      )

注意: MyWidgetsAnimator 将负责 UI 随动画的变化,您将传递 ApiCallStatus 和 success、failed、loading 等小部件,它将负责过渡。

支持

如需支持,请发送电子邮件至 [email protected] 或 Facebook Emad Beltaje

GitHub

查看 Github