Andrei Mukamolau

Android developer, artist

Home

Bookcrossing Mobile: postmortem

Published Aug 28, 2020

Share

Hello! Recently I received copyright infringement notice about my favorite pet project, Bookcrossing Mobile app, and was asked to remove the app from Play Store. On this sad note, I decided to discontinue its development completely and keep its code open and untouched. In this article, I will describe my journey with development of this project, some cool tricks I’ve learned, as well as provide some description of the project for anyone who may be interested in it. Project will rot quickly and will become mostly unusable after a short period of time (but still may serve as a reference), but my experience working on it might be useful for those who are looking to start some mobile app project alone.

Backstory

I’ve started this project to explore RxJava and Firebase, also to build up my portfolio and to learn. Idea came to my mind when I was browsing Play Store in search of the app to exchange books. I’ve been participating in bookcrossing movement since 2010, when the first shelf appeared in my hometown. It was not systemized, and a lot of books were not registered on the website, mostly because at that time our local site was quite inconvenient to use, and the global site was only available in English. So, after not finding the official Bookcrossing app on the Play Store, I decided to create my own app for bookcrossing that will make it easy to release new books and that is convenient to use.

I’ve created a list of desired features, but there was almost immediately appeared one more problem: neither main or local bookcrossing websites didn’t have any API available. So I had two options: HTML parsing on client to extract some data or go with my own backend.

I’ve tried to go with Google services, and started researching Google Cloud Platform stuff for backend. GCP is cool, and it offers a lot of good services, but it all required a lot of effort to invest, and that wasn’t what I wanted. Ideally, I would have preferred to spend as little time on backend development as possible, so I can focus mostly on app side.

Thus, I turned my look to Firebase. It was at that time finishing merging into Google, and there was not so much services they offered, but they promised great developer’s experience and more cool features to come in the future. But anyway it was quite enough for my needs: Realtime Database was quite enough to store basic data that I had, Cloud Storage fitted perfectly for storing cover images for, and Authentication promised to be a really straightforward tool to handle users without much headache. Plus later I’ve added Ads and Analytics as a side effect. There was even convenient FirebaseUI wrapper that helped with binding Firebase services with UI, I’ve used it to display cover images from Firebase Storage and for displaying books in RecyclerView via special Adapter class from this library.

I had an eye on RxJava for a long time, it seemed for me the good choice for the most Android apps due to its good threading abstractions, concise API and functional programming tint. Moreover, it was just hip back in the days, and people were using it extensively in the projects, as well as asked about it on the interviews. So I decided to use this project also as a polygon for experiments with RxJava, to learn it by practice.

These factors shaped the initial architecture of the app and helped decide how it will look like.

Initial development

Once I figured out what to do, I chose then-popular MVP architectural pattern for my new app that was implemented with help of Moxy library. It was quite straightforward, given the amount of code on StackOverflow ready for copying and pasting right into the project. For non-trivial issues I’ve picked some libraries from GitHub with decent amount of stars. I followed examples for Firebase setup and for Moxy, without quite thinking about fitting it into the MVP pattern, so I ended up with a lot of Firebase-related code in views and a lot of business logic in presenters, all mixed up. I even tried to write some tests, but it looked for me like there was not so much to test in terms of business logic, so I haven’t added any tests. To be frank, it wasn’t quite possible to add tests in that situation, because of a coupling business logic with UI, as well as lack of support for tests on Firebase side: I discovered that it was impossible to call any of the Firebase SDKs, even those that are seemingly unbound from the Android SDK, it just immediately crashed because of lacking app ID, and it turned out to be quite hard to set dummy ID for tests. All of these factors contributed to the decision to postpone writing tests for later. This wasn’t a red flag for me back then, since I knew that not many teams are actually writing tests. But after spending enormous amount of time in writing tests for existing codebase of questionable quality, I now always ask about tests during job interviews.

Release

After I’ve implemented most of my ideas, I decided to prepare the app for release, but perform some manual testing of the whole app beforehand. I did test each feature in isolation while developing, but now I was checking everything in combination, each user flow I can come up with. There were some annoying issues with styles for toolbar, but nothing critical. I’ve spent quite a lot of time (couple of days, actually) trying to fix toolbar appearance, but nothing worked. So, out of frustration and poor health condition I’ve postponed release for later.

After few months I got back to releasing process and decided to let that pesky toolbar bug into this release. Spoiler: I’ve fixed it only three years later with help of Chris Banes’ Insetter library. Also, the other bugs that were here I decided to fix after release, since I considered them minor.

