This is a simple application that is built using the Model-View-ViewModel (MVVM) architecture in 100% Kotlin. It has the following features and screens:
This screen is used for listing all the PDF documents contained in the `Download folder. It shows the following:
- A list of items made by filenames with PDF extension
- A search field to perform fuzzy search on the items
- Every item in the list can have a star icon
- Star/Un-star actions should be supported for every item
- Starred items should appear at the beginning of the list
- Tapping on an item will open the PDF
This screen renders the clicked document using PSPDFKIT. It shows the following:
- The rendered PDF document with vertical page scroll direction of the document
- A top bar with the filename (extension included)
- A UI component of your choice to star/un-star the document
- A floating action button that when pressed will export in the download folder the current page as a single paged PDF. The output of the file name will be “[original-document-name] page x.pdf”
The app was built using Android Studio Dolphin | 2021.3.1 so try running at least that version of Android studio. As for testing devices, ensure they are at least running Android API 23 and above. Running UI tests can be done straight from the Android Studio UI.
The app uses the MVVM architecture because it is a nice and simple architecture that makes testing and maintenance
easier. It was also chosen
because it is a popular choice meaning a new developer can pick it up easily making for smooth transitions between
teams. There is also the
added benefit of using a ViewModel
to survive configuration changes. Kotlin Flow
and Coroutines
have been used to
monitor data and
state changes in the app making for a smooth user experience. The full explanation of the architecture used can be found
here on the official Android developer website. For added ease of
maintenance and extra abstraction, the repository pattern is also used, which further helps keep things cleaner.
NOTE ON NAVIGATION: The decision to use the previous navigation component as opposed to Navigation Compose is extremely intentional since there are type checks enforced. Debugging on the Navigation Compose component can be a nightmare especially when you pass a wrong type by mistake. The component will fail at run time whereas the older component will fail at compile time. Additionally, the older navigation component has the added benefit of allowing the use of parcelable types as well as the opportunity to have the flexibility of fragments and experience the best of both worlds. This makes for a cleaner architecture in my opinion.
This app makes use of the following libraries:
- Jetpack🚀
- ViewModel - Manage UI data to survive configuration changes and is lifecycle-aware
- Compose - Android’s modern toolkit for building native UI.
- Navigation - Handle everything needed for in-app navigation
- Room DB - Fluent SQLite database access
- Dagger Hilt - A fast dependency injector for Android and Java.
- PSPDFKit - An SDK for viewing, annotating, editing, and creating PDFs
- kotlinx.coroutines - Library Support for coroutines
- Material Design - build awesome beautiful UIs.🔥🔥
- Timber - A logger with a small, extensible API which provides utility on top of Android's normal Log class.
- Truth - A library for performing assertions in tests
- Mockk - A mocking framework for testing in Android
- Robolectric - For fast and reliable unit tests to Android.
- Turbine - Turbine is a small testing library for
kotlinx.coroutines Flow
Testing has been done in two ways:
- There are unit tests to test the logic of the application. To run the tests, refer to the section above.
- Three devices have been tested with the application as listed below to cover devices both above and below Android 11:
- Samsung Galaxy A02 physical device running Android 11
- Pixel 2 API 30 Emulator
- Pixel 6 API 27 Emulator
The app also uses GitHub actions to build the app and runs whenever a new pull request or merge to the master
branch
happens.
The pipeline also runs unit tests. The status of the build is displayed at the top of this file.
- The biggest challenge in the app has been how to handle the favorite feature from the main list while at the same time providing a smooth user experience. One of the things I did was refresh the entire list once the favorite button was clicked for an item but the list appeared to jump. This forced a rethink where I solved it by working on a background thread to save the favorite in the db then update the list in memory once the favorite operation is completed, which triggers a recomposition of the list in a smooth way.
- Another challenge was in how to choose between a local db or
SharedPreferences
for the favorite feature considering that the files themselves cannot be modified. Eventually, the decision I made was to useRoom DB
because SQL is quite fast in writing and reading. - Initially, the app was using the
MediaStore
APIs for querying the data from the file system, which worked to some extent but the single-page PDF files exported by the app could not be loaded for some reason. This forced a refactor to use the currentgetPdfListFromFile
implementation in thePdfRepoImpl
class, which works seamlessly although it did need a bit of research in determining a unique key for each file. This ended up being thecanonicalPath
of a file, which is absolute and unique.
MIT License
Copyright (c) 2022 Ferdinand Bada
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.