Skip to content

tec8297729/flutter_flexible

Repository files navigation

flexible 脚手架介绍

无需复杂配置即可快速搭建 app 基础架子,让你更加专注业务 ui 实现。

flexible 通过运行一个命令来创建一个 app 应用程序。可在 macOS,Windows 和 Linux 上运行。

flutter版本

Flutter 3.24.2 • channel stable
Tools • Dart 3.5.2

内置集成功能

• 动态环境构建打包,挂载在 app 内部全局参数中,如请求接口动态前缀 url,根据不同打包环境使用不同的接口域名。

• 状态管理:集成 Provider 在 Flutter 项目中,任何页面声明好 store,注入 lib/providers_config.dart 文件内即可使用。

• 页面组件更便捷的接收 路由别名跳转传参,底层已处理无需任何插件支持!简单易用,无学习成本。

• 全局主题一键换色,只需要配置你的主题颜色,调用方法即可。

• 全局浮动调试组件,让你在真机上也能便利的获取错误捕获。

• 全局 context 对象,可在任意位置获取使用,例如在状态管理 provider 层内使用

• OTA 更新 app 功能,内置一套 ui 界面,轻松配置 OTA 更新地址。

PS:其它更多功能介绍往下拉查看 功能介绍区文档,或自行体验探索。

目录


项目结构

  asset/ # 静态资源
  lib/
  |- components/ # 共用widget组件
  |- config/ # 全局配置参数
      |- app_config.dart # APP相关配置,如反向代理、设计稿尺寸等
  |- constants/ # 常量文件夹
  |- provider/ # 全局状态管理
  |- pages/ # 页面ui层
      |- app_main/ # APP主体页面
      |- splash/ # APP闪屏页
  |- service/ # 请求接口抽离层
  |- models/ # 数据类型
  |- routes/ # 路由相关文件夹
  |- utils/ # 工具类



快速入门上手

创建项目

1、全局安装 cli 插件,确保你的电脑中有 node 环境。

npm i -g flib-cli // 全局安装插件

// 方式二:手动下载,但没有全局指令功能
git clone https://github.com/tec8297729/flutter_flexible.git

2、打开终端,输入以下指令创建项目

flib updata // 更新下载模板
flib create  // 创建项目,根据提示步骤往下进行,都有默认参数可直接回车

cli 相关指令介绍

flib create 创建一个flutter项目
flib updata 更新最新flutter模板
flib page <name> 创建一个页面组件

启动项目

进入项目目录文件夹,初始化安装依赖包以及启用 APP(记的开启你的模拟器)

输入以下命令:

flutter pub get
flutter run

PS:安卓如果编译失败,请在android\local.properties更改minSdkVersion版本号

# 调整版本号到19以上,原flutter默认版本为16
flutter.minSdkVersion=19

指令参数介绍

指令也是为了更方便记忆使用,你也可以使用原生 flutter 指令打包等

集成在项目中的指令如下:

命令 说明
npm run start 启动 APP 项目,请提前开好模拟器或连接真机
npm run build 同时打包 APP 的安卓和 IOS,prod 环境
build-apk:test 打包 安卓 文件 test 环境的
build-apk:pre 打包 安卓 文件 pre 环境的
build-apk:prod 打包 安卓 文件 prod 环境的
build-ios:test 打包 IOS 文件 test 环境的
build-ios:pre 打包 IOS 文件 pre 环境的
build-ios:prod 打包 IOS 文件 prod 环境的
build-web:test 打包 web 文件 test 环境
build-web:pre 打包 web 文件 pre 环境
build-web:prod 打包 web 文件 prod 环境
build-windows:test 打包 windows 文件 test 环境
build-windows:pre 打包 windows 文件 pre 环境
build-windows:prod 打包 windows 文件 prod 环境
npm run upsdk 更新 sdk 版本,全局的 flutter 和 dart 版本将更新为最新版本
npm run appkey 验证打包后的安卓 apk 签名信息,需要本机终端安装了keytool命令工具




功能介绍

动态环境变量

默认使用 npm run dev 或是 npm run apk-build:test 等内置语法,是设置好了环境变量参数的,直接运行指令即可。

1、在文件下定义环境参数: lib/config/app_env.dart,例如定义环境变量 baseUrl