App went live on beta track at some date, and after that I’ve noticed quite a few crash reports in Crashlytics. They were caused by the lack of data sanitization and error handling. I’ve fixed them immediately by adding these things.

Mostly app was functional, but first users have uncovered scenarios I wasn’t prepared for. For example, some unusual user navigation flows were leading to crash because of uninitialized variable. It took me the whole day to find a root cause and fix it. Variable was initialized in the wrong lifecycle callback.

Another interesting issue was with ads config. I’ve sourced Firebase Ads integration code from multiple places, and it was complete mess, to be honest. Even more, it didn’t work on release build because of misconfiguration. It took me 2 days to figure out what was wrong and configure it properly. Somehow it slipped from me until I noticed that there is no ads in the Play Store build after couple of months after release. If there was more users, it would cost me quite a few bucks.

I haven’t promoted the app anywhere, and I haven’t bought installations. Not because I’m against it, just didn’t want to bother with marketing stuff and pay for advertisements, more wanted to focus on improving quality. Even without this, there were more than 1500 installs of all time, most of them unfortunately followed by uninstalls.

Big refactoring

Time passed, I was slowly improving some features and fixing bugs, so I would be able to release to the main track, not beta. However, at I/O suddenly Google announced Jetpack with migration to AndroidX and other things. I was excited by the new approach Google has taken and wanted to integrate it as soon as possible. There was a catch: when I started to migrate app to AndroidX, I’ve noticed that some libraries I’ve used started breaking the build with AndroidX, even with Jetifier enabled. After digging into the source code I’ve found that these libraries were using some components of old support libraries that were not migrated to the AndroidX or were removed or renamed. I’ve also noticed that some maintainers of the important libraries abandoned their projects. I was unable to migrate them myself because of poor code style of these libraries, as well as the general complexity of the solutions. There might have been some issues with the Jetifier itself: for example, RxPermissions library was broken by Jetifier, resulting my app to crash at runtime, and I was unable to figure out why: code seemed okay, without much weird hacks.

Another interesting example was with Moxy library for easier building of MVP pattern classes. Issues with AndroidX here were easy to fix, but it turned out that original developers were unable to maintain this library. It was forked by multiple people later on, and even turned into community-driven project (it started as in-house project inside one outsourcing company, but apparently the company has little interest in maintenance of the project after maintainer gave up). But it was much later than I’ve initially started migration.

This all was very frustrating. Amount of work required was immense: I needed to abstract out non-AndroidX dependencies to replace them, unbind Firebase logic from UI to ease migration and find replacement for non-AndroidX dependencies. All this work took more than a year with breaks, and at this time I was able to update some dependencies with their AndroidX variants. As a side effect, I decided to fix architectural inconsistencies as well and move everything to the one architectural pattern: I’ve managed to extract Firebase related code to the proper model layer via data sources and repositories, and this helped me to extract logic from presenter to the UseCase, reducing presenter size dramatically. Previously I was unable to decouple this logic properly because of composition with UI and poor Dagger setup, but with all this fixed, this presenter started to shine like a freshly painted car. It was quite satisfying to look into git diff for merge commit after I’ve finished this migration.

I feel like I need to explain what “poor Dagger setup” means. I had some class like ApiContainer (I don’t remember exact name) where I was injecting Firebase classes in fields, and this class was injected in BasePresenter as dagger.Lazy. This sophisticated optimization coupled each presenter to the base, even if it didn’t need any of the Firebase classes, because somehow I didn’t know about constructor injection. I’ve started to inject required classes directly in the presenter where they were needed, inlining some base class methods and refactoring some logic. This allowed me to remove BasePresenter completely, as well as ApiContainer, and that alone simplified all my presenters dramatically and allowed me to see where I can do further improvements.

State after refactoring

After post-refactoring release was live, I had some time thinking what I can do next. There were quite a lot of things that bugged me, and I wanted to improve them. I decided to create GitHub Project to sort them out, as well as to test this feature. We didn’t use GitHub at work, so I was curious to learn how it works and is it any useful for project management.

Over the years, a lack of tests became the most pain in the bum. You can definitely live happily without any test in your project, they say, but in practice I learned that the presence of tests indicates good health of the project and allows you to iterate faster, despite it takes more time to write tests initially. It was discussed many times over the years in the industry, but for me tests are the must-have in any project.

