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

Android Architecture with MVP and Clean Code

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.

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