In this article, we'll create a super simple Hacker News client with Clean Architecture. We'll also use RxJava, Kotlin, Dagger, and Retrofit to build our app.

You might want to read Clean Architecture on Android where we go through the concepts in Clean Architecture if you haven't already.

Our app, which we'll call HnReader, will consist of a list showing the items currently displayed on the front page of Hacker News. If the user clicks on an item the default browser will display the story. The user should also be able to navigate to the comments and manually refresh the list.

Full source can be found here

Domain

First thing is to define our models. A quick glance at Hacker News gives us a broad idea of which fields to store.

data class Story( val id: Long, val title: String, val url: String, val user: String, val score: Int, val commentsCount: Int, val time: Long )

We also need a Repository class to fetch the stories we wish to display. The domain layer shouldn't know anything about where the stories come from. It can be from a web request, a cache or perhaps we want to fetch stories each midnight and serve them from a database during the day. We'll just create an interface that defines what we need for now and worry about the implementation details later.

interface StoryRepository { fun getFrontPageStories(forceFreshData: Boolean = true): Single<List<Story>> }

One could argue that the forceFreshData flag is leaky abstractions. The domain layer shouldn't know anything about the implementation, but here it seems to assume that there is a cache. Pragmatism wins over correctness this time. And the interface isn't forcing anything on the Data layer; it just says that if there is a cache, please ignore it.

The final thing we need for the domain layer is Use Cases . The only use case we have is to fetch the stories currently on the front page of Hacker News.

abstract class BaseUseCase<T> constructor(val workScheduler: Scheduler, val resultScheduler: Scheduler) { protected val onSuccessStub: (T) -> Unit = {} protected val onErrorStub: (Throwable) -> Unit = { RxJavaPlugins.onError(OnErrorNotImplementedException(it)) } val disposables = CompositeDisposable() fun dispose() { disposables.clear() } protected fun Single<T>.executeUseCase( onSuccess: (T) -> Unit = onSuccessStub, onError: (Throwable) -> Unit = onErrorStub) { disposables += this .subscribeOn(workScheduler) .observeOn(resultScheduler) .subscribe(onSuccess, onError) } }

We'll use a base class to handle common use case code, so our real use cases can focus on the business logic. In our solution, we extend the Single with an executeUseCase method. It's protected so only our use case implementations will be able to use it.

executeUseCase will make sure that we do our work and deliver our results on the correct threads. In this case, we take Schedulers as parameters to simplify for testing. We could also use Schedulers.io() and AndroidSchedulers.mainThread() directly and use RxJavaPlugin to replace the global schedulers during our tests.

executeUseCase will also take care of our disposable and provide named parameters with default implementations so our use case can choose which callbacks it should implement.

class GetFrontPageStoriesUseCase @Inject constructor( val storyRepository: StoryRepository, @WorkScheduler workScheduler: Scheduler, @ResultScheduler resultScheduler: Scheduler ) : BaseUseCase<List<StoryItem>>(workScheduler, resultScheduler) { fun execute(onSuccess: (List<StoryItem>) -> Unit, onError: (Throwable) -> Unit = onErrorStub, forceFresh: Boolean = false) { storyRepository .getFrontPageStories(forceFresh) .map { storiesToItems(it) } .executeUseCase(onSuccess, onError) } private fun storiesToItems(stories: List<Story>): List<StoryItem> { return stories.map { StoryItem( it.title, it.url, "https://news.ycombinator.com/item?id=${it.id}", it.user, it.score, it.commentsCount, it.time ) } } }

GetFrontPageStoriesUseCase uses dependency injection to get an implementation of the StoryRepository and two Schedulers . It doesn't contain much logic but will map the Story model to a model that suits users of this use case.

The scheduler qualifiers are defined as this.

@Qualifier annotation class WorkScheduler @Qualifier annotation class ResultScheduler

That's our entire domain layer. Just a few classes and an interface. No framework dependencies mean that it is easy to test our implementations. We just have to mock our Repositories . Here is a test case making sure our use case asks for stories and converts them correctly.

