玩安卓從 0 到 1 之項目首頁

前言

上一篇文章玩安卓從 0 到 1 之總體概覽感覺沒寫好,果不其然,反響不咋地。當然也正常,既沒有好看的頁面(只是最簡單的md文檔),又因為好久沒寫文章了,國慶假期也剛過,有點懵逼,寫的時候有好多地方想好好說一說但不知道該怎樣描述,于是乎一通粘貼代碼。結(jié)果。。。

以后寫文章可不能這樣了,好看的文章不太會排版,還是盡量注重內(nèi)容吧?,F(xiàn)在寫文章不僅僅是自己的筆記了,寫的不對的話有可能誤人子弟;不過反過來說,想要不被誤導直接去看官網(wǎng)不得了,那里雖然又可能也有錯誤但畢竟是少數(shù)。

牢騷發(fā)到此為止,下面就開始今天的正文吧。

正文

這篇文章說的依然是玩安卓這個 app,按照慣例,還是放一下 Github 地址和 apk 下載地址吧。

apk 下載地址:www.pgyer.com/llj2

Github地址:github.com/zhujiang521…

看到標題應(yīng)該清楚咱們今天要實現(xiàn)的項目的首頁,先來看一下實現(xiàn)好的樣子吧!

[圖片上傳失敗...(image-9d1c06-1602472643702)]

看起來是不是很簡單!結(jié)構(gòu)很清晰,最上面是標題欄,往下是 Banner ,再往下就是文章列表了,很簡單的一個首頁。實現(xiàn)方式有幾種,要么直接使用 RecyclerView 直接排列下來,要么用 LinearLayout 一個一個往下排,其實并沒有哪種實現(xiàn)方式更好,喜歡使用哪種就用哪種不得了!我在這里選擇的使用 LinearLayout 一個一個往下排,簡單清晰明了,挺好!

TitleBar 標題欄

咱們就一個一個來吧!先來看下 TitleBar 在首頁需要的功能:中間的標題、右上角的搜索和點擊事件,之前寫過一篇文章就寫的怎樣自定義 TitleBar :構(gòu)建安卓項目通用TitleBar,有需要的可以看下。

來看下怎樣使用吧:

    <com.zj.core.util.TitleBar
        android:id="@+id/homeTitleBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:backImageVisiable="false"
        app:titleName="首頁" />

很簡單吧,在布局中可以直接指定標題名稱和是否顯示返回按鈕,這里的首頁明顯不需要返回按鈕,所以設(shè)置的 false 。咱們來看下在代碼中怎樣設(shè)置這兩個屬性:

    homeTitleBar.setTitle("標題")
    homeTitleBar.setBackImageVisiable(false)

剛才還提到右上角要顯示一個搜索的文本并要有點擊事件,咱們來看下怎樣寫:

    homeTitleBar.setRightText("搜索")
    homeTitleBar.setRightTextOnClickListener {
        // 點擊事件要實現(xiàn)的邏輯
    }

是不是很簡單,這就完事了,當然如果想寫一個布局每個頁面進行 include 也不是不可以,只是有點麻煩而已,實現(xiàn)效果其實是一樣的,沒有什么對錯好壞之分。

Banner

Banner 的話為了簡單省事就直接使用三方庫了,之前也寫過一篇這個三方庫的簡單使用,可以參考:安卓實現(xiàn)Banner輪播圖自定義圖片(非網(wǎng)絡(luò)圖片)。

三方庫的依賴如下:

implementation 'com.youth.banner:banner:2.1.0'

寫一下使用吧:

<com.youth.banner.Banner
    android:id="@+id/homeBanner"
    android:layout_width="match_parent"
    android:layout_height="@dimen/dp_180" />

布局很簡單,直接寫上就可以了。代碼也不難,只需要寫一個適配器然后放進去設(shè)置開始即可。

先寫一個適配器吧:

open class ImageAdapter(private val mContext: Context, mData: List<BannerBean>) :
    BannerAdapter<BannerBean?, ImageAdapter.BannerViewHolder?>(mData) {
    override fun onCreateHolder(parent: ViewGroup,viewType: Int): BannerViewHolder {
        val imageView = ImageView(parent.context)
        imageView.layoutParams = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )
        imageView.scaleType = ImageView.ScaleType.CENTER_CROP
        return BannerViewHolder(imageView)
    }

    class BannerViewHolder(view: ImageView) :
        RecyclerView.ViewHolder(view) {
        var imageView: ImageView = view
    }

    override fun onBindView(holder: BannerViewHolder?,data: BannerBean?,
                            position: Int,size: Int) {
        Glide.with(mContext).load(if (data?.filePath == null) data?.imagePath else data.filePath).into(holder!!.imageView)
    }
}

二十來行代碼,核心就是通過 Glide 加載一下圖片。

上面說了,要把適配器放進去,放到哪呢?當然是 Banner 中了:

val bannerAdapter = ImageAdapter(context!!, viewModel.bannerList)
homeBanner.adapter = bannerAdapter
// 設(shè)置為圓形指示器并開始
homeBanner.setIndicator(CircleIndicator(context)).start()

到這里就差不多了,但是為了避免內(nèi)存泄露和提升性能,需要在 onResume 頁面可見的時候開始滾動,在 onPause 頁面不可見的時候停止?jié)L動:

    override fun onResume() {
        super.onResume()
        homeBanner.start()
    }
    
    override fun onPause() {
        super.onPause()
        homeBanner.stop()
    }

RecyclerView

排到這里就該用 RecyclerView 來展示文章了,這個布局就不貼了,太簡單了,但想了想還是需要貼一下,因為這里需要有下拉刷新和上拉加載,這里用到了一個三方庫,大家應(yīng)該都不陌生,下面是依賴:

implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.2'

接下來是布局使用方法:

<com.scwang.smartrefresh.layout.SmartRefreshLayout
    android:id="@+id/homeSmartRefreshLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/homeRecycleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</com.scwang.smartrefresh.layout.SmartRefreshLayout>

RecyclerView 的適配器我用的是泓洋大神的開源庫,下面是依賴:

api 'com.zhy:base-rvadapter:3.0.3'

先來看一下下拉刷新和上拉加載怎么使用吧:

homeSmartRefreshLayout.apply {
    setOnRefreshListener { reLayout ->
        reLayout.finishRefresh(measureTimeMillis {
             page = 1
             getArticleList(true)
        }.toInt())
    }
    setOnLoadMoreListener { reLayout ->
        val time = measureTimeMillis {
             page++
             getArticleList(true)
        }.toInt()
        reLayout.finishLoadMore(if (time > 1000) time else 1000)
    }
}

通過回調(diào)的名稱也能猜出來怎么調(diào)用。

適配器的代碼就不往上貼了,大家感興趣的話可以去上面 Github 中下載代碼看。

OK了,首頁布局這就完成了,接下來就該獲取數(shù)據(jù)了!

獲取數(shù)據(jù)

Banner數(shù)據(jù)

這里需要思考一下,Banner 數(shù)據(jù)是否會實時更新,據(jù)我所知,Banner 數(shù)據(jù)最多一周更新一回,目前市面上的玩安卓 app 大部分都是每次進入都會請求下網(wǎng)絡(luò),然后再重新加載,這樣無疑會增加網(wǎng)絡(luò)成本,更何況都是圖片,會消耗用戶的 money 啊,雖然說現(xiàn)在流量不值錢,但也需要盡可能地省?。?/p>

