開始「冰與火百科」開發(fā)之旅!
網(wǎng)絡(luò)數(shù)據(jù)
先說一下我的接口是怎么來的。
存放數(shù)據(jù)
首先確定自己需要一些什么數(shù)據(jù),在滿足自己要求的情況下越簡單越好。對每個詳情頁面,我需要一張圖片和一個 html 顯示描述就夠了。以奶德為例,在服務(wù)器的對應(yīng)目錄下,就會有 Eddard_Stark.png 和 Eddard_Stark.html 這兩個文件。
這一步其實是整個項目最麻煩的地方。圖片還好,但收集整理描述的內(nèi)容真的要非常有耐心,至今才造了十幾條數(shù)據(jù)。
創(chuàng)建數(shù)據(jù)集合
我需要兩個實體類。一個是分類,也就是到時 TabLayout 中的 Tab,另一個就是內(nèi)容。對應(yīng)的要生成兩個 json 文件。
創(chuàng)建一個 java 項目,添加 Gson 依賴,建立兩個要轉(zhuǎn)換 json 的集合:
private static List<TabDTO> tabList = new ArrayList<>();
private static List<ContentDTO> contentList = new ArrayList<>();
再次以奶德為例,往里面添加數(shù)據(jù):
tabList.add(new TabDTO(10100,"person","Stark","史塔克"));
String starkUrl = "person/Stark/";
contentList.add(new ContentDTO(
"Eddard_Stark",
"person",
"Stark",
starkUrl + "Eddard_Stark.png",
"艾德·史塔克",
"臨冬城公爵、北境守護",
starkUrl + "Eddard_Stark.html"));
生成 json 文件
完了輸出為 json 文件就好了,以 content 為例:
Gson gson = new Gson();
String jsonString = gson.toJson(contentList);
File contentJsonFile = new File("../IceAndFireServer/content.json");
try {
OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream(contentJsonFile), "UTF-8");
BufferedWriter bw = new BufferedWriter(osw);
bw.write(jsonString, 0, jsonString.length());
bw.flush();
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
然后將數(shù)據(jù)上傳到網(wǎng)絡(luò)就好了,json 文件所在的網(wǎng)絡(luò)地址就是你的接口了。剛開始我上傳到了 GitHub,但發(fā)現(xiàn)經(jīng)常會發(fā)生靈異事件,導致數(shù)據(jù)無法訪問或者速度超慢,后來又上傳到了九牛云。
這部分內(nèi)容大家看一下就好了,畢竟不是常規(guī)的做法。有興趣的可以到這里,數(shù)據(jù)和代碼都在里面了。
APP主題色
下面終于來到我們的 Android 項目了。
創(chuàng)建 Android 項目后,第一反應(yīng)是主題色得改一改。
在官方 Material Design 的色板里面,我選用了這一套:
對應(yīng)的,color 文件的主題色值修改如下:
<color name="colorPrimary">#607d8b</color>
<color name="colorPrimaryDark">#546e7a</color>
<color name="colorAccent">#40c4ff</color>
索引頁
我也學著別的 APP,做一個索引頁 IndexActivity。就簡單展示一句「挖了蘑菇立死」,噢不對,展示一句「Valar Morghulis」就好了,像這樣:
加入一點簡單的動畫,然后還能做一些耗時的啟動操作。
DataBinding
我會比較在意代碼的簡潔性,在實現(xiàn)同樣功能的情況下代碼越少越好,而且排版一定要看上去舒服,縮進要少,甚至不允許代碼里面有警告。
DataBinding 是一個可以增加代碼簡潔性的東西。這里以索引頁為例,簡單介紹一下它最簡單的一個應(yīng)用,代替 findViewByid。
配置
在對應(yīng) Module 的 build.grade 里配置:
android {
....
dataBinding {
enabled = true
}
}
布局
在需要綁定的布局文件里,最外層增加一個 layout 標簽,比如這里的 activity_index.xml :
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/tv_index"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/valar_morghulis"
android:textColor="@color/color3A3A3A"
android:textSize="36sp" />
</layout>
使用
創(chuàng)建一個成員變量:
private ActivityIndexBinding binding;
注意,這里的變量類型是和布局文件相關(guān)的,比如 ActivityIndexBinding 對應(yīng) activity_index。
然后將原來 setContentView 的地方修改為:
binding = DataBindingUtil.setContentView(this, R.layout.activity_index);
當我要使用布局里的 TextView 的時候,直接用 binding.tvIndex 就可以了。tvIndex 這個名字是和布局里的 id:tv_title 相對應(yīng)的。
DataBinding 的一些更高級的用法這里就不贅述了,網(wǎng)上的教程很多,大家可以多搜索了解一下。
動畫
為了讓索引頁的字更生動,我打算加一個漸變放大的動畫效果。
xml
我這里用的是 View Animation(視圖動畫),動畫過程是通過 xml 文件定義的。在 res/anim 文件夾下新建一個 xml 文件,代碼如下:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:shareInterpolator="true" >
<scale
android:duration="1300"
android:fillAfter="true"
android:fromXScale="0.95"
android:fromYScale="0.95"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1"
android:toYScale="1" />
<alpha
android:duration="1300"
android:fillAfter="true"
android:fromAlpha="0"
android:toAlpha="1" />
</set>
這個的意思很好理解,就是用 1.3 秒的時間,控件大小從 95% 漸變到 100%,透明度從 0 漸變到10%。
使用
執(zhí)行動畫的代碼也很簡單,大家直接看吧:
Animation animation = AnimationUtils
.loadAnimation(this, R.anim.anim_valar_morghulis);
animation.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
SystemClock.sleep(500);
animationComplete = true;
goMainPage();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
SystemClock.sleep(200);
binding.tvIndex.startAnimation(animation);
為了讓用戶能看清動畫,我在里面加入了一些停頓。經(jīng)過我自己的多次試驗,最終定下的這個停頓時常,我認為長度是在能看清動畫的情況下,又不會長到讓人感到厭煩的,效果如下:
耗時操作
前面說到,在索引頁可以做一些耗時的操作。動畫的執(zhí)行總共有兩秒的時間,用戶的時間是寶貴的,要是在這兩秒里面什么都不做就太浪費了。
最耗時的操作,應(yīng)該是調(diào)接口了。
其實剛開始我是進入到首頁才調(diào)接口的,進入不同的頁面獲取不同的數(shù)據(jù)。但這樣會有一個問題,由于我沒有后臺,只有兩個假接口,所以搜索功能就無法實現(xiàn)了。
所以現(xiàn)在改為,在索引頁獲取到所有數(shù)據(jù)并保存起來,在不同分類頁面下通過篩選展示數(shù)據(jù),這樣搜索也可以實現(xiàn)了。
下面就簡單講一下目前比較流行的兩個框架 Retrofit 2 和 Realm,來完成數(shù)據(jù)的獲取和保存。
Retrofit 2
Retrofit 的厲害之處我就不多說了,網(wǎng)上的教程很多的,我只講最最簡單的用法。
配置
在 Module 的 build.grade 里添加依賴:
compile "com.squareup.retrofit2:retrofit:${RETROFIT_VERSION}"
compile "com.squareup.retrofit2:converter-scalars:${RETROFIT_VERSION}"
compile "com.squareup.retrofit2:converter-gson:${RETROFIT_VERSION}"
目前最新版是 2.3.0,大家可以自行替換。
這里面 converter-scalars 是添加 String 類型的返回,converter-gson 是添加 Gson 的支持(返回實體類)。
接口定義
新建一個接口文件(interface),用來統(tǒng)一管理所有要調(diào)用的接口(url),我暫時只有兩個接口,再留一個通用的 Get 請求備用:
public interface RequestServes {
@GET("{url}")
Call<String> get(@Path("url") String url);
@GET("tab.json")
Call<List<TabDTO>> getTab();
@GET("content.json")
Call<List<ContentDTO>> getContent();
}
注解 @GET 后面的內(nèi)容就是要請求的接口,這里不用寫基礎(chǔ)域名(BaseUrl)。
初始化
需要通過 Retrofit.Builder 初始化 Retrofit,調(diào)用 baseUrl 設(shè)置基礎(chǔ)域名:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(ServerAPI.BASE_URL)
//增加返回值為String的支持
.addConverterFactory(ScalarsConverterFactory.create())
//增加返回值為實體類的支持
.addConverterFactory(GsonConverterFactory.create())
.build();
requestServes = retrofit.create(RequestServes.class);
需要注意的是,BaseUrl 必須以斜杠「/」結(jié)尾,否則會報錯。
考慮到可能多個頁面都需要調(diào)用接口,可以把這段代碼放在 BaseActivity 里。
使用
用起來超簡單:
Call<List<TabDTO>> call = requestServes.getTab();
call.enqueue(new Callback<List<TabDTO>>() {
@Override
public void onResponse(@NonNull Call<List<TabDTO>> call,
@NonNull retrofit2.Response<List<TabDTO>> response) {
List<TabDTO> tabList = response.body();
//...
goMainPage();
}
@Override
public void onFailure(@NonNull Call<List<TabDTO>> call, @NonNull Throwable t) {
netError();
}
});
請求失敗的時候,我會彈吐司提醒,并且讓頁面可點擊重試。
Realm
Realm 是 SQLite 的替代者,它更快速、更易用。下面看看 Realm 的簡單使用。
配置
修改 Project 下的 build.gradle :
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath "io.realm:realm-gradle-plugin:3.5.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
目前最新版是 3.5.0。然后再到 Module 的 build.gradle,添加:
apply plugin: 'realm-android'
配置完畢
初始化
在使用 Realm 之前,必須先調(diào)用:
Realm.init(this);
獲取 Realm 實例有以下兩種方法:
Realm mRealm = Realm.getDefaultInstance();
這里有個小細節(jié)。如果實體類的字段發(fā)生了改變,這里是會報錯的。我的做法比較粗暴,清空數(shù)據(jù)庫后再重新獲取:
try {
mRealm = Realm.getDefaultInstance();
} catch (RuntimeException e) {
Realm.deleteRealm(Realm.getDefaultConfiguration());
mRealm = Realm.getDefaultInstance();
}
保存
讓需要保存下來的實體類繼承 RealmObject,然后就可以使用以下代碼保存了:
mRealm.beginTransaction();
mRealm.copyToRealm(list);
mRealm.commitTransaction();
查詢
查詢也很簡單,就一句代碼的事:
List<Data> list = mRealm.where(Data.class).findAll();
復雜查詢這里就不多說了。
需要注意的是,如果要對查詢的結(jié)果進行修改或刪除等操作,則必須要在 transaction 里完成,修改的結(jié)果會同步到數(shù)據(jù)庫。比如,我想對上面查詢到的第一個元素進行修改:
Data data = list.get(0)
mRealm.beginTransaction()
mRealm.copyFromRealm(data)
data.num = 666
mRealm.commitTransaction()
小結(jié)
就先到這吧,一個索引頁都能扯這么多。
其實我沒什么想總結(jié)的,我只想提醒一下:GOT Session 7 !!!