@Test fun execute() { // given: val story = Story(123, "title", "url", "user", 1, 2, 3) val expected = StoryItem(story.title, story.url, "https://news.ycombinator.com/item?id=${story.id}", story.user, story.score, story.commentsCount, story.time) val repo = mock<StoryRepository> { on { getFrontPageStories(any()) } doReturn Single.just(listOf(story)) } val useCase = GetFrontPageStoriesUseCase(repo, Schedulers.single(), Schedulers.single()) val future = CompletableFuture<List<StoryItem>>() // when: useCase.execute({ future.complete(it) }) // then: assertEquals(future.get(), listOf(expected)) }

Presentation

The next step is to look at the presentation module. Here we'll implement the Model-View-Presenter pattern to handle our UI. At least the VP part of MVP, as the domain layer is our M.

interface BasePresenter<in V> { fun onViewAttached(view: V) fun onViewDetach() } interface StoryListView { fun displayStories(stories: List<StoryItem>) fun displayLoading() fun hideLoading() fun displayError() fun startBrowser(url: String) } class StoryListPresenter @Inject constructor(val getFrontPageStories: GetFrontPageStoriesUseCase): BasePresenter<StoryListView> { var view: StoryListView? = null override fun onViewAttached(view: StoryListView) { this.view = view this.view?.displayLoading() getFrontPageStories.execute(this::onFrontPageData, this::onFrontPageError) } override fun onViewDetach() { getFrontPageStories.dispose() view = null } fun onForceFetch() { view!!.displayLoading() getFrontPageStories.execute(this::onFrontPageData, this::onFrontPageError, true) } fun onStoryClicked(story: StoryItem) { view!!.startBrowser(story.url) } fun onCommentClicked(story: StoryItem) { view!!.startBrowser(story.commentsUrl) } private fun onFrontPageData(stories: List<StoryItem>) { view!!.hideLoading() view!!.displayStories(stories) } private fun onFrontPageError(ex: Throwable) { view!!.hideLoading() view!!.displayError() } }

A BasePresenter is a bit of an overkill for our application considering we'll only have one screen, but it will make for a good example. We'll use it later to set up a small 'framework' for attaching/detaching automatically from a Fragment .

The view is a simple interface that a dumb Fragment or Activity should implement later. And the presenter is just a class that reacts on events from the view or our use case.

Just as with the domain layers we don't have any platform dependencies yet so we can easily test our presenter with ordinary unit tests. Here's a test case for example that make sure stories are loaded correctly when the view is attached.

@Test fun onViewAttached() { // given: val someStories = listOf(randomStoryItem()) val onSuccess = argumentCaptor<(List<StoryItem>) -> Unit>() val useCase = mock<GetFrontPageStoriesUseCase>() val view = mock<StoryListView>() val inOrder = inOrder(view) val presenter = StoryListPresenter(useCase) // when: presenter.onViewAttached(view) verify(useCase).execute(onSuccess.capture(), any(), eq(false)) onSuccess.firstValue(someStories) // then: inOrder.verify(view).displayLoading() inOrder.verify(view).hideLoading() inOrder.verify(view).displayStories(someStories) }

Now that we made all platform independent things it would be a good time to setup our dependency injection. It will be handled by Dagger 2 so we'll need a Module and a Component .

@Module class AppModule { @Provides @Singleton @WorkScheduler fun providesWorkScheduler(): Scheduler { return Schedulers.io() } @Provides @Singleton @ResultScheduler fun providesResultScheduler(): Scheduler { return AndroidSchedulers.mainThread() } @Provides @Singleton fun providesStoryRepository(): StoryRepository { return DummyStoryRepository() } private class DummyStoryRepository : StoryRepository { override fun getFrontPageStories(forceFreshData: Boolean): Single<List<Story>> { return Single.just(listOf( Story(1, "Test 1", "https://www.google.se/search?q=1", "user1", 154, 114, System.currentTimeMillis() - 600000), Story(2, "Test 2", "https://www.google.se/search?q=2", "user2", 91, 19, System.currentTimeMillis() - 1200000) )).delay(2, TimeUnit.SECONDS) } } } @Singleton @Component(modules = arrayOf(AppModule::class)) interface AppComponent { fun createStoryListPresenter(): StoryListPresenter } class App : Application() { companion object { lateinit var appComponent: AppComponent } override fun onCreate() { super.onCreate() appComponent = DaggerAppComponent .builder() .build() } }

For now, we'll use a dummy implementation of the StoryRepository so we can test our UI while we build it. Dagger will provide the use case and the presenter automatically.

We went with a simple factory method for creating the StoryListPresenter . Other options would be a inject(Fragment/Activity) in the Component or looking at the newer AndroidInjectionModule .

The factory method is statically available from our App class. That method might be a bit too simplistic for larger projects but works well in our case.

Next up is the UI.

Since we're developing on Android we must deal with configuration changes. In this app, we could just re-create the StoryListPresenter after each configuration change or even lock the app into portrait mode. But as this is an example app we'll implement a solution anyway.

HnReader will be using a Fragment to implement our MVP-view. We could either go through the Activity or use ViewModel from the new Architecture Components to preserve our presenter.

In the ViewModel case, we could have our presenter extend ViewModel . It would feel a bit dirty to introduce an Android dependency in the presenter layer, but since this app only will exist on Android that library is in practice no worse than RxJava for example. And the ViewModel itself is an almost empty abstract class, so it's more of a marker interface and won't affect our testing abilities. We could also create a class responsible for creating and caching our presenter and have it extend ViewModel , that way the Android lib could stay in the UI layer.

The other alternative would be to have our Activity keep a presenter cache or something similar with the help of a retained Fragment or the nonConfigurationInstance methods.

To keep it simple we'll go with the second option. We'll define a cache that our activities can implement, nothing fancy just a key to identify our Presenter and a factory method if nothing is cached since before.

interface PresenterCache { fun <T: BasePresenter<V>, V> getPresenter(key: String, factory: () -> T): T }

We'll implement the PresenterCache in a base activity. The cache will be a simple map, but it can easily be enhanced if needed. For now, we'll use the simpler onRetainCustomNonConfigurationInstance() to store our cache and lastCustomNonConfigurationInstance to get it back but we could easily switch to retained fragment if we want to later.

abstract class BaseActivity : AppCompatActivity(), PresenterCache { private lateinit var presenterMap: MutableMap<String, BasePresenter<*>> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) presenterMap = getStoredOrEmptyMap() } override fun onRetainCustomNonConfigurationInstance(): Any { return presenterMap } @Suppress("UNCHECKED_CAST") override fun <P : BasePresenter<V>, V> getPresenter(key: String, factory: () -> P): P { if (!presenterMap.containsKey(key)) { presenterMap[key] = factory() } return presenterMap[key] as P } @Suppress("UNCHECKED_CAST") private fun getStoredOrEmptyMap(): MutableMap<String, BasePresenter<*>> { if (lastCustomNonConfigurationInstance != null) { return lastCustomNonConfigurationInstance as MutableMap<String, BasePresenter<*>> } return mutableMapOf() } }