所以這里的 Banner 數(shù)據(jù)我進行了一些操作,先去本地數(shù)據(jù)庫中查看是否有 Banner 的數(shù)據(jù),如果有,并且和上回刷新的時間間隔在一天以內(nèi)(這里為了預(yù)防更新,所以暫定的一天),那么就把數(shù)據(jù)庫中的數(shù)據(jù)返回,如果沒有數(shù)據(jù)或者和上回刷新的時間間隔在一天以外的話就去請求網(wǎng)絡(luò)數(shù)據(jù),請求網(wǎng)絡(luò)數(shù)據(jù)又分為成功或失敗,如果失敗則返回失敗信息,頁面進行顯示對應(yīng)頁面;如果成功則記錄下當前的時間,如果數(shù)據(jù)庫查出來有數(shù)據(jù)并且和請求到的數(shù)據(jù)一樣,那么就還返回數(shù)據(jù)庫中的數(shù)據(jù);反之,則把數(shù)據(jù)庫中的 Banner 數(shù)據(jù)刪除,并且把請求到的數(shù)據(jù)插入到數(shù)據(jù)庫中,然后把數(shù)據(jù)返回。

這里說的有點繞,我還是畫個圖給大家看看吧,好理解一些:

[圖片上傳失敗...(image-a0e8eb-1602472643703)]

沒怎么畫過類似的流程圖,之前畫過都是在大學的時候,平時話也就是在本上隨便畫畫,畫的不好或者不對的地方各位多多包涵!大概說下這張圖,意思其實和上面那段話描述的意思一致,從中間粉色的 查看本地數(shù)據(jù)庫 開始,分為各種情況下的數(shù)據(jù)獲取方式。

好了,說了這么多又畫了這么多,是時候看看代碼實現(xiàn)了:

    fun getBanner(application: Application) = fire {
        val spUtils = SPUtils.getInstance()
        val downImageTime by Preference(DOWN_IMAGE_TIME, System.currentTimeMillis())
        val bannerBeanDao = PlayDatabase.getDatabase(application).bannerBeanDao()
        val bannerBeanList = bannerBeanDao.getBannerBeanList()
        if (bannerBeanList.isNotEmpty() && downImageTime > 0 && downImageTime - System.currentTimeMillis() < ONE_DAY) {
            Result.success(bannerBeanList)
        } else {
            val bannerResponse = PlayAndroidNetwork.getBanner()
            if (bannerResponse.errorCode == 0) {
                val bannerList = bannerResponse.data
                spUtils.put(DOWN_IMAGE_TIME, System.currentTimeMillis())
                if (bannerBeanList.isNotEmpty() && bannerBeanList[0].url == bannerList[0].url) {
                    Result.success(bannerBeanList)
                } else {
                    bannerBeanDao.deleteAll()
                    insertBannerList(application, bannerBeanDao, bannerList)
                    Result.success(bannerList)
                }
            } else {
                Result.failure(RuntimeException("response status is ${bannerResponse.errorCode}  msg is ${bannerResponse.errorMsg}"))
            }
        }
    }

其實剛才的邏輯如果看懂了的話這段代碼應(yīng)該看著很簡單,就是按照上面的邏輯來寫的,對了,插入數(shù)據(jù)庫的 insertBannerList 方法還沒寫:

    private suspend fun insertBannerList(
        application: Application,
        bannerBeanDao: BannerBeanDao,
        bannerList: List<BannerBean>
    ) {
        bannerList.forEach {
            val file = Glide.with(application)
                .load(it.imagePath)
                .downloadOnly(SIZE_ORIGINAL, SIZE_ORIGINAL)
                .get()
            it.filePath = file.absolutePath
            bannerBeanDao.insert(it)
        }
    }

代碼都很簡單,重要的是這塊的思路,接下來該看文章列表的數(shù)據(jù)了。

文章列表數(shù)據(jù)

文章的數(shù)據(jù)獲取其實和 Banner 差不多,邏輯基本一樣,都是從數(shù)據(jù)庫中讀取文件,然后判斷是否需要刷新,這塊的時間改為了四小時,因為文章可能一直在更新,所以取了個比較小的值,可以根據(jù)需求自己來定義。文章列表的數(shù)據(jù)和 Banner 的不同之處在于文章列表需要請求兩次,需要判斷當前是第幾頁,如果是第 0 頁的話需要把置頂?shù)奈恼绿砑拥阶钋懊?,如果不是?0 頁的話則只需要把后面的文章添加上。

