Flutter项目架构实践:基于蒲公英API的团队App下载安装器

305 阅读2分钟

1、背景

公司旗下有很多产品线,各种的APP,在给客户演示的时候不是很方便。还有一个问题就是团队Flutter转型,以后新的项目,技术储备成熟之后考虑使用Flutter开发。

2、页面预览

62eb3cbef2c10.gif

3、技术架构

架构图.jpg

4、网络请求

基于http库做网络封装,使用如下:

4.1、pubspec.yaml添加引用

dependencies:
  ...
  http: ^0.13.4
  alice: ^0.2.5

4.2、工具类API.dart

import 'dart:convert';
import 'dart:developer';
import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:gtech_app/Config.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:alice/alice.dart';

enum GTechAPI {
  getAppList,
  getAppDetail,
  getAppBuilds,
  getLocalDBAppList,
  getLocalDBAppDetailBuilds,
  getLocalDBAppBuildsList,
}

enum HTTPMethod { GET, POST }

// GTech API
// @author zhenteng.li@gtechdigi.com
// @time   2022/7/13
class API {
  final GTechAPI api;
  final HTTPMethod method;
  final Map<String, dynamic> parameters;

  API({required this.api, this.method = HTTPMethod.POST, this.parameters = const {}});

  static final Alice alice = Alice(
      showNotification: false,
      showInspectorOnShake: true,
      darkTheme: true);

  String get endpoint {
    switch (api) {
      case GTechAPI.getAppList:
        return '${Config.baseUrl}/app/listMy';
      case GTechAPI.getAppDetail:
        return '${Config.baseUrl}/app/view';
      case GTechAPI.getAppBuilds:
        return '${Config.baseUrl}/app/builds';
    }
    return "";
  }

  final String defaultErrorMessage =
      "There is something wrong with the internet connection";

  String queryStringFromMap(Map<String, dynamic> map) {
    List<String> params = [];

    map.forEach((key, value) {
      params.add("$key=$value");
    });
    return params.join("&");
  }

  Future<dynamic> request(
      {BuildContext? context}) async {
    http.Response response;
    var ioClient = IOClient(HttpClient());

    parameters["_api_key"] = Config.pgyerApiKey;

    switch (method) {
      case HTTPMethod.GET:
        response = await ioClient
            .get(Uri.parse("$endpoint?${queryStringFromMap(parameters)}"))
            .catchError((e) {
        });
        break;
      case HTTPMethod.POST:
        response = await ioClient
            .post(Uri.parse(endpoint), body: parameters)
            .catchError((e) {
        });
        break;
    }

    alice.onHttpResponse(response);

    var bodyString = utf8.decode(response.bodyBytes);
    log("param $parameters\n\n");
    log("\n\napi = $endpoint, code = ${response.statusCode}, response api $bodyString\n\n");

    final result = json.decode(bodyString);

    if (result == null) {
      return AssertionError("JSON parsing error");
    }

    if (result['code'] != 0) {
      return AssertionError(result['message']);
    }

    return result['data'];
  }
}

4.3、使用示例

Future _getNetAppList() async {
  API api = API(api: GTechAPI.getAppList, parameters: {});
  final response = await api.request();

  if (response is Map<String, dynamic>) {
    // TODO 成功
    
  } else {
    // TODO 出错了
    
  }
}

5、多语言

参考:多语言

6、数据库

6.1、pubspec.yaml添加引用

dependencies:
  ...
  path_provider: ^2.0.11
  sqflite: ^2.0.1
  path: ^1.8.0

6.2、工具类DatabaseHelper.dart

import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'dart:async';
import 'dart:io';
import 'package:path_provider/path_provider.dart';

class DatabaseHelper {
  static final _databaseName = "GtechApp.db";
  static final _databaseVersion = 1;

  static final gtAppListTable = 'GtAppList';
  static final gtAppBuildListTable = 'GtAppBuildList';

  // make this a singleton class
  DatabaseHelper._privateConstructor();

  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();

  // only have a single app-wide reference to the database
  static Database? _database;

  Future<Database> get database async {
    if (_database != null) return _database!;
    // lazily instantiate the db the first time it is accessed
    _database = await _initDatabase();
    return _database!;
  }