2、在其它组件页面中直接调用即可

import 'config/app_env.dart' show appEnv;
appEnv.baseUrl // 获取当前环境的url

App 启动屏

App 启动屏图片修改到指定路径中替换成自己的图片


// 这是安卓启动屏图片路径,默认只添加了一个文件加,需要不同分别率在mipmap-**相应文件夹内添加
android\app\src\main\res\mipmap\splash_bg.png

// 这是IOS启动屏图片路径,LaunchImage**.png都替换成自己的启动屏图片
ios\Runner\Assets.xcassets\LaunchImage.imageset\LaunchImage.png

PS:启动屏欢迎页及广告页面在 flutter 组件中定制功能,在 lib\pages\SplashPage 目录中修改

获取全局 context

全局 Key 和全局 context 都存放在全局 common_config.dart 文件中。

PS:你可以把一些全局的类都可以此中使用,从而实现页面更加方便管理

import 'config/common_config.dart' show commonConfig;
commonConfig.getGlobalKey;; // 全局context对象

应用场景:像弹层需要context对象,状态管理层(调用第三方插件有依赖)都可以直接使用全局的context对象透传过去


dio 封装简化使用

已经 dio 请求底层封装,更简化使用

import 'package:flexible/utils/request.dart';
// get请求使用方法,同dio组件request方法
getHomeData() async {
  Map resData = await Request.get(
    'url',
    queryParameters: {'key': 'value'}, // 在url后追加参数?key=value
  );
}

// post请求
getHomeData() async {
  Map resData = await Request.post(
    'http://url',
    data: {'version': version}, // 传递参数
    queryParameters: {'key': 'value'}, // 在url后追加参数?key=value
  );
}

dio 拦截处理

在 lib/utils/dio/interceptors 目录内,扩展请求拦截处理

/*
 * header拦截器
 */
class HeaderInterceptors extends InterceptorsWrapper {
  // 请求拦截
  @override
  onRequest(RequestOptions options) async {
    options.connectTimeout = 15000;
    options.baseUrl = AppConfig.host;
    return options;
  }

  // 响应拦截
  @override
  onResponse(Response response) async {
    return response;
  }

  // 请求失败拦截
  @override
  onError(DioError err) async {}
}

Provider状态管理

1、在任意目录内创建provider目录(建议页面级目录),并且在此目录内建立一个store文件

// home页面
// pages/app_main/home/provider/counterStore.p.dart文件
import 'package:flutter/material.dart';

class CounterStore extends ChangeNotifier {
  int value = 10;
  void increment() {
    value++;
    notifyListeners();
  }
}

2、进入lib/providers_config.dart文件,把刚创建好的store文件在里面声明一下

import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';
import 'pages/app_main/home/provider/counterStore.p.dart';
import 'provider/global.p.dart';
import 'provider/theme_store.p.dart';

List<SingleChildWidget> providersConfig = [
  ChangeNotifierProvider<ThemeStore>(create: (_) => ThemeStore()),
  ChangeNotifierProvider<GlobalStore>(create: (_) => GlobalStore()),
  // 新增的store
  ChangeNotifierProvider<CounterStore>(create: (_) => CounterStore()),
];

3、在页面中使用provider状态管理

// home页面中使用,精简代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider/counterStore.p.dart';

class Home extends StatefulWidget {
  const Home({Key? key, this.params}) : super(key: key);
  final dynamic params;

  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  late CounterStore _counter;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    _counter = Provider.of<CounterStore>(context);
    return Scaffold(
      body: Column(
        children: <Widget>[
          ElevatedButton(
            child: Text(
              // 读取ConterStore中的value变量,显示10
              Text('状态管理值:${context.watch<CounterStore>().value}'),
            ),
            onPressed: (){
              _counter.increment(); // 调用ConterStore类中的increment方法
            },
          )
        ]
      ),
    );
  }
}

ps:provider官方还有更多api使用方式,文档地址


别名路由传参

别名路由传递参数,在接收过程更便捷利与使用。

1、进入路由配置文件 routes/routesData.dart,加入别名传参支持。

// routesData.dart文件
import 'package:flutter/material.dart';
import '../pages/ErrorPage/ErrorPage.dart';
import '../pages/TestDemo/TestDemo.dart';

