基於Kotlin、ViewModel、LiveData和LifeCycle開發的Readhub客戶端

dmx發表於2018-01-10

背景

之前無意中關注了無碼科技的公眾號,由此知道了他們推出的第一個產品Readhub,地址為readhub.me/,主要提供網際網路最新發生的新鮮事,關注了一段時間感覺內容質量還不錯,能夠幫我們篩選掉一定的垃圾資訊。但是它目前只能在瀏覽器和微信公眾號裡面檢視,又加上自己一直想體驗一下谷歌推出的架構元件,所以在簡單分析了一下Readhub Web端的介面之後開發了一個Android版本的客戶端。GitHub地址user-gold-cdn.xitu.io/2018/1/10/1…

效果圖

基於Kotlin、ViewModel、LiveData和LifeCycle開發的Readhub客戶端

具體實現

App架構比較簡單:一個主Activity+三個Fragment。目前Readhub的資訊只有三個分類,分別為熱門話題、科技動態和開發者資訊。其中科技動態和開發者資訊資料模型相同,只是呼叫的就介面不同,可以在很大程度上進行復用。

專案目錄劃分如下,

基於Kotlin、ViewModel、LiveData和LifeCycle開發的Readhub客戶端

和Android官方文件建議的架構基本是一致。

基於Kotlin、ViewModel、LiveData和LifeCycle開發的Readhub客戶端

目前Repository中只是單純的從網路請求資料,沒有做本地快取,程式碼如下

class DataRepository private constructor(context: Context) {
    private val SERVER_ADDRESS = "https://api.readhub.me/"
    private val httpService: Api

    init {
        val builder = Retrofit.Builder()
        builder.baseUrl(SERVER_ADDRESS)
        builder.client(DefaultOkHttpClient.getOkHttpClient(context))
        builder.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
        builder.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        val retrofit = builder.build()
        httpService = retrofit.create(Api::class.java)
    }

    /**
    * 熱門話題
    */

    fun getTopics(lastCursor: Long?, pageSize: Int): Observable<PageResult<Topic>> {
        return httpService.getTopics(lastCursor, pageSize)
    }

    /**
    * 科技動態
    */

    fun getTechNews(lastCursor: Long?, pageSize: Int): Observable<PageResult<News>> {
        return httpService.getTechNews(lastCursor, pageSize)
    }

    /**
    * 開發者資訊
    */

    fun getDevNews(lastCursor: Long?, pageSize: Int): Observable<PageResult<News>> {
        return httpService.getDevNews(lastCursor, pageSize)
    }

    companion object {
        private var instance: DataRepository? = null
        fun getInstance(context: Context): DataRepository {
            if (instance == null) {
                synchronized(DataRepository::class.java) {
                    if (instance == null) {
                        instance = DataRepository(context)
                    }
                }
            }
            return instance!!
        }
    }
}


複製程式碼

ViewModel目前有兩個:TopicViewModelNewsViewModelNewsViewModel用於為科技動態和開發者資訊提供資料,以NewsViewModel為例,

class NewsViewModel(private val newsType: NewsType, private val pageSize:Int) : ViewModel() {

    private val liveData: MutableLiveData<List<News>> = MutableLiveData()
    private var isFirstPage = true
    private var lastCursor: Long = 0L
    private val newsList = ArrayList<News>()
    fun getLiveData(): LiveData<List<News>> {
        lastCursor = System.currentTimeMillis()
        fetchData()
        return liveData
    }

    fun refresh() {
        isFirstPage = true
        lastCursor = System.currentTimeMillis()
        fetchData()
    }

    fun loadMore() {
        isFirstPage = false
        fetchData()
    }

    private fun fetchData() {
        val observable = if (newsType == NewsType.TechNews) {
            DataRepository.getInstance(MyApplication.instance).getTechNews(lastCursor, pageSize)
        } else {
            DataRepository.getInstance(MyApplication.instance).getDevNews(lastCursor, pageSize)
        }
        observable.compose(SchedulerTransformer())
                .subscribe({ data ->
                    if (isFirstPage) {
                        newsList.clear()
                    }
                    newsList.addAll(newsList.size, data.data?.toList()!!)
                    liveData.value = newsList
                    lastCursor = data.data?.last()?.publishDate!!.toDate("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")?.time!!
                }, {
                    liveData.value = null
                })
    }
}

複製程式碼

