項目搭建(上)
一起從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,有需要的同學可以看下。如有好的意見和建議可以提出共同探討。