All the Activity have to do is to extend from BaseActivity .

We'll also set up a BaseFragment that can fetch the Presenter . We'll also implement our only lifecycle methods onViewAttached() & onViewDetach() in here.

abstract class BaseFragment<out P : BasePresenter<V>, in V> : Fragment() { abstract val presenterKey: String abstract fun createPresenter(): P override fun onStart() { super.onStart() getPresenter().onViewAttached(this as V) } override fun onStop() { super.onStop() getPresenter().onViewDetach() } fun getPresenter(): P { return (activity as PresenterCache).getPresenter(presenterKey, this::createPresenter) } }

The last UI part is to set up our Fragment . It's a dumb view that only reacts to calls from StoryListPresenter . No state, and only UI logic.

class StoryListFragment : BaseFragment<StoryListPresenter, StoryListView>(), StoryListView { override val presenterKey = "storyPresenter" override fun createPresenter() = App.appComponent.createStoryListPresenter() private lateinit var recyclerView: RecyclerView private lateinit var refresh: SwipeRefreshLayout private val adapter = StoryListAdapter(emptyList(), this::onStoryClicked, this::onCommentClicked) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val v = inflater.inflate(R.layout.fragment_story_list, container, false) refresh = v.findViewById(R.id.swipeRefresh) refresh.setOnRefreshListener { getPresenter().onForceFetch() } recyclerView = v.findViewById(R.id.recyclerView) recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) recyclerView.adapter = adapter recyclerView.setHasFixedSize(false) return v } override fun displayStories(stories: List<StoryItem>) { adapter.setStories(stories) } override fun displayLoading() { refresh.isRefreshing = true } override fun hideLoading() { refresh.isRefreshing = false } override fun displayError() { Toast.makeText(context, R.string.fetch_error_msg, Toast.LENGTH_LONG).show() } override fun startBrowser(url: String) { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } fun onStoryClicked(story: StoryItem) { getPresenter().onStoryClicked(story) } fun onCommentClicked(story: StoryItem) { getPresenter().onCommentClicked(story) } }

Now, we got a simple working app. It only displays the two dummy stories we created earlier but still, we're almost there.

Data

