終於來到本系列的最後一節咯,本節擼個早報APP來調用下上節編寫的接口~java
寫完這個系列都有種本身是「全棧」工程師的錯覺了~android
APP的功能雖然說比較簡單,可是居然說了全棧,意思意思用Axure RP 8 大概的製做下原型(裝波逼), 涉及到五個頁面,依次是:「今日新聞」,「新聞篩選」,「早報」,「新聞編輯」和「新聞詳情」, 具體頁面邏輯與交互詳情以下:json
原本打算用Sketch意思意思設計下APP界面,不過趕腳比較簡單,並且感受沒啥必要~ 因此直接跳過~(不要問:爲什麼不讓UI妹子幫忙設計一下?)api
直接用Android自帶的控件堆一個吧,APP主色確定是蕾姆藍哇。瀏覽器
打開塵封已久的Android Studio,這一個我終於想起本身的主業:『Android開發仔』, 而不是一個打雜網管,果然摸魚一時爽,一直摸魚一直爽。bash
基於Kotlin+爛大街的MVC模式編寫,項目工程結構如圖所示:服務器
爲了方便管理,把庫依賴,抽取到一個單獨的gradle文件中:微信
接着build.gradle按需添加便可,好比:網絡
implementation depend_lib["kotlin-stdlib-jdk7"]
複製代碼
行吧,項目用到的東西大概就這些,接着開始擼頁面。app
默認會使用系統自定義的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如下的系統)
接着須要定製下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都進行相似的配置,接着是網絡層和數據層。
直接定義一個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)
}
複製代碼
準備工做大概就這些,而後就是整理邏輯,下拉刷新,上拉加載更多,列表左滑幹嗎,又劃 幹嗎等,都比較簡單,只貼一個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)
}
}
複製代碼
編寫完運行下APP,演示下,我如何利用擠地鐵的時間來完成早報的編輯:
流程講解:
流程講解:
流程講解:
流程講解:
流程講解
APP上能作的事情就上面這些,so,即便我沒有帶電腦回家,我仍是能整理早報的,轉發早報的。 (這就是爲什麼我這兩天公號沒更新,依舊能在微信羣裏發文字版早報)。公號發文章仍是離不開電腦的~ 直接訪問:http://域名/news/show_wc_model?date=20190305
,能夠看到下面這樣的公號複製模板。
接着打開微信公號,複製粘貼一波便可,而後是新聞詳情列表,直接把連接: http://域名/news/show_news_list?date=20190305
,添加到原文連接便可。 另外這個原文連接是要添加使用域名,否則只能以預覽的模式打開~
斷斷續續,把這個系列肝完了,有種把早報自動化作成了產品的趕腳。其實在剛開始寫這個系列有點慫, 以爲本身沒準備好:後臺我TM不會又沒人帶着我玩,代碼粗製濫造寫出來會被diss等。後面仍是硬着頭皮,看着書, 擼着文檔把這個最小化的可行產品給組裝出來了。其中最大的收穫就是:只有作出實實在在的東西,思路纔會清晰, 才能獲得鍛鍊,視野也會更加開闊!
還有一些騷話留到我的總結那裏再說吧。
另外不是說這個東西弄出來就完了,後續仍是要迭代優化的,好比目前我就發現了下面這些問題:
目前新聞源僅僅是爬取澎湃新聞+新浪微博某些新聞號的微博,新聞數量比較少。 新聞來源分類問題,目前所有塞一個表裏,想查看微信的新聞,須要滾動好幾頁, 頁面中添加一個tab選項卡,能夠選擇查看某個新聞源的新聞,而不是全塞一個頁面裏。
相同類型的新聞有時不少,好比今天一排下去都是兩會的... 並且還有一些很無聊的也能算新聞的新聞,要辦法儘量對這種無用新聞進行過濾。
一種快速得到有趣新聞的方法就是:偷新聞,就是去截取一些專門發早報的公號發的早報, 而後咧,相比起普通的公號,多一個原文連接的東東。常規的操做是複製新聞,打開瀏覽器, 搜索,而後打開第一個結果,複製連接,而後打開CodingBoy,粘貼連接。這一步其實能夠簡化, 在添加新聞編輯頁直接添加一個搜索按鈕,直接獲取新聞搜索結果的第一個URL,而後自動填充。
固然,遠不止上述這些問題,後面慢慢迭代優化吧~
問:代碼開源嗎?
答:抱歉,目前不打算開源,涉及到我本身服務器的一些信息,並且代碼比較簡單,沒啥必要。 照着文章擼一遍,你也很容易就作出來哈。固然,有興趣研究這個,咱們能夠私下討論討論。
行吧,關於本節就說哪買多,有興趣的話能夠翻閱前年寫個幾篇文章:
行吧,就說這麼多~
Tips:公號目前只是堅持發早報,在慢慢完善,有點心虛,只敢貼個小圖,想看早報的能夠關注下~