背景
之前无意中关注了无码科技的公众号,由此知道了他们推出的第一个产品Readhub,地址为readhub.me/,主要提供互联网最新发生的新鲜事,关注了一段时间感觉内容质量还不错,能够帮我们筛选掉一定的垃圾信息。但是它目前只能在浏览器和微信公众号里面查看,又加上自己一直想体验一下谷歌推出的架构组件,所以在简单分析了一下Readhub Web端的接口之后开发了一个Android版本的客户端。GitHub地址user-gold-cdn.xitu.io/2018/1/10/1…
效果图
具体实现
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> {
return httpService.getTopics(lastCursor, pageSize)
}
/**
* 科技动态
*/
fun getTechNews(lastCursor: Long?, pageSize: Int): Observable> {
return httpService.getTechNews(lastCursor, pageSize)
}
/**
* 开发者资讯
*/
fun getDevNews(lastCursor: Long?, pageSize: Int): Observable> {
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目前有两个:TopicViewModel
和NewsViewModel
,NewsViewModel
用于为科技动态和开发者资讯提供数据,以NewsViewModel
为例,
class NewsViewModel(private val newsType: NewsType, private val pageSize:Int) : ViewModel() {
private val liveData: MutableLiveData> = MutableLiveData()
private var isFirstPage = true
private var lastCursor: Long = 0L
private val newsList = ArrayList()
fun getLiveData(): LiveData> {
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 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 = ArrayList()
private lateinit var newsViewModel: NewsViewModel
private lateinit var newsLiveData: LiveData>
private var adapter: NewsListAdapter? = null
private var newsType: NewsType = NewsType.TechNews
private fun getObserver() = Observer> { 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的作用不仅局限本篇这种形式,具体可以参考官方文档。欢迎大家一起交流使用心得!