Redux for Android with Kotlin in practice, Part 3: Final touches

Alla Dubovska
xorum.io
Published in
4 min readOct 9, 2019

In the first and second parts of this series we’ve covered the initial setup and networking using Redux architecture in Codeforces Watcher. This is an open-source Android application dedicated to users of the well-known platform for competitive programming — Codeforces. There are hundreds of contests each week with thousands of participants. Tasks are varied from quite basic to really advanced.

For now Codeforces Watcher allows users to see upcoming contests and track the activity of chosen users on the platform. We were about to add a few new features, but to make it easier, we decided to apply the right pattern first.

Loading status

Commit: Handle loading status for Contests.

Nobody likes “swipe-to-refresh” spinning forever or no “network operation in progress” at all. But how to deal with them using Redux? Very simple! We will just add loading status into our ContestsState.

data class ContestsState(
val status: Status = Status.IDLE,
val contests: List<Contest> = listOf()
) : StateType {
enum class Status { IDLE, PENDING }
}

Now we need to set Status accordingly to Actions, which are handled in contestsReducer:

when (action) {
is ContestsRequests.FetchContests -> {
newState = newState.copy(
status = ContestsState.Status.PENDING
)
}
is ContestsRequests.FetchContests.Success -> {
newState = newState.copy(
status = ContestsState.Status.IDLE,
contests = action.contests
)
}
is ContestsRequests.FetchContests.Failure -> {
newState = newState.copy(
status = ContestsState.Status.IDLE
)
}
}

And finally, let’s bind Status to our “swipe-to-refresh” element in ContestFragment:

override fun newState(state: ContestsState) {
swipeToRefresh.isRefreshing
= (state.status == ContestsState.Status.PENDING)
contestAdapter.setItems(state.contests.sortedBy(Contest::time))
}

Dispatching failures

Commit: Handle Failure for FetchContests request.

We surely don’t want be the guys who are talking about choosing the right architecture and don’t handling errors :) So let’s handle them right away, in FetchContests request:

response.body()?.result?.let { contests ->
store.dispatch(Success(contests))
} ?: store.dispatch(Failure())

Logic is simple: if we can get contests, it’s success, otherwise — failure. Also don’t forget to dispatch failure in onFailure callback:

override fun onFailure(call: Call<ContestResponse>, t: Throwable) {
store.dispatch(Failure(t))
}

Of course, those actions won’t display any errors to user by themselves. We will show how to add special middleware for this purpose in next articles.

Mutating data

Commit: Filter and sort contests.

If you remember, we’ve actually broken our feature last time and instead of showing upcoming contests, we are showing all of them. It’s time to fix the issue!

Previously we used a Room request to sort and filter all contests fetched from Codeforces and stored to database. This functionality can easily be replicated by simple Kotlin extensions:

is ContestsRequests.FetchContests.Success -> {
newState = newState.copy(
status = ContestsState.Status.IDLE,
contests = action.contests
.filter { it.phase == "BEFORE"}
.sortedBy(Contest::time)

)
}

Ongoing refactoring

Commits: Ongoing refactoring. and Self-review.

At it’s usually happens, in commit called “… refactoring …” something goes wrong. This time it’s a common Android Studio bug, when you move files, paths are added to all relevant classes and then removed. As a result, you have strange line breaks in many places. Issue has been introduced in first commit and fixed in second.

The only important point in these 2 commits is:

@Query("SELECT * FROM contest")
fun getUpcomingContests(): List<Contest>

We are removing LiveData and simplifying SQL request for Room query, because we now use Redux for both functions.

Caching data

Commit: Add RoomController.

To not introduce any functional regressions, the last thing we need to do is saving / fetching Upcoming Contests with Room. Ideally, caching logic should be completely separated from all other logic, which can be easily done with Redux.

object RoomController : StoreSubscriber<AppState> {fun onAppCreated() {
store.subscribe(this) {
it.skipRepeats { oldState, newState ->
oldState.contests == newState.contests
}
}
}
fun fetchAppState() = AppState(
contests = ContestsState(contests = DatabaseClient.contestDao.getUpcomingContests())
)
override fun newState(state: AppState) {
DatabaseClient.contestDao.deleteAll()
DatabaseClient.contestDao.insert(state.contests.contests)
}
}

We’ve just implemented a special StoreSubscriber for Room, which will listen to ContestsState changes once the application is launched. As soon as a new State is available it will rewrite all Upcoming Contests stored in Room database with ones got from Codeforces server.

Also it’s responsible for fetching ContestsState on the application launch to quickly display offline data to users.

To make it work we need call fetchAppState when initializing Store, like this:

val store = Store(
reducer = ::appReducer,
state = RoomController.fetchAppState(),
middleware = listOf(appMiddleware)
)

And don’t forget to call RoomController.onAppCreated() in Application’s onCreate method.

Conclusion

That’s it! We’ve fully migrated one of Codeforces Watcher’s features to Redux, while keeping all other functions fully functional and almost without changes. This is the power of Redux 💪

Have you liked this series? Fall in love with Redux? Wanna see it in your application?

Then don’t hesitate to write us on hello@xorum.io. We are developing native mobile applications, which users love ❤️

--

--

Alla Dubovska
xorum.io

Software engineer (👩‍💻 native iOS development), Mom 👦, Marathon finisher 🏃🏻‍♀️