基于Android和Java后臺(tái)的朋友圈的設(shè)計(jì)和實(shí)現(xiàn)

我的CSDN: ListerCi
我的簡(jiǎn)書(shū): 東方未曦

前言

這是秋招前做的一個(gè)應(yīng)用似舵,當(dāng)時(shí)是想通過(guò)一個(gè)完整的項(xiàng)目來(lái)向面試官展現(xiàn)項(xiàng)目設(shè)計(jì)能力和實(shí)戰(zhàn)能力绅喉,不過(guò)直到秋招最后很多面試官都沒(méi)問(wèn)這個(gè)禽额,想來(lái)是他們覺(jué)得一個(gè)應(yīng)屆生做的東西也不會(huì)包含什么高深的技術(shù)吧下硕。╮(╯▽╰)╭
在本次應(yīng)用的開(kāi)發(fā)過(guò)程中也遇到了很多問(wèn)題线梗,基本都是通過(guò)查閱博客解決的缚俏,在此感謝各位分享技術(shù)的博客大佬惊搏。因此現(xiàn)在我也將應(yīng)用的實(shí)現(xiàn)過(guò)程記錄下來(lái),希望可以幫助到有需要的人忧换。如果博文里有沒(méi)有解釋清楚的地方可以在評(píng)論區(qū)告知恬惯,我會(huì)再做補(bǔ)充。

一亚茬、系統(tǒng)截圖及說(shuō)明

1. 首頁(yè)

首頁(yè)主要顯示背景圖和好友動(dòng)態(tài)酪耳,采用CoordinatorLayout布局來(lái)實(shí)現(xiàn)類(lèi)似微信朋友圈的效果,上方顯示用戶(hù)自己設(shè)置的背景圖刹缝,當(dāng)用戶(hù)在首頁(yè)向下滑動(dòng)時(shí)會(huì)逐漸關(guān)閉背景圖并顯示好友動(dòng)態(tài)碗暗,向上滑動(dòng)到頂部時(shí)又會(huì)逐漸顯示背景圖。
背景圖下方是可以左右滑動(dòng)的“圈內(nèi)動(dòng)態(tài)”和“公開(kāi)動(dòng)態(tài)”標(biāo)簽梢夯,本來(lái)的想法是如果發(fā)表圈內(nèi)動(dòng)態(tài)言疗,只有好友可見(jiàn);如果發(fā)表公開(kāi)動(dòng)態(tài)颂砸,則所有用戶(hù)可見(jiàn)噪奄。不過(guò)最后沒(méi)有實(shí)現(xiàn)公開(kāi)動(dòng)態(tài)的功能,只有圈內(nèi)動(dòng)態(tài)人乓,因此必須是好友才能互相查看動(dòng)態(tài)勤篮。

主頁(yè).gif

2. 動(dòng)態(tài)詳情

動(dòng)態(tài)詳情包括文字及圖片,系統(tǒng)設(shè)定最多支持6張圖片色罚。在動(dòng)態(tài)詳情頁(yè)下方還有點(diǎn)贊數(shù)量以及其他用戶(hù)的評(píng)論碰缔。如果當(dāng)前用戶(hù)點(diǎn)贊了這條動(dòng)態(tài),點(diǎn)贊圖標(biāo)就會(huì)變?yōu)榧t色戳护。

動(dòng)態(tài)詳情.jpg

3. 好友

點(diǎn)擊屏幕下方的“好友”選項(xiàng)卡可以查看用戶(hù)的所有好友金抡。

好友列表.jpg

在該頁(yè)面點(diǎn)擊上方的“查找用戶(hù)”欄跳轉(zhuǎn)到搜索頁(yè)面,輸入用戶(hù)的賬號(hào)可以進(jìn)行精確查詢(xún)姑尺,輸入用戶(hù)名可以進(jìn)行模糊查詢(xún)竟终。

好友搜索.jpg

點(diǎn)擊“好友請(qǐng)求”欄可以查看其它用戶(hù)發(fā)送的好友請(qǐng)求,點(diǎn)擊同意之后切蟋,該請(qǐng)求的狀態(tài)就會(huì)變?yōu)椤耙淹狻薄?/p>

好友請(qǐng)求列表.jpg

4. 個(gè)人界面

點(diǎn)擊屏幕下方的“我”切換到個(gè)人頁(yè)面。

個(gè)人界面.jpg

點(diǎn)擊個(gè)人界面中的“設(shè)置”欄可以前往修改頭像榆芦、用戶(hù)名和個(gè)性簽名柄粹。

設(shè)置頁(yè)面.jpg

5. 發(fā)表動(dòng)態(tài)

首頁(yè)拉到最上方喘鸟,點(diǎn)擊右上角的圖標(biāo)(見(jiàn)圖1)可以發(fā)送動(dòng)態(tài)。

發(fā)布1.jpg

動(dòng)態(tài)包括圖片和文字驻右。根據(jù)設(shè)計(jì)什黑,圖片最多添加6張,因此在添加圖片時(shí)需要對(duì)圖片數(shù)量進(jìn)行限制堪夭。選擇圖片時(shí)右上角會(huì)顯示當(dāng)前已經(jīng)選擇的圖片數(shù)量和最大數(shù)量愕把。

發(fā)布2.jpg

點(diǎn)擊完成后,可以在發(fā)布頁(yè)面看到已經(jīng)選擇的圖片的預(yù)覽森爽。

發(fā)布3.jpg

此時(shí)用戶(hù)可以再次點(diǎn)擊添加圖片的按鈕恨豁,當(dāng)前的圖片最大選擇數(shù)量變?yōu)榱?,以保證不管用戶(hù)分幾次添加圖片爬迟,圖片的數(shù)量都不會(huì)超過(guò)6橘蜜。

發(fā)布4.jpg

點(diǎn)擊完成,預(yù)覽界面如下所示付呕。

發(fā)布5.jpg

二计福、環(huán)境及配置

1. MySQL5.7

