Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Architecture with MVP and Clean Code

Avatar for Gerard Gerard
January 19, 2018

Android Architecture with MVP and Clean Code

MVP or MVVM architectures are adopted by Android developers and allows a better development, test and maintenance of their applications. This talk presents a MVP architecture with a Clean Code architecture which can fit into some Android application.

Avatar for Gerard

Gerard

January 19, 2018
Tweet

More Decks by Gerard

Other Decks in Programming

Transcript

  1. SAMPLE APP LIBRAIRIES USED ▸ Kotlin ▸ Retrofit + OkHttp

    ▸ Picasso ▸ Timber ▸ Dagger ▸ RxJava 2 ▸ Room
  2. interface ClubContracts { interface View { fun onClubsReceived(clubs: List<ClubModelUi>) fun

    onError() } interface Presenter : KContracts.Presenter { } }
  3. class ClubPresenter( private val view: ClubContracts.View ) : ClubContracts.Presenter, KPresenter()

    { private fun load() { // ... view.onClubsReceived(it) } init { load() } }
  4. class ClubFragment : KFragment<ClubContracts.Presenter>(), ClubContracts.View { override fun layout() =

    R.layout.fragment_clubs override fun inject(appComponent: AppComponent) { AndroidSupportInjection.inject(this) } override fun onClubsReceived(clubs: List<ClubModelUi>) { // ... } override fun onError() { // ... } companion object { val TAG = "ClubFragment" fun newInstance(): Fragment { return ClubFragment() } } }
  5. abstract class KFragment<T : KContracts.Presenter> : Fragment() { @Inject lateinit

    var presenter: T override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater!!.inflate(layout(), container, false) } override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) inject(context.component) } @LayoutRes abstract fun layout(): Int abstract fun inject(appComponent: AppComponent) }
  6. data class ClubServiceModel( @SerializedName("id") val id: Int, @SerializedName("name") val name:

    String, @SerializedName("picture") val picture: String, @SerializedName("disable") val disable: Boolean, @SerializedName("address") val address: String, @SerializedName("postcode") val postcode: String, @SerializedName("city") val city: String, @SerializedName("mail") val mail: String, @SerializedName("phone") val phone: String? ) @Entity(tableName = "clubs") data class ClubDaoModel( @PrimaryKey val id: Int, val name: String, val picture: String, val address: String, val city: String, val mail: String, val phone: String?, @ColumnInfo(name = "created_at", index = true) val createdAt: Long )
  7. data class Club( val id: Int, val name: String, val

    picture: String, val address: String, val city: String, val mail: String, val phone: String? ) open class ClubModelUi( val id: Int, val name: String, val pictureLink: String, val phoneLink: Uri, val mapLink: Uri, val mailLink: Uri )
  8. class ClubModelUiMapper : Mapper<Club, ClubModelUi> { override fun to(from: Club):

    ClubModelUi = ClubModelUi( id = from.id, name = from.name, pictureLink = from.picture, phoneLink = if (from.phone != null) "tel:${from.phone}".toUri() else "".toUri(), mapLink = "https://www.google.com/maps/${from.address} ${from.city}".toUri(), mailLink = "mailto:${from.mail}".toUri() ) }
  9. class GetClubUseCase(private val repository: ClubRepository) { fun execute(): Single<List<Club>> {

    return repository.clubs() .toObservable() .flatMapIterable { it } .filter { it.id != 252 } .toList() } }
  10. class ClubRepositoryImpl( private val databaseSource: ClubDatabaseSource, private val serviceSource: ClubServiceSource

    ) : ClubRepository { override fun clubs(): Single<List<Club>> { return databaseSource.clubs() .toObservable() .filter { !it.isEmpty() } .switchIfEmpty(serviceSource.clubs() .doOnSuccess { databaseSource.saveClubs(it) } .toObservable()) .singleOrError() } }
  11. class ClubPresenter( private val view: ClubContracts.View, private val getClubUseCase: GetClubUseCase,

    private val clubMapper: Mapper<Club, ClubModelUi>, private val schedulers: KSchedulers) : ClubContracts.Presenter, KPresenter() { private fun load() { subscription.add(getClubUseCase.execute() .toObservable() .flatMapIterable { it } .map { clubMapper.to(it) } .toList() .subscribeOn(schedulers.newThread) .observeOn(schedulers.main) .subscribe({ view.onClubsReceived(it) }, { Timber.e(it) view.onError() }) ) } init { load() } }
  12. TESTS LIBRAIRIES USED ▸ JUnit ▸ AssertJ + AssertK ▸

    Mockito + Mockito Kotlin ▸ Android Support Test
  13. class ClubUseCaseTest { private val repository: ClubRepository = mock() private

    lateinit var clubUseCase: ClubUseCase @Before fun setUp() { clubUseCase = ClubUseCase(repository) } // tests... }
  14. @Test fun shouldLoadClubs() { val clubExpected = Club(253, "Domyos Lille",

    "", "Professor Langevin", "Lille", "[email protected]", "0606060606") whenever(repository.clubs()).thenReturn(Observable.fromArray(clubExpected).toList()) val clubs = clubUseCase.execute().test().assertNoErrors().values().first() verify(repository).clubs() assert(clubs).hasSize(1) assert(clubs).containsExactly(clubExpected) } @Test fun shouldNotLoadClub252() { whenever(repository.clubs()).thenReturn(Observable.fromArray( Club(252, "Domyos Lille", "", "Professor Langevin", "Lille", "[email protected]", "0606060606")).toList()) val clubs = clubUseCase.execute().test().assertNoErrors().values().first() verify(repository).clubs() assert(clubs).isEmpty() }
  15. class ClubPresenterTest { private val view: ClubContracts.View = mock() private

    val repository: ClubRepository = mock() private val clubMapper: Mapper<Club, ClubModelUi> = mock() private val schedulers: KSchedulers = SchedulersTest() private lateinit var clubUseCase: ClubUseCase @Before fun setUp() { clubUseCase = ClubUseCase(repository) } // tests... }
  16. @Test fun shouldNotifyViewWhenUseCaseGiveUsListOfClubs() { val clubs = arrayListOf(Club(253, "Domyos Club",

    "", "", "", "", "")) val clubExpected: ClubModelUi = mock() whenever(repository.clubs()).thenReturn(Single.just(clubs)) whenever(clubMapper.to(clubs[0])).thenReturn(clubExpected) ClubPresenter(view, clubUseCase, clubMapper, schedulers) verify(view).onClubsReceived(arrayListOf(clubExpected)) } @Test fun shouldNotifyViewWhenAnErrorOccurred() { whenever(repository.clubs()).thenReturn(Single.error(Throwable())) ClubPresenter(view, clubUseCase, clubMapper, schedulers) verify(view).onError() }