In this article, I’ll present a case study of a process I went through when implementing the tabs feature in PSPDFKit for Android, how it led to a multi-document API, and how it eventually helped improve my code and API design skills.
Tabs have been a highly requested feature for quite some time, both from our customers and from PDF Viewer users. When implementing tabs, the requirements were simple: Make it possible for users to open multiple documents in a single activity and provide a simple UI that will allow switching between these loaded documents.
You might be wondering why we need tabs. Opening documents in multiple activities and switching between them is fast, and users can even pop these activities into a multi-window mode and work on multiple documents at the same time, side by side. Well, if PSPDFKit were a simple PDF viewing app, then you might be correct. However, we are not developing just an app but also a framework used in hundreds of different apps. These apps have different use cases that require fast document switching, and many of them have end users who expect tabs.
When we decided to implement tabs, I was the person who was responsible for working on this feature. It seemed to me that such a simple feature could be done in a few days without many issues. It turned out I was wrong. I wanted to share my experience as an example of what could go wrong if you don’t design your code and APIs in advance and rely only on your ballpark approximation of the apparent complexity of a task at hand.
My first approach was highly influenced by the desired final UI we wanted to have for our tab component. I began with
PdfTabBar encapsulating the tabs bar and its UI behavior. This was then controlled via
DocumentTabsController, which is responsible for all business logic and interaction with documents loaded in
Tabs could be controlled programmatically via the
// Add new tab to the end of the tabs bar. val documentTab = activity.documentTabsController.addTab(documentSource) // The returned tab object can be made visible later on. activity.documentTabsController.setVisibleTab(documentTab)
The document switching that happened when making tabs visible was handled by replacing the document inside
PdfActivity and correctly restoring its UI state. This resulted in noticeably slow tab switches. In addition, the whole solution had a messy code structure, including unclear responsibilities of its main classes and an unnecessary complex callback flow (callback hell). This all resulted in multiple bugs in the initial implementation.
After a week of continuing in this direction, I grew increasingly frustrated about my approach and took a day to step back, reevaluate, and come up with a cleaner code design.
The main problem I wanted to solve was document switching performance, as it didn’t match the quality standards of PSPDFKit or me. After looking at the problem as a whole, I tried to stop being influenced by the tabs UI and started treating tabs as only one possible use case for the document switching API. I ended up with a separate multi-document API for managing multiple documents in a single
The previous solution worked on a higher level using the public API of
PdfActivity for switching documents. Implementing this on the lower level where we have full control over PSPDFKit’s internals made for a much more optimized solution in contrast to leaving this to our customers who only have access to public APIs.
The solution I decided upon consists of two separate parts:
DocumentCoordinatoris responsible for managing multiple documents inside a single
PdfActivity. The document coordinator does not care about the existence of the tab bar; it just emits events about changes to managed documents to any interested consumer. This makes it possible to build an entirely custom UI for document switching.
// Documents in the document coordinator and modeled via the `DocumentDescriptor` // class, containing all information required to load the document, // as well as to store and restore its state. val descriptor = DocumentDescriptor.fromUri(documentUri) // Add a new document. activity.documentCoordinator.addDocument(descriptor) // Display the newly added document. activity.documentCoordinator.setVisibleDocument(descriptor) // Remove the document that is no longer needed. activity.documentCoordinator.removeDocument(otherDescriptor)
Let me sidetrack here a bit to explain one aspect of human psychology that is applicable to this situation. The sunk cost fallacy is a human behavior pattern “where investments (i.e., sunk costs) justify further expenditures.” This concept can be easily adapted to software development. Even if you have already spent a lot of time working on a solution, you should not be scared of throwing it out and starting from scratch. Time already spent is not totally lost either — at least you learned something and can use this knowledge to improve by iterating upon your solution.
My initial tabs implementation wasn’t very elegant, led to loads of bugs, and had a far-too-complex control flow for the desired business logic. Throwing it into a bin does not mean that I should feel bad; the time spent helped me learn from my mistakes and design a superior solution.
As you can see, I eventually managed to get to a really nice and powerful API. This was only one of the instances where something similar happened to me or one of my colleagues: Underestimating the complexity of a problem and skipping the formal design process can happen to all of us. Moving forward, we wanted to improve our workflows and try to solve (or at least improve upon) this issue. So we dedicated some time to refactor our development practices.
We ended up adapting a proposal-based approach when working on any larger feature. Nowadays, long before we start working on a feature (it could even be months before, in some cases) we write a proposal. This proposal is usually fairly in-depth in its scope and includes intended use cases, proposed public API design, and explanation of the high-level implementation. While working on the proposal, we usually also identify multiple anticipated implementation roadblocks, which can be discussed and decided upon in advance before the development starts.
Even so, these rigorous preparations could still miss some issues we encounter during the actual development. But in our experience, these are only isolated cases of mostly minor details that won’t lead to major architectural changes.
You could argue against spending your team’s time on writing and reviewing proposals, but we find it’s totally worth it, because:
Our developers have less stress. In addition, we avoid getting into situations where multiple days of work are scrapped because the solution does not meet our high standards.
Multiple people reviewing the proposed solution leads to a better solution, as it naturally brings multiple perspectives of people with different areas of expertise and levels of experience to the table.
Reviewers are not forced into agreeing with the acceptable solution just because its implementation has already burned too much time — we can be much more critical in the proposal stage.
Proposals help us plan our future work since we can do much more realistic time estimates after deciding on the solution we want to take and identifying the most anticipated issues.
I hope I gave you a useful sneak peek into how we work on PSPDFKit for Android. Development is not a linear process, and you should not be scared of throwing your hard-earned solution away as soon as you identify that it is not the best solution. Moreover, you should consider spending more time in the design phase before diving into actual coding. You might even take inspiration from us and adopt a proposal workflow.
But with anything you do, keep in mind that you should always choose a workflow that fits your personality and the personality of your team — maybe you prefer a more rigid approach with formal architectural processes, or maybe you want even more freedom than what we do here at PSPDFKit. The choice is yours!
Keep coding, and best of luck designing code and APIs that won’t haunt you at night.