NewsViewModel有兩個構造引數newsType為一個列舉型別,用於區分是科技動態還是開發者資訊,另一個引數pageSize用於設定分頁大小。由於NewsViewModel含有構造引數,所以我們需要自定義它的建立方式,方式為實現ViewProvider.Factory介面

class NewsViewModelFactory(private val newsType: NewsType, private val pageSize: Int) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(NewsViewModel::class.java)) {
            return NewsViewModel(newsType, pageSize) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

複製程式碼

NewsViewModel本身封裝了下拉重新整理和上拉載入的邏輯,並且提供了相應的方法。在Fragment中只需要在回撥裡面觸發方法即可。這裡面的liveData使用是MutableLiveData即可變的LiveData,因為每次請求資料之後我們需要重新設定liveData裡面的值。這樣的話在對應的Fragment中只需要監聽LiveData做好介面顯示邏輯就可以了。

NewsFragment的程式碼如下

class NewsFragment : Fragment() {
    private val PAGE_SIZE = 10
    private var dataList: List<News> = ArrayList()
    private lateinit var newsViewModel: NewsViewModel
    private lateinit var newsLiveData: LiveData<List<News>>
    private var adapter: NewsListAdapter? = null
    private var newsType: NewsType = NewsType.TechNews

    private fun getObserver() = Observer<List<News>> { newsList ->
        if (newsList != null) {
            dataList = newsList
            if (adapter == null) {
                adapter = NewsListAdapter(context, dataList)
                adapter!!.onItemClickListener = onItemClickListener
                recyclerView.layoutManager = LinearLayoutManager(context)
                recyclerView.adapter = adapter
            } else {
                adapter?.data = dataList
            }
            smartRefreshLayout.finishLoadmore()
            smartRefreshLayout.finishRefresh()
            adapter!!.notifyDataSetChanged()
            recyclerView.scrollToPosition(dataList.size - PAGE_SIZE)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        newsType = arguments?.getNewsType(KEY_NEWS_TYPE)!!
    }

    private val onItemClickListener = object : NewsListAdapter.OnItemClickListener {
        override fun onItemClick(view: View, position: Int) {
            val item = dataList[position]
            val intent = WebViewActivity.makeIntent(context, item.url, item.title, "")
            startActivity(intent)
        }
    }

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View {
        val view = inflater?.inflate(R.layout.news_fragment, container, false)
        return view!!
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        newsViewModel = ViewModelProviders.of(this, NewsViewModelFactory(newsType, PAGE_SIZE)).get(NewsViewModel::class.java)
        newsLiveData = newsViewModel.getLiveData()
        newsLiveData.observe(this, getObserver())
        smartRefreshLayout.setOnRefreshListener {
            newsViewModel.refresh()
        }
        smartRefreshLayout.setOnLoadmoreListener {
            newsViewModel.loadMore()
        }
    }

    companion object {
        val KEY_NEWS_TYPE = "KEY_NEWS_TYPE"
        fun newInstance(newsType: NewsType): NewsFragment {
            val fragment = NewsFragment()
            val bundle = Bundle()
            bundle.putNewsType(KEY_NEWS_TYPE, newsType)
            fragment.arguments = bundle
            return fragment
        }
    }
}

複製程式碼

在onActivityCreated回撥中建立ViewModel並且獲取LiveData進行監聽,在Observer的回撥中進行RecycleView的顯示邏輯處理。關於下拉重新整理和上拉載入這裡使用了SmartRefreshLayout,只需要在回撥中觸發ViewModel中對應的方法,資料獲取成功之後同樣執行Observer中程式碼邏輯。其他程式碼邏輯比較明顯就不在介紹了。

完整程式碼可以檢視user-gold-cdn.xitu.io/2018/1/10/1…

App目前釋出在酷安應用市場www.coolapk.com/apk/name.dm…,歡迎下載試用

總結

按照Android官方建議專案中RxJava和LiveData選擇一個即可。我們這裡兩個都使用了,這裡大家可以根據結合自己的情況選擇。使用LiveData可以不用關心生命週期的問題,但是LiveData本身提供操作符沒有RxJava功能強大;如果選擇RxJava可以結合Rxlifecyle使用來彌補關於生命週期的問題。整體來看Android提供這一套架構元件對我們的開發還是非常有指導意義的,尤其是關於ViewModel的作用不僅侷限本篇這種形式,具體可以參考官方文件。歡迎大家一起交流使用心得!

相關文章