使用Android Architecture Component開發(fā)應(yīng)用(附demo)

image

相關(guān)文章:

今年的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)圖示。

架構(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文檔

正在上映:

正在上映API

即將上映:

即將上映API

Top250:

Top250API

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)容:

  1. 將Retrofit層獲取到的數(shù)據(jù)持久化在數(shù)據(jù)庫中
  2. 從數(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,這些是對我最大的回報和動力,謝謝。


  1. 這里的下拉是指手指從下向上滑動,相反上拉是指從上向下滑動。 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容