基于Kotlin、ViewModel、LiveData和LifeCycle开发的Readhub客户端

7,021 阅读4分钟

背景

之前无意中关注了无码科技的公众号,由此知道了他们推出的第一个产品Readhub,地址为readhub.me/,主要提供互联网最新发生的新鲜事,关注了一段时间感觉内容质量还不错,能够帮我们筛选掉一定的垃圾信息。但是它目前只能在浏览器和微信公众号里面查看,又加上自己一直想体验一下谷歌推出的架构组件,所以在简单分析了一下Readhub Web端的接口之后开发了一个Android版本的客户端。GitHub地址p1-jj.byteimg.com/tos-cn-i-t2…

效果图

具体实现

App架构比较简单:一个主Activity+三个Fragment。目前Readhub的信息只有三个分类,分别为热门话题、科技动态和开发者资讯。其中科技动态和开发者资讯数据模型相同,只是调用的就接口不同,可以在很大程度上进行复用。

项目目录划分如下,

和Android官方文档建议的架构基本是一致。

目前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中代码逻辑。其他代码逻辑比较明显就不在介绍了。

完整代码可以查看p1-jj.byteimg.com/tos-cn-i-t2…

App目前发布在酷安应用市场www.coolapk.com/apk/name.dm…,欢迎下载试用

总结

按照Android官方建议项目中RxJava和LiveData选择一个即可。我们这里两个都使用了,这里大家可以根据结合自己的情况选择。使用LiveData可以不用关心生命周期的问题,但是LiveData本身提供操作符没有RxJava功能强大;如果选择RxJava可以结合Rxlifecyle使用来弥补关于生命周期的问题。整体来看Android提供这一套架构组件对我们的开发还是非常有指导意义的,尤其是关于ViewModel的作用不仅局限本篇这种形式,具体可以参考官方文档。欢迎大家一起交流使用心得!