這里的實現(xiàn)其實我偷懶了,但也不算是偷懶。。。為什么這樣說呢?因為我只緩存了第一頁的文章列表數(shù)據(jù),但其實并不是偷懶,因為文章列表數(shù)據(jù)不定,可能更新頻率很快,緩存了太多頁的數(shù)據(jù)到后來又需要全部更新,亦或者全部刪除再重新插入,得不償失。緩存第一頁的數(shù)據(jù)在于用戶之前已經(jīng)打開過項目了,數(shù)據(jù)也都正常顯示,如果突然沒網(wǎng)了,再次重新打開應(yīng)用不至于大白頁或者顯示沒有網(wǎng)絡(luò),顯示出緩存的數(shù)據(jù)比較優(yōu)雅,如果用戶下拉刷新或者上拉加載的話提醒用戶當前沒有網(wǎng)絡(luò)即可。

既然 Banner 的數(shù)據(jù)都畫了個圖,那么文章列表的數(shù)據(jù)也得來畫一個!

[圖片上傳失敗...(image-e855e1-1602472643703)]

這張圖其實有偷懶了,這里判斷完是否為第一頁之后還要依次判斷置頂文章和列表文章,根據(jù)數(shù)據(jù)庫的數(shù)據(jù)和網(wǎng)絡(luò)請求數(shù)據(jù)是否一致來判斷是否更新數(shù)據(jù)庫的數(shù)據(jù),再將數(shù)據(jù)返回到 ViewModel。

來吧,看下代碼吧,這塊代碼有點多,我只展示下大概的邏輯吧,如果想看完整代碼,還是去 Github 上直接下載代碼就行:

    fun getArticleList(application: Application, query: QueryHomeArticle) = fire {
        coroutineScope {
            val res = arrayListOf<Article>()
            if (query.page == 1) {
                val spUtils = SPUtils.getInstance()
                val downArticleTime by Preference(DOWN_ARTICLE_TIME, System.currentTimeMillis())
                val articleListDao = PlayDatabase.getDatabase(application).browseHistoryDao()
                val articleListTop = articleListDao.getTopArticleList(HOME_TOP)
                val downTopArticleTime by Preference(
                    DOWN_TOP_ARTICLE_TIME,
                    System.currentTimeMillis()
                )
                if (articleListTop.isNotEmpty() && downTopArticleTime > 0 &&
                    downTopArticleTime - System.currentTimeMillis() < FOUR_HOUR && !query.isRefresh
                ) {
                    res.addAll(articleListTop)
                } else {
                    val topArticleListDeferred =
                        async { PlayAndroidNetwork.getTopArticleList() }
                    val topArticleList = topArticleListDeferred.await()
                    if (topArticleList.errorCode == 0) {
                        if (articleListTop.isNotEmpty() && articleListTop[0].link == topArticleList.data[0].link && !query.isRefresh) {
                            res.addAll(articleListTop)
                        } else {
                            res.addAll(topArticleList.data)
                            topArticleList.data.forEach {
                                it.localType = HOME_TOP
                            }
                            spUtils.put(DOWN_TOP_ARTICLE_TIME, System.currentTimeMillis())
                            articleListDao.deleteAll(HOME_TOP)
                            articleListDao.insertList(topArticleList.data)
                        }
                    }
                }
            } else {
                val articleListDeferred =
                    async { PlayAndroidNetwork.getArticleList(query.page - 1) }
                val articleList = articleListDeferred.await()
                if (articleList.errorCode == 0) {
                    res.addAll(articleList.data.datas)
                    Result.success(res)
                } else {
                    Result.failure(
                        RuntimeException(
                            "response status is ${articleList.errorCode}" + "  msg is ${articleList.errorMsg}"
                        )
                    )
                }
            }
        }
    }

大家可以看到我只展示了置頂文章的數(shù)據(jù)緩存,文章列表的原理一樣,就不贅述了。

再放一下這幾個模塊用到的常量吧:

const val ONE_DAY = 1000 * 60 * 60 * 24
const val FOUR_HOUR = 1000 * 60 * 60 * 4
const val DOWN_IMAGE_TIME = "DownImageTime"
const val DOWN_TOP_ARTICLE_TIME = "DownTopArticleTime"
const val DOWN_ARTICLE_TIME = "DownArticleTime"
const val DOWN_PROJECT_ARTICLE_TIME = "DownProjectArticleTime"
const val DOWN_OFFICIAL_ARTICLE_TIME = "DownOfficialArticleTime"

ViewModel

都寫的差不多了就該 ViewModel 登場了,ViewModel 的代碼比較簡單,我直接放上,然后下面簡單描述下吧:

class HomePageViewModel(application: Application) : AndroidViewModel(application) {

    private val pageLiveData = MutableLiveData<QueryHomeArticle>()

    private val refreshLiveData = MutableLiveData<Boolean>()

    val bannerList = ArrayList<BannerBean>()

    val articleList = ArrayList<Article>()

    val articleLiveData = Transformations.switchMap(pageLiveData) { query ->
        HomeRepository.getArticleList(application, query)
    }

    val bannerLiveData = Transformations.switchMap(refreshLiveData) { isRefresh ->
        HomeRepository.getBanner(application,isRefresh)
    }

    fun getBanner(isRefresh: Boolean) {
        refreshLiveData.value = isRefresh
    }

    fun getArticleList(page: Int, isRefresh: Boolean) {
        pageLiveData.value = QueryHomeArticle(page, isRefresh)
    }

}

data class QueryHomeArticle(var page: Int, var isRefresh: Boolean)

和上一篇文章一樣,同樣用的是 AndroidViewModel ,因為在 Repository 中需要用到數(shù)據(jù)庫,所以要使用。

ViewModel 中的邏輯很清晰,定義兩個 ArrayList 來存放數(shù)據(jù),使用 LiveData 來觀察數(shù)據(jù)的改變,兩個 get 方法來調(diào)動方法執(zhí)行以改變數(shù)據(jù)。

橫豎屏適配

到這里應(yīng)該整個邏輯都理通了,代碼應(yīng)該也都寫的差不多了,那么來運行下看看吧!

運行之后發(fā)現(xiàn)豎屏運行顯示正常,但當橫屏顯示的時候,頁面完全無法正常使用!Banner 基本上把所有的空間都占了,文章列表根本無法進行使用!

這個時候就需要橫豎屏適配了,其實橫豎屏適配很簡單,只需要在 res 目錄下建立一個 layout-land 的文件夾,把橫屏的布局放入進去即可,和豎屏布局的名稱一樣就行。

在這里大家可以根據(jù)需求來重新擺放橫屏布局的控件位置。我在這里將屏幕分為兩半,左邊用來顯示 Banner ,右面用來顯示文章列表,大家來簡單看下布局:

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <com.youth.banner.Banner
                android:id="@+id/homeBanner"
                android:layout_width="0dp"
                android:layout_weight="1.5"
                android:layout_height="match_parent" />

            <com.scwang.smartrefresh.layout.SmartRefreshLayout
                android:id="@+id/homeSmartRefreshLayout"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="2">

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/homeRecycleView"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" />

            </com.scwang.smartrefresh.layout.SmartRefreshLayout>

        </LinearLayout>

完成之后變?yōu)槿缦聵邮剑遣皇潜葎偛藕每吹亩?,而且更加容易操作了?/p>

[圖片上傳失敗...(image-21b0fa-1602472643703)]

差不多就先寫到這里吧,其他的下一篇文章再來!

總結(jié)

每次覺得沒多少東西的地方寫著寫著就寫多了,每回想著要好好寫的東西卻死活不知道如何下手寫。這一篇文章簡單走了一遍一個應(yīng)用首頁的簡單實現(xiàn)邏輯,并帶給大家橫豎屏的簡單實現(xiàn)。

寫著寫著就過了午夜12點了,好久沒有寫到這么晚了,是自己這個程序員當?shù)奶环Q職了,也好久沒努力地去學習了,連續(xù)好久沒有進行主動學習,基本一直處于被動學習的局面,不能這樣,自己要加油。

努力,共勉。

?著作權(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ù)。

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