  // this opens the database (and creates it if it doesn't exist)
  _initDatabase() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, _databaseName);
    return await openDatabase(path,
        version: _databaseVersion, onCreate: _onCreate);
  }

  // SQL code to create the database table
  Future _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE $gtAppListTable (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        appKey TEXT,
        buildKey TEXT,
        buildName TEXT,
        buildIcon TEXT,
        buildVersion TEXT,
        buildVersionNo TEXT,
        buildBuildVersion TEXT,
        buildCreated TEXT,
        buildType TEXT,
        buildIdentifier TEXT,
        buildFileName TEXT,
        buildFileSize TEXT
      )
      ''');

    await db.execute('''
      CREATE TABLE $gtAppBuildListTable (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        appKey TEXT,
        buildKey TEXT,
        buildName TEXT,
        buildIcon TEXT,
        buildVersion TEXT,
        buildVersionNo TEXT,
        buildBuildVersion TEXT,
        buildCreated TEXT,
        buildType TEXT,
        buildIdentifier TEXT,
        buildFileName TEXT,
        buildFileSize TEXT,
        buildEnv TEXT
      )
      ''');
  }

  // Helper methods

  // Inserts a row in the database where each key in the Map is a column name
  // and the value is the column value. The return value is the id of the
  // inserted row.
  Future<int> insert(Map<String, dynamic> row, String table) async {
    Database db = await instance.database;
    try {
      var id = await db.insert(table, row);
      //print('inserted row id in local: $id');
      return id;
    } catch (e) {
      //print("error: $e");
      return -1;
    }
  }

  // All of the rows are returned as a list of maps, where each map is
  // a key-value list of columns.
  Future<List<Map<String, dynamic>>?> queryAllRows(String table) async {
    Database db = await instance.database;
    try {
      var rows = await db.query(table);
      //print('$table query all rows:');
      // rows.forEach((row) => print(row));
      return rows;
    } catch (e) {
      //print("error: $e");
      return null;
    }
  }

  Future<List<Map<String, dynamic>>?> queryRowsWhere(
      String table, Map<String, dynamic> where,
      {int limit = 20, int offset = 0}) async {
    Database db = await instance.database;

    var whereClause = where.keys.map((e) => "$e = ?").join(" AND ");
    var whereArgs = where.values.toList();

    try {
      var rows = await db.query(table,
          where: whereClause,
          whereArgs: whereArgs,
          limit: limit,
          orderBy: "id DESC",
          offset: offset);
      //print('$table query ${rows.length} rows');
      return rows;
    } catch (e) {
      //print("error: $e");
      return null;
    }
  }

  Future<List<Map<String, dynamic>>?> queryRowsWhereOrderBy(
      String table, Map<String, dynamic> where, String orderBy,
      {int limit = 20, int offset = 0}) async {
    Database db = await instance.database;

    var whereClause = where.keys.map((e) => "$e = ?").join(" AND ");
    var whereArgs = where.values.toList();

    try {
      var rows = await db.query(table,
          where: whereClause,
          whereArgs: whereArgs,
          limit: limit,
          orderBy: orderBy,
          offset: offset);
      //print('$table query ${rows.length} rows');
      return rows;
    } catch (e) {
      //print("error: $e");
      return null;
    }
  }

  // All of the methods (insert, query, update, delete) can also be done using
  // raw SQL commands. This method uses a raw query to give the row count.
  Future<Object?> queryRowCount(String table,
      {required Map<String, dynamic> where}) async {
    Database db = await instance.database;
    try {
      if (where != null) {
        var whereClause = where.keys.map((e) => "$e = ?").join(" AND ");
        var whereArgs = where.values.toList();

        var row = await db.query(
          table,
          columns: ["COUNT(*)"],
          where: whereClause,
          whereArgs: whereArgs,
        );
        print('$table row count: $row');
        return row.first.values.first;
      }

      var rowCount = Sqflite.firstIntValue(
          await db.rawQuery('SELECT COUNT(*) FROM $table'));
      print('$table row count: $rowCount');
      return rowCount;
    } catch (e) {
      //print("error: $e");
      return -1;
    }
  }

  // We are assuming here that the id column in the map is set. The other
  // column values will be used to update the row.
  Future<int> update(Map<String, dynamic> row, String table,
      Map<String, dynamic> where) async {
    Database db = await instance.database;
    try {
      var rowsAffected = await db.update(table, row,
          where: '${where.keys.first} = ?', whereArgs: [where.values.first]);
      //print('updated $rowsAffected row(s)');
      return rowsAffected;
    } catch (e) {
      //print("error: $e");
      return -1;
    }
  }

  Future<int> insertOrUpdate(Map<String, dynamic> row, String table,
      Map<String, dynamic> where) async {
    var rowCount = await queryRowCount(table, where: where);
    if(rowCount is int && rowCount > 0) {
      // Update
      return update(row, table, where);
    } else {
      // Insert
      where.forEach((key, value) {
        row[key] = value;
      });
      return insert(row, table);
    }
  }

  // Deletes the row specified by the id. The number of affected rows is
  // returned. This should be 1 as long as the row exists.
  Future<int> delete(String id, String table) async {
    Database db = await instance.database;
    try {
      var rowsDeleted =
          await db.delete(table, where: 'id = ?', whereArgs: [id]);
      //print('deleted $rowsDeleted row(s): row $id');
      return rowsDeleted;
    } catch (e) {
      //print("error: $e");
      return -1;
    }
  }

  // Future<int> truncateTable(String table) async {
  //   Database db = await instance.database;
  //   try {
  //     var truncate = await db.rawQuery("TRUNCATE TABLE $table");

  //     print('truncate table $table is $truncate');
  //     return 1;
  //   } catch (e) {
  //     print("error: $e");
  //     return -1;
  //   }
  // }

  Future<int> deleteAllRows(String table) async {
    Database db = await instance.database;
    try {
      var rowsDeleted = await db.delete(table);
      //print('deleted $rowsDeleted row(s)');
      return rowsDeleted;
    } catch (e) {
      //print("error: $e");
      return -1;
    }
  }
}

6.3、使用示例

static Future<List<AppItem>> getDBAppBuildListWhere(AppItem appItem, Map<String, dynamic> where) async {
  var contactList = await DatabaseHelper.instance.queryRowsWhereOrderBy(
      DatabaseHelper.gtAppBuildListTable,
      {"buildIdentifier": appItem.buildIdentifier},
      "buildBuildVersion DESC");
  
  // TODO Other
  
}

7、代码

Github地址

8、参考资料

网络请求

多语言

sqflite数据库

其他

juejin.cn/post/707860…

juejin.cn/post/707533…

juejin.cn/post/707792…

juejin.cn/post/708779…