Skip to content

Example of a basic Android app implementing MVVM, SOLID and Clean code

License

Notifications You must be signed in to change notification settings

IsaacRF/android-mvvm-beercatalog

Repository files navigation

Android Beer Catalog (MVVM App)

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

beercatalog_tablet_demo beercatalog_phone_demo

Main Features

Techs

  • Android Jetpack
    • Navigation components (Navgraph)
    • Room
  • 🗡 Hilt
  • Retrofit

Adaptable layout

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.

navgraph

Also, UI uses the last ConstraintLayout for Android, ensuring a flat and clean UI hierarchy and best performance.

MVVM Architecture

This project uses MVVM (Model - View - Viewmodel) architecture, via new Jetpack ViewModel feature.

image

Development Patterns

This project implements "By feature + layout" structure, Separation of Concerns pattern and SOLID and Clean principles.

By feature + by layout structure

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.

folder-structure

Testing project replicates the same feature structure to ease test running separation

folder-structure-test

LiveData / Observable Pattern

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 -> {}
    }
}

Dependency Inversion and Injection

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() {}

Data

API Calls

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.

PUNK API endpoint

BeerListService

@GET("beers")
fun getBeers(
    @Query("page") page: Int,
    @Query("per_page") perPage: Int
): LiveData<ApiResponse<List<Beer>>>

Local Storage

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>>
}

Image rendering and caching

Beer images are rendered and cached using EpicBitmapRenderer, a Java Android library I also developed.

EpicBitmapRenderer Icon

Testing

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.