一個網際網路app的開發設計(技術選型和架構)
在做一個網際網路應用時, 要考慮技術選型和架構搭建。 先說說技術選型, 以丁丁租房為例在開發時會面對如下問題:
1、圖片處理, image loader、picasso、Glide、Fresco, 推薦使用fresco,因為它使用三級快取、佔記憶體更小;
2、http通訊, 開源框架有很多例如volley,retrofit,okhttp等等, 用法都很簡單也類似, 推薦使用OkHttp,它支援SPDY;
3、崩潰日誌採集, 免費庫也有很多, 騰訊bugly、友盟、Fabric。我們使用的是Fabric, 挺好用的。
4、即時聊天, 三方庫也有很多, 就不多說了。 我們用的是leancloud, 因為它免費:), 而且效果還可以。
5、控制元件, 根據UI需要可以自己寫或用別人寫好的三方庫, 例如Materialdialog,MPAndroidChart, WheelView等等。
6、程序內部訊息傳遞, 例如跟Service互動的binder、或者LocalbroadcastReceiver(基於主執行緒handler實現的,所以不會被其他程序收到,比較安全!)、三方庫EventBus和觀察者模式等等。
7、支付, 可以用支付寶、微信的介面。
8、埋點, 這個就很重要了, 產品經理每天都在盯著這個統計資料, 用他們的話叫做資料引導決策, 有時前端也會弄ABTest, 目的是做出讓使用者更喜歡的東西。 我們用了Countly, 因為它開源, 後臺可以自己做, 埋點資料可以傳到自己的伺服器。 畢竟一些敏感的資料是不想讓三方如友盟知道的。
9、bug熱修復, 目前有很多技術方案, 我們參考了HotFix的方式在專案中落地, 網上有人說適配有問題, 但我們還沒碰到。。。
10、外掛化, 應用典型案例就是支付寶、微信。當app功能非常多、程式碼量很大(超過65535個方法)時要考慮, 我們是計劃實踐一下, 但因為公司突然倒閉, 沒來得及做。
11、程式框架, 對比mvc、mvp和mvvm模式, 我覺得mvp模式比較合適並落地到丁丁租房app裡, 因為mvp很好的實現了程式碼解耦、邏輯分層, 下層對上層透明, 每層只關心自己那點事情就夠了。
12、UI標註,推薦使用標你妹啊網站、app.zeplin.io網站。
下面丁丁租房架構圖, 最上面一層是各個功能。
程式碼結構如下, 按照業務劃分一級目錄, 二級目錄是按照android各元件區分。
從二級目錄看到activity例項化presenter並儲存該引用, presenter通過Activity傳進來的interface回撥到Activity, presenter實現網路介面。
下面附乾貨原始碼, 供參考:
Model程式碼:
/** * http網路相關工具類, 目前使用三方庫OkHttp庫 */ public class NetworkUtils { //網路介面執行週期打點 private static class DotNet { public String tag; //網路介面的標籤 public long timestamp; //時間戳 public String uuid; } private static OkHttpClient sInstance; //初始化OkHttp例項 static { sInstance = new OkHttpClient(); sInstance.setConnectTimeout(10, TimeUnit.SECONDS); //連線超時時間 sInstance.setWriteTimeout(10, TimeUnit.SECONDS); //上傳檔案超時時間 sInstance.setReadTimeout(20, TimeUnit.SECONDS); //下載檔案超時時間 } /** * 格式化請求body * * @param object, 查詢用的引數,Map or entity */ private static RequestBody formatRequestBody( Context context, Object object) { //JSON.toJSONString(object)方法預設執行去除value為null的欄位 /*Iterator iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = (Map.Entry) iterator.next(); //編輯房源時可以傳空字串 if (entry.getValue() == null) { iterator.remove(); } }*/ LinkedHashMap baseMap = GatewayUtils.getInstance().getBase(context); //獲取base引數 Iterator it = baseMap.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); if (entry.getValue() == null || StringUtils.isNull(entry.getValue().toString())) { it.remove(); } } String param = JSON.toJSONString(object); //生成param引數 String sign = ParamBuild.getSign(object, baseMap); //計算sign引數 baseMap.put("sign", sign); String base = JSON.toJSONString(baseMap); RequestBody formBody = new FormEncodingBuilder() .add("base", base) .add("param", param) .build(); return formBody; } /** * 格式化請求body * * @param object, 查詢用的引數,Map or entity * @param dotNet, 打點要用的引數 */ private static RequestBody formatRequestBodyExt( Context context, Object object, DotNet dotNet) { //JSON.toJSONString(object)方法預設執行去除value為null的欄位 /*Iterator iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = (Map.Entry) iterator.next(); //編輯房源時可以傳空字串 if (entry.getValue() == null) { iterator.remove(); } }*/ LinkedHashMap baseMap = GatewayUtils.getInstance().getBase(context); //獲取base引數 Iterator it = baseMap.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); if (entry.getValue() == null || StringUtils.isNull(entry.getValue().toString())) { it.remove(); } } try { //網路介面執行週期打點時的引數 String uuid = (String) baseMap.get("uuid"); dotNet.timestamp = System.currentTimeMillis(); dotNet.uuid = uuid; } catch (Exception ex) { ex.printStackTrace(); } String param = JSON.toJSONString(object); //生成param引數 String sign = ParamBuild.getSign(object, baseMap); //計算sign引數 baseMap.put("sign", sign); String base = JSON.toJSONString(baseMap); RequestBody formBody = new FormEncodingBuilder() .add("base", base) .add("param", param) .build(); return formBody; } /** * 與伺服器非同步互動, 並返回結果字串 * * @param ctx 應用上下文 * @param url 伺服器url地址 * @param map 查詢引數 * @param tag 標籤,用於取消請求 * @param listener 回撥 */ public static void asyncWithServer(final Context ctx, String url, Map map, String tag, final OnNetworkListener listener) { if (ctx == null || url == null || listener == null || tag == null || map == null ) { return; } DotNet dotNet = new DotNet(); dotNet.tag = tag; final RequestBody requestBody = formatRequestBodyExt(ctx, map, dotNet); //組織請求包體 Request request = new Request.Builder() .tag(dotNet) .url(url) .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") .post(requestBody) .build(); sInstance.newCall(request).enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { DotNet dotTag = (DotNet)request.tag(); listener.onFailure(dotTag.tag); } @Override public void onResponse(Response response) throws IOException { DotNet dotTag = (DotNet)response.request().tag(); if (!response.isSuccessful()) { //失敗 listener.onFailure(dotTag.tag); } else { //Countly打點, 統計介面執行週期 try { dotInterfaceTag(ctx, dotTag.timestamp, dotTag.uuid, response.request().urlString()); } catch (Exception ex) { ex.printStackTrace(); } String jsonBody = response.body().string(); //新增json解析異常邏輯,https://fabric.io/cb5ab5/android/apps/com.dingding.client/issues/561cf641f5d3a7f76bc3e0df Map<String, Object> map = null; try { map = JSON.parseObject(jsonBody); } catch (JSONException ex) { ex.printStackTrace(); } finally { if (map == null || map.size() == 0) { listener.onFailure(dotTag.tag); //解析失敗時按照介面失敗處理 return; } } //更新token, 可能引起效能問題, 相當於做了2次json解析 if (map.containsKey("token")) { String token = (String) map.get("token"); GatewayUtils.getInstance().setToken(ctx, token); } Integer code = (Integer) map.get("code"); if (code == 100016) {//code為100016時為token過期 重新登入 GatewayUtils.getInstance().setDefult(ctx); Intent intent = new Intent(); intent.setAction("com.dingding.client.NewLoginActivity"); intent.putExtra("code", code); intent.putExtra("from", "2"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ctx.startActivity(intent); } listener.onSuccess(jsonBody, dotTag.tag); } } }); } /** * 與伺服器非同步互動, 並返回類物件 * * @param ctx 應用上下文 * @param url 伺服器url地址 * @param map 查詢引數 * @param tag 標籤,用於取消請求 * @param listener 回撥 * @param clz, 解析結果類 可以傳入null null是為了考慮有些返回值沒有data節點 * @param isList ,傳入true則返回解析為列表,傳入false則返回為物件 */ public static void asyncWithServerExt(Context ctx, String url, Map map, String tag, final OnNetworkListener listener, final Class<?> clz, final boolean isList) { asyncWithServerExt(ctx, url, map, tag, listener, clz, isList, null); } /** * 與伺服器非同步互動, 並返回類物件 * * @param ctx 應用上下文 * @param url 伺服器url地址 * @param object 查詢引數,Map or entity * @param tag 標籤,用於取消請求 * @param listener 回撥 * @param clz, 解析結果類 可以傳入null null是為了考慮有些返回值沒有data節點 * @param isList ,傳入true則返回解析為列表,傳入false則返回為物件 */ public static void asyncWithServerExt(final Context ctx, String url, Object object, String tag, final OnNetworkListener listener, final Class<?> clz, final boolean isList, final String key) { if (ctx == null || url == null || listener == null || tag == null || object == null ) { return; } DotNet dotNet = new DotNet(); dotNet.tag = tag; RequestBody requestBody = formatRequestBodyExt(ctx, object, dotNet); //組織請求包體, 會修改時間戳和uuid Request request = new Request.Builder() .tag(dotNet) .url(url) .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") .post(requestBody) .build(); sInstance.newCall(request).enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { DotNet dotNet = (DotNet) request.tag(); listener.onFailure(dotNet.tag); } @Override public void onResponse(Response response) throws IOException { DotNet dotTag = (DotNet) response.request().tag(); if (!response.isSuccessful()) { //失敗 listener.onFailure(dotTag.tag); } else { //Countly打點, 統計介面執行週期 try { dotInterfaceTag(ctx, dotTag.timestamp, dotTag.uuid, response.request().urlString()); } catch (Exception ex) { ex.printStackTrace(); } //json解析並回調 DataFactory factory = new ResultObjectFactory(); ResultObject result; if (clz == null) { result = factory.createResultObjectNoCls(ctx, response.body().string()); } else { result = factory.createResultObject(ctx, isList, response.body().string(), clz, key); } if (result == null) { listener.onFailure(dotTag.tag); } else { listener.onSuccessExt(result, dotTag.tag); } } } }); } /** * Countly打點計算每個介面的執行週期 * @param url, 請求的url */ private static void dotInterfaceTag(Context ctx, long timestamp, String uuid, String url) { long diff = Math.abs(System.currentTimeMillis() - timestamp); HashMap<String, Object> paramMap = new HashMap<>(); paramMap.put("uuid", uuid); paramMap.put("time", diff + ""); paramMap.put("url", url); Log.d("NetPerformance", "url:" + url + ", diff:" + diff); Statistics.dotNetPerformance(ctx, paramMap); } /** * 與伺服器非同步互動, 並返回類物件 * * @param ctx 應用上下文 * @param url 伺服器url地址 * @param map 查詢引數 * @param tag 標籤,用於取消請求 * @param listener 回撥 * @param clz, 解析結果類 可以傳null */ public static void asyncWithServerExt(Context ctx, String url, Map map, String tag, final OnNetworkListener listener, final Class<?> clz) { asyncWithServerExt(ctx, url, map, tag, listener, clz, false); } /** * 與伺服器同步互動, 必須在非UI執行緒執行! */ public static String syncWithServer( Context ctx, String url, Map map) { if (ctx == null || url == null || map == null) { return ""; } String retStr = null; //伺服器返回的包體 RequestBody requestBody = formatRequestBody(ctx, map); //組織請求包體 Request request = new Request.Builder() .url(url) .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") .post(requestBody) .build(); Response response; try { response = sInstance.newCall(request).execute(); if (response != null && response.isSuccessful()) { retStr = response.body().string(); //包體 } } catch (IOException ex) { ex.printStackTrace(); } finally { } return retStr; } //返回ResultObject public static ResultObject syncWithServer( Context ctx, String url, Map map, final Class<?> clz) { RequestBody requestBody = formatRequestBody(ctx, map); //組織請求包體 Request request = new Request.Builder() .url(url) .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") .post(requestBody) .build(); ResultObject result; Response response; DataFactory factory; try { response = sInstance.newCall(request).execute(); factory = new ResultObjectFactory(); result = factory.createResultObject(ctx, false, response.body().string(), clz, null); } catch (IOException ex) { result = new ResultObject(); result.setCode(-1); result.setSuccess(false); result.setMessage("網路錯誤"); } return result; }
Presenter程式碼:
/** * Presenter基類 */ public abstract class BasePresenter { public HashMap<String, Object> mKeyMap = new HashMap<String, Object>(); //請求的引數 public HashMap<String, Object> mFilterMap = new HashMap<String, Object>(); //請求的引數 public Handler mHandler = new Handler(); /** * UI主執行緒handler, 用於更新view */ protected HashSet<String> mTagList = new HashSet<String>(); public IBaseView mIView; /** * 與後臺互動時的引數 * * @return */ public abstract HashMap<String, Object> getParams(); /** * 獲取應用上下文 */ public abstract Context getContext(); /** * 設定應用上下文, 使用ApplicationContext */ public abstract void setContext(Context ctx); /** * 設定tag, 用於刪除佇列中的網路請求, 也可以區分不同的介面 * * @param tag */ public abstract void setTag(String tag); /** * 獲取tag, 用於刪除佇列中的網路請求,也可以區分不同的介面 */ public abstract String getTag(); /** * 獲取回撥函式的引用, 不同業務的邏輯不同 */ public abstract OnNetworkListener getListener(); /** * 設定View引用,派生類可以擴充套件IBaseView介面類, 在activity裡實例化,並通過強制類轉換賦值。 */ public void setView(IBaseView view) { mIView = view; } /** * 獲取View引用, 派生類可以擴充套件IBaseView介面類,在activity裡實例化,並通過強制類轉換賦值。 */ public IBaseView getView() { return mIView; } /** * 在ui主執行緒執行更新介面的操作 */ public void updateUI(Runnable runnable) { mHandler.post(runnable); } /** * 清空引數 */ public void resetParams() { mKeyMap.clear(); mFilterMap.clear(); } /** * 與後臺互動, 派生類可以例項化回撥函式OnNetworkListener。 * 回撥函式會執行onSuccess(String body) * * @param url, 網址 * @param listener, model層的回撥。 為空時使用getListener() * @return boolean, 是否成功傳遞到model層 */ protected boolean asyncWithServer(String url, OnNetworkListener listener) { String tag = getTag(); Context ctx = getContext(); OnNetworkListener callback; if (listener == null) { callback = getListener(); } else { callback = listener; } if (tag == null || callback == null || ctx == null) { return false; } mTagList.add(tag); NetworkUtils.asyncWithServer( ctx, url, getParams(), tag, callback); return true; } /** * 與後臺互動, 派生類可以例項化回撥函式OnNetworkListener。 * 回撥函式會執行onSuccess(ResultObject result) * * @param url, 網址 * @param clz, 解析的類名 * @param listener, model層的回撥。 為空時使用getListener() * @return boolean, 是否成功傳遞到model層 */ protected boolean asyncWithServerExt(String url, Class<?> clz, OnNetworkListener listener) { return asyncWithServerExt(url, clz, listener, false); } /** * 與後臺互動, 派生類可以例項化回撥函式OnNetworkListener。 * 回撥函式會執行onSuccess(ResultObject result) * * @param url, 網址 * @param clz, 解析的類名 * @param listener, model層的回撥。 為空時使用getListener() * @param isList ,傳入true則返回解析為列表,傳入false則返回為物件 * @return boolean, 是否成功傳遞到model層 */ protected boolean asyncWithServerExt(String url, Class<?> clz, OnNetworkListener listener, final boolean isList) { return asyncWithServerExt(url, clz, listener, isList, null); } /** * 與後臺互動, 派生類可以例項化回撥函式OnNetworkListener。 * 回撥函式會執行onSuccess(ResultObject result) * * @param url, 網址 * @param clz, 解析的類名 * @param listener, model層的回撥。 為空時使用getListener() * @param isList ,傳入true則返回解析為列表,傳入false則返回為物件 * @return boolean, 是否成功傳遞到model層 */ protected boolean asyncWithServerExt(String url, Class<?> clz, OnNetworkListener listener, final boolean isList, final String key) { String tag = getTag(); Context ctx = getContext(); OnNetworkListener callback; if (listener == null) { callback = getListener(); } else { callback = listener; } if (tag == null || callback == null || ctx == null) { return false; } mTagList.add(tag); NetworkUtils.asyncWithServerExt( ctx, url, getParams(), tag, callback, clz, isList, key); return true; } /** * 釋放當前presenter的請求 * 在activity或fragment的onDestory函式中呼叫 */ public void cancelRequests() { for (Iterator<String> iterator = mTagList.iterator(); iterator.hasNext(); ) { String tag = iterator.next(); NetworkUtils.cancelRequestByTag(tag); } }