背景
之前無意中關注了無碼科技的公眾號,由此知道了他們推出的第一個產品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<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目前有兩個:TopicViewModel
和NewsViewModel
,NewsViewModel
用於為科技動態和開發者資訊提供資料,以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的作用不僅侷限本篇這種形式,具體可以參考官方文件。歡迎大家一起交流使用心得!