鑒于應(yīng)用是支持中文的,數(shù)據(jù)庫(kù)的編碼肯定要修改為utf-8徽职,不過(guò)數(shù)據(jù)庫(kù)的utf-8編碼最多占用3個(gè)字節(jié)象颖,無(wú)法支持占用字節(jié)更多的emoji,怎么辦姆钉?幸好從MySQL5.5.3開(kāi)始说订,數(shù)據(jù)庫(kù)提供了一種新的編碼,那就是utf8mb4育韩。因此首先需要將數(shù)據(jù)庫(kù)的編碼進(jìn)行修改克蚂,修改完數(shù)據(jù)庫(kù)的編碼后,還需要對(duì)表中字段的編碼格式進(jìn)行修改筋讨。具體如下所示:
(1)修改mysql配置文件my.cnf(windows為my.ini)
my.cnf一般在etc/mysql/my.cnf位置埃叭。找到后請(qǐng)?jiān)谝韵氯糠掷锾砑尤缦聝?nèi)容:
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'
隨后重啟數(shù)據(jù)庫(kù),通過(guò)命令SHOW VARIABLES WHERE Variable_name LIKE 'character_set_%' OR Variable_name LIKE 'collation%';檢查編碼悉罕,得到如下結(jié)果表示成功赤屋。

Variable_name Value
character_set_client utf8mb4
character_set_connection utf8mb4
character_set_database utf8mb4
character_set_filesystem binary
character_set_results utf8mb4
character_set_server utf8mb4
character_set_system utf8
character_sets_dir C:\Program Files\MySQL\MySQL Server 5.7\share\charsets\
collation_connection utf8mb4_unicode_ci
collation_database utf8mb4_unicode_ci
collation_server utf8mb4_unicode_ci

(2)修改數(shù)據(jù)庫(kù)表和表中的字段的編碼格式
目前還未建表,先講一下修改編碼格式的方法壁袄,建表之后可以根據(jù)需要修改某些字段的編碼类早。本應(yīng)用中支持emoji的字段為:動(dòng)態(tài)文字、動(dòng)態(tài)評(píng)論嗜逻、用戶(hù)名和用戶(hù)簽名涩僻。
首先需要修改表的編碼格式:
ALTER TABLE `tablename` DEFAULT CHARACTER SET utf8mb4;
之后需要修改特定字段的編碼格式:
ALTER TABLE `tablename` CHANGE `字段名` `字段名` VARCHAR(36) CHARACTER SET utf8mb4 NOT NULL;
如果要將整個(gè)表的字段編碼修改,則使用如下語(yǔ)句:
alter table `tablename` convert to character set utf8mb4;

2. Eclipse中添加服務(wù)器包

本應(yīng)用的服務(wù)器接口使用Servlet編寫(xiě),熟悉框架的朋友可以使用Spring逆日。
在向服務(wù)器添加庫(kù)的時(shí)候嵌巷,有的朋友會(huì)從Tomcat下復(fù)制jar包到項(xiàng)目的lib路徑下,但是最好的辦法是將服務(wù)器的環(huán)境直接添加室抽。
在Eclipse點(diǎn)擊上方選項(xiàng)欄的Project->Properties->Java Build Path右方Add Library->Server Runtime選擇Tomcat對(duì)應(yīng)的版本即可搪哪。

3. 云服務(wù)器及應(yīng)用部署

本次應(yīng)用使用的是阿里云服務(wù)器,如果是學(xué)生的話(huà)坪圾,直接使用阿里云學(xué)生特權(quán)購(gòu)買(mǎi)即可晓折,大概¥120一年。在云服務(wù)器上需要安裝JDK8兽泄、Tomcat以及MySQL數(shù)據(jù)庫(kù)漓概。
在本地編寫(xiě)完服務(wù)器程序后打包成war包放置到服務(wù)器tomcat/webapps/目錄下,隨后在tomcat/bin/目錄下點(diǎn)擊startup即可啟動(dòng)服務(wù)器已日。

三垛耳、系統(tǒng)功能模塊劃分

1. 注冊(cè)登錄

注冊(cè)較為簡(jiǎn)單,用戶(hù)在客戶(hù)端填寫(xiě)賬號(hào)和密碼即可飘千,系統(tǒng)會(huì)為該用戶(hù)安排一個(gè)默認(rèn)的用戶(hù)名和頭像堂鲜。
用戶(hù)使用注冊(cè)的賬號(hào)密碼登錄之后,客戶(hù)端會(huì)在本地保存登錄憑證护奈,以便用戶(hù)下一次打開(kāi)APP時(shí)免去登錄缔莲。

2. 用戶(hù)信息管理

信息管理主要為背景圖修改、頭像修改霉旗、用戶(hù)名修改痴奏、個(gè)性簽名修改。這些信息都存儲(chǔ)在user表中厌秒,用戶(hù)信息管理都是修改user表中的字段读拆。

3. 添加好友

用戶(hù)可以通過(guò)搜索賬號(hào)或用戶(hù)名來(lái)檢索用戶(hù),通過(guò)賬號(hào)搜索時(shí)為精確查詢(xún)鸵闪,輸入的賬號(hào)必須和用戶(hù)賬號(hào)完全相同檐晕;通過(guò)用戶(hù)名搜索時(shí)是模糊查詢(xún),有一部分匹配即可蚌讼。搜索到用戶(hù)后即可向其發(fā)送好友申請(qǐng)等待通過(guò)辟灰。
用戶(hù)可以在“好友申請(qǐng)”界面查看其他用戶(hù)發(fā)來(lái)的申請(qǐng),如果點(diǎn)擊“同意”篡石,則兩位用戶(hù)之間建立好友關(guān)系芥喇,可以互相之間查看動(dòng)態(tài)。好友關(guān)系通過(guò)用戶(hù)關(guān)系表來(lái)保存凰萨,如果ID為1的用戶(hù)和ID為2的用戶(hù)是好友關(guān)系继控,那么表中會(huì)存儲(chǔ)(1, 2)和(2, 1)這兩組數(shù)據(jù)械馆。

4. 發(fā)送動(dòng)態(tài)

在Android客戶(hù)端上傳動(dòng)態(tài)時(shí),將用戶(hù)輸入的文字作為一個(gè)參數(shù)湿诊,將選擇的每張圖片各作為一個(gè)參數(shù)再調(diào)用接口狱杰。
在服務(wù)器端瘦材,接口循環(huán)接收參數(shù)并判斷當(dāng)前上傳的參數(shù)是文字還是文件厅须,如果是文字則保存到動(dòng)態(tài)表中的文字字段;如果是文件食棕,則按順序保存到指定目錄下并在表中保存圖片的路徑或名字朗和。

5. 時(shí)間線(xiàn)