The last thing we need to is to connect to a real data source. There are plenty of API:s to choose from but we'll use this one. We would also like our data to be cached locally so we don't have to fetch from the network all the time.

class CachedStoryRepository @Inject constructor(restApi: RestApi, storyCache: StoryCache): StoryRepository { private val cacheOrApi: Single<List<Story>> private val apiDirectly: Single<List<Story>> init { val apiWithWriteToCache = restApi .getFrontPage() .doOnNext { storyCache.setFrontPage(it) } val fromCache = storyCache.getFrontPage() cacheOrApi = Observable.concat(fromCache, apiWithWriteToCache).firstOrError() apiDirectly = apiWithWriteToCache.firstOrError() } override fun getFrontPageStories(forceFreshData: Boolean): Single<List<Story>> { if (forceFreshData) { return apiDirectly } return cacheOrApi } } interface RestApi { fun getFrontPageStories(): Observable<List<Story>> } interface StoryCache { fun getFrontPageStories(): Observable<List<Story>> fun setFrontPageStories(stories: List<Story>) }

Our repository will take two classes; one for fetching from the API and one for fetching from- and writing to cache. We'll chain the API call to always write to cache on successful fetch. And finally, we setup cacheOrApi which takes from the cache if it contains fresh data, otherwise it will fetch from API. And apiDirectly which just use our "API then write to cache"-chain directly.

Another option is to expose an Observable instead of Single and send cached data first and fresh data as soon as the web request had finished. In this case, it felt like bad UX though. A user may find it annoying if they start interacting with the cached data only to have stories 'jump around' when the fresh data arrives.

The API implementation is straightforward; we take a service that fetches our stories and then we transform each story so it suits our models. In this case, we have to fix the URL for self-posts, set an empty user for job posts and change the timestamp to milliseconds.

The HackerNewsService will be implemented by Retrofit .

class RestApiImpl @Inject constructor(private val hnService: HackerNewsService) : RestApi { override fun getFrontPageStories(): Observable<List<Story>> { return hnService.getFrontPageStories() .map { it.map(this::apiToStory) } } private fun apiToStory(apiStory: ApiStory): Story { return Story( apiStory.id, apiStory.title, fixUrl(apiStory.url), apiStory.user ?: "", apiStory.points, apiStory.comments_count, apiStory.time * 1000 ) } private fun fixUrl(url: String): String { return when { url.startsWith("item?id=") -> "https://news.ycombinator.com/$url" url.startsWith("http") -> url else -> "http://$url" } } } interface HackerNewsService { @GET("news") fun getFrontPageStories(): Observable<List<ApiStory>> }

For caching we'll set up a simple in-memory cache with some max age for invalidation. This cache is both overly complex and unnecessary for this use case. The best solution here would probably be to use Okhttp to add caching directly into our Retrofit request. But as before, this is an example app so we can do what we feel like.

class InMemStoryCache constructor(private val maxAge: Long) : StoryCache { private var frontPageCache: CacheEntry<List<Story>>? = null override fun getFrontPage(): Observable<List<Story>> { return frontPageCache.toObservable() } override fun setFrontPage(stories: List<Story>) { frontPageCache = CacheEntry(stories) } private data class CacheEntry<out T>(val item: T, val time: Long = System.currentTimeMillis()) private fun <T> CacheEntry<T>?.toObservable(): Observable<T> { if (this == null || this.isStale()) { return Observable.empty() } return Observable.just(item) } private fun CacheEntry<*>.isStale(): Boolean { return System.currentTimeMillis() > time + maxAge } }

The last thing we need to do in this app is to wire it all together in the Dagger- Module and add an INTERNET permission to our manifest.

@Module class AppModule { ... @Provides @Singleton fun providesStoryCache(): StoryCache { return InMemStoryCache(5 * 60 * 60 * 1000) } @Provides @Singleton fun providesRestApi(hnService: HackerNewsService): RestApi { return RestApiImpl(hnService) } @Provides @Singleton fun providesStoryRepository(restApi: RestApi, storyCache: StoryCache): StoryRepository { return CachedStoryRepository(restApi, storyCache) } @Provides @Singleton fun providesRetrofit(): Retrofit { return Retrofit.Builder() .baseUrl("http://node-hnapi.herokuapp.com/") .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build() } @Provides @Singleton fun providesHackerNewsService(retrofit: Retrofit): HackerNewsService { return retrofit.create(HackerNewsService::class.java) } }

The full code can be found here.