项目搭建(上)
一起从0开始Flutter实战!
作为Flutter实战的开篇,我们需要介绍下我们要做的内容以及我们的准备工作,为了能让我们的实战顺利进行需要一个开放的API接口服务平台,选择了半天最终选择了一个开发者平台,感谢玩安卓的开发者提供的开放API,可以让我们在练习一些项目的时候使用。玩安卓的开放API提供了很多的功能,具体的功能可以参照玩安卓API。
确定了我们要做什么我们就可以着手准备了,我们先把项目的结构分为哪些模块进行设计。
lib
-constants //存放一些常量
-events //跨界面的事件传递
-model //解析的数据和数据库数据
-network //网络请求以及网络配置
-pages //单独页面的集合
-route //页面的路由配置,避免多出配置导致的混乱
-utils //一些工具类
-widgets //小的工具组件,单独的widget
整体的一个结构就出来了,根据习惯我们还是先准备一些工具类,和网络的请求,先把与业务无关的工具准备好,这样让我们以后开始的时候更加的顺畅。网络的三方库配置在以前已经有介绍,不再多说了,我们当时也说在以后的实践中是需要进行一个封装的,我们一起来研究下如何能做一个合理的封装:
我选择的是创建一个单例的Http配置类,这样我们可以对Dio进行一次配置通用的效果:
class HttpConfig {
static String BASE_URL = "";
static HttpConfig _config;
static HttpConfig get instance => _getInstance();
Dio dio;
HttpConfig._internal() {
var baseOption = BaseOptions(
connectTimeout: 6000,
sendTimeout: 3000,
baseUrl: BASE_URL,
headers: HttpHeaderConfig.getHeaderConfig(),//这里使用的一个方法而不是一个变量来配置的header,因为有些header我们会放到sharePerence里,所以我们使用一个方法来可以随时进行读取
);
dio = Dio(baseOption);
dio.interceptors.add(InterceptorsWrapper(//这里创建一个拦截器,拦截器在我们根据需要进行消息的拦截
onRequest: (option) {},
onResponse: (response) {
if (response.statusCode == 400 || response.statusCode == 404) {}//这里拦截了错误的状态的返回
},
onError: (error) {}));
}
static HttpConfig _getInstance() {
if (_config == null) {
_config = HttpConfig._internal();
}
return _config;
}
//这里创建一个get请求的方法,不是直接把dio做成公用而是提供get方法是为了以后如果我们替换整个网络请求库的时候方便更换,还有一个原因,是我们在一般请求的时候需要对参数进行签名,这里可以帮助我们做统一的处理。
void get<T>(String path,
{Map<String, dynamic> params,
Function(T t) onSuccess, //这里对返回结果已经做了处理也是为了以后如果网络框架修改后能低成本迁移
Function(int error) onError}) async {
Response response;
try {
response = await dio.get(path, queryParameters: params);
if (response.statusCode == 200 && onSuccess != null) {//先做过滤,这样我们在处理的时候就省去了很多重复代码
onSuccess(response.data);
} else if (onError != null) {
onError(response.statusCode); //如果状态码不对,则返回错误数据
}
} catch (e) {
if (onError != null && response != null) {
onError(-1); //如果抛出了异常,则返回错误数据
}
}
}
//参照get的说明
void post<T>(String path,
{Map<String, dynamic> data,
Function(T t) onSuccess,
Function(int error) onError}) async {
Response response;
try {
response = await dio.post(path, data: data);
if (response.statusCode == 200 && onSuccess != null) {
onSuccess(response.data);
} else if (onError != null) {
onError(response.statusCode);
}
} catch (e) {
if (onError != null && response != null) {
onError(-1);
}
}
}
}
创建的HttpHeader的方法:
class HttpHeaderConfig {
static Map<String, dynamic> getHeaderConfig() {
Map<String,dynamic> config = Map<String,dynamic>();
config[""] = "";//这里我们堆放我们的header,如果需要从数据库或者sharePerence中获取数据可以在这里获取
return config;
}
}
网络请求的方法我们就算是封装完了,然后我们可能还需要将网络返回的数据进行一个保存,我们先将sharePreferences进行一个封装。
sharePreference的第三方引入前面也有介绍过了,我们可以看下前面是如何进行引入的。然后我们对SharedPreferences进行封装:这里的封装参照了阿里开源的Flutter-Go,
//这里也是创建了一个单例的SharedPreferences
class SharedPrefrencesUtils {
static SharedPrefrencesUtils _prefrencesUtils;
static SharedPreferences _sharedPerence;
static Future<SharedPrefrencesUtils> get instance async {
return await _getInstance();
}
//因为SharedPreferences的初始化是耗时操作,需要在读取的时候需要添加await关键字,这也是这里的单例与网络请求有些许不同的原因。
static Future<SharedPrefrencesUtils> _getInstance() async {
if (_prefrencesUtils == null) {
_prefrencesUtils = SharedPrefrencesUtils._internal();
}
if (_sharedPerence == null) {
await _prefrencesUtils._init();
}
return _prefrencesUtils;
}
Future _init() async {
_sharedPerence = await SharedPreferences.getInstance();
}
SharePrefrencesUtils._internal() {}
//检查是否存在某个Key
bool hasKey(String key) {
if (_beforeCheck() || key == null) {
return false;
}
return _sharedPerence.containsKey(key);
}
//存放字符串
Future<bool> putString(String key, String value) {
if (_beforeCheck()) return null;
return _sharedPerence.setString(key, value);
}
Future<bool> putBool(String key, bool value) {
if (_beforeCheck()) return null;
return _sharedPerence.setBool(key, value);
}
Future<bool> putDouble(String key, double value) {
if (_beforeCheck()) return null;
return _sharePerence.setDouble(key, value);
}
Future<bool> putInt(String key, int value) {
if (_beforeCheck()) return null;
return _sharePerence.setInt(key, value);
}
String getString(String key) {
if (_beforeCheck()) return null;
return _sharedPerence.getString(key);
}
bool getBool(String key) {
if (_beforeCheck()) return null;
return _sharedPerence.getBool(key);
}
double getDouble(String key) {
if (_beforeCheck()) return null;
return _sharedPerence.getDouble(key);
}
int getInt(String key) {
if (_beforeCheck()) return null;
return _sharedPerence.getInt(key);
}
static bool _beforeCheck() {
if (_sharedPerence == null) {
return true;
}
return false;
}
}
只是有SharedPreferences还不能满足我们现在APP的需要,我们还需要使用数据库来帮助我们存储更多的数据内容,数据库的引入在前面的介绍中我们也都已经进行了介绍,现在把数据库来进行一个封装:
class DataBaseUtils {
static DataBaseUtils _dataBaseUtils;
static Database _dataBase;
static String _DATABASE_PATH = "exercise_db.db";
static int _DATABASE_VERSION = 1;
static String _CREATE_TABLE =
"create table my_table (id integer primary key,userid text,password text)";
static Future<DataBaseUtils> get instance async {
return await _getInstance();
}
//因为DataBase的初始化是耗时操作,需要在读取的时候需要添加await关键字,这也是这里的单例与网络请求有些许不同的原因。
static Future<DataBaseUtils> _getInstance() async {
if (_dataBaseUtils == null) {
_dataBaseUtils = DataBaseUtils._internal();
}
if (_dataBase == null) {
await _dataBaseUtils._init();
}
return _dataBaseUtils;
}
Future _init() async {
_dataBase = await openDatabase(_DATABASE_PATH, version: _DATABASE_VERSION,
onCreate: (dataBase, version) {
if (version == 1 && dataBase.isOpen) {
dataBase.execute(_CREATE_TABLE);
}
}, onUpgrade: (dataBase, oldVersion, newVersion) {
//这里预留了更新的操作,在version为1的时候并不会触发
if (oldVersion == 1 && newVersion == 1 && dataBase.isOpen) {}
});
}
///插入数据
///tableName 需要插入的表名
///params 需要插入的数据
Future<int> insert(String tableName, Map<String, dynamic> params) async {
if (_dataBase != null && _dataBase.isOpen) {
var dataId = await _dataBase.insert(tableName, params);
return dataId;
}
return 0;
}
///删除数据
/// tableName 需要查的表名
/// conditions 查询的条件
/// mod 查询的模式,是否是用And连接起来的查询条件,可选['And','Or']
Future<int> delete(String tableName,
{Map<String, dynamic> conditions, String mod = "And"}) async {
return await _dataBase.delete(tableName,
where: _formatCondition(conditions, mod: mod));
}
///更新数据
/// tableName 需要查的表名
/// values 需要更新的数据
/// conditions 查询的条件
/// mod 查询的模式,是否是用And连接起来的查询条件,可选['And','Or']
Future<int> update(String tableName,
{Map<String, dynamic> values,
Map<String, dynamic> conditions,
String mod = "And"}) async {
return await _dataBase.update(tableName, values,
where: _formatCondition(conditions, mod: mod));
}
/// tableName 需要查的表名
/// conditions 查询的条件
/// mod 查询的模式,是否是用And连接起来的查询条件,可选['And','Or']
Future<List> query(String tableName,
{Map<String, dynamic> conditions, String mod = "And"}) async {
if (_dataBase != null && _dataBase.isOpen) {
return await _dataBase.query(tableName,
where: _formatCondition(conditions, mod: mod));
}
return null;
}
DataBaseUtils._internal() {}
///格式化数据,将条件组合格式化为String
String _formatCondition(Map<String, dynamic> conditions,
{String mod = "And"}) {
var conditionStr = "";
var index = 0;
conditions.forEach((key, value) {
if (value == null) {
return;
}
if (value.runtimeType == String) {
conditionStr = '$conditionStr $key like $value';
}
if (value.runtimeType == int ||
value.runtimeType == double ||
value.runtimeType == bool) {
conditionStr = '$conditionStr $key = $value';
}
if (index >= 0 && index < conditions.length - 1) {
conditionStr = '$conditionStr $mod';
}
index++;
});
return conditionStr;
}
void close(){
if(_dataBase != null && _dataBase.isOpen){
_dataBase.close();
}
}
}
现在与我们数据请求相关的内容就封装好了,从网络请求到数据的持久化都已经准备好了,下面我们准备一下资源的管理。
项目已经同步更新到了GitHub,有需要的同学可以看下。如有好的意见和建议可以提出共同探讨。