那么一個(gè)用戶(hù)發(fā)送的動(dòng)態(tài)怎么顯示在他好友的動(dòng)態(tài)列表中呢?這里就需要時(shí)間線(xiàn)這張表的幫助簿晓。當(dāng)某位用戶(hù)發(fā)表動(dòng)態(tài)的時(shí)候眶拉,系統(tǒng)會(huì)先將該動(dòng)態(tài)的文字和圖片保存,得到該動(dòng)態(tài)的主鍵ID憔儿。再搜索該用戶(hù)的所有好友忆植,對(duì)每個(gè)好友,以(好友ID, 動(dòng)態(tài)ID)的形式在時(shí)間線(xiàn)表中添加一條記錄谒臼。
當(dāng)一個(gè)用戶(hù)在首頁(yè)請(qǐng)求好友的動(dòng)態(tài)列表時(shí)朝刊,后臺(tái)會(huì)在時(shí)間線(xiàn)表中搜索“好友ID”字段等于該用戶(hù)ID的記錄,拉取最近的N條記錄并倒序返回蜈缤。

6. 評(píng)論管理

本應(yīng)用的評(píng)論系統(tǒng)較為簡(jiǎn)單拾氓,用戶(hù)只能對(duì)動(dòng)態(tài)進(jìn)行評(píng)論,無(wú)法回復(fù)在動(dòng)態(tài)下評(píng)論的其他用戶(hù)底哥。當(dāng)然咙鞍,如果要實(shí)現(xiàn)回復(fù)功能,只需要在每條評(píng)論上再加個(gè)字段表明該評(píng)論是回復(fù)哪條評(píng)論的即可趾徽。

7. 點(diǎn)贊管理

由于每個(gè)用戶(hù)只能為某條動(dòng)態(tài)點(diǎn)贊一次续滋,因此需要一個(gè)表來(lái)保存動(dòng)態(tài)的點(diǎn)贊情況。這里為了簡(jiǎn)化業(yè)務(wù)孵奶,將點(diǎn)贊設(shè)置為無(wú)法取消疲酌。當(dāng)用戶(hù)點(diǎn)贊某條動(dòng)態(tài)時(shí),動(dòng)態(tài)表中對(duì)應(yīng)動(dòng)態(tài)的點(diǎn)贊數(shù)量字段+1拒课,并在點(diǎn)贊表中保存(動(dòng)態(tài)ID, 用戶(hù)ID)形式的記錄徐勃。用戶(hù)查看某條動(dòng)態(tài)時(shí),如果(動(dòng)態(tài)ID, 用戶(hù)ID)記錄存在早像,則將點(diǎn)贊圖標(biāo)設(shè)置為紅色僻肖。
雖然本應(yīng)用采用了上述的方式管理點(diǎn)贊,但是很容易產(chǎn)生大量記錄降低運(yùn)行效率卢鹦。一種解決辦法是使用非關(guān)系型數(shù)據(jù)庫(kù)例如redis臀脏,通過(guò)鍵值對(duì)來(lái)存儲(chǔ)為某條動(dòng)態(tài)點(diǎn)贊的所有用戶(hù)劝堪。如果堅(jiān)持用關(guān)系型數(shù)據(jù)庫(kù)處理點(diǎn)贊,可以使用類(lèi)似(動(dòng)態(tài)ID, [用戶(hù)1, 用戶(hù)2, 用戶(hù)3...])的方式處理揉稚。

四秒啦、數(shù)據(jù)庫(kù)設(shè)計(jì)

1. 用戶(hù)表user

用戶(hù)表需要記錄用戶(hù)的登陸憑證(賬號(hào)、密碼)以及用戶(hù)的個(gè)人信息搀玖,包括用戶(hù)名余境、頭像、背景和簽名等灌诅。

字段名 數(shù)據(jù)類(lèi)型 長(zhǎng)度 約束 描述
id int 11 主鍵, 自增 用戶(hù)ID
account varchar 20 主鍵 賬號(hào)
password varchar 30 非空 密碼
username varchar 30 用戶(hù)名
icon varchar 100 頭像文件的名字
background varchar 100 背景圖片的名字
signature varchar 60 個(gè)性簽名

2. 好友關(guān)系表user_link

字段名 數(shù)據(jù)類(lèi)型 長(zhǎng)度 約束 描述
id int 11 主鍵, 自增 唯一ID
userid int 11 主鍵 用戶(hù)ID
linkid int 11 非空 好友ID
remark varchar 20 備注名

3. 好友申請(qǐng)表friendrequest

用戶(hù)的好友請(qǐng)求頁(yè)面會(huì)顯示該用戶(hù)收到的請(qǐng)求芳来,由于請(qǐng)求分為“未同意”和“已同意”兩種,因此表中需要一個(gè)status字段標(biāo)注當(dāng)前請(qǐng)求的狀態(tài)猜拾,0表示未同意即舌,1表示已同意。如果用戶(hù)同意了某個(gè)好友請(qǐng)求挎袜,那么該請(qǐng)求的狀態(tài)就會(huì)從0變?yōu)?顽聂,同時(shí)兩人建立好友關(guān)系。

字段名 數(shù)據(jù)類(lèi)型 長(zhǎng)度 約束 描述
id int 11 主鍵, 自增 唯一ID
sender int 11 非空 發(fā)送請(qǐng)求的用戶(hù)ID
receiver int 11 非空 接到請(qǐng)求的用戶(hù)ID
status int 1 該請(qǐng)求的狀態(tài)

4. 動(dòng)態(tài)詳情表moments_message

字段名 數(shù)據(jù)類(lèi)型 長(zhǎng)度 約束 描述
id int 11 主鍵, 自增 唯一ID
userid int 11 非空 用戶(hù)ID
content varchar 200 非空 動(dòng)態(tài)的文字內(nèi)容
picture1 varchar 100 圖片1
picture2 varchar 100 圖片2
picture3 varchar 100 圖片3
picture4 varchar 100 圖片4
picture5 varchar 100 圖片5
picture6 varchar 100 圖片6
createtime datetime 創(chuàng)建時(shí)間
likes int 11 點(diǎn)贊數(shù)量(默認(rèn)0)
comments int 11 評(píng)論數(shù)量(默認(rèn)0)

5. 時(shí)間線(xiàn)表timeline

字段名 數(shù)據(jù)類(lèi)型 長(zhǎng)度 約束 描述
id int 11 主鍵, 自增 唯一ID
userid int 11 非空 用戶(hù)ID
momentid int 11 非空 動(dòng)態(tài)ID

6. 評(píng)論表moments_comment

