background

I accidentally followed the public account of Codeless Technology before, so I know the first product Readhub launched by them. The address is readhub.me/, which mainly provides the latest news of the Internet. After paying attention to it for some time, I feel that the content quality is good and can help us filter out some junk information. However, it can only be viewed in the browser and wechat public account at present, and I always want to experience the architecture components launched by Google, so I developed an Android version of the client after a simple analysis of the Readhub Web interface. Making address p1-jj.byteimg.com/tos-cn-i-t2…

rendering

The specific implementation

App architecture is relatively simple: one main Activity+ three fragments. Readhub currently has only three categories of information: hot Topics, tech news, and developer news. The technology dynamic model is the same as the developer information data model, but the interface is different, which can be reused to a large extent.

The project catalog is divided as follows,

The architecture is basically the same as suggested in the Official Android documentation.

Currently, Repository simply requests data from the network and does not cache it locally

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)
    }

    /** * Developer info */

    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)
                    }
                }
            }
            returninstance!! }}}Copy the code

There are two viewModels at present: TopicViewModel and NewsViewModel. NewsViewModel is used to provide data for technology news and developer information. Take NewsViewModel as an example.

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(a): LiveData<List<News>> {
        lastCursor = System.currentTimeMillis()
        fetchData()
        return liveData
    }

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

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

    private fun fetchData(a) {
        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}}})Copy the code

NewsViewModel has two construction parameters, newsType, which is an enumerated type to distinguish between tech news and developer news, and pageSize, which sets the pageSize. Since NewsViewModel has construction parameters, we need to customize how it is created by implementing the ViewProvider.factory interface

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")}}Copy the code

The NewsViewModel itself encapsulates the pull-down refresh and pull-up load logic and provides the corresponding methods. In the Fragment, you just need to fire the method inside the callback. The liveData used here is a MutableLiveData MutableLiveData, because we need to reset the value of liveData after each request. In this way, in the corresponding Fragment only need to listen to LiveData interface display logic can be done.

The code for NewsFragment is as follows

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(a) = 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 {
        valview = 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
        }
    }
}

Copy the code

Create ViewModel in onActivityCreated callback and obtain LiveData for listening, RecycleView display logic processing in Observer callback. For pull-down refresh and pull-up load, SmartRefreshLayout is used here. It only needs to trigger the corresponding methods in the ViewModel in the callback and execute the Observer code logic once the data is retrieved. The other code logic is more obvious.

The full code can be viewed at p1-jj.byteimg.com/tos-cn-i-t2…

App currently published in cool Ann application market www.coolapk.com/apk/name.dm… , welcome to download the trial

conclusion

Choose either RxJava or LiveData according to the Android official recommendation project. So we’re using both here, and you can make a choice based on your own situation. With LiveData, you don’t have to worry about life cycles, but LiveData itself provides operators that are not as powerful as RxJava; If you choose RxJava, you can use it in conjunction with Rxlifecyle to remedy the lifecycle issues. Overall, Android provides a set of architectural components for our development is very instructive, especially about the role of the ViewModel is not limited to this form, please refer to the official documentation. Welcome to exchange experience!