偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

coder-pig發表於2019-03-05

0x1 簡述

終於來到本系列的最後一節咯,本節擼個早報APP來呼叫下上節編寫的介面~

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

寫完這個系列都有種自己是「全棧」工程師的錯覺了~


0x2 產品原型設計環節

APP的功能雖說比較簡單,但是竟然說了全棧,意思意思用Axure RP 8 大概的製作下原型(裝波逼), 涉及到五個頁面,依次是:「今日新聞」,「新聞篩選」,「早報」,「新聞編輯」和「新聞詳情」, 具體頁面邏輯與互動詳情如下:

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾


0x3 本該有的設計環節

本來打算用Sketch意思意思設計下APP介面,不過趕腳比較簡單,而且感覺沒啥必要~ 所以直接跳過~(不要問:為何不讓UI妹子幫忙設計一下?)

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

直接用Android自帶的控制元件堆一個吧,APP主色肯定是蕾姆藍哇。


0x4 手擼APP環節

開啟塵封已久的Android Studio,這一個我終於想起自己的主業:『Android開發仔』, 而不是一個打雜網管,果真摸魚一時爽,一直摸魚一直爽。

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

1、專案結構

基於Kotlin+爛大街的MVC模式編寫,專案工程結構如圖所示:

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

為了方便管理,把庫依賴,抽取到一個單獨的gradle檔案中:

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

接著build.gradle按需新增即可,比如:

 implementation depend_lib["kotlin-stdlib-jdk7"]
複製程式碼

行吧,專案用到的東西大概就這些,接著開始擼頁面。


2、自定義Toolbar

預設會使用系統自定義的ActionBar,需要修改下styles.xml,把預設的:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

<!-- 改為 -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
複製程式碼

接著佈局中新增Toolbar

    <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="0dp"
            app:title="摳腚男孩"
            android:background="@color/colorPrimaryDark"
            app:titleTextColor="@color/colorPrimary"
            android:layout_height="?attr/actionBarSize"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"/>
複製程式碼

執行後的效果:(看到網上很多要加setSupportActionBar(toolbar)設定下,應該是為了相容5.0以下的系統)

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

接著需要定製下toolbar,新增兩個按鈕,分別進入新聞篩選頁和早報頁展示頁。在app/res/menu目錄 下新建一個main_menu.xml檔案,新增選單相關的配置:

<menu 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"
      tools:context="ui.MainActivity">
    <item
            android:id="@+id/action_choose"
            android:icon="@drawable/ic_bar_choose"
            android:orderInCategory="80"
            android:title="篩選新聞"
            app:showAsAction="ifRoom"/>

    <item
            android:id="@+id/action_morning"
            android:icon="@drawable/ic_bar_morning"
            android:orderInCategory="90"
            android:title="早報"
            app:showAsAction="ifRoom"/>
</menu>
複製程式碼

接著設定下呼叫下toolbar的inflateMenu()方法載入下:

toolbar.inflateMenu(R.menu.main_menu)
複製程式碼

執行後的頁面:

接著新增下item的點選事件:

        toolbar.setOnMenuItemClickListener {
            when(it.itemId) {
                R.id.action_choose -> {}
                R.id.action_morning -> {}
            }
            true
        }
複製程式碼

接著講所有頁面的toolbar都進行類似的配置,接著是網路層和資料層。

3、網路層與資料層

直接定義一個News類,新建新聞介面會傳遞遺傳JSON,直接實現Serializable介面:

class News : Serializable {
    var id: Int? = 0
    var title: String? = null
    var url: String? = null
    var create_time: String? = null

    override fun toString() = "News(id=$id, title=$title, url=$url, create_time=$create_time)"
}
複製程式碼

響應結果集

class BaseResult<T> : Serializable {
    var code: String? = null
    var msg: String? = null
    var data: T? = null
}
複製程式碼

API介面

interface CPAPIService {

    /* 檢視新聞 */
    @GET("news/show")
    fun fetchNews(
        @Query("kind") kind: Int,
        @Query("count") count: Int,
        @Query("page") page: Int
    ): Flowable<BaseResult<ArrayList<News>>>

    /* 查詢新聞條數 */
    @GET("news/{kind}/count")
    fun fetchNewsCount(
        @Path("kind") kind: Int
    ): Flowable<BaseResult<Int>>

    /* 刪除新聞 */
    @FormUrlEncoded
    @POST("news/destroy")
    fun deleteNewsByNid(
        @Field("kind") kind: Int,
        @Field("nid") nid: Int
    ): Flowable<BaseResult<String>>