At that stage, most of the logic wasn’t testable due to binding of some Firebase related classes to UI just for loading a cover image from Cloud Storage. Untangling it required a few weeks of work, mostly to extract logic to appropriate layers. I chose Fernando Cejas’ clean architecture approach with use cases, repositories and datasources. It looked like an over-complication at first, but I decided to use datasources to wrap Firebase calls, and put parsing and mapping in the repositories that depend on those datasources. And logic resided in the usecases named after specific action user needed to perform, like add books to “stash” (favorites list) or claim book via scanning its QR code. Keeping Firebase stuff in the datasources allowed to mock the access to it inside the tests of the repos. All these layers were covered with exhaustive tests. In the end I’ve got a fine looking presenters with nicely composed Rx chains that I was finally able to cover with tests, but realized that there was no need to cover presenters since logic was already tested, and presenters were only acting as the holders of usecases and Rx subscriptions. Also, I reduced dependency on an old RxJava adapter library for Firebase, because it was full of Java static methods, and it wasn’t completely thread-safe, so I tried to rewrite adapter functions inside my repos. Basically, wrapping Firebase stuff returned as Task into Rx Observables turned out to be the main function of the repositories. As a side effect of this, some repos ended up being quite anemic, i.e. just proxying datasource methods, like this. It’s not ideal, but these references were required by FirebaseUI’s RecyclerView adapter, so it was really necessary to expose it like this to avoid writing this adapter by myself. I was thinking about it, but decided to postpone the implementation for later due to the trickiness of the adapter.

As the next step, I decided to reimplement book position selection. Initially, I was resolving book position inside the special Cloud Function by the position’s name provided by the user and user’s city. After some time in production I found out that it doesn’t work most of the time, because users tend to write descriptions of the places that are not easily searchable on the map, i.e. not the names of the places, like “Stan’s Coffee shop”, but something more descriptive about the place, like “At my apartments” or “Shelf near the entrance to the library”. In addition, there were some weird crashes inside Promises of the Google Maps JS SDK. I initially wanted to add location picker, but it seemed too complex, thus I ended up implementing this weird automatic resolver. Though it was fun to work with Cloud Functions when they were just released, I figured that it was just about the time to do it properly, so I decided to remove this broken automation and implement proper location picker.

Well, to pick location on the map, first, you need a map. As a map provider, initially I chose Mapbox: it seemed to be great alternative to the Google Maps that is more detailed and is convenient to use. I even planned to use it across the whole app, e.g. to show books on the map and show location of the particular book inside some small static map in book screen. I’ve used their SDK before in my other projects for reverse geocoding, but I have never used their maps and wanted to try. Unfortunately, it didn’t happen, and the whole Mapbox integration led to a lot of frustration.

I’ve added Mapbox quickly, since its API was quite similar to Google Maps. In basic shape it worked fine, but issues started to arise when I started to add logic for the main feature – selecting position. Various random bugs started to appear on the map, and eventually a lot of weird crashes in native code started to happen. There was an open issue on GitHub for that, but it was quickly closed with a recommendation to use newer version. Unfortunately, newer version contained the same bug. Unfortunately, project’s repo has been moved since then, and now I cannot find the issue.

Another frustrating issue was in the documentation for Mapbox. Information appeared to be scattered across different pages, and it took me quite a lot of time to gather all the pieces together to understand what’s going on here at all. For example, it was quite challenging to find the description of the GeoJSON format they’re using. For some reason, it wasn’t easily discoverable through search, and there was little mention of it in docs. As far as I understood, they’ve used a lot of terminology from GeoJSON standard, without quite referencing definition of this terminology back to the standard in their docs or at least explicitly mentioning this terminology as related to GeoJSON (or I was too stupid to understand this sophisticated documentation). Plus, SDK was not quite covered with javadoc comments, only with basic ones.

I’ve spent week or two trying to solve the ever growing amount of bugs and glitches until “Screw it!” moment. I decided not to bother with Mapbox any longer and rewrote all the code to Google Maps in a couple of hours. It even fit more nicely with Rx due to slightly more open interface, although it’s only my humble opinion. Since Maps SDK was not quite designed with regard to Rx, it required me to use regular hack with PublishSubject to get it working, and Google Maps SDK had all the necessary callbacks I needed for my business logic. Mapbox SDK required some extra config: for example, map required some extra style config, because default style wasn’t set (or wasn’t properly working in the first place), despite default style was just fine for me. Also, they had their own solution for managing location permissions that wasn’t fitting well with what I had used in the app, although I don’t remember whether it was really necessary to use their solution or not.