final Map<String, WidgetBuilder> routesData = {
  // 路由/testDemo 添加别名路由传参支持。
  '/testDemo': (BuildContext context, {params}) => TestDemo(params: params),
  // error路由不加入别名传参功能,
  '/error': (BuildContext context, {params}) => ErrorPage(),
};

2、在页面中使用别名跳转,直接使用原生别名跳转方法即可
// 某页面跳转
Navigator.pushNamed(
  context,
  '/testDemo',
  arguments: {'data': 'hello world'}, // 传递参数
);

3、在接收的子页面直接读取params参数变量即可。
// 子页面组件使用及接收
class testDemo extends StatefulWidget {
  testDemo({Key? key, this.params}) : super(key: key);
  final params; // 别名传参接收变量

  @override
  _testDemoState createState() => _testDemoState();
}
class _testDemoState extends State<testDemo>{
  @override
  void initState() {
    super.initState();
    print(widget.params); // 路由别名参数
  }
}

OTA 更新 App 版本

1、添加安卓的存储权限申请标签(默认已添加, 可跳过此步),如有删除安卓目录生成过的,请自行添加一下。

安卓权限配置文件 android\app\src\main\AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.flutter_flexible">
    <!-- 添加读写权限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
        <!--  Android 10(API 29) -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <!-- Android 11(API 30) -->
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
    <application>
      <!-- 将以下提供程序引用添加到节点内 -->
      <provider
          android:name="sk.fourq.otaupdate.OtaUpdateFileProvider"
          android:authorities="${applicationId}.ota_update_provider"
          android:exported="false"
          android:grantUriPermissions="true">
          <meta-data
              android:name="android.support.FILE_PROVIDER_PATHS"
              android:resource="@xml/filepaths" />
      </provider>
    </application>
</manifest>

2、在 lib\components\UpdateAppVersion\getNewAppVer.dart 文件中,getNewAppVer 方法直接运行更新 APP 版本,但有少部份需要自己实现,已标注 TODO 位置,指定 APP 下载地址和获取新版本的接口替换。

// TODO:替换成自己的获取新版本APP的接口
Map resData = await getNewVersion();
// 模拟参数结构如下  {"code":"0","message":"success","data":{"version":"1.1.0","info":["修复bug提升性能","增加彩蛋有趣的功能页面","测试功能"]}}

UpdateAppVersion(
  // TODO: 传入新版本APP相关参数、版本号、更新内容、下载地址等
  version: resData['version'] ?? '', // 版本号
  info: (resData['info'] as List).cast<String>() ?? [], // 更新内容介绍
  // ios是苹果应用商店地址
  iosUrl: 'itms-apps://itunes.apple.com/cn/app/id414478124?mt=8',
  // 安卓APK下载地址
  androidUrl: 'https://www.jonhuu.com/download/aweme.apk',
)

3、在指定页面运行 检查 APP 版本函数,默认在 lib\pages\AppMain\AppMain.dart 中,运行检查更新 APP 函数,你可以指定其它位置运行检查新版本。

import 'package:flexible/components/UpdateAppVersion/UpdateAppVersion.dart' show getNewAppVer;

getNewAppVer(); // 在指定组件页面 执行更新检查

全局主题切换功能

目前有内置几个主题,轻松切换整体 app 颜色主题功能,只需专注配置 app 各个参数颜色

如果要自定义 app 主题,把配置参数文件放入 lib\config\themes 文件夹中,然后 part 到 index_theme.dart 文件中统一管理。

案例如下:

import 'package:flutter/material.dart';
// 以下你配置的全局主题颜色参数
part 'themeBlueGrey.dart';
part 'themeLightBlue.dart';
part 'themePink.dart';

主题配色具体可以参考是关配色文件 themeBlueGrey.dart 等。

在需要替换主题的页面中调用如下:

import 'package:flexible/constants/themes/index_theme.dart' show themeBlueGrey; // 主题文件
import 'package:flexible/provider/themeStore.p.dart'; // 全局主题状态管理
ThemeStore _theme = Provider.of<ThemeStore>(context);
_theme.setTheme(themeBlueGrey); // 替换主题,注入主题配置即可

灰度主题

灰度主题只有 app 首页生效,针对特殊场景使用,此功能不需要单独配置主题文件,直接使用即可。


