先給出服務端的架構圖。html
因爲服務端開發有Java、PHP背景,爲了快速完成開發任務,咱們選擇PHP做爲服務端開發語言,順便也把數據庫定爲MySQL。考慮後期擴展和數據庫訪問性能,擬引入Redis非關係型數據庫。同時爲了提升數據讀的性能,在雲服務器和數據庫之間用上緩存,併爲數據庫主從備份、讀寫分離。服務器就不搭建在本地了,管理是一大問題。如今雲服務器一大把,七牛、阿里雲、騰訊雲、百度雲、金山雲等等,技術成熟,並且價格還算公道。在此咱們選擇阿里雲。爲了應對可能面臨的併發問題,雲服務器要考慮負載均衡。項目中可能存在大量的須要上傳和下載照片和視頻,咱們選擇阿里雲的開放存儲服務,同時爲了提高各個地區的下載體驗,咱們引入CDN。客戶端經過API Service和服務端交換數據,圖片和視頻的下載直接經過CDN。android
根據需求和原型設計,可能的模塊劃分以下:git
服務端與客戶端使用JSON交換數據,使用自定義JSON格式,約定返回code、message,實體封裝在result中,支持單個實體、實體列表、多個實體列表,定義以下:github
{ "code":500, "message":"系統異常,請稍後重試", "result":"" }
{ "code":200, "message":"登陸成功", "result":{ "user":{ "userId":1, "nickName":"Leo", "email":"Leo@xxx.com", "gender":0 } } }
{ "code":200, "message":"SUCCESS", "result":{ "album":{ "kid":{ "kidId":1, "nickName":"LEE", "gender":1, "birthday":"2014-3-6",
...... }, "media":{ "mediaId":123, "mediaType":1, "createdTime":193743546746, "mediaDescription":"這是小孩第一次出去春遊",
...... } } }
}
主要API接口設計以下:數據庫
http://api.xxx.com/service/v1.0/user/login http://api.xxx.com/service/v1.0/user/third-login http://api.xxx.com/service/v1.0/user/register http://api.xxx.com/service/v1.0/user/logout http://api.xxx.com/service/v1.0/user/info/update http://api.xxx.com/service/v1.0/album/upload http://api.xxx.com/service/v1.0/album/update http://api.xxx.com/service/v1.0/album/delete ......
也許你看到了,API作了二級域名映射,同時爲了服務端後期API版本的升級管理,在URL中加上了版本標識V1.0。命名方面我儘可能作到restful的風格。對了,此處沒有使用Https。爲了解決數據傳輸的安全,我作了點特別的處理:對請求體和響應結果進行RSA加密(若是服務端返回的數據稍稍過大,這個RSA嚴重影響客戶端解密,後來我換成了AES),全部請求爲POST請求,因此API URL後面沒有帶參數,你也看不到任何請求相關的信息。api
根據需求和原型設計,數據庫的設計大概須要兩週時間。其實一週基本搞定了,但爲了考慮充分,留出一週時間來檢驗和調整。數據庫E-R圖略。緩存
Android自己就是MVC,因此我不打算引入MVP或MVVM。個人理念是職責分層,快速推出Android 1.0。主要的包結構以下:安全
工程的搭建和包的劃分有各類各樣的,適合本身的就好了。想討論或想看別人怎麼作的,點擊這裏:App工程結構搭建:幾種常見Android代碼架構分析服務器
註冊登陸,我的信息,個人小孩,相冊管理,消息通知,系統設置等等。restful
重複發明輪子是不可取的。有些模塊根本不必本身寫。如下是引入的第三方庫,以及優點說明。
public interface DataCallback { void onSuccess(Object result); void onFailure(Object result); }
先看一下登陸的序列圖:
HttpManager類負責調用AsyncHttpWrapper中的post方法,和對服務端返回的數據解密、JSON轉對象、回調上層;AsyncHttpWrapper則負責請求體的封裝加密和其它的校驗參數封裝。看一下HttpManager類的post方法:
public void post(Context context, String url, RequestParams params, final String modelName, final DataCallback callback) { AsyncHttpWrapper.post(context, url, params, new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { try { if (modelName != null) { handleResponse(responseBody, callback, modelName); } else { String response = new String(responseBody); // 解密 response = AES128.getInstance().decrypt(AppUtil.decodeReplace(response)); // JSON轉對象 BaseMessage message = AppUtil.getMessage(response); if (callback != null) { // 若是自定義code是200 if (Coder.CODE_200.equals(message.getCode())) { callback.onSuccess(message.getMessage()); } else { callback.onFailure(new ServerError(message.getCode())); } } } } catch (JSONException e) { LogUtil.e(e); callback.onFailure("服務端返回的數據不能解析成JSON"); } catch (Exception e) { LogUtil.e(e); callback.onFailure(e); } } @Override public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) { if (callback != null) { callback.onFailure(error); if (responseBody != null) { String s = new String(responseBody); LogUtil.e(s); } } } }); }
AsyncHttpWrapper中的post方法
public static void post(Context context, String url, RequestParams params, AsyncHttpResponseHandler responseHandler) { // 設置請求頭部信息 generateHeader(context); // 加密請求參數 String encryParams = AES128.getInstance().encrypt(params.toString()); RequestParams requestParams = new RequestParams(); requestParams.put("param", AppUtil.encodeReplace(encryParams)); client.post(context, url, requestParams, responseHandler); }
private static AsyncHttpClient client = new AsyncHttpClient();
爲了加快開發速度,重用代碼,Adapter的使用有技巧。每次在getView中查找控件id、利用ViewHolder、賦值,最後返回convertView,看着都是差很少的代碼。是時候脫離這個苦海了。先看怎麼解決共用的ViewHolder問題。
public static <T extends View> T get(View view, int id) { SparseArrayCompat<View> viewHolder = (SparseArrayCompat<View>) view.getTag(); if (viewHolder == null) { viewHolder = new SparseArrayCompat<>(); view.setTag(viewHolder); } View childView = viewHolder.get(id); if (childView == null) { childView = view.findViewById(id); viewHolder.put(id, childView); } return (T) childView; }
ViewHolder的做用,就是經過convertView.setTag與convertView進行綁定。當convertView複用時,直接從與之對應的ViewHolder(getTag)中拿到convertView佈局中的控件,省去了findViewById的時間。上面的代碼就是這樣的原理。
而後就是CommonAdapter了。
public abstract class CommonAdapter<T> extends BaseAdapter { protected LayoutInflater inflater; protected Context context; protected List<T> datas; protected final int itemLayoutId; public CommonAdapter(Context context, List<T> datas, int itemLayoutId) { this.context = context; this.inflater = LayoutInflater.from(context); this.datas = datas; this.itemLayoutId = itemLayoutId; } @Override public int getCount() { return datas != null ? datas.size() : 0; } @Override public T getItem(int position) { return datas.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { final CommonViewHolder viewHolder = getViewHolder(position, convertView, parent); convert(viewHolder, getItem(position), position); return viewHolder.getConvertView(); }
public abstract void convert(CommonViewHolder viewHolder, T item, int position); }
好了,不貼代碼了。看不明白了請點擊這裏:Android 快速開發系列 打造萬能的ListView GridView 適配器
如AES128加密類、BitmapUtils、SecurePreferences、StringUtil、ToastUtil、IOUtil等等。
爲了保證數據交換、加解密正常,首先對某一個接口進行測試,以驗證API Service能正常跑通。好比能夠先對登陸進行模擬測試,看是否成功,同時包括異常的測試,服務端是否是處理了邊界異常,返回給客戶端的都是封裝過的異常信息,而不是拋一個敏感信息給客戶端。提早進行接口測試有助於咱們的基礎組件運行沒問題,方便後期其它模塊的快速集成。
基礎組件封裝好後,除了少許的從網絡獲取數據邏輯和本地數據庫的增刪改查,客戶端基本上就是界面的佈局工做了。界面開發基本看熟練程度和自定義View的重用。
好了,基本就這些。兩個Android開發人員兩個月內完成確定是能夠的,前提是至少有一個熟手。後面再談談 MVP,畢竟這個客戶端設計無法進行單元測試,若是業務邏輯愈來愈複雜,Activity的職責會愈來愈重,問題多多,不利於後期維護。