Later I managed to extract map related stuff to the delegate to be able to share logic in different places. It also allowed me to decouple map from the particular UI and, potentially, try once again to switch to the newer Mapbox SDK. Because frankly they have improved their SDK’s API in newer versions, but I don’t know about the bugs.

Buried plans

Before I received DMCA takedown request, I was working on integrating analytics. At work, we use analytics extensively for each app feature, and it really helps to see which part is actually useful for our customers. It inspired me to integrate analytics further. I wasn’t aware about the privacy downsides of Google Analytics back then, I basically just wanted to learn how people use my app, so I decided to use Firebase Analytics first, keeping space for other analytics providers just for the sake of doing it. I wanted to have analytics layer decoupled from the particular analytics provider, so I was planning to have core analytics module with interfaces, and module for each provider, and register provider inside app’s Dagger module. Also, analytics module should have been used only in usecases, because I wanted to track how often each feature is used, not the user input. Screen tracking would also be useful, but some providers required Activity for it to be around.

While I was researching different analytics providers, I found out that they all have different features and different API that were not quite compatible. It was hard to find good interface for major analytics providers, and most importantly, they often required Context or Activity instance, which wasn’t fitting to my app’s architecture and desired structure of the analytics module. I was trying to get some time and effort to handle this, but it was too late.

As another planned big thing I wanted to migrate to Jetpack ViewModel. Given the amount of views and presenters in the project, it didn’t seem as much work at the first glance, but actually it meant replacing the whole architectural pattern, which potentially can take a lot of effort. After refactoring I had clearly separated layers, but the main problem was that Rx subscriptions were stored both in presenters and in views, and that wasn’t supposed to happen with ViewModels. As far as I understood, in ViewModels it was only possible to subscribe to stuff in views (with little help from AutoDispose to make subscriptions lifecycle-aware). And handling inner subscriptions in the viewmodels seemed unreachable for me, because there were so many things I didn’t know how to do in ViewModels.

Main reason for this migration was to avoid situation with Moxy: when it became abandoned, it blocked migration to AndroidX, and the whole confusion with multiple forks that followed didn’t make things easier. And despite it’s not clear whether Google will stick to their proposed ViewModel and MVVM architecture or create something completely different, at least it would be safer to expect them to provide some migration mechanism, or at least generous deprecation policy, as they usually do in their libraries or in Android SDK itself. In that case any change in architecture forced from outside will be easier.

Also, back then RxJava 3 was just around the corner, so I was planning to migrate to it as soon as everything will be ready, i.e. RxJava-based libraries I was using for UI and stuff. Mostly it would be just some grepping of the imports, but it was needed to have all the dependencies to update to RxJava 3 as well. I didn’t consider moving to coroutines due to sheer size of the RxJava-based code, it would be simply killer job to migrate to structured concurrency or even Kotlin Flow.

Conclusion

I described my journey with this project that took 4 years. It was mostly fun to explore new technical things in this project, as well as working with some domain that is quite close to me. Despite takedown, I am satisfied with this journey. With all these highs and lows, I learned a lot and built a good thing for a portfolio. This project served me as an entry point to the public speaking: I’ve used it as a reference to my talk (in Russian) about Firebase, when it was quite new and fresh, and no one talked about it in local meetups. Also, it allowed me to learn how to manage complex refactorings and how to write tests for the reactive code.

Main lesson I took from there is that copyright is quite challenging area, so it’s worth diving deeper into it before committing to the new project to avoid problems with it in the future. Check if any word you want to use in your project’s branding is someone’s copyrighted trademark.

Also, if you’re going to start some pet project to challenge yourself or to get to know some new and shiny technologies, it would be really useful to keep high standards of code style and architecture from the beginning. After you do initial commit, setup static analyser, enforce code style rules via git hook or some basic CI check, write tests, etc. It will allow you to enjoy working on the beautifully organized project years after its start. It may seem like a lot of hassle before actual fun, but it pays out when you will need to integrate some new feature with some new shiny library in it. And when you get stuck in your day job fiddling with some dull legacy code, you will be able to use your pet project as a breath of fresh air to keep going and reduce burnout risks (however, that’s another topic to discuss).

Please reach me out on Twitter if you have any questions or suggestions. Cheers!