Android App From Scratch Part 3 — Implementing App Logic

Faruk Toptaş
Android Bits
Published in
6 min readFeb 5, 2017

In this tutorial series, I will try to create an RSS Reader app step by step. Through this series I will explain:

  1. How to use Model-View-Presenter in an Android App
  2. Implementing must have libraries
  3. Implementing App Logic
  4. Creating unit tests with JUnit
  5. Creating Android Instrumentations tests with Espresso
  6. Continuous Integration with Travis-CI

In this part I will convert our dummy app to a real app. Parsing RSS urls and showing them on a pretty screen. And browsing RSS items with Chrome Custom Tabs.

I created a JSON asset file that includes some popular sites RSS urls. I keep in in assets folder.

[
{
"i": 10001,
"n": "Mashable",
"l": "http://feeds.mashable.com/Mashable"
},
{
"i": 1002,
"n": "Engadget",
"l": "https://www.engadget.com/rss.xml"
},
{
"i": 1003,
"n": "Wired",
"l": "https://www.wired.com/feed/"
},
{
"i": 1004,
"n": "Thenextweb",
"l": "http://feeds2.feedburner.com/thenextweb"
}
]

I parse this JSON file with GSON library as array of Feed object withFeedParser class. Each Feed object is passed to fragments of ViewPager.

I change MainContract to a real interface for my app.

interface MainContract {

// User actions. Presenter will implement
interface Presenter : BaseMvpPresenter<View> {
fun loadRssFragments()

}

// Action callbacks. Activity/Fragment will implement
interface View : BaseView {
fun onLoadRssFragments(feeds: List<Feed>)
}

}

And MainPresenter only calls onLoadRssFragments() method.

class MainPresenter @Inject constructor(private val repository: MainRepository) :
BasePresenter<MainContract.View>(), MainContract.Presenter {

override fun loadRssFragments() {
view?.onLoadRssFragments(repository.parseFeeds())
}
}

Repositories are data local/remote providers. They are abstracted with interfaces. Using interfaces will make unit tests easier.

Implementation doesn’t matter. I care for the behaviour

class MainRepositoryImpl(private val context: Context) : MainRepository {

override fun parseFeeds(): List<Feed> {
val jsonString = Utils.readFromAssets(context, MainRepositoryImpl.RSS_FILE)
val gson = Gson()
val feeds = gson.fromJson(jsonString, Array<Feed>::class.java)
return Arrays.asList(*feeds)
}

companion object {
private const val RSS_FILE = "rss.json"
}

}

interface MainRepository {
fun parseFeeds(): List<Feed>
}

MainActivity then fill ViewPager items with Feed objects.

override fun onLoadRssFragments(feeds: List<Feed>) {
val fragmentList = ArrayList<RssFragment>()
val titles = ArrayList<String>()
for (feed in feeds) {
fragmentList.add(RssFragment.newInstance(feed))
titles.add(feed.title)
}

val adapter = RssFragmentAdapter(supportFragmentManager, fragmentList, titles)
viewPager.adapter = adapter
}

RssFragment is a fragment shows a list of RSS items. We can see all actions and callbacks from RssContract.

interface RssContract {

// User actions. Presenter will implement
interface Presenter : BaseMvpPresenter<View> {
fun loadRssItems(feed: Feed, fromCache: Boolean)
fun browseRssUrl(rssItem: RssItem)
}

// Action callbacks. Activity/Fragment will implement
interface View : BaseView, AsyncCallbackView {
fun onRssItemsLoaded(rssItems: List<RssItem>)
fun onBrowse(rssItem: RssItem)
}
}

Here is the implementation of RssFragment.

class RssFragment : BaseFragment(), RssContract.View, SwipeRefreshLayout.OnRefreshListener, RssItemsAdapter.OnItemClickListener {

@Inject
lateinit var presenter: RssContract.Presenter

private lateinit var feed: Feed
private lateinit var adapter: RssItemsAdapter
private var listener: OnItemSelectListener? = null


override val layoutResource = R.layout.fragment_rss

override fun inject(component: ActivityComponent) {
component.inject(this)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
feed = arguments!!.getSerializable(KEY_FEED) as Feed
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.attach(this)
adapter = RssItemsAdapter(activity!!, this)

recyclerView.layoutManager = LinearLayoutManager(activity)
recyclerView.adapter = adapter
swRefresh.setOnRefreshListener(this)
presenter.loadRssItems(feed, true)
}

override fun onRssItemsLoaded(rssItems: List<RssItem>) {
adapter.setItems(rssItems)
tvNoItems.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
}

override fun onBrowse(rssItem: RssItem) {
listener?.onItemSelected(rssItem)
}

override fun onItemSelected(rssItem: RssItem) {
presenter.browseRssUrl(rssItem)
}

override fun onRefresh() {
presenter.loadRssItems(feed, false)
}

override fun showLoading() {
if (isAdded) swRefresh.isRefreshing = true
}

override fun hideLoading() {
if (isAdded) swRefresh.isRefreshing = false
}

override fun onFail(error: Error) {
tvNoItems.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}

override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is OnItemSelectListener) {
listener = context
}
}