    /* 更新篩選新聞 */
    @FormUrlEncoded
    @POST("news/update")
    fun updateNews(
        @Field("news") news: String
    ): Flowable<BaseResult<String>>

    /* 生成日報 */
    @FormUrlEncoded
    @POST("news/insert_morning")
    fun generateNews(
        @Field("date") date: String
    ): Flowable<BaseResult<String>>

    /* 獲取微信傳播複製模板 */
    @GET("news/show_copy_model")
    fun fetchCopyNews(
        @Query("date") date: String
    ): Flowable<BaseResult<String>>
}
複製程式碼

OkHttpClient構造

object CPOkHttp {
    private const val HTTP_CONNECT_TIMEOUT = 15L
    private const val HTTP_READ_TIMEOUT = 30L
    private const val HTTP_WRITE_TIMEOUT = 15L

    var httpClient: OkHttpClient by Delegates.notNull()

    fun init() {
        val logging = HttpLoggingInterceptor { message -> Timber.tag("OkHttps").d(message) }
        logging.level = HttpLoggingInterceptor.Level.BODY

        val builder = OkHttpClient.Builder()
        httpClient = with(builder) {
            connectTimeout(HTTP_CONNECT_TIMEOUT, TimeUnit.SECONDS)
            readTimeout(HTTP_READ_TIMEOUT, TimeUnit.SECONDS)
            writeTimeout(HTTP_WRITE_TIMEOUT, TimeUnit.SECONDS)
            addInterceptor(logging)
            build()
        }
    }
}
複製程式碼

Retrofit構造

class CPRetrofit private constructor() {
    private val baseUrl = "伺服器地址"
    var retrofit: Retrofit? = null

    companion object {
        @Volatile
        private var instance: CPRetrofit? = null

        fun init(): CPRetrofit {
            if (instance == null) {
                synchronized(CPRetrofit::class.java) {
                    if (instance == null) {
                        instance = CPRetrofit()
                    }
                }
            }
            return instance!!
        }
    }

    init {
        CPOkHttp.init()
        val builder = Retrofit.Builder()
        retrofit = with(builder) {
            client(CPOkHttp.httpClient)
            addConverterFactory(GsonConverterFactory.create())
            addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            baseUrl(baseUrl)
            build()
        }
    }
}
複製程式碼

然後在Application類中完成初始化

class App : Application() {
    companion object {
        var instance: App by Delegates.notNull()
        var apis by Delegates.notNull<CPAPIService>()
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
        apis = CPRetrofit.init().retrofit?.create(CPAPIService::class.java) as CPAPIService
    }
}
複製程式碼

接著寫一個工具方法,用來處理一些常見的網路請求異常

fun processRequestException(e: Throwable) {
    when (e) {
        is ConnectException, is SocketException -> shortToast(getTextRes(R.string.network_connected_exception))
        is SocketTimeoutException -> shortToast(getTextRes(R.string.network_socket_time_out))
        is JsonSyntaxException -> shortToast(getTextRes(R.string.network_json_syntax_exception))
        is UnknownHostException -> shortToast(getTextRes(R.string.network_unknown_host))
        else -> Timber.d(e)
    }
}
複製程式碼

接著就可以直接用了,比如查詢新聞條數:

    private fun fetchNewsCount() {
        val subscribe: Disposable = App.apis.fetchNewsCount(1)
            .compose(RxSchedulers.compose())
            .subscribe ({ result ->
                shortToast("今天的新聞有${result.data}條")
            }, {throwable -> processRequestException(throwable) })
        mSubscriptions.add(subscribe)
    }
複製程式碼

4、頁面編寫

準備工作大概就這些,然後就是整理邏輯,下拉重新整理,上拉載入更多,列表左滑幹嘛,又劃 幹嘛等,都比較簡單,只貼一個MainActivity作為參考吧,其他頁面類似:

class MainActivity : BaseActivity() {
    private var mCurPage = 0    //當前頁
    private var mCanLoadMore = true  //能否載入更多
    private var mAdapter: NewsAdapter? = null
    private var mData = ArrayList<News>()
    private var mTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
        override fun getMovementFlags(p0: RecyclerView, p1: RecyclerView.ViewHolder) : Int {
            val swiped = ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
            return makeMovementFlags(0, swiped)
        }
        override fun onMove(p0: RecyclerView, p1: RecyclerView.ViewHolder, p2: RecyclerView.ViewHolder): Boolean {
            return false
        }

        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
            //左滑刪除,右滑加入篩選池
            when (direction) {
                ItemTouchHelper.RIGHT -> {
                    insertChoose(mData[viewHolder.adapterPosition])
                }
            }
            mAdapter?.remove(viewHolder.adapterPosition)
        }
    })
    private var mItemRemoveListener = object : NewsAdapter.onItemRemoveListener {
        override fun onItemRemove(pos: Int) {
            removeNews(mData[pos].id!!)
            mData.removeAt(pos)
        }
    }

    override fun prepare() {}

    override fun inflateLayoutId() = R.layout.activity_main

    override fun init() {
        toolbar.inflateMenu(R.menu.menu_main)
        toolbar.setOnMenuItemClickListener {
            when (it.itemId) {
                R.id.action_choose -> startActivity(Intent(this@MainActivity, ChooseActivity::class.java))
                R.id.action_morning -> startActivity(Intent(this@MainActivity, MorningActivity::class.java))
            }
            true
        }
        srl_refresh.setColorSchemeColors(ContextCompat.getColor(this, R.color.colorPrimaryDark))
        srl_refresh.setOnRefreshListener {
            mCurPage = 0
            fetchNews()
        }
        mAdapter = NewsAdapter(this)
        mAdapter?.setItemRemoveListener(mItemRemoveListener)
        mTouchHelper.attachToRecyclerView(rec_list)
        rec_list.adapter = mAdapter
        rec_list.layoutManager = LinearLayoutManager(this)
        rec_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                val layoutManager = recyclerView.layoutManager as LinearLayoutManager?
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    if (mCanLoadMore) {
                        if (layoutManager!!.itemCount - recyclerView.childCount <= layoutManager.findFirstVisibleItemPosition()) {
                            ++mCurPage
                            fetchNews()
                        }
                    }
                }
            }
        })
        fetchNewsCount()
        fetchNews()
    }

    private fun fetchNews() {
        val subscribe: Disposable = App.apis.fetchNews(1, 100, mCurPage)
            .compose(RxSchedulers.compose())
            .doOnSubscribe { srl_refresh.isRefreshing = true }
            .doFinally { srl_refresh.isRefreshing = false }
            .subscribe ({ news ->
                var newsData = news.data
                if(mCurPage == 0) {
                    if(newsData != null || newsData?.size != 0) {
                        mData.clear()
                        mData.addAll(news.data!!)
                        mAdapter?.refresh(news.data!!)
                        mCanLoadMore = true
                    }
                } else{
                    if(newsData != null && newsData.size != 0) {
                        mData.addAll(news.data!!)
                        mAdapter?.addAll(news.data!!)
                    } else {
                        mCanLoadMore = false
                        shortToast("沒有更多了...")
                    }
                }
            }, {throwable -> processRequestException(throwable) })
        mSubscriptions.add(subscribe)
    }

    /* 獲取新聞數量 */
    private fun fetchNewsCount() {
        val subscribe: Disposable = App.apis.fetchNewsCount(1)
            .compose(RxSchedulers.compose())
            .subscribe ({ result ->
                shortToast("今天的新聞有${result.data}條")
            }, {throwable -> processRequestException(throwable) })
        mSubscriptions.add(subscribe)
    }

    /* 移除新聞 */
    private fun removeNews(nid: Int) {
        val subscribe: Disposable = App.apis.deleteNewsByNid(1, nid)
            .compose(RxSchedulers.compose())
            .subscribe ({ result ->
                shortToast(result.msg!!)
            }, {throwable -> processRequestException(throwable) })
        mSubscriptions.add(subscribe)
    }

    /* 新聞新增到篩選池 */
    private fun insertChoose(news: News) {
        val subscribe: Disposable = App.apis.updateNews(Gson().toJson(news))
            .compose(RxSchedulers.compose())
            .subscribe ({}, {throwable -> processRequestException(throwable) })
        mSubscriptions.add(subscribe)
    }
}
複製程式碼

0x5 結果展示環節

編寫完執行下APP,演示下,我如何利用擠地鐵的時間來完成早報的編輯:

1、篩選有意思的新聞到篩選池中

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

流程講解

  • 1.進入APP查詢爬取到的新聞數,載入100條新聞,上拉可載入更多。
  • 2.左滑刪除新聞,右滑把新聞加入篩選池,同時刪除。
  • 3.長按某條新聞可以檢視新聞的詳情,詳情頁右上角可以複製新聞URL。

2、進入篩選池篩選生成早報的15條新聞

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

