
相關(guān)文章:
- 【翻譯】安卓架構(gòu)組件(1)-App架構(gòu)指導(dǎo)
- 【翻譯】安卓架構(gòu)組件(2)-添加組件到你的項目中
- 【翻譯】安卓架構(gòu)組件(3)-處理生命周期
- 【翻譯】安卓架構(gòu)組件(4)-LiveData
- 【翻譯】安卓架構(gòu)組件(5)-ViewModel
- 【翻譯】安卓架構(gòu)組件(6)-Room持久化類庫
- 【翻譯】安卓架構(gòu)組件(7)-分頁庫
- 安裝包下載地址
今年的Google I/O大會發(fā)布了一系列的類庫,統(tǒng)稱為架構(gòu)組件(Architecture Component),旨在幫助開發(fā)者構(gòu)建健壯、易于測試和易于維護(hù)的應(yīng)用。從我目前掌握的情況來看,這一系列類庫為開發(fā)者指明了比較清晰的架構(gòu)思路,確實可以顯著提高應(yīng)用的開發(fā)效率且對質(zhì)量有一定的保證。于是我盡快翻譯了官方所有相關(guān)的文檔(即上面的1-6篇)。我最初接觸時這些類庫還處于alpha-3版本,時至今日已經(jīng)發(fā)布了1.0版本,趨于穩(wěn)定。同時我發(fā)現(xiàn)Google又發(fā)布了分頁的類庫,于是有了第七篇翻譯。在翻譯的文章中,有朋友詢問能否提供一份Demo,以提供一個較為清晰直觀的印象,于是就有了這篇文章。在文章的最后我會提供app的演示,安裝包下載以及將項目開源至github。
本文并不會詳細(xì)介紹每個類庫,相關(guān)的內(nèi)容還請在上面的文章中查閱。本文的主要目的是使用這些類庫來開發(fā)一款應(yīng)用,并介紹整個的開發(fā)過程,從而看到這一系列類庫的用法以及特點。
此外在整個應(yīng)用的構(gòu)建中我們不會涉及到所有的用法(根據(jù)我在實際項目的使用情況,本文所介紹的內(nèi)容足以應(yīng)付日常的需求),如果需要深入了解,請自行研究源碼。
0.準(zhǔn)備
要構(gòu)建一款什么樣的應(yīng)用?閱讀本文的大量讀者都是Android應(yīng)用的開發(fā)者,涉及服務(wù)端研發(fā)的并不多,為了避免增加不必要的學(xué)習(xí)曲線,我認(rèn)為自行開發(fā)服務(wù)端接口是沒有意義的。所以經(jīng)過考慮,我選中了豆瓣的電影API,因此該應(yīng)用的所有數(shù)據(jù)源皆獲取自豆瓣。
所以本文會介紹一個電影信息app的開發(fā)過程。
關(guān)于本文開發(fā)中所需要了解的技術(shù)如下:
- 基本的Android開發(fā)經(jīng)驗,如RecyclerView之類控件的使用等
- RxJava
- Kotlin
- Retrofit2
我相信對于閱讀本文的讀者,這些要求并不是什么問題。在響應(yīng)式編程如此火熱的現(xiàn)在,RxJava和Retrofit已經(jīng)成了很多項目的必備基礎(chǔ)技術(shù),而Kotlin已經(jīng)成為官方宣布支持的語言。
好了,現(xiàn)在讓我們開始。
本文會分成兩個階段編寫,在第一個階段我們會使用Android架構(gòu)組件編寫三個電影列表,即下文中的正在上映,即將上映和Top250。為了整個demo的完善性,電影詳情界面的編寫會在后面完成,截止本文發(fā)布時,只完成了第一個階段。
1.基本的界面編寫
這一部分沒有什么可值得介紹的地方,但是需要說明一下我們的界面。我們會選取三個列表進(jìn)行展示:
- 正在上映
- 即將上映
- Top250
正在上映是正在院線上映的電影列表,即將上映是即將在院線上映的電影列表,Top250是指評分最高的250部電影。
需要說明的是,“正在上映”和“即將上映”和具體的城市綁定,為了簡化該部分對本文核心內(nèi)容的影響,我們在接口數(shù)據(jù)的請求時會使用默認(rèn)的城市,即北京。
界面如下:

你可以使用任何你熟悉的類庫來完成這三個Fragment,需要實現(xiàn)下拉加載和上拉更新等操作[1]。
2.架構(gòu)綜述
界面的編寫不是什么困難的工作,真正需要我們關(guān)心的是整個應(yīng)用的架構(gòu)是怎樣的?;仡櫼幌?a target="_blank" rel="nofollow">第一篇文章,我們在這里給出我們的架構(gòu)圖示。

三個Fragment會通過各自的ViewModel獲取數(shù)據(jù),而所有的ViewModel都會從MovieRepository拿到數(shù)據(jù)。在MovieRepository中,我們通過Retrofit從豆瓣API獲得數(shù)據(jù),存儲在Room中,而MovieRepository則從Room獲取數(shù)據(jù)。
3.網(wǎng)絡(luò)層
現(xiàn)在我們開始編寫這個應(yīng)用的主要部分,首先從網(wǎng)絡(luò)請求入手,即架構(gòu)圖示中的Retrofit部分。
3.1 豆瓣API
首先我們需要查閱豆瓣的API文檔
正在上映:

即將上映:

Top250:

3.2接口編寫
于是有了API接口的編寫:
interface DoubanApi {
/**
* @param city 表示院線所在城市,可為空,如果為空則默認(rèn)為北京市
*
* @return [MoviesResp]
*/
@GET("v2/movie/in_theaters")
fun retrieveInTheaters(@Query("city") city: String?): Observable<MoviesResp>
/**
* 即將上映的電影
* @param start 開始,默認(rèn)為0
* @param count 每次請求數(shù)量,默認(rèn)為20
*
* @return [MoviesResp]
*/
@GET("v2/movie/coming_soon")
fun retrieveComingSoon(@Query("start") start: Int?, @Query("count") count: Int?): Observable<MoviesResp>
/**
* Top250 評分最高的電影
* @param start 開始,默認(rèn)為0
* @param count 每次請求數(shù)量,默認(rèn)為20
*
* @return [MoviesResp]
*/
@GET("v2/movie/top250")
fun retrieveTop250(@Query("start") start: Int?, @Query("count") count: Int?): Observable<MoviesResp>
}
需要說明的是,這里的MoviesResp是豆瓣API接口返回json數(shù)據(jù)所對應(yīng)的實體類,詳細(xì)的內(nèi)容可在demo中查看。
3.3 Retrofit編寫
接下來是Retrofit的編寫工作,由于本文不是講解Retrofit的內(nèi)容,因此在這里直接給出代碼。
class DoubanRetrofit {
private val TAG: String = this.javaClass.simpleName
private val PAGESIZE = Constant.PAGESIZE//20
companion object {
private val API = buildAPI()
private fun buildAPI(): DoubanApi {
return Retrofit.Builder()
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(JacksonConverterFactory.create())
.baseUrl(Constant.url)// "http://api.douban.com/"
.build()
.create(DoubanApi::class.java)
}
private var instance: DoubanRetrofit? = null
@Synchronized
fun get(): DoubanRetrofit {
if (null == instance) {
instance = DoubanRetrofit()
}
return instance!!
}
}
/**
* 正在上映
*/
fun inTheaterMovies():Observable<MoviesResp>{
return API.retrieveInTheaters(null)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
}
/**
* 即將上映
* @param start 開始位置
*/
fun commingSoonMovies(start:Int):Observable<MoviesResp>{
return API.retrieveComingSoon(start,PAGESIZE)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
}
/**
* 評分top250電影
* @param start 開始位置
*/
fun top250Movies(start:Int):Observable<MoviesResp>{
return API.retrieveTop250(start,PAGESIZE)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
}
}
上面的代碼并不是最終版本,我們會在后面進(jìn)行修改
正如在架構(gòu)圖示所描述的那樣,網(wǎng)絡(luò)請求層獲取的數(shù)據(jù)會通過ROOM存儲在本地數(shù)據(jù)庫,所以接下來我們要先完成持久層的編寫。
4.持久層
我們已經(jīng)完成了網(wǎng)絡(luò)層的編寫,接下來需要進(jìn)行持久層的編寫。
4.1 實體類
我們需要定義電影的實體類。由于我們應(yīng)用的當(dāng)前版本只展示了電影列表,那么不妨先從這個角度來看看界面是怎樣的:

于是我們有了實體類:
/**
* 項目 : ArchitectureComponentDemo
* 作者 : Chuckifan
* 時間 : 2017/11/26 16:40
* 內(nèi)容 : 電影列表項,用于持久化
* @param id 電影id
* @param avatar 電影圖片
* @param title 電影名稱
* @param rating 電影評分
* @param director 導(dǎo)演
* @param casts 主演
* @param genres 類型
* @param year 年份
* @param isInTheater true 正在上映
* @param isComming true 即將上映
* @param isTop250 true top250
*/
data class Movie(var id: Long = 0,
var avatar:String?="",
var title: String? = "未知", var rating: Float? = 0f, var ratingStr: String? = "(0.0)", var director: String? = "未知",
var casts: String? = "未知", var genres: String? = "未知", var year: String? = "未知",
var isInTheater: Boolean = false, var isComming:Boolean = false,var isTop250: Boolean = false)
上面的注釋已經(jīng)很清晰地說明了每個成員變量的用途。
4.2 ROOM
4.2.1 實體類
如果僅僅是定義了這樣的一個類,那么它和API返回數(shù)據(jù)所對應(yīng)的實體類就沒有任何區(qū)別了,我們接下來需要使用ROOM對這個實體類進(jìn)行標(biāo)記,使其可以映射為關(guān)系數(shù)據(jù)庫中的數(shù)據(jù)元素:
/**
* 項目 : ArchitectureComponentDemo
* 作者 : Chuckifan
* 時間 : 2017/11/26 16:40
* 內(nèi)容 : 電影列表項,用于持久化
* @param id 電影id
* @param avatar 電影圖片
* @param title 電影名稱
* @param rating 電影評分
* @param director 導(dǎo)演
* @param casts 主演
* @param genres 類型
* @param year 年份
* @param isInTheater true 正在上映
* @param isComming true 即將上映
* @param isTop250 true top250
*/
@Entity
data class Movie(@PrimaryKey val id: Long = 0,
var avatar:String?="",
var title: String? = "未知", var rating: Float? = 0f, var ratingStr: String? = "(0.0)", var director: String? = "未知",
var casts: String? = "未知", var genres: String? = "未知", var year: String? = "未知",
var isInTheater: Boolean = false, var isComming:Boolean = false,val isTop250: Boolean = false)
這里插入一點和本應(yīng)用編寫無關(guān)的內(nèi)容。我個人主張不直接使用遠(yuǎn)程服務(wù)所返回的數(shù)據(jù)直接持久化到數(shù)據(jù)庫,這是為了將持久層與網(wǎng)絡(luò)層解耦,如果我們不想使用豆瓣的數(shù)據(jù)接口還可以使用其他服務(wù)接口,比如IMDb。我們唯一需要做的工作是修改從接口數(shù)據(jù)實體類到持久化類的轉(zhuǎn)化方法。
但是本文最重要的目的是介紹一個用于熟悉安卓架構(gòu)組件的demo。我在本文之前曾經(jīng)用Java寫過一個版本,在那個版本中為了熟悉這套類庫以及節(jié)省時間,我直接使用了豆瓣返回的數(shù)據(jù),那么就涉及到實體類的嵌套,以及列表數(shù)據(jù)的映射處理,關(guān)于這部分內(nèi)容的介紹請參閱這里。在本文不會對這部分內(nèi)容進(jìn)行介紹,僅僅會給出相關(guān)代碼,當(dāng)然這部分代碼不會在最后的應(yīng)用出現(xiàn):
Subject:電影條目
@Entity
public class Subject {
@Embedded
private Rating rating;
private List<String> genres;
private String title;
private List<Cast> casts;
private int collect_count;
private String original_title;
private String subtype;
private List<Director> directors;
private String year;
@Embedded
private Image images;
private String alt;
@PrimaryKey
private String id;
public Rating getRating() {
return rating;
}
public void setRating(Rating rating) {
this.rating = rating;
}
public List<String> getGenres() {
return genres;
}
public void setGenres(List<String> genres) {
this.genres = genres;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public List<Cast> getCasts() {
return casts;
}
public void setCasts(List<Cast> casts) {
this.casts = casts;
}
public int getCollect_count() {
return collect_count;
}
public void setCollect_count(int collect_count) {
this.collect_count = collect_count;
}
public String getOriginal_title() {
return original_title;
}
public void setOriginal_title(String original_title) {
this.original_title = original_title;
}
public String getSubtype() {
return subtype;
}
public void setSubtype(String subtype) {
this.subtype = subtype;
}
public List<Director> getDirectors() {
return directors;
}
public void setDirectors(List<Director> directors) {
this.directors = directors;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public Image getImages() {
return images;
}
public void setImages(Image images) {
this.images = images;
}
public String getAlt() {
return alt;
}
public void setAlt(String alt) {
this.alt = alt;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Rating:評分
public class Rating {
private int max;
private double average;
private String stars;
private int min;
public void setMax(int max) {
this.max = max;
}
public int getMax() {
return max;
}
public void setAverage(double average) {
this.average = average;
}
public double getAverage() {
return average;
}
public void setStars(String stars) {
this.stars = stars;
}
public String getStars() {
return stars;
}
public void setMin(int min) {
this.min = min;
}
public int getMin() {
return min;
}
}
Cast:主演,最多可獲得4個,數(shù)據(jù)結(jié)構(gòu)為影人的簡化描述,
public class Cast {
private String alt;
@Embedded
private Avatar avatars;
private String name;
private String id;
public void setAlt(String alt) {
this.alt = alt;
}
public String getAlt() {
return alt;
}
public Avatar getAvatars() {
return avatars;
}
public void setAvatars(Avatar avatars) {
this.avatars = avatars;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Director:導(dǎo)演,數(shù)據(jù)結(jié)構(gòu)為影人的簡化描述
public class Director {
private String alt;
@Embedded
private Avatar avatars;
private String name;
private String id;
public String getAlt() {
return alt;
}
public void setAlt(String alt) {
this.alt = alt;
}
public Avatar getAvatars() {
return avatars;
}
public void setAvatars(Avatar avatars) {
this.avatars = avatars;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Image:電影海報圖,分別提供288px x 465px(大),96px x 155px(中) 64px x 103px(小)尺寸
public class Image {
private String small;
private String large;
private String medium;
public void setSmall(String small) {
this.small = small;
}
public String getSmall() {
return small;
}
public void setLarge(String large) {
this.large = large;
}
public String getLarge() {
return large;
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getMedium() {
return medium;
}
}
Avatar:影人頭像,分別提供420px x 600px(大),140px x 200px(中) 70px x 100px(小)尺寸
public class Avatar{
private String small;
private String large;
private String medium;
public void setSmall(String small) {
this.small = small;
}
public String getSmall() {
return small;
}
public void setLarge(String large) {
this.large = large;
}
public String getLarge() {
return large;
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getMedium() {
return medium;
}
}
以及Converter:
public class ListConverter {
private static final ObjectMapper mapper = new ObjectMapper();
@TypeConverter
public static String strList2Json(List<String> value) {
String result = null;
try {
result = mapper.writeValueAsString(value);
} catch (IOException e) {
LogUtils.e(e);
}
return result;
}
@TypeConverter
public static List<String> json2StrList(String json) {
List<String> result = null;
try {
JavaType javaType = getCollectionType(ArrayList.class, String.class);
result = mapper.readValue(json, javaType);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
@TypeConverter
public static String castList2Json(List<Cast> value) {
String result = null;
try {
result = mapper.writeValueAsString(value);
} catch (IOException e) {
LogUtils.e(e);
}
return result;
}
@TypeConverter
public static List<Cast> json2CastList(String json) {
List<Cast> result = null;
try {
JavaType javaType = getCollectionType(ArrayList.class, Cast.class);
result = mapper.readValue(json, javaType);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
@TypeConverter
public static String directorList2Json(List<Director> value) {
String result = null;
try {
result = mapper.writeValueAsString(value);
} catch (IOException e) {
LogUtils.e(e);
}
return result;
}
@TypeConverter
public static List<Director> json2DirectorList(String json) {
List<Director> result = null;
try {
JavaType javaType = getCollectionType(ArrayList.class, Director.class);
result = mapper.readValue(json, javaType);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
private static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
}
}
轉(zhuǎn)換類的使用在文章中有介紹,這里不再贅述。
4.2.2 DAO
在使用分頁庫之前我們無法完整的介紹DAO的代碼,這里暫時只給出寫入和刪除的方法:
@Dao
interface MovieDao {
/**
* 保存電影,沖突時替換
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun save(movies: List<Movie>)
/**
* 刪除正在上映的電影
*/
@Query("DELETE FROM Movie WHERE isInTheater")
fun removeIntheaterMovies()
/**
* 刪除即將上映的電影
*/
@Query("DELETE FROM Movie WHERE isComming")
fun removeCommingSoonMovies()
/**
* 刪除TOP250的電影
*/
@Query("DELETE FROM Movie WHERE isTop250")
fun removeTop250Movies()
}
4.2.3 Database
接下來是數(shù)據(jù)庫類的編寫:
@Database(entities = arrayOf(Movie::class),version = BuildConfig.VERSION_CODE)
abstract class MovieDatabse : RoomDatabase(){
abstract fun movieDao():MovieDao
companion object {
private var instance: MovieDatabse? = null
@Synchronized
fun get(context: Context): MovieDatabse {
if (instance == null) {
instance = Room.databaseBuilder(context.applicationContext,
MovieDatabse::class.java, "MovieDB")
.build()
}
return instance!!
}
}
}
至此,持久層的編寫可以暫時告一段落。
5. Repository和分頁
在這一部分,我們要完成Repository的編寫。正如架構(gòu)圖示多描述的那樣,Repository層的數(shù)量往往和DAO相對應(yīng)。我們需要在Repository層完成接口數(shù)據(jù)的獲取以及轉(zhuǎn)換為持久化數(shù)據(jù),同時需要完成持久化數(shù)據(jù)的完成。
5.1 完善DAO
首先完善DAO中獲取電影數(shù)據(jù)的部分:
@Dao
interface MovieDao {
/**
* 保存電影,沖突時替換
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun save(movies: List<Movie>)
/**
* 刪除正在上映的電影
*/
@Query("DELETE FROM Movie WHERE isInTheater")
fun removeIntheaterMovies()
/**
* 刪除即將上映的電影
*/
@Query("DELETE FROM Movie WHERE isComming")
fun removeCommingSoonMovies()
/**
* 刪除TOP250的電影
*/
@Query("DELETE FROM Movie WHERE isTop250")
fun removeTop250Movies()
/**
* 獲取正在上映的電影
*/
@Query("SELECT * FROM Movie WHERE isInTheater")
fun queryIntheaterMovies():LivePagedListProvider<Int,Movie>
/**
* 獲取正在上映的電影
*/
@Query("SELECT * FROM Movie WHERE isComming")
fun queryCommingsoonMovies():LivePagedListProvider<Int,Movie>
/**
* 獲取top250的電影
*/
@Query("SELECT * FROM Movie WHERE isTop250 ORDER BY rating DESC")
fun queryTOP250Movies():LivePagedListProvider<Int,Movie>
}
截止目前,這篇文章不會涉及到LiveData的使用,我會盡快完善電影詳情部分,到時候會補充LiveData的內(nèi)容。
5.2完善Retrofit
接下來我們可以完善Retrofit的部分,使得從豆瓣獲取的數(shù)據(jù)可以轉(zhuǎn)化為我們持久層的實體類:
class DoubanRetrofit {
private val TAG: String = this.javaClass.simpleName
private val PAGESIZE = Constant.PAGESIZE//20
companion object {
private val API = buildAPI()
private fun buildAPI(): DoubanApi {
return Retrofit.Builder()
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(JacksonConverterFactory.create())
.baseUrl(Constant.url)// "http://api.douban.com/"
.build()
.create(DoubanApi::class.java)
}
private var instance: DoubanRetrofit? = null
@Synchronized
fun get(): DoubanRetrofit {
if (null == instance) {
instance = DoubanRetrofit()
}
return instance!!
}
}
/**
* 正在上映
*/
fun inTheaterMovies(): Observable<List<Movie>> {
return API.retrieveInTheaters(null)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.concatMap({ origin ->
Observable.just(origin.subjects)
}).map({ subjects ->
convert(subjects, true, false, false)
})
}
/**
* 即將上映
* @param start 開始位置
*/
fun commingSoonMovies(start: Int): Observable<List<Movie>> {
return API.retrieveComingSoon(start, PAGESIZE)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.concatMap({ origin ->
Observable.just(origin.subjects)
}).map({ subjects ->
convert(subjects, false, true, false)
})
}
/**
* 評分top250電影
* @param start 開始位置
*/
fun top250Movies(start: Int): Observable<List<Movie>> {
return API.retrieveTop250(start, PAGESIZE)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.concatMap({ origin ->
Observable.just(origin.subjects)
}).map({ subjects ->
convert(subjects, false, false, true)
})
}
/**
* [Subject]列表轉(zhuǎn)為[Movie]列表
*/
private fun convert(subjects: List<Subject>, isInTheater: Boolean, isComming: Boolean, isTop250: Boolean): List<Movie> {
val movies: List<Movie> = subjects.map { item ->
//評分
val rating: Float
val ratingStr: String
if (null == item.rating) {
ratingStr = "(0.0)"
rating = 0.0f
} else {
rating = (item.rating.average / 2).toFloat()
ratingStr = "(${item.rating.average})"
}
//導(dǎo)演
val directors: String = if (null == item.directors) {
""
} else {
item.directors.joinToString("/","","",-1,"...",{it ->
it.name
})
}
//主演
val casts = if (null == item.casts) {
""
} else {
item.casts.joinToString("/","","",-1,"...",{it ->
it.name
})
}
//類型
val genres = if (null == item.genres) {
""
} else {
item.genres.joinToString("/","","",-1,"...")
}
val movie = Movie(item.id.toLong()
, item.images.medium, item.title, rating, ratingStr, directors, casts, genres,
item.year, isInTheater, isComming, isTop250)
movie
}
return movies
}
}
5.4 Repository
完善了DAO和Retrofit的編寫以后,我們就可以開始編寫Repository層的代碼。在Repository層,我們需要完成以下兩個內(nèi)容:
- 將Retrofit層獲取到的數(shù)據(jù)持久化在數(shù)據(jù)庫中
- 從數(shù)據(jù)庫中獲取數(shù)據(jù)
就是說,Repository層連接了Retrofit和ROOM,就像架構(gòu)圖示所描述的那樣。
class MovieRepository(context: Context) {
private val dao: MovieDao = MovieDatabse.get(context).movieDao()
private val inTheaterMovies = dao.queryIntheaterMovies()
private val commingsoonMovies = dao.queryCommingsoonMovies()
private val top250Movies = dao.queryTOP250Movies()
private val retrofit = DoubanRetrofit.get()
/**
* 獲取正在上映的電影
*/
fun getInTheaterMovies() = inTheaterMovies
/**
* 獲取即將上映的電影
*/
fun getCommingsoonMovies() = commingsoonMovies
/**
* 獲取TOP250的電影
*/
fun getTop250Movies() = top250Movies
/**
* 刷新正在上映的電影,并刪除之前的數(shù)據(jù)
*/
fun refreshInTheaterMovies(view: RefreshView){
retrofit.inTheaterMovies()
.subscribe({movies->
ioThread {
dao.removeIntheaterMovies()
dao.save(movies)
}
}, { error ->
view.onError(error)
view.onRefreshCompleted()
}, {
view.onRefreshCompleted()
})
}
/**
* 刷新即將上映的電影,并刪除之前的數(shù)據(jù)
*/
fun refreshCommingsoonMovies(view: RefreshView){
retrofit.commingSoonMovies(0)
.subscribe({movies->
ioThread {
dao.removeCommingSoonMovies()
dao.save(movies)
}
}, { error ->
view.onError(error)
view.onRefreshCompleted()
}, {
view.onRefreshCompleted()
})
}
/**
* 刷新TOP250的電影,并刪除之前的數(shù)據(jù)
*/
fun refreshTop250Movies(view:RefreshView){
retrofit.top250Movies(0)
.subscribe({movies->
ioThread {
dao.removeTop250Movies()
dao.save(movies)
}
}, { error ->
view.onError(error)
view.onRefreshCompleted()
}, {
view.onRefreshCompleted()
})
}
/**
* 加載更多即將上映的電影
*/
fun loadMoreCommingsoonMovies(start:Int, view: RefreshView){
retrofit.commingSoonMovies(start)
.subscribe({movies->
ioThread {
dao.save(movies)
}
}, { error ->
view.onError(error)
view.onLoadMoreCompleted()
}, {
view.onLoadMoreCompleted()
})
}
/**
* 加載更多TOP250的電影
*/
fun loadMoreTop250Movies(start: Int, view:RefreshView){
retrofit.top250Movies(start)
.subscribe({movies->
ioThread {
dao.save(movies)
}
}, { error ->
view.onError(error)
view.onLoadMoreCompleted()
}, {
view.onLoadMoreCompleted()
})
}
}
至此,Repository完成。
6. ViewModel
三個ViewModel層的編寫比較相似,區(qū)別在于正在上映的ViewModel沒有分頁加載、每個ViewModel所調(diào)用Repository的方法不同。這里我們選取即將上映的ViewModel進(jìn)行介紹:
class CommingsoonViewModel(app: Application) : AndroidViewModel(app) {
private val repo = MovieRepository(app)
private val datas = repo.getCommingsoonMovies()
private var start:Int = 0
fun getData() = datas.create(0, PagedList.Config.Builder()
.setPageSize(Constant.PAGESIZE)
.setEnablePlaceholders(Constant.ENABLE_PLACEHOLDERS)
.build())
fun refresh(view: RefreshView){
start = 0
repo.refreshCommingsoonMovies(view)
}
fun loadmore(view:RefreshView){
start += Constant.PAGESIZE
repo.loadMoreCommingsoonMovies(start,view)
}
}
回顧一下架構(gòu)圖示,ViewModel層調(diào)用Repository的方法。我們在這里使用PagedList對分頁數(shù)據(jù)進(jìn)行配置。
7. UI補遺
最后我們再來看看UI方面有哪些需要補充的地方。
7.1 item_movie
首先是列表項的layout,item_movie:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:orientation="horizontal"
>
<ImageView android:id="@+id/picture" android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:contentDescription="@string/movie_face"
android:src="@mipmap/ic_launcher"/>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical"
>
<TextView android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/title_text_size"
tools:text="肖申克的救贖 The Shawshank Redemption"/>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<RatingBar android:id="@+id/rating_bar"
style="@style/Base.Widget.AppCompat.RatingBar.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:numStars="5"
/>
<TextView android:id="@+id/rating_des" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
tools:text=" (5.2)"/>
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView android:id="@+id/textView" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/director"/>
<TextView android:id="@+id/director" android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="弗蘭克·德拉邦特"/>
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="@string/actor"
/>
<TextView android:id="@+id/actors" android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="蒂姆·羅賓斯 / 摩根·弗里曼 / 鮑勃·岡頓 "/>
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="@string/type"
/>
<TextView android:id="@+id/type" android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="犯罪/劇情 "/>
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="@string/year"
/>
<TextView android:id="@+id/year" android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="1994"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
7.2 MovieAdapter
其次是用于RecyclerView的MovieAdapter:
class MovieAdapter : PagedListAdapter<Movie, MovieViewHolder>(diffCallback) {
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.bindTo(getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder =
MovieViewHolder(parent)
companion object {
/**
* This diff callback informs the PagedListAdapter how to compute list differences when new
* PagedLists arrive.
* <p>
* When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to
* detect there's only a single item difference from before, so it only needs to animate and
* rebind a single view.
*
* @see android.support.v7.util.DiffUtil
*/
private val diffCallback = object : DiffCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem.id == newItem.id
/**
* Note that in kotlin, == checking on data classes compares all contents, but in Java,
* typically you'll implement Object#equals, and use it to compare object contents.
*/
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem == newItem
}
}
}
7.3 Fragment
最后是我們Fragment中的關(guān)鍵代碼片段:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(CommingsoonViewModel::class.java)
viewModel.getData().observeForever(Observer(adapter::setList))
onRefresh()
}
8. 總結(jié)(附demo)
app的演示:點擊跳轉(zhuǎn)
安裝包下載地址以及開源代碼地址:點擊跳轉(zhuǎn)
本文介紹了使用安卓架構(gòu)組件構(gòu)建一個簡單應(yīng)用的過程。誠然,目前為止我們沒有介紹到所有相關(guān)類庫的基本使用,我會在后面盡量補充LiveData等相關(guān)內(nèi)容的介紹。
但是從另一個角度來看,正如谷歌自己所說,這套類庫只是提供一個架構(gòu)設(shè)計的思路和參考,如果我們有更好的選擇,完全可以不用關(guān)心這套組件。而這套組件中的類庫也是可選擇的,比如,你在持久層有更好的選擇,就不需要使用Room。
本文書寫以及代碼的編寫是在業(yè)余時間完成的,難免倉促和疏忽。如果您有任何問題和建議歡迎在文章下方評論,或者在github上提issue,我會及時回復(fù)并定期整理在文章中。
另外,歡迎您為文章點贊以及在github項目中點擊star,這些是對我最大的回報和動力,謝謝。
-
這里的下拉是指手指從下向上滑動,相反上拉是指從上向下滑動。 ?