字段名 數(shù)據(jù)類(lèi)型 長(zhǎng)度 約束 描述
id int 11 主鍵, 自增 唯一ID
momentid int 11 非空 動(dòng)態(tài)ID
userid int 11 非空 用戶(hù)ID
content varchar 100 非空 評(píng)論內(nèi)容

7. 點(diǎn)贊表moments_like

字段名 數(shù)據(jù)類(lèi)型 長(zhǎng)度 約束 描述
id int 11 主鍵, 自增 唯一ID
momentid int 11 非空 動(dòng)態(tài)ID
userid int 11 非空 用戶(hù)ID

五盯仪、后臺(tái)的部分實(shí)現(xiàn)

本應(yīng)用后臺(tái)的實(shí)現(xiàn)比較基礎(chǔ)紊搪,主要是兩個(gè)方面,一是通過(guò)數(shù)據(jù)庫(kù)連接池和DAO實(shí)現(xiàn)數(shù)據(jù)的增刪改查磨总;二是通過(guò)Servlet實(shí)現(xiàn)數(shù)據(jù)的上傳下載嗦明。這里挑選一部分介紹,其他大同小異蚪燕。

1. 數(shù)據(jù)庫(kù)連接池

本應(yīng)用使用的數(shù)據(jù)庫(kù)連接池是druid連接池娶牌,由于數(shù)據(jù)庫(kù)連接池本身依賴(lài)于jdbc的驅(qū)動(dòng),所以需要在項(xiàng)目的WebContent/WEB-INF/lib下添加mysql-connector-java的包馆纳,為了支持uft8mb4編碼诗良,建議使用高版本的jar包(這里使用的版本是5.1.34),然后添加druid連接池的jar包鲁驶,不要忘了對(duì)jar包執(zhí)行add to build path操作鉴裹。數(shù)據(jù)庫(kù)連接池一般通過(guò)文件進(jìn)行配置,在項(xiàng)目的src文件下添加配置文件druid.properties钥弯,如下所示径荔。
第一行聲明了數(shù)據(jù)庫(kù)連接的驅(qū)動(dòng),最后一行聲明了連接的編碼為utf8mb4脆霎,其余url总处、username和password等信息注意修改。

driverClassName=com.mysql.jdbc.Driver
url=jdbc\:mysql\://localhost\:3306/moments
username=root
password=root
filters=stat
initialSize=2
minIdle=1
maxActive=300
maxWait=60000
timeBetweenEvictionRunsMillis=60000
minEvictableIdleTimeMillis=300000
validationQuery=SELECT 'x'
testWhileIdle=true
testOnBorrow=false
testOnReturn=false
poolPreparedStatements=false
maxPoolPreparedStatementPerConnectionSize=20
connectionInitSqls=[set names utf8mb4]

添加完配置文件之后睛蛛,需要一個(gè)類(lèi)對(duì)數(shù)據(jù)庫(kù)連接池的基本操作進(jìn)行封裝鹦马。新建DbPoolConnection類(lèi)胧谈,該類(lèi)使用單例模式,保證在程序的運(yùn)行過(guò)程中只存在一個(gè)DbPoolConnection實(shí)例荸频。類(lèi)中通過(guò)靜態(tài)方法loadPropertyFile()來(lái)加載druid.properties文件中的配置菱肖,通過(guò)getConnection()方法獲取一個(gè)連接,通過(guò)releaseConnection(Connection connection)方法釋放一個(gè)連接旭从。

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.alibaba.druid.pool.DruidPooledConnection;

public class DbPoolConnection {

    private static DbPoolConnection databasePool = null;
    private static DruidDataSource dds = null;

