Project architecture follows the principles of Clean Architecture. Benefits of using the "clean" approach:
- Modularisation - faster build times thanks to incremental builds
- Code is independent of frameworks (loose coupling between modules thanks to separate models + communication through interfaces)
- Code can be easily swapped in and out (for example to test a different database implementation)
- It's a pleasure to write tests (you can check few test examples in data, domain and UI modules)
This layer uses the Android Framework to create all of the UI components. It receives data from the presentation layer and then maps it using an appropriate mapper.
- Koin - dependency injection for smaller projects (it is easier to setup than a dagger but doesn't provide a compile-time validation and therefore should be used with caution)
- Navigation component - for generating navigation graph and easy screen to screen navigation
- ConstraintLayout - for constructing complex XML layouts with ease
- Adapter delegates - delegate pattern for android adapters. Thanks to this our list screen is more extensible
- PdfPreview - widget for displaying pdf files within a viewpager
- Coroutines - a flexible approach for handling asynchronous operations (less boilerplate code than rxjava)
This layer uses the MVVM approach to orchestrate communication between UI and data modules. ViewModel communicates with data through the use cases, maps retrieved data and then exposes it with immutable UI state class. This UI state class is called Resource and has three subclasses (Loading, Success, Error). There are two View Models present in this module:
- DocumentPreviewViewModel for orchestrating actions/states on document preview screen (displaying loading state, fetching, handling errors, etc)
- DocumentsViewModel for orchestrating action/states on documents list screen (fetching documents, sorting, etc)
- View Model - ViewModels are restored after configuration changes (language change, rotation, keyboard availability, multi-window mode turned on) and can be used to provide chunks of data to the next instance of Activity and/or View hierarchy of fragment that is coming from back stack.
- Live Data - LiveData is a lifecycle-aware observable and thanks to that we don't have to remove observers (from Activity/Fragment) manually, and we can be sure that the data from it will not be passed to components with a state other than Started/Resumed.
Domain layer contain various UseCases used in our app. These use cases retrieve data from data modules through the interfaces (DocumentsRepository in our case) and pass it into the Presentation Layer. Our app defines three use cases:
- GetDocumentsListUseCase for fetching documents
- GetDocumentFileUseCase for fetching pdf files
- SortDocumentsUseCase for sorting documents with chosen sort order
- Pure Kotlin module
Access point for getting data from the correct data source (Cache/Remote/File storage). It contains the implementation of DocumentsRepository (defined in the domain module). This implementation fetches documents from remote and saves them to cache if it's a first app launch (otherwise it will retrieve them from the cache without remote access). DocumentsRepository is also responsible for providing communication between local/remote sources and file storage to fetch document pdf file.
- Pure Kotlin module
This module handles the communication with remote sources (in our cases it is a simple API call to fetch document pdf by using Retrofit interface). It also provides a documents list that was defined for the recruitment task.
- Retrofit - Current standard in network communication
- OkHttp - An HTTP & HTTP/2 client for Android and Java applications
- Joda time - For parsing document dates
Cache module communicates with the local database (Room) which is used to cache documents
- Room - an easy to use library that abstracts SQLite implementation