Slide 1

Slide 1 text

ANDROID ARCHITECTURE WITH MVP AND CLEAN CODE Gérard Paligot

Slide 2

Slide 2 text

INSTORE

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

PLANNING DOMYOS SAMPLE APP

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

SAMPLE APP LIBRAIRIES USED ▸ Kotlin ▸ Retrofit + OkHttp ▸ Picasso ▸ Timber ▸ Dagger ▸ RxJava 2 ▸ Room

Slide 10

Slide 10 text

WHAT IS MVP AND CLEAN CODE? MVP

Slide 11

Slide 11 text

interface ClubContracts { interface View { fun onClubsReceived(clubs: List) fun onError() } interface Presenter : KContracts.Presenter { } }

Slide 12

Slide 12 text

class ClubPresenter( private val view: ClubContracts.View ) : ClubContracts.Presenter, KPresenter() { private fun load() { // ... view.onClubsReceived(it) } init { load() } }

Slide 13

Slide 13 text

class ClubFragment : KFragment(), ClubContracts.View { override fun layout() = R.layout.fragment_clubs override fun inject(appComponent: AppComponent) { AndroidSupportInjection.inject(this) } override fun onClubsReceived(clubs: List) { // ... } override fun onError() { // ... } companion object { val TAG = "ClubFragment" fun newInstance(): Fragment { return ClubFragment() } } }

Slide 14

Slide 14 text

abstract class KFragment : 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) }

Slide 15

Slide 15 text

WHAT IS MVP AND CLEAN CODE? CLEAN CODE

Slide 16

Slide 16 text

WHAT IS MVP AND CLEAN CODE? CLEAN CODE

Slide 17

Slide 17 text

MODELS

Slide 18

Slide 18 text

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 )

Slide 19

Slide 19 text

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 )

Slide 20

Slide 20 text

class ClubModelUiMapper : Mapper { 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() ) }

Slide 21

Slide 21 text

USECASE

Slide 22

Slide 22 text

class GetClubUseCase(private val repository: ClubRepository) { fun execute(): Single> { return repository.clubs() .toObservable() .flatMapIterable { it } .filter { it.id != 252 } .toList() } }

Slide 23

Slide 23 text

class ClubRepositoryImpl( private val databaseSource: ClubDatabaseSource, private val serviceSource: ClubServiceSource ) : ClubRepository { override fun clubs(): Single> { return databaseSource.clubs() .toObservable() .filter { !it.isEmpty() } .switchIfEmpty(serviceSource.clubs() .doOnSuccess { databaseSource.saveClubs(it) } .toObservable()) .singleOrError() } }

Slide 24

Slide 24 text

class ClubPresenter( private val view: ClubContracts.View, private val getClubUseCase: GetClubUseCase, private val clubMapper: Mapper, 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() } }

Slide 25

Slide 25 text

TESTS

Slide 26

Slide 26 text

TESTS LIBRAIRIES USED ▸ JUnit ▸ AssertJ + AssertK ▸ Mockito + Mockito Kotlin ▸ Android Support Test

Slide 27

Slide 27 text

class ClubUseCaseTest { private val repository: ClubRepository = mock() private lateinit var clubUseCase: ClubUseCase @Before fun setUp() { clubUseCase = ClubUseCase(repository) } // tests... }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

class ClubPresenterTest { private val view: ClubContracts.View = mock() private val repository: ClubRepository = mock() private val clubMapper: Mapper = mock() private val schedulers: KSchedulers = SchedulersTest() private lateinit var clubUseCase: ClubUseCase @Before fun setUp() { clubUseCase = ClubUseCase(repository) } // tests... }

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

DEMO

Slide 32

Slide 32 text

itcommunities.slack.com

Slide 33

Slide 33 text

android-dev-france.slack.com

Slide 34

Slide 34 text

ANDROID ARCHITECTURE WITH MVP AND CLEAN CODE twitter.com/ github.com/ GerardPaligot