import './lib/provider/global.p.dart';
GlobalStore globalStore = Provider.of<GlobalStore>(context);
globalStore.setGrayTheme(true); // 设置灰度模式

PS:一般灰度主题模式用于特殊纪念日才使用,如需所有页面都展示此效果,可参考lib\pages\app_main\app_main.dart内build函数代码,使用到所有页面

全局路由监听

默认监听全局路由页面,只需要添加你的第三方统计埋点即可,如需要某页面 tab 监听还需要你手动继承类,并且实现相关方法。

具体实现由 ana_page_loop 插件完成,详细插件文档》》 https://github.com/tec8297729/ana_page_loop

1、先找到如下文件 lib\utils\myAppSetup\anaPageLoopInit.dart,配置第三方统计方法,如果想指定路由不监听处理事件,写入相关路由名称即可。

// 找到如下文件 lib\utils\myAppSetup\anaPageLoopInit.dart
void anaPageLoopInit() {
  anaPageLoop.init(
    beginPageFn: (name) {
      // TODO: 第三方埋点统计开始
    },
    endPageFn: (name) {
      // TODO: 第三方埋点统计结束
    },
    routeRegExp: ['/appMain'], // 过滤路由
    debug: false,
  );
}

如果你的项目很简单,此时你已经完整了全局埋点处理,只需要添加一下第三方埋点方法即可。
要是你需要独立统计 PageView 或是 Tab 组件中页面的,接着往第二步操作。

2、首先提供了二个 mixin 继承类使用,用在你需要独立统计的页面,并且记得把当前独立统计的页面路由过滤掉,例如/home 页面是独立统计四个页面,所以需要过滤整体的/home 路由。

PageViewListenerMixin类:用于监听类PageView组件
TabViewListenerMixin类:用于监听类TabBar组件

演示在 PageView 组件中的使用如下:

// 当前路由页面名称是 /appMain
class _AppMainState extends State<AppMain> with PageViewListenerMixin {
  PageController pageController;

  @override
  void initState() {
    super.initState();
    pageController = PageController();
  }

  @override
  void dispose() {
    pageController?.dispose();
    super.dispose();
  }

  // 实现PageViewListenerMixin类上的方法
  @override
  PageViewMixinData initPageViewListener() {
    return PageViewMixinData(
      controller: pageController, // 传递PageController控制器
      tabsData: ['首页', '分类', '购物车', '我的中心'], // 自定义每个页面记录的名称
    );
  }

  // 调用如下几个生命周期
  @override
  void didPopNext() {
    super.didPopNext();
  }

  @override
  void didPop() {
    super.didPop();
  }

  @override
  void didPush() {
    super.didPush();
  }

  @override
  void didPushNext() {
    super.didPushNext();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        controller: pageController, // 控制器
        children: <Widget>[],
      ),
    );
  }
}

build渠道标记

1、打包时注入ANDROID_CHANNEL参数,标记渠道参数

flutter build apk --dart-define=ANDROID_CHANNEL=flutter # 打包,并标记渠道为flutter
flutter run --dart-define=ANDROID_CHANNEL=flutter # 本地运行(开发环境),查看当前渠道

PS: 打包不同渠道命令可统一写到package.json文件内(生成多条指令),使用npm run xxxx批量执行打包,具体可参考package.json文件命令

示例:批量执行多条指令

{
  "scripts": {
    // 终端执行npm run build,代表同时执行build-apk:prod、build-ios:prod、build-web:prod、build-windows:prod几条指令
    "build": "npm run build-apk:prod && npm run build-ios:prod && npm run build-web:prod && npm run build-windows:prod",
    "build-apk:prod": "flutter build apk --dart-define=INIT_ENV=prod --dart-define=ANDROID_CHANNEL=flutter",
    "build-ios:prod": "flutter build ios --dart-define=INIT_ENV=prod",
    "build-web:prod": "flutter build web --dart-define=INIT_ENV=prod",
    "build-windows:prod": "flutter build windows --dart-define=INIT_ENV=prod",
  }
}

2、在flutter端获取渠道变量

import 'lib/config/app_env.dart' show appEnv; // 获取环境类
appEnv.getAppChannel() // 获取渠道参数