流程講解

  • 1.點選不開心的選單圖示可進入新聞篩選頁。
  • 2.左滑刪除新聞,右滑進入新聞編輯頁,長按可檢視新聞詳情。
  • 3.點選加號可以手動新增新聞,直接進入新聞編輯頁。

3、在新聞編輯中對新聞進行二次編輯

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

流程講解

  • 1.如果從新聞列表過來的,自動填充新聞資訊,新增那裡進來則空白;
  • 2.編輯新聞連結後,按小鍵盤的完成,載入url,實時預覽;
  • 3.此處配合錘子Big Band使用,非常酸爽。
  • 4.編輯完成後點選右上角儲存按鈕即可儲存。

4、湊夠15條以上的新聞點選生成早報

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

流程講解

  • 1.當篩選池中的新聞大於等於15條時,點選頂部生成按鈕,會彈出生成對話方塊,確定是否生成;
  • 2.點選確定,如果沒生成則生成,生成後跳早報頁;如果已生成,跳早報頁,
  • 注:每天的早報頁只能生成一次!!!

5、早報頁複製文字版早報

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

流程講解

  • 1.長按早報文字可以直接複製到剪貼簿,然後到微信群裡直接貼上即可;
  • 2.右上角可以選擇檢視某一天的早報;

6、複製公眾號文章版早報

APP上能做的事情就上面這些,so,即使我沒有帶電腦回家,我還是能整理早報的,轉發早報的。 (這就是為何我這兩天公號沒更新,依舊能在微信群裡發文字版早報)。公號發文章還是離不開電腦的~ 直接訪問:http://域名/news/show_wc_model?date=20190305,可以看到下面這樣的公號複製模板。

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

接著開啟微信公號,複製貼上一波即可,然後是新聞詳情列表,直接把連結: http://域名/news/show_news_list?date=20190305,新增到原文連結即可。 另外這個原文連結是要新增使用域名,不然只能以預覽的模式開啟~

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾


0x∞ 本系列結語

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾

斷斷續續,把這個系列肝完了,有種把早報自動化做成了產品的趕腳。其實在剛開始寫這個系列有點慫, 覺得自己沒準備好:後臺我TM不會又沒人帶著我玩,程式碼粗製濫造寫出來會被diss等。後面還是硬著頭皮,看著書, 擼著文件把這個最小化的可行產品給組裝出來了。其中最大的收穫就是:只有做出實實在在的東西,思路才會清晰, 才能得到鍛鍊,視野也會更加開闊!還有一些騷話留到個人總結那裡再說吧。

另外不是說這個東西弄出來就完了,後續還是要迭代優化的,比如目前我就發現了下面這些問題:

  • 新聞源

目前新聞源僅僅是爬取澎湃新聞+新浪微博某些新聞號的微博,新聞數量比較少。 新聞來源分類問題,目前全部塞一個表裡,想檢視微信的新聞,需要滾動好幾頁, 頁面中新增一個tab選項卡,可以選擇檢視某個新聞源的新聞,而不是全塞一個頁面裡。

  • 同質化新聞

相同型別的新聞有時很多,比如今天一排下去都是兩會的... 而且還有一些很無聊的也能算新聞的新聞,要辦法儘可能對這種無用新聞進行過濾。

  • 手動新增新聞不方便

一種快速獲得有趣新聞的方法就是:偷新聞,就是去擷取一些專門發早報的公號發的早報, 然後咧,相比起普通的公號,多一個原文連結的東東。常規的操作是複製新聞,開啟瀏覽器, 搜尋,然後開啟第一個結果,複製連結,然後開啟CodingBoy,貼上連結。這一步其實可以簡化, 在新增新聞編輯頁直接新增一個搜尋按鈕,直接獲取新聞搜尋結果的第一個URL,然後自動填充。

當然,遠不止上述這些問題,後面慢慢迭代優化吧~

問:程式碼開源嗎?

答:抱歉,目前不打算開源,涉及到我自己伺服器的一些資訊,而且程式碼比較簡單,沒啥必要。 照著文章擼一遍,你也很容易就做出來哈。當然,有興趣研究這個,我們可以私下討論討論。

行吧,關於本節就說哪買多,有興趣的話可以翻閱前年寫個幾篇文章:

行吧,就說這麼多~

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾


Tips:公號目前只是堅持發早報,在慢慢完善,有點心虛,只敢貼個小圖,想看早報的可以關注下~

偷個懶,公號摳腚早報80%自動化——5.意思意思擼個APP收下尾


相關文章