override fun onDetach() {
super.onDetach()
listener = null
}

companion object {
private const val KEY_FEED = "key_feed"

fun newInstance(feed: Feed): RssFragment {
val rssFragment = RssFragment()
val bundle = Bundle()
bundle.putSerializable(KEY_FEED, feed)
rssFragment.arguments = bundle
return rssFragment
}
}

interface OnItemSelectListener {
fun onItemSelected(rssItem: RssItem)
}
}
  • newInstance(Feed feed) Static method to create a Fragment instance. a Feed object is given and stored in fragment.
  • init(@Nullable Bundle state) This method is called in onCreateView() method. RecyclerView adapter creation and binding to RecyclerView is done here. Initial presenter method loadRssItems() is called here.
  • inject() DI is done here. Each fragment extends from BaseFragment should implement this method and inject.
  • showLoading()/hideLoading() methods to show/hide loading indicator, triggers from Presenter.
  • onRssItemsLoaded(List<RssItem> rssItems) Presenter fetches and parses the RSS then calls this method with parsed RssItem objects. Adapter will be filled with this objects.
  • onAttach()/onDetach() OnItemSelectListener is binded/unbinded here, a listener for item selection for RecyclerView.
  • onItemSelected(RssItem rssItem) is triggered when RecyclerView item is selected.

RssPresenter.kt

RssPresenter has the responsibility for all interactions on RssFragment After fetching and parsing RSS url, all items are cached in SessionData singleton object. In callbacks for async operations, view should be checked if it is attached to the presenter or not withisAttached() method. All UI changes (populating list, showing a fail error, hide/show indicators etc. ) is triggered by the presenter.

class RssPresenter @Inject
constructor(private val repository: RssRepository,
private val cache: RssCache)
:
BasePresenter<RssContract.View>(), RssContract.Presenter, RssResponseListener {


override fun loadRssItems(feed: Feed, fromCache: Boolean) {
if (cache.hasUrl(feed.url) && fromCache) {
view?.onRssItemsLoaded(cache.getContent(feed.url))
} else {
view?.showLoading()
repository.fetchRss(feed.url, this)
}
}

override fun browseRssUrl(rssItem: RssItem) {
view?.onBrowse(rssItem)
}


override fun getResponse(url: String, response: RssResponse) {
response.success?.apply {
if (isNotEmpty()) {
cache.addContent(url, this)
view?.onRssItemsLoaded(this)
}
}

response.error?.apply {
view?.onFail(this)
}

view?.hideLoading()
}

}

I keep all the logic in the Presenter and Repositories are responsible for the data layer. Presenters don’t matter how data is provided. Here is my RssRepository:

class RssRepositoryImpl @Inject constructor(private val service: RssService) : RssRepository {

override fun fetchRss(url: String, listener: RssResponseListener) {
service.getRss(url).enqueue(object : ApiCallback<RssFeed>() {
override fun onSuccess(response: RssFeed) {
listener.getResponse(url, RssResponse.success(response.items))
}

override fun onFail(error: Error) {
listener.getResponse(url, RssResponse.error(error))
}
})
}


}

interface RssRepository {
fun fetchRss(url: String, listener: RssResponseListener)
}

RssResponse is a simple data class wraps success and error objects:

data class RssResponse(val success: List<RssItem>? = null,
val error: Error? = null
) {

companion object {
fun success(data: List<RssItem>?): RssResponse = RssResponse(data, null)

fun error(error: Error) = RssResponse(null, error)
}
}

As you can see RssService is a Retrofit service and injected into repository. I have a NetworkModule for injection network related objects:

@Module
class NetworkModule {

@Provides
@Singleton
fun provideOkHttpClient() = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY })
.build()

@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient) = Retrofit.Builder()
.baseUrl("http://example.com")
.addConverterFactory(RssConverterFactory.create())
.client(client)
.build()

@Provides
@Singleton
fun provideApiService(retrofit: Retrofit) = retrofit.create(RssService::class.java)


}

Presenters and Repositories are feature related objects. So I separated them into another module ActivityModule:

@Module
class ActivityModule {

@Provides
@ActivityScope
fun provideMainRepository(app: Application): MainRepository = MainRepositoryImpl(app)

@Provides
@ActivityScope
fun provideMainPresenter(repository: MainRepository): MainContract.Presenter = MainPresenter(repository)

@Provides
@ActivityScope
fun provideRssRepository(service: RssService): RssRepository = RssRepositoryImpl(service)

@Provides
@ActivityScope
fun provideRssPresenter(repository: RssRepository,
cache: RssCache): RssContract.Presenter = RssPresenter(repository, cache)

}

As I mentioned before all provided methods’ return type should be interfaces.

In order to have a better organised structure, all features should be in a different folder. It will be easy to find what your are searching.

Here is the full source code for this part.

Continue reading next part?

If you liked the article, please 👏👏👏 so more people can see it! Also, you can follow me on Medium

--

--