Author: IsaacRF239
Minimum SDK: API 20
Target SDK: API 29
Android MVVM, master-detail app built in modern architecture using SOLID and CLEAN principles, that shows a beer catalog and allows user to interact with it changing beer availability. This development is part of a code challenge which requisites you can read here
- 📱 Adaptable layout for Tablets and Phones of any size
- ⏹ Clean UI with ConstraintLayout
- 🏗 Architecture SOLID and CLEAN compliant, under MVVM pattern
- 📦 Full API and BBDD consumption abstraction
- 💉 Dependency Inversion and Injection
- 📸 Image rendering and auto caching
Techs
App is displayed in two pane mode on tablets, and master-detail navigation in phones, making the most of screen space. Navigation is made via Android's Navigation Component using NavGraph alongside two fragments (one for master and another one for detail). Fragments allow to easily configure layout distribution and navigation depending on screen size, and NavGraph allows to configure navigation transitions and required info in one place.
Also, UI uses the last ConstraintLayout for Android, ensuring a flat and clean UI hierarchy and best performance.
This project uses MVVM (Model - View - Viewmodel) architecture, via new Jetpack ViewModel feature.
This project implements "By feature + layout" structure, Separation of Concerns pattern and SOLID and Clean principles.
Project architecture combines the "By feature" structure, consisting on separating all files concerning to a specific feature (for example, an app screen / section) on its own package, plus the "By layout" classic structure, separating all files serving a similar purpose on its own sub-package.
By feature structure complies with the Separation of Concerns and encapsulation patterns, also making the app highly scalable, modular and way easier to manipulate, as deleting or adding features impact only app base layer and refactor is minimum to non-existent.
Testing project replicates the same feature structure to ease test running separation
Data is handled via LiveData / Observable Pattern instead of RxJava, as it's better performant and includes a series of benefits as, for example, avoiding manual app lifecycle management. Information retrieved from network is also stored on Room persistent DB to allow product availability manipulation and data cache.
BeerListViewModel
val beerList: LiveData<NetworkResource<List<Beer>>> = beerListRepository.getBeers()
BeerListFragment
//Observe live data changes and update UI accordingly
beerListViewModel.beerList.observe(this) {
when(it.status) {
Status.LOADING -> {}
Status.SUCCESS -> {}
Status.ERROR -> {}
}
}
Project implements Dependency Inversion (SOLID) and Injection to isolate modules, avoid inter-dependencies and make testing easier
Dependency Injection is handled via Hilt, a library that uses Dagger under the hood easing its implementation via @ annotations, and is developed and recommended to use by Google.
ViewModel is retrieved from activity to share it between master and detail fragments, allowing detail to modify data and reflect it on master list. The Module located on di package is in charge of defining implementations for everything that is injected in the app.
GithubNdApp (App main class)
@HiltAndroidApp
class AndroidBaseApp : Application() {}
MainActivity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {}
BeerListFragment
class BeerListFragment : Fragment() {
private val beerListViewModel: BeerListViewModel by activityViewModels()
}
BeerListViewModel
class BeerListViewModel @ViewModelInject constructor (
private val beerListRepository: BeerListRepository,
@Assisted private val state: SavedStateHandle
) : ViewModel() {}
API Calls are handled via retrofit, declaring calls via an interface, and automatically deserialized by Gson into model objects. This app manages to achieve retrofit abstraction using interfaces and call adapter factories.
BeerListService
@GET("beers")
fun getBeers(
@Query("page") page: Int,
@Query("per_page") perPage: Int
): LiveData<ApiResponse<List<Beer>>>
Cache and local storage is managed via Room, using data classes as entities, and dao interfaces for DDBB access.
Data class
@Entity
data class Beer (
@PrimaryKey
val id: Int, ...
)
DAO
@Dao
abstract class BeerRoomDao: BeerDao {
@Insert(onConflict = ABORT)
abstract override fun insert(vararg beers: Beer)
@Insert(onConflict = IGNORE)
abstract override fun insert(beers: List<Beer>)
@Update
abstract override fun update(beer: Beer)
@Query("SELECT * FROM Beer WHERE id = :beerId")
abstract override fun load(beerId: Int): Beer?
@Query("SELECT * FROM Beer")
abstract override fun load(): LiveData<List<Beer>>
}
Beer images are rendered and cached using EpicBitmapRenderer, a Java Android library I also developed.
All business logic, services and database interactions are tested
Every test is isolated, and all API calls are mocked using Mockito and okhttp Mockwebserver to avoid test results to depend on external sources, becoming unrealiable and possibly leading to unexpected results.
Database is tested via a instrumentation test because best testing practices require a real device (or emulator) to test database interactions using Room in-memory database builder.