    static {
        Properties properties = loadPropertyFile("druid.properties");
        try {
            dds = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private DbPoolConnection() { }

    public static synchronized DbPoolConnection getInstance() {
        if (null == databasePool) {
            databasePool = new DbPoolConnection();
        }
        return databasePool;
    }

    public static DruidPooledConnection getConnection() throws SQLException {
        return dds.getConnection();
    }
    
    public static void releaseConnection(Connection connection) throws SQLException {
        connection.close();
    }

    public static Properties loadPropertyFile(String fullFile) {
        String webRootPath = null;
        if (null == fullFile || fullFile.equals(""))
            throw new IllegalArgumentException("Properties file path can not be null: " + fullFile);
        webRootPath = DbPoolConnection.class.getClassLoader().getResource("").getPath();
        InputStream inputStream = null;
        Properties p = null;
        try {
            String sglPath = webRootPath + File.separator + fullFile;
            sglPath = URLDecoder.decode(sglPath, "utf-8"); // 關(guān)鍵
            inputStream = new FileInputStream(new File(sglPath));
            p = new Properties();
            p.load(inputStream);
        } catch (FileNotFoundException e) {
            throw new IllegalArgumentException("Properties file not found: " + fullFile);
        } catch (IOException e) {
            throw new IllegalArgumentException("Properties file can not be loading: " + fullFile);
        } finally {
            try {
                if (inputStream != null)
                    inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return p;
    }
}

2. 數(shù)據(jù)的增刪改查

這里定義一個(gè)泛型類(lèi)DAO<T>并使用QueryRunner 封裝數(shù)據(jù)庫(kù)增刪改查的基本操作稳强,這里的T可以代表任何數(shù)據(jù)實(shí)體,如果需要對(duì)某個(gè)實(shí)體進(jìn)行操作遇绞,定義一個(gè)繼承自DAO<T>的類(lèi)即可褐健。DAO<T>如下所示:

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import com.lister.db.DbPoolConnection;

public class DAO<T> {
    
    private QueryRunner queryRunner = new QueryRunner();
    private Class<T> clazz;
    
    @SuppressWarnings("unchecked")
    public DAO() {
        // 獲得帶有泛型的父類(lèi)
        Type superClass = getClass().getGenericSuperclass();
        // ParameterizedType 是參數(shù)化類(lèi)型刹帕,即泛型
        if (superClass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) superClass;
            // 因?yàn)榉盒涂赡苡卸鄠€(gè)侦另,所以使用參數(shù)類(lèi)型數(shù)組保存
            Type[] typeArgs = parameterizedType.getActualTypeArguments();
            if (typeArgs != null && typeArgs.length > 0) {
                if (typeArgs[0] instanceof Class) {
                    clazz = (Class<T>) typeArgs[0];
                }
            }
        }
    }
    
    /**
     * 獲取到數(shù)據(jù)庫(kù)中的某個(gè)字段的值
     * @throws SQLException 
     */
    @SuppressWarnings("unchecked")
    public <E> E getValue(String sql, Object ... args) throws SQLException {
        Connection connection = null;
        try {
            connection = DbPoolConnection.getConnection();
            return (E) queryRunner.query(connection, sql, new ScalarHandler(), args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            DbPoolConnection.releaseConnection(connection);
        }
        return null;
    }

    /**
     * 獲取到數(shù)據(jù)庫(kù)中的 N 條記錄的列表
     * @throws SQLException 
     */
    public List<T> getList(String sql, Object ... args) throws SQLException {
        Connection connection = null;
        try {
            connection = DbPoolConnection.getConnection();
            return queryRunner.query(connection, sql, new BeanListHandler<>(clazz), args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            DbPoolConnection.releaseConnection(connection);
        }
        return null;
    }
    
    /**
     * 獲取到數(shù)據(jù)庫(kù)中的一條記錄
     * @throws SQLException 
     */
    public T get(String sql, Object ... args) throws SQLException {
        Connection connection = null;
        try {
            connection = DbPoolConnection.getConnection();
            return queryRunner.query(connection, sql, new BeanHandler<>(clazz), args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            DbPoolConnection.releaseConnection(connection);
        }
        return null;
    }
    
    /**
     * 進(jìn)行 insert, delete, update 操作
     * @throws SQLException 
     */
    public void update(String sql, Object ... args) throws SQLException {
        Connection connection = null;
        try {
            connection = DbPoolConnection.getConnection();
            queryRunner.update(connection, sql, args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            DbPoolConnection.releaseConnection(connection);
        }
    }
    
}

有了DAO這個(gè)基類(lèi)之后柳洋,就可以通過(guò)繼承自它的類(lèi)來(lái)對(duì)指定的表的數(shù)據(jù)進(jìn)行操作莹捡。
舉個(gè)例子,假如要對(duì)moments_comment表進(jìn)行操作,首先定義實(shí)體類(lèi)Comment芒涡,實(shí)體類(lèi)中的字段需要與數(shù)據(jù)庫(kù)表內(nèi)的字段一一對(duì)應(yīng)卖漫。

public class Comment {
    Integer id;
    Integer momentid;
    Integer userid;
    String content;
    
    // getters and setters
}

那么對(duì)評(píng)論表有哪些操作呢费尽?無(wú)非就是兩個(gè):1. 為某條動(dòng)態(tài)下添加一條評(píng)論; 2. 獲取某條動(dòng)態(tài)下的所有評(píng)論。根據(jù)對(duì)表的操作寫(xiě)出如下接口羊始。

import java.util.List;
import com.lister.model.Comment;

public interface CommentDAO {
    
    // 為某條動(dòng)態(tài)添加評(píng)論
    public void addComment(Integer momentid, Integer userid, String content) throws Exception;
    
    //獲取某條動(dòng)態(tài)所有評(píng)論
    public List<Comment> getComments(Integer id) throws Exception;
}

有了實(shí)體類(lèi)和接口之后旱幼,可以編寫(xiě)具體的操作來(lái)實(shí)現(xiàn)接口了。定義一個(gè)繼承自DAO<Comment>并實(shí)現(xiàn)了CommentDAO接口的類(lèi)突委。方法中的?表示占位符柏卤,在最后調(diào)用DAO基類(lèi)的方法時(shí)用參數(shù)填充SQL語(yǔ)句里的占位符冬三。

import java.util.List;
import com.lister.dao.CommentDAO;
import com.lister.dao.DAO;
import com.lister.model.Comment;

public class CommentDAOImpl extends DAO<Comment> implements CommentDAO {

    @Override
    public void addComment(Integer momentid, Integer userid, String content) throws Exception {
        String sql = "insert into moments_comment values(0, ?, ?, ?)";
        update(sql, momentid, userid, content);
    }

    @Override
    public List<Comment> getComments(Integer momentid) throws Exception {
        String sql = "select * from moments_comment where momentid = ? order by id desc";
        return getList(sql, momentid);
    }

}

之后即可通過(guò)CommentDAOImpl的實(shí)例來(lái)操作評(píng)論表。對(duì)于其他表的操作缘缚,也可以使用類(lèi)似的方式勾笆。

3. 文件的上傳

應(yīng)用中的頭像修改和發(fā)表動(dòng)態(tài)都涉及到圖片文件的上傳,這里以頭像修改為例介紹文件上傳的方式桥滨。
為了支持文件上傳窝爪,需要添加jar包,文件上傳依賴(lài)common-fileupload和common-io這兩個(gè)包齐媒。而在服務(wù)器端接收文件蒲每,與平時(shí)的接收參數(shù)有一點(diǎn)差別帅刀,如果只有文字參數(shù)矢劲,一般通過(guò)參數(shù)名來(lái)獲取參數(shù)值;如果帶有文件參數(shù)他膳,則通過(guò)一個(gè)循環(huán)接收參數(shù)并判斷該參數(shù)是普通參數(shù)(文字)還是文件參數(shù)双妨,再進(jìn)行對(duì)應(yīng)的操作淮阐。服務(wù)器端頭像修改的代碼如下,已通過(guò)注釋詳細(xì)介紹刁品。

    /**
     * 接口:處理 changeicon.user 請(qǐng)求 修改頭像
     * @throws Exception 
     */
    public void changeicon(HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();
        /**
         * 由于上傳頭像時(shí)包含文件, 使用 FileItem 獲取參數(shù)
         */
        String idString = "0";
        String account = "";
        String picpath = "";
        try {
            // 得到上傳文件的保存目錄
            String realPath = this.getServletContext().getRealPath("/upload");
            String tempPath = "C:\\tempPath"; // 臨時(shí)目錄
            // 判斷存放上傳文件的目錄是否存在
            File f = new File(realPath);
            if (!f.exists() && !f.isDirectory()) {
                f.mkdir();
            }
            // 判斷臨時(shí)目錄是否存在
            File tempFilePath = new File(tempPath);
            if (!tempFilePath.isDirectory()) {
                tempFilePath.mkdir();
            }
            /**
             * 使用Apache文件上傳組件處理文件上傳步驟
             */
            // 1. 設(shè)置環(huán)境:創(chuàng)建一個(gè)DiskFileItemFactory工廠
            DiskFileItemFactory factory = new DiskFileItemFactory();
            // 設(shè)置上傳文件的臨時(shí)目錄
            factory.setRepository(tempFilePath);
            // 2. 核心操作類(lèi): 創(chuàng)建一個(gè)文件上傳解析器。
            ServletFileUpload upload = new ServletFileUpload(factory);
            // 解決上傳"文件名"的中文亂碼
            upload.setHeaderEncoding("UTF-8");
            // 3. 判斷 enctype:判斷提交上來(lái)的數(shù)據(jù)是否是上傳表單的數(shù)據(jù)
            if (!ServletFileUpload.isMultipartContent(request)) {
                System.out.println("不是上傳文件浩姥,終止");
                // 按照傳統(tǒng)方式獲取數(shù)據(jù)
                return;
            }
            // 4. 使用ServletFileUpload解析器解析上傳數(shù)據(jù)挑随,
            // 解析結(jié)果返回的是一個(gè)List<FileItem>集合
            // 每一個(gè)FileItem對(duì)應(yīng)一個(gè)Form表單的輸入項(xiàng)
            List<FileItem> items = upload.parseRequest(request);
            for (FileItem item : items) {
                // 如果 fileItem 中封裝的是普通輸入項(xiàng)的數(shù)據(jù)
                if (item.isFormField()) {
                    String filedName = item.getFieldName();// 普通輸入項(xiàng)數(shù)據(jù)的名
                    // 解決普通輸入項(xiàng)的數(shù)據(jù)的中文亂碼問(wèn)題
                    String filedValue = item.getString("UTF-8");
                    if (filedName.equals("account")) {
                        account = filedValue;
                    } else if (filedName.equals("id")) {
                        idString = filedValue;
                    }
                } else {
                    // 如果 fileItem 中封裝的是上傳文件, 得到上傳的文件名稱(chēng)
                    String fileName = item.getName();
                    // 多個(gè)文件上傳輸入框有空 的 異常處理
                    if (fileName == null || "".equals(fileName.trim())) {
                        continue;
                    }
                    // 處理上傳文件的文件名的路徑, 截取字符串只保留文件名部分
                    // 截取留最后一個(gè)"\"之后,+1 截取向右移一位 "\a.txt"-->"a.txt"
                    fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
                    String suffix = fileName.substring(fileName.lastIndexOf("."));
                    // 使用當(dāng)前時(shí)間作為新的文件名
                    SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
                    fileName = account + df.format(new Date()) + suffix;
                    // 拼接上傳路徑: 存放路徑 + 上傳的文件名
                    String filePath = realPath + "\\" + fileName;
                    // 構(gòu)建輸入輸出流
                    InputStream in = item.getInputStream();
                    OutputStream out = new FileOutputStream(filePath);
                    byte b[] = new byte[1024];
                    int len = -1;
                    while ((len = in.read(b)) != -1) {
                        out.write(b, 0, len);
                    }
                    out.close(); in.close();
                    // 刪除臨時(shí)文件
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    item.delete();
                    // picpath 賦值
                    picpath = fileName;
                }
            }
            // 循環(huán)完畢, 添加記錄
            Integer id = Integer.parseInt(idString);
            if (id > 0 && !account.trim().equals("")) {
                if (userDAO.isUserExist(id, account) != 0) {
                    userDAO.changeUserIcon(id, picpath);
                    RevertMessage revertMessage = new RevertMessage(true, "頭像上傳成功");
                    writer.print(gson.toJson(revertMessage));
                } else {
                    // 用戶(hù)不存在
                    RevertMessage revertMessage = new RevertMessage(false, "用戶(hù)不存在");
                    writer.print(gson.toJson(revertMessage));
                }
            } else {
                // 賬號(hào)為空
                RevertMessage revertMessage = new RevertMessage(false, "賬號(hào)為空");
                writer.print(gson.toJson(revertMessage));
            }
        } catch (FileUploadException e) {
            // 上傳異常
            System.out.println(e.getMessage());
            RevertMessage revertMessage = new RevertMessage(false, "文件上傳異常");
            writer.print(gson.toJson(revertMessage));
        }
    }

六勒叠、Android的部分實(shí)現(xiàn)

1. 首頁(yè)布局

用戶(hù)進(jìn)入應(yīng)用后兜挨,屏幕下方的選項(xiàng)卡默認(rèn)為“動(dòng)態(tài)”,上方顯示用戶(hù)自定義的背景和頭像眯分,點(diǎn)擊右上角的圖案可發(fā)送動(dòng)態(tài)拌汇。當(dāng)用戶(hù)在首頁(yè)向下滑動(dòng)查看好友的動(dòng)態(tài)時(shí),上方的背景圖會(huì)逐漸消失弊决,具體效果可見(jiàn)第一張圖噪舀。那么怎么實(shí)現(xiàn)呢?
Android的CoordinatorLayout就可以實(shí)現(xiàn)這類(lèi)效果飘诗。在布局時(shí)与倡,使用CoordinatorLayout布局包裹CollapsingToolbarLayout,從CollapsingToolbarLayout這個(gè)布局的名字我們可以看出一些端倪昆稿。Collapse什么意思纺座?坍塌,折疊溉潭。因此這是一個(gè)會(huì)折疊的布局净响,只要對(duì)它的行為做一些設(shè)置少欺,就可以實(shí)現(xiàn)滑動(dòng)后折疊的效果。因此可以在CollapsingToolbarLayout中放置背景圖馋贤、用戶(hù)名和頭像等控件赞别。布局效果如下:

首頁(yè)布局.png

布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.lister.momentsandroid.activity.MomentsActivity">

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.design.widget.AppBarLayout
            android:id="@+id/moments_appbar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true">
            <android.support.design.widget.CollapsingToolbarLayout
                android:id="@+id/moments_collapse_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:collapsedTitleTextAppearance="@style/ToolBarTitleText"
                app:contentScrim="@color/mainPurple"
                app:expandedTitleMarginEnd="48dp"
                app:expandedTitleMarginStart="48dp"
                app:expandedTitleTextAppearance="@style/transparentText"
                app:layout_scrollFlags="scroll|exitUntilCollapsed"
                android:fitsSystemWindows="true">
                <LinearLayout
                    android:id="@+id/moments_info_linear"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    app:layout_collapseMode="pin"
                    app:layout_collapseParallaxMultiplier="0.7">
                    <RelativeLayout
                        android:layout_width="match_parent"
                        android:layout_height="240dp"
                        android:background="@color/light_gray">
                        <ImageView
                            android:id="@+id/moments_background"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:scaleType="centerCrop"/>
                        <LinearLayout
                            android:id="@+id/moments_post"
                            android:layout_width="80dp"
                            android:layout_height="80dp"
                            android:layout_alignParentTop="true"
                            android:layout_alignParentRight="true"
                            android:clickable="true"
                            android:focusable="true">
                            <ImageView
                                android:layout_width="24dp"
                                android:layout_height="24dp"
                                android:layout_marginTop="30dp"
                                android:layout_marginLeft="30dp"
                                android:src="@drawable/moments_post_icon"/>
                        </LinearLayout>

                        <LinearLayout
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_alignParentBottom="true"
                            android:layout_alignParentEnd="true"
                            android:layout_marginBottom="15dp"
                            android:layout_marginRight="20dp"
                            android:orientation="vertical">
                            <TextView
                                android:id="@+id/moments_text_name"
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textSize="14sp"
                                android:textColor="@color/white"
                                android:layout_marginBottom="8dp"/>
                            <ImageView
                                android:id="@+id/moments_icon"
                                android:layout_width="80dp"
                                android:layout_height="80dp"
                                android:background="@color/white"
                                android:padding="2dp"/>
                        </LinearLayout>
                    </RelativeLayout>
                </LinearLayout>

                <android.support.v7.widget.Toolbar
                    android:id="@+id/toolbar"
                    android:layout_width="match_parent"
                    android:layout_height="45dp"
                    android:clickable="true"
                    app:layout_collapseMode="pin"
                    app:layout_scrollFlags="scroll|enterAlways"
                    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                    app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                    app:titleTextAppearance="@style/Toolbar.TitleText"/>
            </android.support.design.widget.CollapsingToolbarLayout>

            <android.support.design.widget.TabLayout
                android:id="@+id/moments_tab_layout"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_gravity="bottom"
                android:background="#ffffff"
                android:fillViewport="false"
                app:layout_scrollFlags="scroll"
                app:tabIndicatorColor="@color/mainPurple"
                app:tabIndicatorHeight="2dp"
                app:tabSelectedTextColor="@color/mainPurple"
                app:tabTextColor="#ced0d3">
                <android.support.design.widget.TabItem
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="圈內(nèi)動(dòng)態(tài)"/>
                <android.support.design.widget.TabItem
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="公開(kāi)動(dòng)態(tài)"/>
            </android.support.design.widget.TabLayout>

        </android.support.design.widget.AppBarLayout>

        <android.support.v4.view.ViewPager
            android:id="@+id/moments_pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ffffff"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
    </android.support.design.widget.CoordinatorLayout>

</LinearLayout>

布局文件中用到了兩個(gè)樣式:

<style name="ToolBarTitleText" parent="TextAppearance.AppCompat.Medium">
      <item name="android:textColor">#ffffffff</item>
      <item name="android:textSize">16sp</item>
      <item name="android:textStyle">bold</item>
</style>

<style name="transparentText" parent="TextAppearance.AppCompat.Small">
      <item name="android:textColor">#00000000</item>
</style>

在Activity文件中,需要對(duì)ToolBar和AppBarLayout進(jìn)行設(shè)置掸掸。在onOffsetChanged方法中監(jiān)聽(tīng)折疊的程度并逐漸改變ToolBar的顏色氯庆,當(dāng)背景圖區(qū)域折疊超過(guò)一半的時(shí)候,屏幕上方的ToolBar顯示“動(dòng)態(tài)”扰付,否則不顯示文字堤撵。

    @BindView(R.id.toolbar) Toolbar mToolbar;
    @BindView(R.id.moments_collapse_layout) CollapsingToolbarLayout mCollapsingToolbarLayout;
    @BindView(R.id.moments_appbar_layout) AppBarLayout mAppBarLayout;
    @BindView(R.id.moments_info_linear) LinearLayout mInfoLinear;
    // ......

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_moments);
        ButterKnife.bind(this);
        // 注冊(cè) EventBus
        EventBus.getDefault().register(this);
        // 設(shè)置 ToolBar
        setSupportActionBar(mToolbar);
        // AppBarLayout
        mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                // 逐漸修改顏色
                mToolbar.setBackgroundColor(changeAlpha(getResources().getColor(R.color.mainPurple),
                        Math.abs(verticalOffset * 1.0f) / appBarLayout.getTotalScrollRange()));
                if (verticalOffset <= -mInfoLinear.getHeight() / 2) {
                    mCollapsingToolbarLayout.setTitle("動(dòng)態(tài)");
                } else {
                    mCollapsingToolbarLayout.setTitle("");
                }
            }
        });
        // ......
    }

2. 聯(lián)網(wǎng)操作封裝

本應(yīng)用使用OKHTTP框架進(jìn)行聯(lián)網(wǎng)操作,新建HttpUtils類(lèi)封裝http請(qǐng)求羽莺。首先在類(lèi)中定義一些私有變量來(lái)做緩存控制实昨,如下所示:

private static final CacheControl FORCE_NETWORK = new CacheControl.Builder().noCache().build();
private static final CacheControl FORCE_CACHE = new CacheControl.Builder()
            .onlyIfCached()
            .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
            .build();
// 緩存控制
public static final String TYPE_FORCE_CACHE = "TYPE_FORCE_CACHE";
public static final String TYPE_FORCE_NETWORK = "TYPE_FORCE_NETWORK";
public static final String TYPE_CACHE_CONTROL = "TYPE_CACHE_CONTROL";

來(lái)看最基礎(chǔ)的post請(qǐng)求,在上傳的參數(shù)只有文字的情況下調(diào)用此方法盐固。

    public static Object[] postHttp(Context context, String url, HashMap<String, String> params, String cacheType, int cacheSeconds) {
        try {
            // 緩存文件夾
            File cacheFile = new File(context.getExternalCacheDir().toString(), "cache");
            // 緩存大小為50M
            int cacheSize = 50 * 1024 * 1024;
            // 創(chuàng)建緩存對(duì)象
            final Cache cache = new Cache(cacheFile, cacheSize);

            OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(10, TimeUnit.SECONDS)
                    .readTimeout(20, TimeUnit.SECONDS)
                    .cache(cache)
                    .build();

            FormBody.Builder formBodyBuilder = new FormBody.Builder();
            for (Map.Entry<String, String> entry : params.entrySet())
                formBodyBuilder.add(entry.getKey(), entry.getValue());
            RequestBody formBody = formBodyBuilder.build();

            CacheControl cacheControl = null;
            if (cacheType.equals(TYPE_CACHE_CONTROL)) {
                cacheControl = new CacheControl.Builder()
                        .maxAge(cacheSeconds, TimeUnit.SECONDS).build();
            }
            if (cacheType.equals(TYPE_FORCE_CACHE)) {
                cacheControl = FORCE_CACHE;
            }
            if (cacheType.equals(TYPE_FORCE_NETWORK)) {
                cacheControl = FORCE_NETWORK;
            }

            Request request = new Request.Builder()
                    .cacheControl(cacheControl)
                    .url(url)
                    .post(formBody)
                    .build();
            Response response = mOkHttpClient.newCall(request).execute();
            String result = response.body().string();
            // 處理result并return
        } catch (Exception e) {
            // ......
        }
    }

調(diào)用示例:

// 參數(shù)分別為 Context, 接口地址, HashMap<String, String>參數(shù), 緩存控制, 緩存時(shí)間
// 這里的TYPE_FORCE_NETWORK指不用緩存, 強(qiáng)制聯(lián)網(wǎng)獲取資源
return HttpUtils.postHttp(ChangeNameActivity.this,
                        IPConstant.CHANGE_USER_NAME, params,
                        HttpUtils.TYPE_FORCE_NETWORK, 0);

那么如果上傳的參數(shù)里包含圖片呢荒给?那么就需要兩類(lèi)參數(shù),也就是兩個(gè)Map來(lái)保存參數(shù)刁卜,一個(gè)保存文字志电,一個(gè)保存文件。

    public static Object[] postHttpJPGLinked(
            Context context, String url, LinkedHashMap<String, String> params, LinkedHashMap<String, File> pictures) {
        try {
            OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(10, TimeUnit.SECONDS)
                    .readTimeout(20, TimeUnit.SECONDS)
                    .build();
            MultipartBody.Builder multipartBody = new MultipartBody.Builder();
            multipartBody.setType(MultipartBody.FORM);
            for (Map.Entry<String, String> entry : params.entrySet())
                multipartBody.addFormDataPart(entry.getKey(), entry.getValue());
            for (Map.Entry<String, File> entry : pictures.entrySet()) {
                RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpg"), entry.getValue());
                multipartBody.addFormDataPart(entry.getKey(), entry.getKey() + ".jpg", fileBody);
            }
            RequestBody requestBody = multipartBody.build();

            Request request = new Request.Builder()
                    .url(url)
                    .post(requestBody)
                    .build();
            Response response = mOkHttpClient.newCall(request).execute();
            String result = response.body().string();
            // ......
        } catch (Exception e) {
            // ......
        }
    }

3. 發(fā)送動(dòng)態(tài)在相冊(cè)選取圖片

從相冊(cè)選取圖片主要是參考下方的第10篇博客蛔趴,之后將圖片添加到一個(gè)List里方便上傳挑辆。由于鴻洋大神的博客已經(jīng)講得很詳細(xì)了,我就不多說(shuō)了孝情。

總結(jié)

做這個(gè)應(yīng)用的時(shí)候趕著秋招鱼蝉,一直剩下幾個(gè)坑沒(méi)填,例如上傳圖片前沒(méi)有壓縮箫荡、密碼沒(méi)有加密和沒(méi)有使用token等······但是做應(yīng)用的過(guò)程中魁亦,我更多思考的是朋友圈如何實(shí)現(xiàn)組群權(quán)限以及高并發(fā)。如果有朋友了解組群權(quán)限和集群這方面內(nèi)容的羔挡,希望能分享一下洁奈,拜謝~
另:最近又有個(gè)新的項(xiàng)目構(gòu)思,是做一個(gè)主要功能為視頻上傳和觀看的應(yīng)用婉弹,這次有充分的時(shí)間將之前沒(méi)完善的方面全部做好睬魂,完成以后依舊會(huì)以博客的形式分享。

參考:

  1. 微信朋友圈架構(gòu):http://www.reibang.com/p/3fb3652ff450
  2. 數(shù)據(jù)庫(kù)設(shè)計(jì):https://www.zhihu.com/question/21909660
  3. MySQL修改編碼為utf8mb4:https://www.cnblogs.com/shihaiming/p/5855616.html
  4. 將表字段編碼修改為utf8mb4:https://blog.csdn.net/luo4105/article/details/50804148
  5. 添加服務(wù)器包:https://blog.csdn.net/evan_leung/article/details/50647112
  6. Servlet注解:https://blog.csdn.net/mytt_10566/article/details/71077154
  7. 文件上傳:https://www.cnblogs.com/liuyangv/p/8298997.html
  8. Tomcat部署項(xiàng)目:https://www.cnblogs.com/ysocean/p/6893446.html
  9. CoordinatorLayout打造詳情頁(yè):http://www.reibang.com/p/5287d090e777
  10. 仿微信圖片選擇器:https://blog.csdn.net/lmj623565791/article/details/39943731/
  11. OKHTTP:https://blog.csdn.net/lmj623565791/article/details/47911083
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末镀赌,一起剝皮案震驚了整個(gè)濱河市氯哮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖喉钢,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姆打,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡肠虽,警方通過(guò)查閱死者的電腦和手機(jī)幔戏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)税课,“玉大人闲延,你說(shuō)我怎么就攤上這事『妫” “怎么了垒玲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)找颓。 經(jīng)常有香客問(wèn)我合愈,道長(zhǎng),這世上最難降的妖魔是什么击狮? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任佛析,我火速辦了婚禮,結(jié)果婚禮上彪蓬,老公的妹妹穿的比我還像新娘寸莫。我一直安慰自己,他們只是感情好档冬,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布储狭。 她就那樣靜靜地躺著,像睡著了一般捣郊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上慈参,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天呛牲,我揣著相機(jī)與錄音,去河邊找鬼驮配。 笑死娘扩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壮锻。 我是一名探鬼主播琐旁,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼猜绣!你這毒婦竟也來(lái)了灰殴?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤掰邢,失蹤者是張志新(化名)和其女友劉穎牺陶,沒(méi)想到半個(gè)月后伟阔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡掰伸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年皱炉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狮鸭。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡合搅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出歧蕉,到底是詐尸還是另有隱情灾部,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布廊谓,位于F島的核電站梳猪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蒸痹。R本人自食惡果不足惜春弥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叠荠。 院中可真熱鬧匿沛,春花似錦、人聲如沸榛鼎。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)者娱。三九已至抡笼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間黄鳍,已是汗流浹背推姻。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留框沟,地道東北人藏古。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像忍燥,于是被迫代替她去往敵國(guó)和親拧晕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容