From 09be56129163fd02d2c272dae36aad8569bd856a Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Tue, 2 Jan 2018 12:09:26 +0000 Subject: [PATCH 01/42] Setup data module --- Data/.gitignore | 1 + Data/build.gradle | 21 +++++++++++++++++++++ dependencies.gradle | 25 +++++++++++++++++++++++-- settings.gradle | 2 +- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 Data/.gitignore create mode 100644 Data/build.gradle diff --git a/Data/.gitignore b/Data/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/Data/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Data/build.gradle b/Data/build.gradle new file mode 100644 index 0000000..5e9b65c --- /dev/null +++ b/Data/build.gradle @@ -0,0 +1,21 @@ +apply plugin: 'kotlin' + +dependencies { + def dataDependencies = rootProject.ext.dataDependencies + def dataTestDependencies = rootProject.ext.dataTestDependencies + + compile project(':Domain') + + implementation dataDependencies.javaxAnnotation + implementation dataDependencies.kotlin + implementation dataDependencies.javaxInject + implementation dataDependencies.rxKotlin + + testImplementation dataTestDependencies.junit + testImplementation dataTestDependencies.kotlinJUnit + testImplementation dataTestDependencies.mockito + testImplementation dataTestDependencies.assertj +} + +sourceCompatibility = "1.6" +targetCompatibility = "1.6" diff --git a/dependencies.gradle b/dependencies.gradle index bf3ff15..501dd58 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -5,18 +5,24 @@ ext { androidMinSdkVersion = 21 androidTargetSdkVersion = 26 androidCompileSdkVersion = 26 - kotlinVersion = '1.1.3-2' + kotlinVersion = '1.1.4-3' //Libraries - supportLibraryVersion = '26.0.1' + supportLibraryVersion = '26.1.0' rxJavaVersion = '2.0.2' javaxAnnotationVersion = '1.0' javaxInjectVersion = '1' + rxJavaVersion = '2.0.2' + rxKotlinVersion = '2.1.0' + androidAnnotationsVersion = '21.0.3' + daggerVersion = '2.11' //Testing jUnitVersion = '4.12' assertJVersion = '3.8.0' mockitoKotlinVersion = '1.5.0' + robolectricVersion = '3.4.2' + mockitoVersion = '1.9.5' domainDependencies = [ javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", @@ -30,4 +36,19 @@ ext { assertj: "org.assertj:assertj-core:${assertJVersion}" ] + dataDependencies = [ + rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", + kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", + javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", + javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", + ] + + dataTestDependencies = [ + junit: "junit:junit:${jUnitVersion}", + kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion}", + assertj: "org.assertj:assertj-core:${assertJVersion}", + mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", + robolectric: "org.robolectric:robolectric:${robolectricVersion}" + ] + } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 063b1b0..859bf76 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':Domain' +include ':Domain', ':Data' From 7495a5cf6b413519eb609924bb7a1bb4af1d4fef Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Tue, 2 Jan 2018 12:20:01 +0000 Subject: [PATCH 02/42] Create data models --- .../co/joebirch/data/mapper/EntityMapper.kt | 9 +++++++++ .../co/joebirch/data/mapper/ProjectMapper.kt | 19 +++++++++++++++++++ .../co/joebirch/data/model/ProjectEntity.kt | 5 +++++ 3 files changed, 33 insertions(+) create mode 100644 Data/src/main/java/co/joebirch/data/mapper/EntityMapper.kt create mode 100644 Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt create mode 100644 Data/src/main/java/co/joebirch/data/model/ProjectEntity.kt diff --git a/Data/src/main/java/co/joebirch/data/mapper/EntityMapper.kt b/Data/src/main/java/co/joebirch/data/mapper/EntityMapper.kt new file mode 100644 index 0000000..6af5a38 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/mapper/EntityMapper.kt @@ -0,0 +1,9 @@ +package co.joebirch.data.mapper + +interface EntityMapper { + + fun mapFromEntity(entity: E): D + + fun mapToEntity(domain: D): E + +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt b/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt new file mode 100644 index 0000000..899d8f2 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt @@ -0,0 +1,19 @@ +package co.joebirch.data.mapper + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.domain.model.Project +import javax.inject.Inject + +class ProjectMapper @Inject constructor(): EntityMapper { + + override fun mapFromEntity(entity: ProjectEntity): Project { + return Project(entity.id, entity.name, entity.fullName, entity.starCount, + entity.dateCreated, entity.ownerName, entity.ownerAvatar) + } + + override fun mapToEntity(domain: Project): ProjectEntity { + return ProjectEntity(domain.id, domain.name, domain.fullName, domain.starCount, + domain.dateCreated, domain.ownerName, domain.ownerAvatar) + } + +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/model/ProjectEntity.kt b/Data/src/main/java/co/joebirch/data/model/ProjectEntity.kt new file mode 100644 index 0000000..784fd72 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/model/ProjectEntity.kt @@ -0,0 +1,5 @@ +package co.joebirch.data.model + +class ProjectEntity(val id: String, val name: String, val fullName: String, + val starCount: String, val dateCreated: String, + val ownerName: String, val ownerAvatar: String) \ No newline at end of file From 5ffe69d5674d977e85d8dea5eb9122b85397893e Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Tue, 2 Jan 2018 13:41:12 +0000 Subject: [PATCH 03/42] Fix compilation issues --- .../joebirch/data/ProjectsDataRepository.kt | 27 +++++++++++++++++ .../data/repository/ProjectsDataStore.kt | 19 ++++++++++++ .../data/store/ProjectsCacheDataStore.kt | 29 +++++++++++++++++++ .../data/store/ProjectsDataStoreFactory.kt | 18 ++++++++++++ .../data/store/ProjectsRemoteDataStore.kt | 29 +++++++++++++++++++ .../domain/repository/ProjectsRepository.kt | 6 ++-- 6 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt create mode 100644 Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt create mode 100644 Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt create mode 100644 Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt create mode 100644 Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt diff --git a/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt b/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt new file mode 100644 index 0000000..a5722e0 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt @@ -0,0 +1,27 @@ +package co.joebirch.data + +import co.joebirch.data.mapper.ProjectMapper +import co.joebirch.data.store.ProjectsDataStoreFactory +import co.joebirch.domain.model.Project +import co.joebirch.domain.repository.ProjectsRepository +import io.reactivex.Completable +import io.reactivex.Observable +import javax.inject.Inject + +class ProjectsDataRepository @Inject constructor( + private val projectMapper: ProjectMapper, + private val factory: ProjectsDataStoreFactory) + : ProjectsRepository { + + override fun getProjects(): Observable> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun bookmarkProject(projectId: String): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getBookmarkedProjects(): Observable> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt b/Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt new file mode 100644 index 0000000..78f5c2f --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt @@ -0,0 +1,19 @@ +package co.joebirch.data.repository + +import co.joebirch.data.model.ProjectEntity +import io.reactivex.Completable +import io.reactivex.Observable + +interface ProjectsDataStore { + + fun getProjects(): Observable> + + fun clearProjects(): Completable + + fun getBookmarkedProjects(): Observable> + + fun setProjectBookmarked(projectId: String): Completable + + fun setProjectNotBookmarked(projectId: String): Completable + +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt new file mode 100644 index 0000000..373e792 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt @@ -0,0 +1,29 @@ +package co.joebirch.data.store + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsDataStore +import io.reactivex.Completable +import io.reactivex.Observable + +class ProjectsCacheDataStore : ProjectsDataStore { + + override fun getProjects(): Observable> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun clearProjects(): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getBookmarkedProjects(): Observable> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun setProjectBookmarked(projectId: String): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun setProjectNotBookmarked(projectId: String): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt new file mode 100644 index 0000000..518fd64 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt @@ -0,0 +1,18 @@ +package co.joebirch.data.store + +import co.joebirch.data.repository.ProjectsDataStore +import javax.inject.Inject + +class ProjectsDataStoreFactory @Inject constructor( + private val projectsCacheDataStore: ProjectsCacheDataStore, + private val projectsRemoteDataStore: ProjectsRemoteDataStore) { + + fun getCacheDataStore(): ProjectsDataStore { + return projectsCacheDataStore + } + + fun getRemoteDataStore(): ProjectsDataStore { + return projectsRemoteDataStore + } + +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt new file mode 100644 index 0000000..df2bae9 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt @@ -0,0 +1,29 @@ +package co.joebirch.data.store + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsDataStore +import io.reactivex.Completable +import io.reactivex.Observable + +class ProjectsRemoteDataStore : ProjectsDataStore { + + override fun getProjects(): Observable> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun clearProjects(): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getBookmarkedProjects(): Observable> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun setProjectBookmarked(projectId: String): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun setProjectNotBookmarked(projectId: String): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt b/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt index 98ef373..378968a 100644 --- a/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt +++ b/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt @@ -2,14 +2,14 @@ package co.joebirch.domain.repository import co.joebirch.domain.model.Project import io.reactivex.Completable -import io.reactivex.Single +import io.reactivex.Observable interface ProjectsRepository { - fun getProjects(): Single> + fun getProjects(): Observable> fun bookmarkProject(projectId: String): Completable - fun getBookmarkedProjects(): Single> + fun getBookmarkedProjects(): Observable> } \ No newline at end of file From c21289812f6e59924079402589b39140007b4cb0 Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Sun, 7 Jan 2018 13:54:07 +0000 Subject: [PATCH 04/42] Create data layer interfaces and data stores --- .../joebirch/data/ProjectsDataRepository.kt | 27 ----------------- .../joebirch/data/repository/ProjectsCache.kt | 27 +++++++++++++++++ .../data/repository/ProjectsDataStore.kt | 19 ------------ .../data/repository/ProjectsRemote.kt | 10 +++++++ .../data/store/ProjectsCacheDataStore.kt | 29 ------------------- .../data/store/ProjectsDataStoreFactory.kt | 18 ------------ .../data/store/ProjectsRemoteDataStore.kt | 29 ------------------- 7 files changed, 37 insertions(+), 122 deletions(-) delete mode 100644 Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt create mode 100644 Data/src/main/java/co/joebirch/data/repository/ProjectsCache.kt delete mode 100644 Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt create mode 100644 Data/src/main/java/co/joebirch/data/repository/ProjectsRemote.kt delete mode 100644 Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt delete mode 100644 Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt delete mode 100644 Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt diff --git a/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt b/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt deleted file mode 100644 index a5722e0..0000000 --- a/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt +++ /dev/null @@ -1,27 +0,0 @@ -package co.joebirch.data - -import co.joebirch.data.mapper.ProjectMapper -import co.joebirch.data.store.ProjectsDataStoreFactory -import co.joebirch.domain.model.Project -import co.joebirch.domain.repository.ProjectsRepository -import io.reactivex.Completable -import io.reactivex.Observable -import javax.inject.Inject - -class ProjectsDataRepository @Inject constructor( - private val projectMapper: ProjectMapper, - private val factory: ProjectsDataStoreFactory) - : ProjectsRepository { - - override fun getProjects(): Observable> { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun bookmarkProject(projectId: String): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun getBookmarkedProjects(): Observable> { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } -} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/repository/ProjectsCache.kt b/Data/src/main/java/co/joebirch/data/repository/ProjectsCache.kt new file mode 100644 index 0000000..c267c9a --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/repository/ProjectsCache.kt @@ -0,0 +1,27 @@ +package co.joebirch.data.repository + +import co.joebirch.data.model.ProjectEntity +import io.reactivex.Completable +import io.reactivex.Observable + +interface ProjectsCache { + + fun clearProjects(): Completable + + fun saveProjects(projects: List): Completable + + fun getProjects(): Observable> + + fun getBookmarkedProjects(): Observable> + + fun setProjectAsBookmarked(projectId: String): Completable + + fun setProjectAsNotBookmarked(projectId: String): Completable + + fun areProjectsCached(): Boolean + + fun setLastCacheTime(lastCache: Long): Completable + + fun isProjectsCacheExpired(): Boolean + +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt b/Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt deleted file mode 100644 index 78f5c2f..0000000 --- a/Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt +++ /dev/null @@ -1,19 +0,0 @@ -package co.joebirch.data.repository - -import co.joebirch.data.model.ProjectEntity -import io.reactivex.Completable -import io.reactivex.Observable - -interface ProjectsDataStore { - - fun getProjects(): Observable> - - fun clearProjects(): Completable - - fun getBookmarkedProjects(): Observable> - - fun setProjectBookmarked(projectId: String): Completable - - fun setProjectNotBookmarked(projectId: String): Completable - -} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/repository/ProjectsRemote.kt b/Data/src/main/java/co/joebirch/data/repository/ProjectsRemote.kt new file mode 100644 index 0000000..b708c16 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/repository/ProjectsRemote.kt @@ -0,0 +1,10 @@ +package co.joebirch.data.repository + +import co.joebirch.data.model.ProjectEntity +import io.reactivex.Observable + +interface ProjectsRemote { + + fun getProjects(): Observable> + +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt deleted file mode 100644 index 373e792..0000000 --- a/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt +++ /dev/null @@ -1,29 +0,0 @@ -package co.joebirch.data.store - -import co.joebirch.data.model.ProjectEntity -import co.joebirch.data.repository.ProjectsDataStore -import io.reactivex.Completable -import io.reactivex.Observable - -class ProjectsCacheDataStore : ProjectsDataStore { - - override fun getProjects(): Observable> { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun clearProjects(): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun getBookmarkedProjects(): Observable> { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun setProjectBookmarked(projectId: String): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun setProjectNotBookmarked(projectId: String): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } -} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt deleted file mode 100644 index 518fd64..0000000 --- a/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt +++ /dev/null @@ -1,18 +0,0 @@ -package co.joebirch.data.store - -import co.joebirch.data.repository.ProjectsDataStore -import javax.inject.Inject - -class ProjectsDataStoreFactory @Inject constructor( - private val projectsCacheDataStore: ProjectsCacheDataStore, - private val projectsRemoteDataStore: ProjectsRemoteDataStore) { - - fun getCacheDataStore(): ProjectsDataStore { - return projectsCacheDataStore - } - - fun getRemoteDataStore(): ProjectsDataStore { - return projectsRemoteDataStore - } - -} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt deleted file mode 100644 index df2bae9..0000000 --- a/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt +++ /dev/null @@ -1,29 +0,0 @@ -package co.joebirch.data.store - -import co.joebirch.data.model.ProjectEntity -import co.joebirch.data.repository.ProjectsDataStore -import io.reactivex.Completable -import io.reactivex.Observable - -class ProjectsRemoteDataStore : ProjectsDataStore { - - override fun getProjects(): Observable> { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun clearProjects(): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun getBookmarkedProjects(): Observable> { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun setProjectBookmarked(projectId: String): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun setProjectNotBookmarked(projectId: String): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } -} \ No newline at end of file From 008c3bdd35e41576f7417d29a12e80aa9400b67a Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Sun, 7 Jan 2018 13:54:15 +0000 Subject: [PATCH 05/42] Implement data stores --- .../data/repository/ProjectsDataStore.kt | 21 ++++++++++ .../data/store/ProjectsCacheDataStore.kt | 39 +++++++++++++++++++ .../data/store/ProjectsDataStoreFactory.kt | 29 ++++++++++++++ .../data/store/ProjectsRemoteDataStore.kt | 38 ++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt create mode 100644 Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt create mode 100644 Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt create mode 100644 Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt diff --git a/Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt b/Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt new file mode 100644 index 0000000..967daaf --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/repository/ProjectsDataStore.kt @@ -0,0 +1,21 @@ +package co.joebirch.data.repository + +import co.joebirch.data.model.ProjectEntity +import io.reactivex.Completable +import io.reactivex.Observable + +interface ProjectsDataStore { + + fun getProjects(): Observable> + + fun saveProjects(projects: List): Completable + + fun clearProjects(): Completable + + fun getBookmarkedProjects(): Observable> + + fun setProjectAsBookmarked(projectId: String): Completable + + fun setProjectAsNotBookmarked(projectId: String): Completable + +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt new file mode 100644 index 0000000..7a320ef --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt @@ -0,0 +1,39 @@ +package co.joebirch.data.store + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsCache +import co.joebirch.data.repository.ProjectsDataStore +import io.reactivex.Completable +import io.reactivex.Observable +import javax.inject.Inject + +class ProjectsCacheDataStore @Inject constructor( + private val projectsCache: ProjectsCache) + : ProjectsDataStore { + + override fun getProjects(): Observable> { + return projectsCache.getProjects() + } + + override fun saveProjects(projects: List): Completable { + return projectsCache.saveProjects(projects) + .andThen(projectsCache.setLastCacheTime(System.currentTimeMillis())) + } + + override fun clearProjects(): Completable { + return projectsCache.clearProjects() + } + + override fun getBookmarkedProjects(): Observable> { + return projectsCache.getBookmarkedProjects() + } + + override fun setProjectAsBookmarked(projectId: String): Completable { + return projectsCache.setProjectAsBookmarked(projectId) + } + + override fun setProjectAsNotBookmarked(projectId: String): Completable { + return projectsCache.setProjectAsNotBookmarked(projectId) + } + +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt new file mode 100644 index 0000000..2d6b9d9 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt @@ -0,0 +1,29 @@ +package co.joebirch.data.store + +import co.joebirch.data.repository.ProjectsCache +import co.joebirch.data.repository.ProjectsDataStore +import javax.inject.Inject + +class ProjectsDataStoreFactory @Inject constructor( + private val projectsCache: ProjectsCache, + private val projectsCacheDataStore: ProjectsCacheDataStore, + private val projectsRemoteDataStore: ProjectsRemoteDataStore) { + + fun getDataStore(): ProjectsDataStore { + return if (projectsCache.areProjectsCached() && + !projectsCache.isProjectsCacheExpired()) { + projectsCacheDataStore + } else { + projectsRemoteDataStore + } + } + + fun getCacheDataStore(): ProjectsDataStore { + return projectsCacheDataStore + } + + fun getRemoteDataStore(): ProjectsDataStore { + return projectsRemoteDataStore + } + +} \ No newline at end of file diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt new file mode 100644 index 0000000..5cc970c --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt @@ -0,0 +1,38 @@ +package co.joebirch.data.store + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsDataStore +import co.joebirch.data.repository.ProjectsRemote +import io.reactivex.Completable +import io.reactivex.Observable +import javax.inject.Inject + +class ProjectsRemoteDataStore @Inject constructor( + private val projectsRemote: ProjectsRemote) + : ProjectsDataStore { + + override fun getProjects(): Observable> { + return projectsRemote.getProjects() + } + + override fun saveProjects(projects: List): Completable { + throw UnsupportedOperationException("Saving projects isn't supported here...") + } + + override fun clearProjects(): Completable { + throw UnsupportedOperationException("Clearing projects isn't supported here...") + } + + override fun getBookmarkedProjects(): Observable> { + throw UnsupportedOperationException("Getting bookmarked projects isn't supported here...") + } + + override fun setProjectAsBookmarked(projectId: String): Completable { + throw UnsupportedOperationException("Setting bookmarks isn't supported here...") + } + + override fun setProjectAsNotBookmarked(projectId: String): Completable { + throw UnsupportedOperationException("Setting bookmarks isn't supported here...") + } + +} \ No newline at end of file From 9a1cb8b2e82d4896dd1c09742f881e853c85f2b6 Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Sun, 28 Jan 2018 11:56:18 +0000 Subject: [PATCH 06/42] Implement project use cases --- Domain/build.gradle | 4 +- ...{SingleUseCase.kt => ObservableUseCase.kt} | 12 ++-- .../{browse => bookmark}/BookmarkProject.kt | 10 ++-- .../GetBookmarkedProjects.kt | 13 ++-- .../interactor/bookmark/UnbookmarkProject.kt | 27 +++++++++ .../domain/interactor/browse/GetProjects.kt | 17 ------ .../java/co/joebirch/domain/model/Project.kt | 5 -- .../domain/repository/ProjectsRepository.kt | 15 ----- .../bookmarked/GetBookmarkedProjectsTest.kt | 13 ++-- .../interactor}/browse/GetProjectsTest.kt | 23 +++----- .../test/ProjectDataFactory.kt | 4 +- .../browse/BookmarkProjectTest.kt | 59 ------------------- app/build.gradle | 2 +- .../githubtrending/ExampleInstrumentedTest.kt | 2 +- build.gradle | 2 +- dependencies.gradle | 2 +- 16 files changed, 65 insertions(+), 145 deletions(-) rename Domain/src/main/java/co/joebirch/domain/interactor/{SingleUseCase.kt => ObservableUseCase.kt} (65%) rename Domain/src/main/java/co/joebirch/domain/interactor/{browse => bookmark}/BookmarkProject.kt (68%) rename Domain/src/main/java/co/joebirch/domain/interactor/{bookmarked => bookmark}/GetBookmarkedProjects.kt (50%) create mode 100644 Domain/src/main/java/co/joebirch/domain/interactor/bookmark/UnbookmarkProject.kt delete mode 100644 Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt delete mode 100644 Domain/src/main/java/co/joebirch/domain/model/Project.kt delete mode 100644 Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt rename Domain/src/test/java/co/joebirch/{githubtrending => domain/interactor}/bookmarked/GetBookmarkedProjectsTest.kt (80%) rename Domain/src/test/java/co/joebirch/{githubtrending => domain/interactor}/browse/GetProjectsTest.kt (69%) rename Domain/src/test/java/co/joebirch/{githubtrending => domain}/test/ProjectDataFactory.kt (86%) delete mode 100644 Domain/src/test/java/co/joebirch/githubtrending/browse/BookmarkProjectTest.kt diff --git a/Domain/build.gradle b/Domain/build.gradle index ad01f2a..dc1e8db 100644 --- a/Domain/build.gradle +++ b/Domain/build.gradle @@ -1,14 +1,12 @@ apply plugin: 'kotlin' dependencies { + testImplementation domainTestDependencies.junit def domainDependencies = rootProject.ext.domainDependencies def domainTestDependencies = rootProject.ext.domainTestDependencies - implementation domainDependencies.javaxAnnotation implementation domainDependencies.javaxInject implementation domainDependencies.rxJava - - testImplementation domainTestDependencies.junit testImplementation domainTestDependencies.mockito testImplementation domainTestDependencies.assertj } diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/SingleUseCase.kt b/Domain/src/main/java/co/joebirch/domain/interactor/ObservableUseCase.kt similarity index 65% rename from Domain/src/main/java/co/joebirch/domain/interactor/SingleUseCase.kt rename to Domain/src/main/java/co/joebirch/domain/interactor/ObservableUseCase.kt index 4c9a205..0ffbbd2 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/SingleUseCase.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/ObservableUseCase.kt @@ -1,21 +1,21 @@ package co.joebirch.domain.interactor import co.joebirch.domain.executor.PostExecutionThread -import io.reactivex.Single +import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable -import io.reactivex.observers.DisposableSingleObserver +import io.reactivex.observers.DisposableObserver import io.reactivex.schedulers.Schedulers -abstract class SingleUseCase constructor( +abstract class ObservableUseCase constructor( private val postExecutionThread: PostExecutionThread) { private val disposables = CompositeDisposable() - protected abstract fun buildUseCaseSingle(params: Params? = null): Single + protected abstract fun buildUseCaseObservable(params: Params? = null): Observable - open fun execute(singleObserver: DisposableSingleObserver, params: Params? = null) { - val single = this.buildUseCaseSingle(params) + open fun execute(singleObserver: DisposableObserver, params: Params? = null) { + val single = this.buildUseCaseObservable(params) .subscribeOn(Schedulers.io()) .observeOn(postExecutionThread.scheduler) addDisposable(single.subscribeWith(singleObserver)) diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt b/Domain/src/main/java/co/joebirch/domain/interactor/bookmark/BookmarkProject.kt similarity index 68% rename from Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt rename to Domain/src/main/java/co/joebirch/domain/interactor/bookmark/BookmarkProject.kt index 8491b92..db845ba 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/bookmark/BookmarkProject.kt @@ -1,4 +1,4 @@ -package co.joebirch.domain.interactor.browse +package co.joebirch.domain.interactor.bookmark import co.joebirch.domain.executor.PostExecutionThread import co.joebirch.domain.interactor.CompletableUseCase @@ -6,11 +6,12 @@ import co.joebirch.domain.repository.ProjectsRepository import io.reactivex.Completable import javax.inject.Inject -class BookmarkProject @Inject constructor(private val projectsRepository: ProjectsRepository, - postExecutionThread: PostExecutionThread) +class BookmarkProject @Inject constructor( + private val projectsRepository: ProjectsRepository, + postExecutionThread: PostExecutionThread) : CompletableUseCase(postExecutionThread) { - public override fun buildUseCaseCompletable(params: Params?): Completable { + override fun buildUseCaseCompletable(params: Params?): Completable { if (params == null) throw IllegalArgumentException("Params can't be null!") return projectsRepository.bookmarkProject(params.projectId) } @@ -22,5 +23,4 @@ class BookmarkProject @Inject constructor(private val projectsRepository: Projec } } } - } \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt b/Domain/src/main/java/co/joebirch/domain/interactor/bookmark/GetBookmarkedProjects.kt similarity index 50% rename from Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt rename to Domain/src/main/java/co/joebirch/domain/interactor/bookmark/GetBookmarkedProjects.kt index 2de7bf6..752a6f2 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/bookmark/GetBookmarkedProjects.kt @@ -1,18 +1,19 @@ -package co.joebirch.domain.interactor.bookmarked +package co.joebirch.domain.interactor.bookmark import co.joebirch.domain.executor.PostExecutionThread -import co.joebirch.domain.interactor.SingleUseCase +import co.joebirch.domain.interactor.ObservableUseCase import co.joebirch.domain.model.Project import co.joebirch.domain.repository.ProjectsRepository -import io.reactivex.Single +import io.reactivex.Observable import javax.inject.Inject -open class GetBookmarkedProjects @Inject constructor( +class GetBookmarkedProjects @Inject constructor( private val projectsRepository: ProjectsRepository, postExecutionThread: PostExecutionThread) - : SingleUseCase, Nothing>(postExecutionThread) { + : ObservableUseCase, Nothing>(postExecutionThread) { - public override fun buildUseCaseSingle(params: Nothing?): Single> { + public override fun buildUseCaseObservable(params: Nothing?): Observable> { return projectsRepository.getBookmarkedProjects() } + } \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/bookmark/UnbookmarkProject.kt b/Domain/src/main/java/co/joebirch/domain/interactor/bookmark/UnbookmarkProject.kt new file mode 100644 index 0000000..b555c8a --- /dev/null +++ b/Domain/src/main/java/co/joebirch/domain/interactor/bookmark/UnbookmarkProject.kt @@ -0,0 +1,27 @@ +package co.joebirch.domain.interactor.bookmark + +import co.joebirch.domain.executor.PostExecutionThread +import co.joebirch.domain.interactor.CompletableUseCase +import co.joebirch.domain.repository.ProjectsRepository +import io.reactivex.Completable +import javax.inject.Inject + +class UnbookmarkProject @Inject constructor( + private val projectsRepository: ProjectsRepository, + postExecutionThread: PostExecutionThread) + : CompletableUseCase(postExecutionThread){ + + override fun buildUseCaseCompletable(params: Params?): Completable { + if (params == null) throw IllegalArgumentException("Params can't be null!") + return projectsRepository.unbookmarkProject(params.projectId) + } + + data class Params constructor(val projectId: String) { + companion object { + fun forProject(projectId: String): Params { + return Params(projectId) + } + } + } + +} \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt b/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt deleted file mode 100644 index d9fa62f..0000000 --- a/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt +++ /dev/null @@ -1,17 +0,0 @@ -package co.joebirch.domain.interactor.browse - -import co.joebirch.domain.executor.PostExecutionThread -import co.joebirch.domain.interactor.SingleUseCase -import co.joebirch.domain.model.Project -import co.joebirch.domain.repository.ProjectsRepository -import io.reactivex.Single -import javax.inject.Inject - -class GetProjects @Inject constructor(private val projectsRepository: ProjectsRepository, - postExecutionThread: PostExecutionThread) - : SingleUseCase, Nothing>(postExecutionThread) { - - public override fun buildUseCaseSingle(params: Nothing?): Single> { - return projectsRepository.getProjects() - } -} \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/model/Project.kt b/Domain/src/main/java/co/joebirch/domain/model/Project.kt deleted file mode 100644 index 5ba47ee..0000000 --- a/Domain/src/main/java/co/joebirch/domain/model/Project.kt +++ /dev/null @@ -1,5 +0,0 @@ -package co.joebirch.domain.model - -class Project(val id: String, val name: String, val fullName: String, - val starCount: String, val dateCreated: String, - val ownerName: String, val ownerAvatar: String) \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt b/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt deleted file mode 100644 index 98ef373..0000000 --- a/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt +++ /dev/null @@ -1,15 +0,0 @@ -package co.joebirch.domain.repository - -import co.joebirch.domain.model.Project -import io.reactivex.Completable -import io.reactivex.Single - -interface ProjectsRepository { - - fun getProjects(): Single> - - fun bookmarkProject(projectId: String): Completable - - fun getBookmarkedProjects(): Single> - -} \ No newline at end of file diff --git a/Domain/src/test/java/co/joebirch/githubtrending/bookmarked/GetBookmarkedProjectsTest.kt b/Domain/src/test/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjectsTest.kt similarity index 80% rename from Domain/src/test/java/co/joebirch/githubtrending/bookmarked/GetBookmarkedProjectsTest.kt rename to Domain/src/test/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjectsTest.kt index ef28f25..aefb087 100644 --- a/Domain/src/test/java/co/joebirch/githubtrending/bookmarked/GetBookmarkedProjectsTest.kt +++ b/Domain/src/test/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjectsTest.kt @@ -1,12 +1,13 @@ -package co.joebirch.githubtrending.bookmarked +package co.joebirch.domain.interactor.bookmarked import co.joebirch.domain.executor.PostExecutionThread -import co.joebirch.domain.interactor.bookmarked.GetBookmarkedProjects +import co.joebirch.domain.interactor.bookmark.GetBookmarkedProjects import co.joebirch.domain.model.Project import co.joebirch.domain.repository.ProjectsRepository -import co.joebirch.githubtrending.test.ProjectDataFactory +import co.joebirch.domain.test.ProjectDataFactory import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever +import io.reactivex.Observable import io.reactivex.Single import org.junit.Before import org.junit.Test @@ -33,7 +34,7 @@ class GetBookmarkedProjectsTest { stubProjectsRepositoryGetBookmarkedProjects( Single.just(ProjectDataFactory.makeProjectList(2))) - val testObserver = getBookmarkedProjects.buildUseCaseSingle().test() + val testObserver = getBookmarkedProjects.buildUseCaseObservable().test() testObserver.assertComplete() } @@ -42,11 +43,11 @@ class GetBookmarkedProjectsTest { stubProjectsRepositoryGetBookmarkedProjects( Single.just(ProjectDataFactory.makeProjectList(2))) - getBookmarkedProjects.buildUseCaseSingle().test() + getBookmarkedProjects.buildUseCaseObservable().test() verify(projectsRepository).getBookmarkedProjects() } - private fun stubProjectsRepositoryGetBookmarkedProjects(single: Single>) { + private fun stubProjectsRepositoryGetBookmarkedProjects(single: Observable>) { whenever(projectsRepository.getBookmarkedProjects()) .thenReturn(single) } diff --git a/Domain/src/test/java/co/joebirch/githubtrending/browse/GetProjectsTest.kt b/Domain/src/test/java/co/joebirch/domain/interactor/browse/GetProjectsTest.kt similarity index 69% rename from Domain/src/test/java/co/joebirch/githubtrending/browse/GetProjectsTest.kt rename to Domain/src/test/java/co/joebirch/domain/interactor/browse/GetProjectsTest.kt index 7af8046..ac36b4b 100644 --- a/Domain/src/test/java/co/joebirch/githubtrending/browse/GetProjectsTest.kt +++ b/Domain/src/test/java/co/joebirch/domain/interactor/browse/GetProjectsTest.kt @@ -1,21 +1,16 @@ -package co.joebirch.githubtrending.browse +package co.joebirch.domain.interactor.browse import co.joebirch.domain.executor.PostExecutionThread -import co.joebirch.domain.interactor.browse.GetProjects -import co.joebirch.domain.model.Project import co.joebirch.domain.repository.ProjectsRepository -import co.joebirch.githubtrending.test.ProjectDataFactory +import co.joebirch.domain.test.ProjectDataFactory import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever import io.reactivex.Single import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations -import org.mockito.junit.MockitoJUnitRunner -@RunWith(MockitoJUnitRunner::class) class GetProjectsTest { private lateinit var getProjects: GetProjects @@ -30,18 +25,16 @@ class GetProjectsTest { @Test fun getProjectsCompletes() { - stubProjectsRepositoryGetProjects( - Single.just(ProjectDataFactory.makeProjectList(2))) - + stubProjectsRepositoryGetProjects(Single.just( + ProjectDataFactory.makeProjectList(2))) val testObserver = getProjects.buildUseCaseSingle().test() testObserver.assertComplete() } @Test fun getProjectsCallsRepository() { - stubProjectsRepositoryGetProjects( - Single.just(ProjectDataFactory.makeProjectList(2))) - + stubProjectsRepositoryGetProjects(Single.just( + ProjectDataFactory.makeProjectList(2))) getProjects.buildUseCaseSingle().test() verify(projectsRepository).getProjects() } @@ -49,9 +42,7 @@ class GetProjectsTest { @Test fun getProjectsReturnsData() { val projects = ProjectDataFactory.makeProjectList(2) - stubProjectsRepositoryGetProjects( - Single.just(projects)) - + stubProjectsRepositoryGetProjects(Single.just(projects)) val testObserver = getProjects.buildUseCaseSingle().test() testObserver.assertValue(projects) } diff --git a/Domain/src/test/java/co/joebirch/githubtrending/test/ProjectDataFactory.kt b/Domain/src/test/java/co/joebirch/domain/test/ProjectDataFactory.kt similarity index 86% rename from Domain/src/test/java/co/joebirch/githubtrending/test/ProjectDataFactory.kt rename to Domain/src/test/java/co/joebirch/domain/test/ProjectDataFactory.kt index 309603d..66bc3af 100644 --- a/Domain/src/test/java/co/joebirch/githubtrending/test/ProjectDataFactory.kt +++ b/Domain/src/test/java/co/joebirch/domain/test/ProjectDataFactory.kt @@ -1,6 +1,4 @@ -package co.joebirch.githubtrending.test - -import co.joebirch.domain.model.Project +package co.joebirch.domain.test object ProjectDataFactory { diff --git a/Domain/src/test/java/co/joebirch/githubtrending/browse/BookmarkProjectTest.kt b/Domain/src/test/java/co/joebirch/githubtrending/browse/BookmarkProjectTest.kt deleted file mode 100644 index d2ed829..0000000 --- a/Domain/src/test/java/co/joebirch/githubtrending/browse/BookmarkProjectTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package co.joebirch.githubtrending.browse - -import co.joebirch.domain.executor.PostExecutionThread -import co.joebirch.domain.interactor.browse.BookmarkProject -import co.joebirch.domain.repository.ProjectsRepository -import co.joebirch.githubtrending.test.ProjectDataFactory -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.whenever -import io.reactivex.Completable -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.mockito.junit.MockitoJUnitRunner - -@RunWith(MockitoJUnitRunner::class) -class BookmarkProjectTest { - - private lateinit var bookmarkProject: BookmarkProject - @Mock lateinit var projectsRepository: ProjectsRepository - @Mock lateinit var postExecutionThread: PostExecutionThread - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - bookmarkProject = BookmarkProject(projectsRepository, postExecutionThread) - } - - @Test - fun bookmarkProjectCompletes() { - stubProjectsRepositoryBookmarkProject(Completable.complete()) - - val testObserver = bookmarkProject.buildUseCaseCompletable( - BookmarkProject.Params(ProjectDataFactory.randomUuid())).test() - testObserver.assertComplete() - } - - @Test(expected = IllegalArgumentException::class) - fun bookmarkProjectThrowsException() { - bookmarkProject.buildUseCaseCompletable().test() - } - - @Test - fun bookmarkProjectRepository() { - val projectId = ProjectDataFactory.randomUuid() - stubProjectsRepositoryBookmarkProject(Completable.complete()) - - bookmarkProject.buildUseCaseCompletable(BookmarkProject.Params(projectId)).test() - verify(projectsRepository).bookmarkProject(projectId) - } - - private fun stubProjectsRepositoryBookmarkProject(completable: Completable) { - whenever(projectsRepository.bookmarkProject(any())) - .thenReturn(completable) - } - -} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 806e43b..1eceb3f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 26 defaultConfig { - applicationId "co.joebirch.githubtrending" + applicationId "co.joebirch.domain" minSdkVersion 21 targetSdkVersion 26 versionCode 1 diff --git a/app/src/androidTest/java/co/joebirch/githubtrending/ExampleInstrumentedTest.kt b/app/src/androidTest/java/co/joebirch/githubtrending/ExampleInstrumentedTest.kt index c68450e..facc7b9 100644 --- a/app/src/androidTest/java/co/joebirch/githubtrending/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/co/joebirch/githubtrending/ExampleInstrumentedTest.kt @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getTargetContext() - assertEquals("co.joebirch.githubtrending", appContext.packageName) + assertEquals("co.joebirch.domain", appContext.packageName) } } diff --git a/build.gradle b/build.gradle index a1e0904..07993c4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.1.51' + ext.kotlin_version = '1.2.10' repositories { google() jcenter() diff --git a/dependencies.gradle b/dependencies.gradle index bf3ff15..eac24c7 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -5,7 +5,7 @@ ext { androidMinSdkVersion = 21 androidTargetSdkVersion = 26 androidCompileSdkVersion = 26 - kotlinVersion = '1.1.3-2' + kotlinVersion = '1.2.10' //Libraries supportLibraryVersion = '26.0.1' From 9a6e6ea8322f05d9cf0828f8370aae5ebfaa43fa Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Sun, 28 Jan 2018 11:56:24 +0000 Subject: [PATCH 07/42] Add model classes --- .../domain/interactor/browse/GetProjects.kt | 19 +++++++++++++++++++ .../java/co/joebirch/domain/model/Project.kt | 5 +++++ .../domain/repository/ProjectsRepository.kt | 17 +++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt create mode 100644 Domain/src/main/java/co/joebirch/domain/model/Project.kt create mode 100644 Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt b/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt new file mode 100644 index 0000000..8baa96f --- /dev/null +++ b/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt @@ -0,0 +1,19 @@ +package co.joebirch.domain.interactor.browse + +import co.joebirch.domain.executor.PostExecutionThread +import co.joebirch.domain.interactor.ObservableUseCase +import co.joebirch.domain.model.Project +import co.joebirch.domain.repository.ProjectsRepository +import io.reactivex.Observable +import javax.inject.Inject + +class GetProjects @Inject constructor( + private val projectsRepository: ProjectsRepository, + postExecutionThread: PostExecutionThread) + : ObservableUseCase, Nothing>(postExecutionThread) { + + override fun buildUseCaseObservable(params: Nothing?): Observable> { + return projectsRepository.getProjects() + } + +} \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/model/Project.kt b/Domain/src/main/java/co/joebirch/domain/model/Project.kt new file mode 100644 index 0000000..5ba47ee --- /dev/null +++ b/Domain/src/main/java/co/joebirch/domain/model/Project.kt @@ -0,0 +1,5 @@ +package co.joebirch.domain.model + +class Project(val id: String, val name: String, val fullName: String, + val starCount: String, val dateCreated: String, + val ownerName: String, val ownerAvatar: String) \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt b/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt new file mode 100644 index 0000000..d901a5b --- /dev/null +++ b/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt @@ -0,0 +1,17 @@ +package co.joebirch.domain.repository + +import co.joebirch.domain.model.Project +import io.reactivex.Completable +import io.reactivex.Observable + +interface ProjectsRepository { + + fun getProjects(): Observable> + + fun bookmarkProject(projectId: String): Completable + + fun unbookmarkProject(projectId: String): Completable + + fun getBookmarkedProjects(): Observable> + +} \ No newline at end of file From 6192f656cfe7fca3e7c06a6fe4e80e3f42eab18c Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Sun, 11 Feb 2018 20:18:45 +0000 Subject: [PATCH 08/42] Add repository --- .../joebirch/data/ProjectsDataRepository.kt | 33 +++++++++++++++++++ .../interactor/browse/BookmarkProject.kt | 1 + .../domain/repository/ProjectsRepository.kt | 2 ++ 3 files changed, 36 insertions(+) create mode 100644 Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt diff --git a/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt b/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt new file mode 100644 index 0000000..18fffca --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt @@ -0,0 +1,33 @@ +package co.joebirch.data + +import co.joebirch.data.mapper.ProjectMapper +import co.joebirch.data.store.ProjectsDataStoreFactory +import co.joebirch.domain.model.Project +import co.joebirch.domain.repository.ProjectsRepository +import io.reactivex.Completable +import io.reactivex.Observable +import javax.inject.Inject + +class ProjectsDataRepository @Inject constructor( + private val mapper: ProjectMapper, + private val factory: ProjectsDataStoreFactory) + : ProjectsRepository { + + override fun getProjects(): Observable> { + return factory.getDataStore().getProjects() + .map { it.map { mapper.mapFromEntity(it) } } + } + + override fun bookmarkProject(projectId: String): Completable { + return factory.getCacheDataStore().setProjectAsBookmarked(projectId) + } + + override fun unbookmarkProject(projectId: String): Completable { + return factory.getCacheDataStore().setProjectAsNotBookmarked(projectId) + } + + override fun getBookmarkedProjects(): Observable> { + return factory.getCacheDataStore().getBookmarkedProjects() + .map { it.map { mapper.mapFromEntity(it) } } + } +} \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt b/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt index 8491b92..73048d5 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt @@ -13,6 +13,7 @@ class BookmarkProject @Inject constructor(private val projectsRepository: Projec public override fun buildUseCaseCompletable(params: Params?): Completable { if (params == null) throw IllegalArgumentException("Params can't be null!") return projectsRepository.bookmarkProject(params.projectId) + return projectsRepository.unbookmarkProject(params.projectId) } data class Params constructor(val projectId: String) { diff --git a/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt b/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt index 378968a..d901a5b 100644 --- a/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt +++ b/Domain/src/main/java/co/joebirch/domain/repository/ProjectsRepository.kt @@ -10,6 +10,8 @@ interface ProjectsRepository { fun bookmarkProject(projectId: String): Completable + fun unbookmarkProject(projectId: String): Completable + fun getBookmarkedProjects(): Observable> } \ No newline at end of file From 03fb79f593ec5c86503e3e0db022bec54a076fcf Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Wed, 14 Feb 2018 12:08:11 +0000 Subject: [PATCH 09/42] Create remote layer --- Remote/.gitignore | 1 + Remote/build.gradle | 27 ++++++++++++ .../co/joebirch/remote/model/OwnerModel.kt | 6 +++ .../co/joebirch/remote/model/ProjectModel.kt | 7 +++ .../remote/model/ProjectsResponseModel.kt | 3 ++ .../remote/service/GithubTrendingService.kt | 15 +++++++ .../service/GithubTrendingServiceFactory.kt | 44 +++++++++++++++++++ dependencies.gradle | 24 ++++++++++ settings.gradle | 2 +- 9 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 Remote/.gitignore create mode 100644 Remote/build.gradle create mode 100644 Remote/src/main/java/co/joebirch/remote/model/OwnerModel.kt create mode 100644 Remote/src/main/java/co/joebirch/remote/model/ProjectModel.kt create mode 100644 Remote/src/main/java/co/joebirch/remote/model/ProjectsResponseModel.kt create mode 100644 Remote/src/main/java/co/joebirch/remote/service/GithubTrendingService.kt create mode 100644 Remote/src/main/java/co/joebirch/remote/service/GithubTrendingServiceFactory.kt diff --git a/Remote/.gitignore b/Remote/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/Remote/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Remote/build.gradle b/Remote/build.gradle new file mode 100644 index 0000000..49cb09f --- /dev/null +++ b/Remote/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'kotlin' + +dependencies { + def remoteDependencies = rootProject.ext.remoteDependencies + def remoteTestDependencies = rootProject.ext.remoteTestDependencies + + compile project(':Data') + + implementation remoteDependencies.javaxAnnotation + implementation remoteDependencies.kotlin + implementation remoteDependencies.javaxInject + implementation remoteDependencies.rxKotlin + implementation remoteDependencies.gson + implementation remoteDependencies.okHttp + implementation remoteDependencies.okHttpLogger + implementation remoteDependencies.retrofit + implementation remoteDependencies.retrofitConverter + implementation remoteDependencies.retrofitAdapter + + implementation remoteTestDependencies.junit + implementation remoteTestDependencies.kotlinJUnit + implementation remoteTestDependencies.mockito + implementation remoteTestDependencies.assertj +} + +sourceCompatibility = "1.7" +targetCompatibility = "1.7" diff --git a/Remote/src/main/java/co/joebirch/remote/model/OwnerModel.kt b/Remote/src/main/java/co/joebirch/remote/model/OwnerModel.kt new file mode 100644 index 0000000..310b1e3 --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/model/OwnerModel.kt @@ -0,0 +1,6 @@ +package co.joebirch.remote.model + +import com.google.gson.annotations.SerializedName + +class OwnerModel(@SerializedName("login") val ownerName: String, + @SerializedName("avatar_url") val ownerAvatar: String) \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/model/ProjectModel.kt b/Remote/src/main/java/co/joebirch/remote/model/ProjectModel.kt new file mode 100644 index 0000000..3b6499e --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/model/ProjectModel.kt @@ -0,0 +1,7 @@ +package co.joebirch.remote.model + +import com.google.gson.annotations.SerializedName + +class ProjectModel(val name: String, @SerializedName("full_name") val fullName: String, + @SerializedName("stargazers_count") val starCount: Int, + @SerializedName("created_at") val dateCreated: String) \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/model/ProjectsResponseModel.kt b/Remote/src/main/java/co/joebirch/remote/model/ProjectsResponseModel.kt new file mode 100644 index 0000000..be8b286 --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/model/ProjectsResponseModel.kt @@ -0,0 +1,3 @@ +package co.joebirch.remote.model + +class ProjectsResponseModel(val items: List) \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingService.kt b/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingService.kt new file mode 100644 index 0000000..6808fb6 --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingService.kt @@ -0,0 +1,15 @@ +package co.joebirch.remote.service + +import co.joebirch.remote.model.ProjectsResponseModel +import retrofit2.http.GET +import retrofit2.http.Query + +interface GithubTrendingService { + + @GET("search/repositories") + fun searchRepositories(@Query("q") query: String, + @Query("sort") sortBy: String, + @Query("order") order: String) + : ProjectsResponseModel + +} \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingServiceFactory.kt b/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingServiceFactory.kt new file mode 100644 index 0000000..aa4c262 --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingServiceFactory.kt @@ -0,0 +1,44 @@ +package co.joebirch.remote.service + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import java.util.concurrent.TimeUnit + +class GithubTrendingServiceFactory { + + fun makeGithubTrendingService(isDebug: Boolean): GithubTrendingService { + val okHttpClient = makeOkHttpClient( + makeLoggingInterceptor((isDebug))) + return makeGithubTrendingService(okHttpClient) + } + + private fun makeGithubTrendingService(okHttpClient: OkHttpClient): GithubTrendingService { + val retrofit = Retrofit.Builder() + .baseUrl("https://api.github.com/") + .client(okHttpClient) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + return retrofit.create(GithubTrendingService::class.java) + } + + private fun makeOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(httpLoggingInterceptor) + .connectTimeout(120, TimeUnit.SECONDS) + .readTimeout(120, TimeUnit.SECONDS) + .build() + } + + private fun makeLoggingInterceptor(isDebug: Boolean): HttpLoggingInterceptor { + val logging = HttpLoggingInterceptor() + logging.level = if (isDebug) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } + return logging + } + +} \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 501dd58..a86f708 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -16,6 +16,9 @@ ext { rxKotlinVersion = '2.1.0' androidAnnotationsVersion = '21.0.3' daggerVersion = '2.11' + gsonVersion = '2.8.1' + okHttpVersion = '3.8.1' + retrofitVersion = '2.3.0' //Testing jUnitVersion = '4.12' @@ -51,4 +54,25 @@ ext { robolectric: "org.robolectric:robolectric:${robolectricVersion}" ] + remoteDependencies = [ + gson: "com.google.code.gson:gson:${gsonVersion}", + rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", + kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", + javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", + javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", + androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", + okHttp: "com.squareup.okhttp3:okhttp:${okHttpVersion}", + okHttpLogger: "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}", + retrofit: "com.squareup.retrofit2:retrofit:${retrofitVersion}", + retrofitConverter: "com.squareup.retrofit2:converter-gson:${retrofitVersion}", + retrofitAdapter: "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}" + ] + + remoteTestDependencies = [ + junit: "junit:junit:${jUnitVersion}", + kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", + assertj: "org.assertj:assertj-core:${assertJVersion}", + mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}" + ] + } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 859bf76..3067ecc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':Domain', ':Data' +include ':Domain', ':Data', ':Remote' From 414fe4d9944e82c93968c16d5b1ad41e8cd50d7b Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Mon, 19 Feb 2018 12:41:40 +0000 Subject: [PATCH 10/42] Add mapper class and repository class --- .../java/co/joebirch/remote/mapper/ModelMapper.kt | 7 +++++++ .../remote/mapper/ProjectsResponseModelMapper.kt | 13 +++++++++++++ .../java/co/joebirch/remote/model/ProjectModel.kt | 6 ++++-- .../remote/service/GithubTrendingService.kt | 3 ++- 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 Remote/src/main/java/co/joebirch/remote/mapper/ModelMapper.kt create mode 100644 Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt diff --git a/Remote/src/main/java/co/joebirch/remote/mapper/ModelMapper.kt b/Remote/src/main/java/co/joebirch/remote/mapper/ModelMapper.kt new file mode 100644 index 0000000..9ee118d --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/mapper/ModelMapper.kt @@ -0,0 +1,7 @@ +package co.joebirch.remote.mapper + +interface ModelMapper { + + fun mapFromModel(model: M): E + +} \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt new file mode 100644 index 0000000..75259e6 --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt @@ -0,0 +1,13 @@ +package co.joebirch.remote.mapper + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.remote.model.ProjectModel + +class ProjectsResponseModelMapper: ModelMapper { + + override fun mapFromModel(model: ProjectModel): ProjectEntity { + return ProjectEntity(model.id, model.name, model.fullName, model.starCount.toString(), + model.dateCreated, model.owner.ownerName, model.owner.ownerAvatar) + } + +} \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/model/ProjectModel.kt b/Remote/src/main/java/co/joebirch/remote/model/ProjectModel.kt index 3b6499e..133921e 100644 --- a/Remote/src/main/java/co/joebirch/remote/model/ProjectModel.kt +++ b/Remote/src/main/java/co/joebirch/remote/model/ProjectModel.kt @@ -2,6 +2,8 @@ package co.joebirch.remote.model import com.google.gson.annotations.SerializedName -class ProjectModel(val name: String, @SerializedName("full_name") val fullName: String, +class ProjectModel(val id: String, val name: String, + @SerializedName("full_name") val fullName: String, @SerializedName("stargazers_count") val starCount: Int, - @SerializedName("created_at") val dateCreated: String) \ No newline at end of file + @SerializedName("created_at") val dateCreated: String, + val owner: OwnerModel) \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingService.kt b/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingService.kt index 6808fb6..906bd73 100644 --- a/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingService.kt +++ b/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingService.kt @@ -1,6 +1,7 @@ package co.joebirch.remote.service import co.joebirch.remote.model.ProjectsResponseModel +import io.reactivex.Observable import retrofit2.http.GET import retrofit2.http.Query @@ -10,6 +11,6 @@ interface GithubTrendingService { fun searchRepositories(@Query("q") query: String, @Query("sort") sortBy: String, @Query("order") order: String) - : ProjectsResponseModel + : Observable } \ No newline at end of file From 376d3b8269a83af98935f24551912ad6b42f2f5a Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Sun, 4 Mar 2018 13:25:56 +0000 Subject: [PATCH 11/42] Implement remote impl --- .../co/joebirch/remote/ProjectsRemoteImpl.kt | 21 +++++++++++++++++++ .../mapper/ProjectsResponseModelMapper.kt | 13 ------------ 2 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 Remote/src/main/java/co/joebirch/remote/ProjectsRemoteImpl.kt delete mode 100644 Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt diff --git a/Remote/src/main/java/co/joebirch/remote/ProjectsRemoteImpl.kt b/Remote/src/main/java/co/joebirch/remote/ProjectsRemoteImpl.kt new file mode 100644 index 0000000..d9d9b17 --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/ProjectsRemoteImpl.kt @@ -0,0 +1,21 @@ +package co.joebirch.remote + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsRemote +import co.joebirch.remote.mapper.ProjectsResponseModelMapper +import co.joebirch.remote.service.GithubTrendingService +import io.reactivex.Observable +import javax.inject.Inject + +class ProjectsRemoteImpl @Inject constructor( + private val service: GithubTrendingService, + private val mapper: ProjectsResponseModelMapper) + : ProjectsRemote { + + override fun getProjects(): Observable> { + return service.searchRepositories("language:kotlin", "stars", "desc") + .map { + it.items.map { mapper.mapFromModel(it) } + } + } +} \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt deleted file mode 100644 index 75259e6..0000000 --- a/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt +++ /dev/null @@ -1,13 +0,0 @@ -package co.joebirch.remote.mapper - -import co.joebirch.data.model.ProjectEntity -import co.joebirch.remote.model.ProjectModel - -class ProjectsResponseModelMapper: ModelMapper { - - override fun mapFromModel(model: ProjectModel): ProjectEntity { - return ProjectEntity(model.id, model.name, model.fullName, model.starCount.toString(), - model.dateCreated, model.owner.ownerName, model.owner.ownerAvatar) - } - -} \ No newline at end of file From 14f51644df5f68dcee39586f973853d4589668b0 Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Sun, 4 Mar 2018 13:26:15 +0000 Subject: [PATCH 12/42] Add project mapper --- .../remote/mapper/ProjectsResponseModelMapper.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt diff --git a/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt new file mode 100644 index 0000000..595a159 --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt @@ -0,0 +1,12 @@ +package co.joebirch.remote.mapper + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.remote.model.ProjectModel + +class ProjectsResponseModelMapper: ModelMapper { + + override fun mapFromModel(model: ProjectModel): ProjectEntity { + return ProjectEntity() + } + +} \ No newline at end of file From 7d69d875886773ab7d8a54832f0bd4aa6cad02bf Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Mon, 5 Mar 2018 12:46:23 +0000 Subject: [PATCH 13/42] Build cache layer --- .../domain/interactor/SingleUseCase.kt | 8 +-- .../domain/interactor/browse/GetProjects.kt | 4 +- .../mapper/ProjectsResponseModelMapper.kt | 3 +- .../mapper/ProjectsResponseModelMapperTest.kt | 28 +++++++++ .../remote/test/factory/DataFactory.kt | 32 ++++++++++ .../remote/test/factory/OwnerFactory.kt | 11 ++++ .../remote/test/factory/ProjectFactory.kt | 14 +++++ cache/.gitignore | 1 + cache/build.gradle | 46 ++++++++++++++ cache/proguard-rules.pro | 21 +++++++ .../cache/ExampleInstrumentedTest.java | 26 ++++++++ cache/src/main/AndroidManifest.xml | 2 + .../co/joebirch/cache/ProjectsCacheImpl.kt | 63 +++++++++++++++++++ .../joebirch/cache/dao/CachedProjectsDao.kt | 23 +++++++ .../co/joebirch/cache/db/ProjectConstants.kt | 11 ++++ .../co/joebirch/cache/db/ProjectsDatabase.kt | 33 ++++++++++ .../co/joebirch/cache/mapper/CacheMapper.kt | 9 +++ .../cache/mapper/CachedProjectMapper.kt | 17 +++++ .../co/joebirch/cache/model/CachedProject.kt | 17 +++++ cache/src/main/res/values/strings.xml | 3 + .../co/joebirch/cache/ExampleUnitTest.java | 17 +++++ dependencies.gradle | 28 +++++++++ settings.gradle | 2 +- 23 files changed, 411 insertions(+), 8 deletions(-) create mode 100644 Remote/src/test/Java/co/joebirch/remote/mapper/ProjectsResponseModelMapperTest.kt create mode 100644 Remote/src/test/Java/co/joebirch/remote/test/factory/DataFactory.kt create mode 100644 Remote/src/test/Java/co/joebirch/remote/test/factory/OwnerFactory.kt create mode 100644 Remote/src/test/Java/co/joebirch/remote/test/factory/ProjectFactory.kt create mode 100644 cache/.gitignore create mode 100644 cache/build.gradle create mode 100644 cache/proguard-rules.pro create mode 100644 cache/src/androidTest/java/co/joebirch/cache/ExampleInstrumentedTest.java create mode 100644 cache/src/main/AndroidManifest.xml create mode 100644 cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt create mode 100644 cache/src/main/java/co/joebirch/cache/dao/CachedProjectsDao.kt create mode 100644 cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt create mode 100644 cache/src/main/java/co/joebirch/cache/db/ProjectsDatabase.kt create mode 100644 cache/src/main/java/co/joebirch/cache/mapper/CacheMapper.kt create mode 100644 cache/src/main/java/co/joebirch/cache/mapper/CachedProjectMapper.kt create mode 100644 cache/src/main/java/co/joebirch/cache/model/CachedProject.kt create mode 100644 cache/src/main/res/values/strings.xml create mode 100644 cache/src/test/java/co/joebirch/cache/ExampleUnitTest.java diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/SingleUseCase.kt b/Domain/src/main/java/co/joebirch/domain/interactor/SingleUseCase.kt index 4c9a205..25831ef 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/SingleUseCase.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/SingleUseCase.kt @@ -1,10 +1,10 @@ package co.joebirch.domain.interactor import co.joebirch.domain.executor.PostExecutionThread -import io.reactivex.Single +import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable -import io.reactivex.observers.DisposableSingleObserver +import io.reactivex.observers.DisposableObserver import io.reactivex.schedulers.Schedulers abstract class SingleUseCase constructor( @@ -12,9 +12,9 @@ abstract class SingleUseCase constructor( private val disposables = CompositeDisposable() - protected abstract fun buildUseCaseSingle(params: Params? = null): Single + protected abstract fun buildUseCaseSingle(params: Params? = null): Observable - open fun execute(singleObserver: DisposableSingleObserver, params: Params? = null) { + open fun execute(singleObserver: DisposableObserver, params: Params? = null) { val single = this.buildUseCaseSingle(params) .subscribeOn(Schedulers.io()) .observeOn(postExecutionThread.scheduler) diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt b/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt index d9fa62f..f9b312d 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt @@ -4,14 +4,14 @@ import co.joebirch.domain.executor.PostExecutionThread import co.joebirch.domain.interactor.SingleUseCase import co.joebirch.domain.model.Project import co.joebirch.domain.repository.ProjectsRepository -import io.reactivex.Single +import io.reactivex.Observable import javax.inject.Inject class GetProjects @Inject constructor(private val projectsRepository: ProjectsRepository, postExecutionThread: PostExecutionThread) : SingleUseCase, Nothing>(postExecutionThread) { - public override fun buildUseCaseSingle(params: Nothing?): Single> { + public override fun buildUseCaseSingle(params: Nothing?): Observable> { return projectsRepository.getProjects() } } \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt index 595a159..75259e6 100644 --- a/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt +++ b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt @@ -6,7 +6,8 @@ import co.joebirch.remote.model.ProjectModel class ProjectsResponseModelMapper: ModelMapper { override fun mapFromModel(model: ProjectModel): ProjectEntity { - return ProjectEntity() + return ProjectEntity(model.id, model.name, model.fullName, model.starCount.toString(), + model.dateCreated, model.owner.ownerName, model.owner.ownerAvatar) } } \ No newline at end of file diff --git a/Remote/src/test/Java/co/joebirch/remote/mapper/ProjectsResponseModelMapperTest.kt b/Remote/src/test/Java/co/joebirch/remote/mapper/ProjectsResponseModelMapperTest.kt new file mode 100644 index 0000000..6114ce5 --- /dev/null +++ b/Remote/src/test/Java/co/joebirch/remote/mapper/ProjectsResponseModelMapperTest.kt @@ -0,0 +1,28 @@ +package co.joebirch.remote.mapper + +import co.joebirch.remote.test.factory.ProjectFactory +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import kotlin.test.assertEquals + +@RunWith(JUnit4::class) +open class ProjectsResponseModelMapperTest { + + private val mapper = ProjectsResponseModelMapper() + + @Test + fun mapFromModelMapsData() { + val model = ProjectFactory.makeProjectModel() + val entity = mapper.mapFromModel(model) + + assertEquals(model.name, entity.name) + assertEquals(model.fullName, entity.fullName) + assertEquals(model.id, entity.id) + assertEquals(model.starCount.toString(), entity.starCount) + assertEquals(model.dateCreated, entity.dateCreated) + assertEquals(model.owner.ownerName, entity.ownerName) + assertEquals(model.owner.ownerAvatar, entity.ownerAvatar) + } + +} \ No newline at end of file diff --git a/Remote/src/test/Java/co/joebirch/remote/test/factory/DataFactory.kt b/Remote/src/test/Java/co/joebirch/remote/test/factory/DataFactory.kt new file mode 100644 index 0000000..575da27 --- /dev/null +++ b/Remote/src/test/Java/co/joebirch/remote/test/factory/DataFactory.kt @@ -0,0 +1,32 @@ +package co.joebirch.remote.test.factory + +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +object DataFactory { + + fun randomUuid(): String { + return UUID.randomUUID().toString() + } + + fun randomInt(): Int { + return ThreadLocalRandom.current().nextInt(0, 1000 + 1) + } + + fun randomLong(): Long { + return randomInt().toLong() + } + + fun randomBoolean(): Boolean { + return Math.random() < 0.5 + } + + fun makeStringList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomUuid()) + } + return items + } + +} \ No newline at end of file diff --git a/Remote/src/test/Java/co/joebirch/remote/test/factory/OwnerFactory.kt b/Remote/src/test/Java/co/joebirch/remote/test/factory/OwnerFactory.kt new file mode 100644 index 0000000..f3bb953 --- /dev/null +++ b/Remote/src/test/Java/co/joebirch/remote/test/factory/OwnerFactory.kt @@ -0,0 +1,11 @@ +package co.joebirch.remote.test.factory + +import co.joebirch.remote.model.OwnerModel + +object OwnerFactory { + + fun makeOwnerModel(): OwnerModel { + return OwnerModel(DataFactory.randomUuid(), DataFactory.randomUuid()) + } + +} \ No newline at end of file diff --git a/Remote/src/test/Java/co/joebirch/remote/test/factory/ProjectFactory.kt b/Remote/src/test/Java/co/joebirch/remote/test/factory/ProjectFactory.kt new file mode 100644 index 0000000..2fe41a5 --- /dev/null +++ b/Remote/src/test/Java/co/joebirch/remote/test/factory/ProjectFactory.kt @@ -0,0 +1,14 @@ +package co.joebirch.remote.test.factory + +import co.joebirch.remote.model.ProjectModel + +object ProjectFactory { + + fun makeProjectModel(): ProjectModel { + return ProjectModel(DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomInt(), DataFactory.randomUuid(), + OwnerFactory.makeOwnerModel()) + } + +} \ No newline at end of file diff --git a/cache/.gitignore b/cache/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/cache/.gitignore @@ -0,0 +1 @@ +/build diff --git a/cache/build.gradle b/cache/build.gradle new file mode 100644 index 0000000..2b9f3c0 --- /dev/null +++ b/cache/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + def globalConfiguration = rootProject.extensions.getByName("ext") + + compileSdkVersion globalConfiguration["androidCompileSdkVersion"] + buildToolsVersion globalConfiguration["androidBuildToolsVersion"] + + defaultConfig { + minSdkVersion globalConfiguration["androidMinSdkVersion"] + targetSdkVersion globalConfiguration["androidTargetSdkVersion"] + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildToolsVersion '26.0.2' +} + +dependencies { + def cacheDependencies = rootProject.ext.cacheDependencies + def cacheTestDependencies = rootProject.ext.cacheTestDependencies + + compileOnly cacheDependencies.javaxAnnotation + + implementation project(':Data') + + implementation cacheDependencies.kotlin + implementation cacheDependencies.javaxInject + implementation cacheDependencies.rxKotlin + implementation cacheDependencies.roomRuntime + kapt cacheDependencies.roomCompiler + + testImplementation cacheTestDependencies.junit + testImplementation cacheTestDependencies.kotlinJUnit + testImplementation cacheTestDependencies.mockito + testImplementation cacheTestDependencies.assertj + testImplementation cacheTestDependencies.archTesting + testImplementation cacheTestDependencies.roomTesting +} diff --git a/cache/proguard-rules.pro b/cache/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/cache/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/cache/src/androidTest/java/co/joebirch/cache/ExampleInstrumentedTest.java b/cache/src/androidTest/java/co/joebirch/cache/ExampleInstrumentedTest.java new file mode 100644 index 0000000..627285e --- /dev/null +++ b/cache/src/androidTest/java/co/joebirch/cache/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package co.joebirch.cache; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("co.joebirch.cache.test", appContext.getPackageName()); + } +} diff --git a/cache/src/main/AndroidManifest.xml b/cache/src/main/AndroidManifest.xml new file mode 100644 index 0000000..242d6f6 --- /dev/null +++ b/cache/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt b/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt new file mode 100644 index 0000000..8e85a36 --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt @@ -0,0 +1,63 @@ +package co.joebirch.cache + +import co.joebirch.cache.db.ProjectsDatabase +import co.joebirch.cache.mapper.CachedProjectMapper +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsCache +import io.reactivex.Completable +import io.reactivex.Observable +import javax.inject.Inject + +class ProjectsCacheImpl @Inject constructor( + private val projectsDatabase: ProjectsDatabase, + private val mapper: CachedProjectMapper) + : ProjectsCache { + + override fun clearProjects(): Completable { + return Completable.defer { + projectsDatabase.cachedProjectsDao().deleteProjects() + Completable.complete() + } + } + + override fun saveProjects(projects: List): Completable { + return Completable.defer { + projects.forEach { + projectsDatabase.cachedProjectsDao().insertProject( + mapper.mapToCached(it)) + } + Completable.complete() + } + } + + override fun getProjects(): Observable> { + return Observable.defer { + Observable.just(projectsDatabase.cachedProjectsDao().getProjects() + .map { mapper.mapFromCached(it) }) + } + } + + override fun getBookmarkedProjects(): Observable> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun setProjectAsBookmarked(projectId: String): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun setProjectAsNotBookmarked(projectId: String): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun areProjectsCached(): Boolean { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun setLastCacheTime(lastCache: Long): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun isProjectsCacheExpired(): Boolean { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/dao/CachedProjectsDao.kt b/cache/src/main/java/co/joebirch/cache/dao/CachedProjectsDao.kt new file mode 100644 index 0000000..4df5afb --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/dao/CachedProjectsDao.kt @@ -0,0 +1,23 @@ +package co.joebirch.cache.dao + +import android.arch.persistence.room.Dao +import android.arch.persistence.room.Insert +import android.arch.persistence.room.OnConflictStrategy +import android.arch.persistence.room.Query +import co.joebirch.cache.db.ProjectConstants.DELETE_PROJECTS +import co.joebirch.cache.db.ProjectConstants.QUERY_PROJECTS +import co.joebirch.cache.model.CachedProject + +@Dao +abstract class CachedProjectsDao { + + @Query(QUERY_PROJECTS) + abstract fun getProjects(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertProject(cachedProject: CachedProject) + + @Query(DELETE_PROJECTS) + abstract fun deleteProjects() + +} \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt b/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt new file mode 100644 index 0000000..9702a61 --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt @@ -0,0 +1,11 @@ +package co.joebirch.cache.db + +object ProjectConstants { + + const val TABLE_NAME = "projects" + + const val QUERY_PROJECTS = "SELECT * FROM $TABLE_NAME" + + const val DELETE_PROJECTS = "DELETE * FROM $TABLE_NAME" + +} \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/db/ProjectsDatabase.kt b/cache/src/main/java/co/joebirch/cache/db/ProjectsDatabase.kt new file mode 100644 index 0000000..c04facd --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/db/ProjectsDatabase.kt @@ -0,0 +1,33 @@ +package co.joebirch.cache.db + +import android.arch.persistence.room.Database +import android.arch.persistence.room.Room +import android.arch.persistence.room.RoomDatabase +import android.content.Context +import co.joebirch.cache.dao.CachedProjectsDao +import co.joebirch.cache.model.CachedProject +import javax.inject.Inject + +@Database(entities = arrayOf(CachedProject::class), version = 1) +abstract class ProjectsDatabase @Inject constructor(): RoomDatabase() { + + abstract fun cachedProjectsDao(): CachedProjectsDao + + private var INSTANCE: ProjectsDatabase? = null + private val lock = Any() + + fun getInstance(context: Context): ProjectsDatabase { + if (INSTANCE == null) { + synchronized(lock) { + if (INSTANCE == null) { + INSTANCE = Room.databaseBuilder(context.applicationContext, + ProjectsDatabase::class.java, "projects.db") + .build() + } + return INSTANCE as ProjectsDatabase + } + } + return INSTANCE as ProjectsDatabase + } + +} \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/mapper/CacheMapper.kt b/cache/src/main/java/co/joebirch/cache/mapper/CacheMapper.kt new file mode 100644 index 0000000..4c7734e --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/mapper/CacheMapper.kt @@ -0,0 +1,9 @@ +package co.joebirch.cache.mapper + +interface CacheMapper { + + fun mapFromCached(type: C): E + + fun mapToCached(type: E): C + +} \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/mapper/CachedProjectMapper.kt b/cache/src/main/java/co/joebirch/cache/mapper/CachedProjectMapper.kt new file mode 100644 index 0000000..6c1be76 --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/mapper/CachedProjectMapper.kt @@ -0,0 +1,17 @@ +package co.joebirch.cache.mapper + +import co.joebirch.cache.model.CachedProject +import co.joebirch.data.model.ProjectEntity + +class CachedProjectMapper: CacheMapper { + + override fun mapFromCached(type: CachedProject): ProjectEntity { + return ProjectEntity(type.id, type.name, type.fullName, type.starCount, + type.dateCreated, type.ownerName, type.ownerAvatar) + } + + override fun mapToCached(type: ProjectEntity): CachedProject { + return CachedProject(type.id, type.name, type.fullName, type.starCount, + type.dateCreated, type.ownerName, type.ownerAvatar) + } +} \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/model/CachedProject.kt b/cache/src/main/java/co/joebirch/cache/model/CachedProject.kt new file mode 100644 index 0000000..e6f1b46 --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/model/CachedProject.kt @@ -0,0 +1,17 @@ +package co.joebirch.cache.model + +import android.arch.persistence.room.Entity +import android.arch.persistence.room.PrimaryKey +import co.joebirch.cache.db.ProjectConstants + +@Entity(tableName = ProjectConstants.TABLE_NAME) +data class CachedProject( + @PrimaryKey + var id: String, + var name: String, + var fullName: String, + var starCount: String, + var dateCreated: String, + var ownerName: String, + var ownerAvatar: String +) \ No newline at end of file diff --git a/cache/src/main/res/values/strings.xml b/cache/src/main/res/values/strings.xml new file mode 100644 index 0000000..632b7ed --- /dev/null +++ b/cache/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Cache + diff --git a/cache/src/test/java/co/joebirch/cache/ExampleUnitTest.java b/cache/src/test/java/co/joebirch/cache/ExampleUnitTest.java new file mode 100644 index 0000000..a73a91f --- /dev/null +++ b/cache/src/test/java/co/joebirch/cache/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package co.joebirch.cache; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index a86f708..4b54750 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -19,6 +19,7 @@ ext { gsonVersion = '2.8.1' okHttpVersion = '3.8.1' retrofitVersion = '2.3.0' + roomVersion = '1.0.0' //Testing jUnitVersion = '4.12' @@ -26,6 +27,8 @@ ext { mockitoKotlinVersion = '1.5.0' robolectricVersion = '3.4.2' mockitoVersion = '1.9.5' + androidSupportRunnerVersion = '1.0.0' + androidSupportRulesVersion = '1.0.0' domainDependencies = [ javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", @@ -54,6 +57,31 @@ ext { robolectric: "org.robolectric:robolectric:${robolectricVersion}" ] + cacheDependencies = [ + daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", + dagger: "com.google.dagger:dagger:${daggerVersion}", + gson: "com.google.code.gson:gson:${gsonVersion}", + rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", + kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", + javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", + javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", + androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", + roomRuntime: "android.arch.persistence.room:runtime:${roomVersion}", + roomCompiler: "android.arch.persistence.room:compiler:${roomVersion}", + roomRxJava: "android.arch.persistence.room:rxjava2:${roomVersion}" + ] + + cacheTestDependencies = [ + junit: "junit:junit:${jUnitVersion}", + kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", + assertj: "org.assertj:assertj-core:${assertJVersion}", + mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", + roomTesting: "android.arch.persistence.room:testing:${roomVersion}", + archTesting: "android.arch.core:core-testing:${roomVersion}", + supportRunner: "com.android.support.test:runner:${androidSupportRunnerVersion}", + supportRules: "com.android.support.test:rules:${androidSupportRulesVersion}" + ] + remoteDependencies = [ gson: "com.google.code.gson:gson:${gsonVersion}", rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", diff --git a/settings.gradle b/settings.gradle index 3067ecc..ee3d8b0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':Domain', ':Data', ':Remote' +include ':Domain', ':Data', ':Remote', ':cache' From 78fde31058eff65044d17974bd12f1a516eacc7a Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Sat, 10 Mar 2018 11:32:14 +0000 Subject: [PATCH 14/42] Implement cache layer --- .../co/joebirch/data/model/ProjectEntity.kt | 3 ++- .../co/joebirch/cache/ProjectsCacheImpl.kt | 27 ++++++++++++++----- .../joebirch/cache/dao/CachedProjectsDao.kt | 9 +++++++ .../java/co/joebirch/cache/dao/ConfigDao.kt | 19 +++++++++++++ .../co/joebirch/cache/db/ConfigConstants.kt | 9 +++++++ .../co/joebirch/cache/db/ProjectConstants.kt | 11 ++++++++ .../co/joebirch/cache/db/ProjectsDatabase.kt | 7 ++++- .../cache/mapper/CachedProjectMapper.kt | 7 +++-- .../co/joebirch/cache/model/CachedProject.kt | 6 ++++- .../java/co/joebirch/cache/model/Config.kt | 7 +++++ 10 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 cache/src/main/java/co/joebirch/cache/dao/ConfigDao.kt create mode 100644 cache/src/main/java/co/joebirch/cache/db/ConfigConstants.kt create mode 100644 cache/src/main/java/co/joebirch/cache/model/Config.kt diff --git a/Data/src/main/java/co/joebirch/data/model/ProjectEntity.kt b/Data/src/main/java/co/joebirch/data/model/ProjectEntity.kt index 784fd72..4da6401 100644 --- a/Data/src/main/java/co/joebirch/data/model/ProjectEntity.kt +++ b/Data/src/main/java/co/joebirch/data/model/ProjectEntity.kt @@ -2,4 +2,5 @@ package co.joebirch.data.model class ProjectEntity(val id: String, val name: String, val fullName: String, val starCount: String, val dateCreated: String, - val ownerName: String, val ownerAvatar: String) \ No newline at end of file + val ownerName: String, val ownerAvatar: String, + val isBookmarked: Boolean) \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt b/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt index 8e85a36..b981fbf 100644 --- a/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt +++ b/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt @@ -2,6 +2,7 @@ package co.joebirch.cache import co.joebirch.cache.db.ProjectsDatabase import co.joebirch.cache.mapper.CachedProjectMapper +import co.joebirch.cache.model.Config import co.joebirch.data.model.ProjectEntity import co.joebirch.data.repository.ProjectsCache import io.reactivex.Completable @@ -38,26 +39,40 @@ class ProjectsCacheImpl @Inject constructor( } override fun getBookmarkedProjects(): Observable> { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return Observable.defer { + Observable.just(projectsDatabase.cachedProjectsDao().getBookmarkedProjects() + .map { mapper.mapFromCached(it) }) + } } override fun setProjectAsBookmarked(projectId: String): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return Completable.defer { + projectsDatabase.cachedProjectsDao().setBookmarkStatus(true, projectId) + Completable.complete() + } } override fun setProjectAsNotBookmarked(projectId: String): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return Completable.defer { + projectsDatabase.cachedProjectsDao().setBookmarkStatus(false, projectId) + Completable.complete() + } } override fun areProjectsCached(): Boolean { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return projectsDatabase.cachedProjectsDao().getProjects().isNotEmpty() } override fun setLastCacheTime(lastCache: Long): Completable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return Completable.defer { + projectsDatabase.configDao().insertConfig(Config(lastCache)) + Completable.complete() + } } override fun isProjectsCacheExpired(): Boolean { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + val currentTime = System.currentTimeMillis() + val expirationTime = (60 * 10 * 1000).toLong() + return currentTime - projectsDatabase.configDao().getConfig().lastCacheTime > expirationTime } } \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/dao/CachedProjectsDao.kt b/cache/src/main/java/co/joebirch/cache/dao/CachedProjectsDao.kt index 4df5afb..0df2ec3 100644 --- a/cache/src/main/java/co/joebirch/cache/dao/CachedProjectsDao.kt +++ b/cache/src/main/java/co/joebirch/cache/dao/CachedProjectsDao.kt @@ -5,7 +5,9 @@ import android.arch.persistence.room.Insert import android.arch.persistence.room.OnConflictStrategy import android.arch.persistence.room.Query import co.joebirch.cache.db.ProjectConstants.DELETE_PROJECTS +import co.joebirch.cache.db.ProjectConstants.QUERY_BOOKMARKED_PROJECTS import co.joebirch.cache.db.ProjectConstants.QUERY_PROJECTS +import co.joebirch.cache.db.ProjectConstants.QUERY_UPDATE_BOOKMARK_STATUS import co.joebirch.cache.model.CachedProject @Dao @@ -20,4 +22,11 @@ abstract class CachedProjectsDao { @Query(DELETE_PROJECTS) abstract fun deleteProjects() + @Query(QUERY_BOOKMARKED_PROJECTS) + abstract fun getBookmarkedProjects(): List + + @Query(QUERY_UPDATE_BOOKMARK_STATUS) + abstract fun setBookmarkStatus(isBookmarked: Boolean, + projectId: String) + } \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/dao/ConfigDao.kt b/cache/src/main/java/co/joebirch/cache/dao/ConfigDao.kt new file mode 100644 index 0000000..fb26b6b --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/dao/ConfigDao.kt @@ -0,0 +1,19 @@ +package co.joebirch.cache.dao + +import android.arch.persistence.room.Dao +import android.arch.persistence.room.Insert +import android.arch.persistence.room.OnConflictStrategy +import android.arch.persistence.room.Query +import co.joebirch.cache.db.ConfigConstants +import co.joebirch.cache.model.Config + +@Dao +abstract class ConfigDao { + + @Query(ConfigConstants.QUERY_CONFIG) + abstract fun getConfig(): Config + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertConfig(config: Config) + +} \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/db/ConfigConstants.kt b/cache/src/main/java/co/joebirch/cache/db/ConfigConstants.kt new file mode 100644 index 0000000..5646d6d --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/db/ConfigConstants.kt @@ -0,0 +1,9 @@ +package co.joebirch.cache.db + +object ConfigConstants { + + const val TABLE_NAME = "config" + + const val QUERY_CONFIG = "SELECT * FROM $TABLE_NAME" + +} \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt b/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt index 9702a61..88c4f1d 100644 --- a/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt +++ b/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt @@ -4,8 +4,19 @@ object ProjectConstants { const val TABLE_NAME = "projects" + const val COLUMN_PROJECT_ID = "project_id" + + const val COLUMN_IS_BOOKMARKED = "is_bookmarked" + const val QUERY_PROJECTS = "SELECT * FROM $TABLE_NAME" const val DELETE_PROJECTS = "DELETE * FROM $TABLE_NAME" + const val QUERY_BOOKMARKED_PROJECTS = "SELECT * FROM $TABLE_NAME " + + "WHERE $COLUMN_IS_BOOKMARKED = 1" + + const val QUERY_UPDATE_BOOKMARK_STATUS = "UPDATE $TABLE_NAME " + + "SET $COLUMN_IS_BOOKMARKED = :isBookmarked WHERE " + + "$COLUMN_PROJECT_ID = :projectId" + } \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/db/ProjectsDatabase.kt b/cache/src/main/java/co/joebirch/cache/db/ProjectsDatabase.kt index c04facd..6455a09 100644 --- a/cache/src/main/java/co/joebirch/cache/db/ProjectsDatabase.kt +++ b/cache/src/main/java/co/joebirch/cache/db/ProjectsDatabase.kt @@ -5,14 +5,19 @@ import android.arch.persistence.room.Room import android.arch.persistence.room.RoomDatabase import android.content.Context import co.joebirch.cache.dao.CachedProjectsDao +import co.joebirch.cache.dao.ConfigDao import co.joebirch.cache.model.CachedProject +import co.joebirch.cache.model.Config import javax.inject.Inject -@Database(entities = arrayOf(CachedProject::class), version = 1) +@Database(entities = arrayOf(CachedProject::class, + Config::class), version = 1) abstract class ProjectsDatabase @Inject constructor(): RoomDatabase() { abstract fun cachedProjectsDao(): CachedProjectsDao + abstract fun configDao(): ConfigDao + private var INSTANCE: ProjectsDatabase? = null private val lock = Any() diff --git a/cache/src/main/java/co/joebirch/cache/mapper/CachedProjectMapper.kt b/cache/src/main/java/co/joebirch/cache/mapper/CachedProjectMapper.kt index 6c1be76..81026f5 100644 --- a/cache/src/main/java/co/joebirch/cache/mapper/CachedProjectMapper.kt +++ b/cache/src/main/java/co/joebirch/cache/mapper/CachedProjectMapper.kt @@ -7,11 +7,14 @@ class CachedProjectMapper: CacheMapper { override fun mapFromCached(type: CachedProject): ProjectEntity { return ProjectEntity(type.id, type.name, type.fullName, type.starCount, - type.dateCreated, type.ownerName, type.ownerAvatar) + type.dateCreated, type.ownerName, type.ownerAvatar, + type.isBookmarked) } override fun mapToCached(type: ProjectEntity): CachedProject { return CachedProject(type.id, type.name, type.fullName, type.starCount, - type.dateCreated, type.ownerName, type.ownerAvatar) + type.dateCreated, type.ownerName, type.ownerAvatar, + type.isBookmarked) } + } \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/model/CachedProject.kt b/cache/src/main/java/co/joebirch/cache/model/CachedProject.kt index e6f1b46..c3a4b02 100644 --- a/cache/src/main/java/co/joebirch/cache/model/CachedProject.kt +++ b/cache/src/main/java/co/joebirch/cache/model/CachedProject.kt @@ -1,5 +1,6 @@ package co.joebirch.cache.model +import android.arch.persistence.room.ColumnInfo import android.arch.persistence.room.Entity import android.arch.persistence.room.PrimaryKey import co.joebirch.cache.db.ProjectConstants @@ -7,11 +8,14 @@ import co.joebirch.cache.db.ProjectConstants @Entity(tableName = ProjectConstants.TABLE_NAME) data class CachedProject( @PrimaryKey + @ColumnInfo(name = ProjectConstants.COLUMN_PROJECT_ID) var id: String, var name: String, var fullName: String, var starCount: String, var dateCreated: String, var ownerName: String, - var ownerAvatar: String + var ownerAvatar: String, + @ColumnInfo(name = ProjectConstants.COLUMN_IS_BOOKMARKED) + var isBookmarked: Boolean ) \ No newline at end of file diff --git a/cache/src/main/java/co/joebirch/cache/model/Config.kt b/cache/src/main/java/co/joebirch/cache/model/Config.kt new file mode 100644 index 0000000..cb30f1c --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/model/Config.kt @@ -0,0 +1,7 @@ +package co.joebirch.cache.model + +import android.arch.persistence.room.Entity +import co.joebirch.cache.db.ConfigConstants + +@Entity(tableName = ConfigConstants.TABLE_NAME) +class Config(val lastCacheTime: Long) \ No newline at end of file From a2ea5b5779e4c3909d03805b32dc5d9376b08849 Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Sat, 10 Mar 2018 20:28:49 +0000 Subject: [PATCH 15/42] Add cache tests --- .../co/joebirch/data/mapper/ProjectMapper.kt | 2 +- .../bookmarked/GetBookmarkedProjects.kt | 4 +- .../bookmarked/GetBookmarkedProjectsTest.kt | 2 +- .../githubtrending/browse/GetProjectsTest.kt | 2 +- cache/build.gradle | 2 + .../cache/ExampleInstrumentedTest.java | 26 ----------- .../co/joebirch/cache/ProjectsCacheImpl.kt | 2 +- .../co/joebirch/cache/db/ProjectConstants.kt | 2 +- .../java/co/joebirch/cache/model/Config.kt | 6 ++- .../co/joebirch/cache/ExampleUnitTest.java | 17 ------- .../cache/mapper/CachedProjectMapperTest.kt | 44 +++++++++++++++++++ .../cache/test/factory/ConfigDataFactory.kt | 11 +++++ .../cache/test/factory/DataFactory.kt | 32 ++++++++++++++ .../cache/test/factory/ProjectDataFactory.kt | 24 ++++++++++ 14 files changed, 125 insertions(+), 51 deletions(-) delete mode 100644 cache/src/androidTest/java/co/joebirch/cache/ExampleInstrumentedTest.java delete mode 100644 cache/src/test/java/co/joebirch/cache/ExampleUnitTest.java create mode 100644 cache/src/test/java/co/joebirch/cache/mapper/CachedProjectMapperTest.kt create mode 100644 cache/src/test/java/co/joebirch/cache/test/factory/ConfigDataFactory.kt create mode 100644 cache/src/test/java/co/joebirch/cache/test/factory/DataFactory.kt create mode 100644 cache/src/test/java/co/joebirch/cache/test/factory/ProjectDataFactory.kt diff --git a/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt b/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt index 899d8f2..262e6db 100644 --- a/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt +++ b/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt @@ -13,7 +13,7 @@ class ProjectMapper @Inject constructor(): EntityMapper override fun mapToEntity(domain: Project): ProjectEntity { return ProjectEntity(domain.id, domain.name, domain.fullName, domain.starCount, - domain.dateCreated, domain.ownerName, domain.ownerAvatar) + domain.dateCreated, domain.ownerName, domain.ownerAvatar, false) } } \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt b/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt index 2de7bf6..c895516 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt @@ -4,7 +4,7 @@ import co.joebirch.domain.executor.PostExecutionThread import co.joebirch.domain.interactor.SingleUseCase import co.joebirch.domain.model.Project import co.joebirch.domain.repository.ProjectsRepository -import io.reactivex.Single +import io.reactivex.Observable import javax.inject.Inject open class GetBookmarkedProjects @Inject constructor( @@ -12,7 +12,7 @@ open class GetBookmarkedProjects @Inject constructor( postExecutionThread: PostExecutionThread) : SingleUseCase, Nothing>(postExecutionThread) { - public override fun buildUseCaseSingle(params: Nothing?): Single> { + public override fun buildUseCaseSingle(params: Nothing?): Observable> { return projectsRepository.getBookmarkedProjects() } } \ No newline at end of file diff --git a/Domain/src/test/java/co/joebirch/githubtrending/bookmarked/GetBookmarkedProjectsTest.kt b/Domain/src/test/java/co/joebirch/githubtrending/bookmarked/GetBookmarkedProjectsTest.kt index ef28f25..7bdd2dc 100644 --- a/Domain/src/test/java/co/joebirch/githubtrending/bookmarked/GetBookmarkedProjectsTest.kt +++ b/Domain/src/test/java/co/joebirch/githubtrending/bookmarked/GetBookmarkedProjectsTest.kt @@ -48,7 +48,7 @@ class GetBookmarkedProjectsTest { private fun stubProjectsRepositoryGetBookmarkedProjects(single: Single>) { whenever(projectsRepository.getBookmarkedProjects()) - .thenReturn(single) + .thenReturn(single.toObservable()) } } \ No newline at end of file diff --git a/Domain/src/test/java/co/joebirch/githubtrending/browse/GetProjectsTest.kt b/Domain/src/test/java/co/joebirch/githubtrending/browse/GetProjectsTest.kt index 7af8046..4dd928a 100644 --- a/Domain/src/test/java/co/joebirch/githubtrending/browse/GetProjectsTest.kt +++ b/Domain/src/test/java/co/joebirch/githubtrending/browse/GetProjectsTest.kt @@ -58,7 +58,7 @@ class GetProjectsTest { private fun stubProjectsRepositoryGetProjects(single: Single>) { whenever(projectsRepository.getProjects()) - .thenReturn(single) + .thenReturn(single.toObservable()) } } \ No newline at end of file diff --git a/cache/build.gradle b/cache/build.gradle index 2b9f3c0..434051c 100644 --- a/cache/build.gradle +++ b/cache/build.gradle @@ -43,4 +43,6 @@ dependencies { testImplementation cacheTestDependencies.assertj testImplementation cacheTestDependencies.archTesting testImplementation cacheTestDependencies.roomTesting + androidTestImplementation 'com.android.support:support-annotations:27.1.0' + androidTestCompile 'junit:junit:4.12' } diff --git a/cache/src/androidTest/java/co/joebirch/cache/ExampleInstrumentedTest.java b/cache/src/androidTest/java/co/joebirch/cache/ExampleInstrumentedTest.java deleted file mode 100644 index 627285e..0000000 --- a/cache/src/androidTest/java/co/joebirch/cache/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package co.joebirch.cache; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("co.joebirch.cache.test", appContext.getPackageName()); - } -} diff --git a/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt b/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt index b981fbf..fef0819 100644 --- a/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt +++ b/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt @@ -65,7 +65,7 @@ class ProjectsCacheImpl @Inject constructor( override fun setLastCacheTime(lastCache: Long): Completable { return Completable.defer { - projectsDatabase.configDao().insertConfig(Config(lastCache)) + projectsDatabase.configDao().insertConfig(Config(lastCacheTime = lastCache)) Completable.complete() } } diff --git a/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt b/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt index 88c4f1d..a45b832 100644 --- a/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt +++ b/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt @@ -10,7 +10,7 @@ object ProjectConstants { const val QUERY_PROJECTS = "SELECT * FROM $TABLE_NAME" - const val DELETE_PROJECTS = "DELETE * FROM $TABLE_NAME" + const val DELETE_PROJECTS = "DELETE FROM $TABLE_NAME" const val QUERY_BOOKMARKED_PROJECTS = "SELECT * FROM $TABLE_NAME " + "WHERE $COLUMN_IS_BOOKMARKED = 1" diff --git a/cache/src/main/java/co/joebirch/cache/model/Config.kt b/cache/src/main/java/co/joebirch/cache/model/Config.kt index cb30f1c..cebf416 100644 --- a/cache/src/main/java/co/joebirch/cache/model/Config.kt +++ b/cache/src/main/java/co/joebirch/cache/model/Config.kt @@ -1,7 +1,11 @@ package co.joebirch.cache.model import android.arch.persistence.room.Entity +import android.arch.persistence.room.PrimaryKey import co.joebirch.cache.db.ConfigConstants @Entity(tableName = ConfigConstants.TABLE_NAME) -class Config(val lastCacheTime: Long) \ No newline at end of file +class Config( + @PrimaryKey(autoGenerate = true) + var id: Int = -1, + var lastCacheTime: Long) \ No newline at end of file diff --git a/cache/src/test/java/co/joebirch/cache/ExampleUnitTest.java b/cache/src/test/java/co/joebirch/cache/ExampleUnitTest.java deleted file mode 100644 index a73a91f..0000000 --- a/cache/src/test/java/co/joebirch/cache/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package co.joebirch.cache; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/cache/src/test/java/co/joebirch/cache/mapper/CachedProjectMapperTest.kt b/cache/src/test/java/co/joebirch/cache/mapper/CachedProjectMapperTest.kt new file mode 100644 index 0000000..176e65a --- /dev/null +++ b/cache/src/test/java/co/joebirch/cache/mapper/CachedProjectMapperTest.kt @@ -0,0 +1,44 @@ +package co.joebirch.cache.mapper + +import co.joebirch.cache.model.CachedProject +import co.joebirch.cache.test.factory.ProjectDataFactory +import co.joebirch.data.model.ProjectEntity +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import kotlin.test.assertEquals + +@RunWith(JUnit4::class) +class CachedProjectMapperTest { + + private val mapper = CachedProjectMapper() + + @Test + fun mapFromCachedMapsData() { + val model = ProjectDataFactory.makeCachedProject() + val entity = mapper.mapFromCached(model) + + assertEqualData(model, entity) + } + + @Test + fun mapToCachedMapsData() { + val entity = ProjectDataFactory.makeProjectEntity() + val model = mapper.mapToCached(entity) + + assertEqualData(model, entity) + } + + private fun assertEqualData(model: CachedProject, + entity: ProjectEntity) { + assertEquals(model.id, entity.id) + assertEquals(model.fullName, entity.fullName) + assertEquals(model.name, entity.name) + assertEquals(model.dateCreated, entity.dateCreated) + assertEquals(model.starCount, entity.starCount) + assertEquals(model.isBookmarked, entity.isBookmarked) + assertEquals(model.ownerName, entity.ownerName) + assertEquals(model.ownerAvatar, entity.ownerAvatar) + } + +} \ No newline at end of file diff --git a/cache/src/test/java/co/joebirch/cache/test/factory/ConfigDataFactory.kt b/cache/src/test/java/co/joebirch/cache/test/factory/ConfigDataFactory.kt new file mode 100644 index 0000000..b2b1876 --- /dev/null +++ b/cache/src/test/java/co/joebirch/cache/test/factory/ConfigDataFactory.kt @@ -0,0 +1,11 @@ +package co.joebirch.cache.test.factory + +import co.joebirch.cache.model.Config + +object ConfigDataFactory { + + fun makeCachedConfig(): Config { + return Config(DataFactory.randomInt(), DataFactory.randomLong()) + } + +} \ No newline at end of file diff --git a/cache/src/test/java/co/joebirch/cache/test/factory/DataFactory.kt b/cache/src/test/java/co/joebirch/cache/test/factory/DataFactory.kt new file mode 100644 index 0000000..c206b15 --- /dev/null +++ b/cache/src/test/java/co/joebirch/cache/test/factory/DataFactory.kt @@ -0,0 +1,32 @@ +package co.joebirch.cache.test.factory + +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +object DataFactory { + + fun randomUuid(): String { + return UUID.randomUUID().toString() + } + + fun randomInt(): Int { + return ThreadLocalRandom.current().nextInt(0, 1000 + 1) + } + + fun randomLong(): Long { + return randomInt().toLong() + } + + fun randomBoolean(): Boolean { + return Math.random() < 0.5 + } + + fun makeStringList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomUuid()) + } + return items + } + +} \ No newline at end of file diff --git a/cache/src/test/java/co/joebirch/cache/test/factory/ProjectDataFactory.kt b/cache/src/test/java/co/joebirch/cache/test/factory/ProjectDataFactory.kt new file mode 100644 index 0000000..c23f666 --- /dev/null +++ b/cache/src/test/java/co/joebirch/cache/test/factory/ProjectDataFactory.kt @@ -0,0 +1,24 @@ +package co.joebirch.cache.test.factory + +import co.joebirch.cache.model.CachedProject +import co.joebirch.data.model.ProjectEntity + +object ProjectDataFactory { + + fun makeCachedProject(): CachedProject { + return CachedProject(DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomBoolean()) + } + + fun makeProjectEntity(): ProjectEntity { + return ProjectEntity(DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomBoolean()) + } + +} \ No newline at end of file From 5842790075d980ae762b5e341e6eff960b8b745f Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Mon, 12 Mar 2018 08:13:20 +0000 Subject: [PATCH 16/42] Merge master --- .../co/joebirch/data/mapper/ProjectMapper.kt | 2 +- .../data/store/ProjectsCacheDataStore.kt | 2 +- .../data/store/ProjectsDataStoreFactory.kt | 8 +- .../data/store/ProjectsRemoteDataStore.kt | 2 +- .../data/ProjectsDataRepositoryTest.kt | 168 ++++++++++++++++++ .../joebirch/data/mapper/ProjectMapperTest.kt | 43 +++++ .../data/store/ProjectsCacheDataStoreTest.kt | 165 +++++++++++++++++ .../store/ProjectsDataStoreFactoryTest.kt | 57 ++++++ .../data/store/ProjectsRemoteDataStoreTest.kt | 64 +++++++ .../joebirch/data/test/factory/DataFactory.kt | 24 +++ .../data/test/factory/ProjectFactory.kt | 22 +++ .../bookmarked/GetBookmarkedProjects.kt | 2 +- .../domain/interactor/browse/GetProjects.kt | 2 +- 13 files changed, 552 insertions(+), 9 deletions(-) create mode 100644 Data/src/test/java/co/joebirch/data/ProjectsDataRepositoryTest.kt create mode 100644 Data/src/test/java/co/joebirch/data/mapper/ProjectMapperTest.kt create mode 100644 Data/src/test/java/co/joebirch/data/store/ProjectsCacheDataStoreTest.kt create mode 100644 Data/src/test/java/co/joebirch/data/store/ProjectsDataStoreFactoryTest.kt create mode 100644 Data/src/test/java/co/joebirch/data/store/ProjectsRemoteDataStoreTest.kt create mode 100644 Data/src/test/java/co/joebirch/data/test/factory/DataFactory.kt create mode 100644 Data/src/test/java/co/joebirch/data/test/factory/ProjectFactory.kt diff --git a/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt b/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt index 899d8f2..e81d44b 100644 --- a/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt +++ b/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt @@ -4,7 +4,7 @@ import co.joebirch.data.model.ProjectEntity import co.joebirch.domain.model.Project import javax.inject.Inject -class ProjectMapper @Inject constructor(): EntityMapper { +open class ProjectMapper @Inject constructor() : EntityMapper { override fun mapFromEntity(entity: ProjectEntity): Project { return Project(entity.id, entity.name, entity.fullName, entity.starCount, diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt index 7a320ef..9cd9d0f 100644 --- a/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt @@ -7,7 +7,7 @@ import io.reactivex.Completable import io.reactivex.Observable import javax.inject.Inject -class ProjectsCacheDataStore @Inject constructor( +open class ProjectsCacheDataStore @Inject constructor( private val projectsCache: ProjectsCache) : ProjectsDataStore { diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt index 2d6b9d9..fe1cd6c 100644 --- a/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt @@ -4,21 +4,21 @@ import co.joebirch.data.repository.ProjectsCache import co.joebirch.data.repository.ProjectsDataStore import javax.inject.Inject -class ProjectsDataStoreFactory @Inject constructor( +open class ProjectsDataStoreFactory @Inject constructor( private val projectsCache: ProjectsCache, private val projectsCacheDataStore: ProjectsCacheDataStore, private val projectsRemoteDataStore: ProjectsRemoteDataStore) { - fun getDataStore(): ProjectsDataStore { + open fun getDataStore(): ProjectsDataStore { return if (projectsCache.areProjectsCached() && - !projectsCache.isProjectsCacheExpired()) { + !projectsCache.isProjectsCacheExpired()) { projectsCacheDataStore } else { projectsRemoteDataStore } } - fun getCacheDataStore(): ProjectsDataStore { + open fun getCacheDataStore(): ProjectsDataStore { return projectsCacheDataStore } diff --git a/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt b/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt index 5cc970c..8e337ce 100644 --- a/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsRemoteDataStore.kt @@ -7,7 +7,7 @@ import io.reactivex.Completable import io.reactivex.Observable import javax.inject.Inject -class ProjectsRemoteDataStore @Inject constructor( +open class ProjectsRemoteDataStore @Inject constructor( private val projectsRemote: ProjectsRemote) : ProjectsDataStore { diff --git a/Data/src/test/java/co/joebirch/data/ProjectsDataRepositoryTest.kt b/Data/src/test/java/co/joebirch/data/ProjectsDataRepositoryTest.kt new file mode 100644 index 0000000..20beb78 --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/ProjectsDataRepositoryTest.kt @@ -0,0 +1,168 @@ +package co.joebirch.data + +import co.joebirch.data.mapper.ProjectMapper +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsDataStore +import co.joebirch.data.store.ProjectsDataStoreFactory +import co.joebirch.data.test.factory.DataFactory +import co.joebirch.data.test.factory.ProjectFactory +import co.joebirch.domain.model.Project +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever +import io.reactivex.Completable +import io.reactivex.Observable +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ProjectsDataRepositoryTest { + + private val mapper = mock() + private val factory = mock() + private val store = mock() + private val repository = ProjectsDataRepository(mapper, factory) + + @Before + fun setup() { + stubProjectsDataStoreFactoryGetDataStore() + } + + @Test + fun getProjectsCompletes() { + stubProjectsDataStoreGetProjects(Observable.just(listOf( + ProjectFactory.makeProjectEntity()))) + stubProjectMapperMapFromEntity(any(), ProjectFactory.makeProject()) + + val testObserver = repository.getProjects().test() + testObserver.assertComplete() + } + + @Test + fun getProjectsCallsDataStore() { + stubProjectsDataStoreGetProjects(Observable.just(listOf( + ProjectFactory.makeProjectEntity()))) + stubProjectMapperMapFromEntity(any(), ProjectFactory.makeProject()) + + repository.getProjects().test() + verify(store).getProjects() + } + + @Test + fun getProjectsReturnsData() { + val response = listOf(ProjectFactory.makeProjectEntity()) + val data = listOf(ProjectFactory.makeProject()) + stubProjectsDataStoreGetProjects(Observable.just(response)) + stubProjectMapperMapFromEntity(response[0], data[0]) + + val testObserver = repository.getProjects().test() + testObserver.assertValue(data) + } + + @Test + fun bookmarkProjectCompletes() { + stubProjectsDataStoreFactoryGetCacheDataStore() + stubProjectsDataStoreBookmarkProject(Completable.complete()) + val testObserver = repository.bookmarkProject(DataFactory.randomString()).test() + testObserver.assertComplete() + } + + @Test + fun bookmarkProjectCallsDataStore() { + stubProjectsDataStoreFactoryGetCacheDataStore() + val projectId = DataFactory.randomString() + stubProjectsDataStoreBookmarkProject(Completable.complete()) + repository.bookmarkProject(projectId).test() + verify(store).setProjectAsBookmarked(projectId) + } + + @Test + fun unbookmarkProjectCompletes() { + stubProjectsDataStoreFactoryGetCacheDataStore() + stubProjectsDataStoreUnBookmarkProject(Completable.complete()) + val testObserver = repository.unbookmarkProject(DataFactory.randomString()).test() + testObserver.assertComplete() + } + + @Test + fun unbookmarkProjectCallsDataStore() { + stubProjectsDataStoreFactoryGetCacheDataStore() + val projectId = DataFactory.randomString() + stubProjectsDataStoreUnBookmarkProject(Completable.complete()) + repository.unbookmarkProject(projectId).test() + verify(store).setProjectAsNotBookmarked(projectId) + } + + @Test + fun getBookmarkedProjectsCompletes() { + stubProjectsDataStoreFactoryGetCacheDataStore() + stubProjectsDataStoreGetBookmarkedProjects(Observable.just(listOf( + ProjectFactory.makeProjectEntity()))) + stubProjectMapperMapFromEntity(any(), ProjectFactory.makeProject()) + + val testObserver = repository.getBookmarkedProjects().test() + testObserver.assertComplete() + } + + @Test + fun getBookmarkedProjectsCallsDataStore() { + stubProjectsDataStoreFactoryGetCacheDataStore() + stubProjectsDataStoreGetBookmarkedProjects(Observable.just(listOf( + ProjectFactory.makeProjectEntity()))) + stubProjectMapperMapFromEntity(any(), ProjectFactory.makeProject()) + + repository.getBookmarkedProjects().test() + verify(store).getBookmarkedProjects() + } + + @Test + fun getBookmarkedProjectsReturnsData() { + stubProjectsDataStoreFactoryGetCacheDataStore() + val response = listOf(ProjectFactory.makeProjectEntity()) + val data = listOf(ProjectFactory.makeProject()) + stubProjectsDataStoreGetBookmarkedProjects(Observable.just(response)) + stubProjectMapperMapFromEntity(response[0], data[0]) + + val testObserver = repository.getBookmarkedProjects().test() + testObserver.assertValue(data) + } + + private fun stubProjectsDataStoreFactoryGetDataStore() { + whenever(factory.getDataStore()) + .thenReturn(store) + } + + private fun stubProjectsDataStoreFactoryGetCacheDataStore() { + whenever(factory.getCacheDataStore()) + .thenReturn(store) + } + + private fun stubProjectsDataStoreGetProjects(observable: Observable>) { + whenever(store.getProjects()) + .thenReturn(observable) + } + + private fun stubProjectsDataStoreGetBookmarkedProjects(observable: Observable>) { + whenever(store.getBookmarkedProjects()) + .thenReturn(observable) + } + + private fun stubProjectsDataStoreBookmarkProject(completable: Completable) { + whenever(store.setProjectAsBookmarked(any())) + .thenReturn(completable) + } + + private fun stubProjectsDataStoreUnBookmarkProject(completable: Completable) { + whenever(store.setProjectAsNotBookmarked(any())) + .thenReturn(completable) + } + + private fun stubProjectMapperMapFromEntity(entity: ProjectEntity, + model: Project) { + whenever(mapper.mapFromEntity(entity)) + .thenReturn(model) + } +} \ No newline at end of file diff --git a/Data/src/test/java/co/joebirch/data/mapper/ProjectMapperTest.kt b/Data/src/test/java/co/joebirch/data/mapper/ProjectMapperTest.kt new file mode 100644 index 0000000..be257b9 --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/mapper/ProjectMapperTest.kt @@ -0,0 +1,43 @@ +package co.joebirch.data.mapper + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.test.factory.ProjectFactory +import co.joebirch.domain.model.Project +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import kotlin.test.assertEquals + +@RunWith(JUnit4::class) +class ProjectMapperTest { + + private val mapper = ProjectMapper() + + @Test + fun mapFromEntityMapsData() { + val entity = ProjectFactory.makeProjectEntity() + val model = mapper.mapFromEntity(entity) + + assertEqualData(entity, model) + } + + @Test + fun mapToEntityMapsData() { + val model = ProjectFactory.makeProject() + val entity = mapper.mapToEntity(model) + + assertEqualData(entity, model) + } + + private fun assertEqualData(entity: ProjectEntity, + model: Project) { + assertEquals(entity.id, model.id) + assertEquals(entity.name, model.name) + assertEquals(entity.fullName, model.fullName) + assertEquals(entity.starCount, model.starCount) + assertEquals(entity.dateCreated, model.dateCreated) + assertEquals(entity.ownerName, model.ownerName) + assertEquals(entity.ownerAvatar, model.ownerAvatar) + } + +} \ No newline at end of file diff --git a/Data/src/test/java/co/joebirch/data/store/ProjectsCacheDataStoreTest.kt b/Data/src/test/java/co/joebirch/data/store/ProjectsCacheDataStoreTest.kt new file mode 100644 index 0000000..38af8cc --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/store/ProjectsCacheDataStoreTest.kt @@ -0,0 +1,165 @@ +package co.joebirch.data.store + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsCache +import co.joebirch.data.test.factory.DataFactory +import co.joebirch.data.test.factory.ProjectFactory +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever +import io.reactivex.Completable +import io.reactivex.Observable +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ProjectsCacheDataStoreTest { + + private val cache = mock() + private val store = ProjectsCacheDataStore(cache) + + @Test + fun getProjectsCompletes() { + stubProjectsCacheGetProjects(Observable.just(listOf(ProjectFactory.makeProjectEntity()))) + val testObserver = store.getProjects().test() + testObserver.assertComplete() + } + + @Test + fun getProjectsReturnsData() { + val data = listOf(ProjectFactory.makeProjectEntity()) + stubProjectsCacheGetProjects(Observable.just(data)) + val testObserver = store.getProjects().test() + testObserver.assertValue(data) + } + + @Test + fun getProjectsCallsCacheSource() { + stubProjectsCacheGetProjects(Observable.just(listOf(ProjectFactory.makeProjectEntity()))) + store.getProjects().test() + verify(cache).getProjects() + } + + @Test + fun saveProjectsCompletes() { + stubProjectsCacheSaveProjects(Completable.complete()) + stubProjectsCacheSetLastCacheTime(Completable.complete()) + val testObserver = store.saveProjects(listOf(ProjectFactory.makeProjectEntity())).test() + testObserver.assertComplete() + } + + @Test + fun saveProjectsCallsCacheStore() { + val data = listOf(ProjectFactory.makeProjectEntity()) + stubProjectsCacheSaveProjects(Completable.complete()) + stubProjectsCacheSetLastCacheTime(Completable.complete()) + store.saveProjects(data).test() + verify(cache).saveProjects(data) + } + + @Test + fun clearProjectsCompletes() { + stubProjectsClearProjects(Completable.complete()) + val testObserver = store.clearProjects().test() + testObserver.assertComplete() + } + + @Test + fun clearProjectsCallsCacheStore() { + stubProjectsClearProjects(Completable.complete()) + store.clearProjects().test() + verify(cache).clearProjects() + } + + @Test + fun getBookmarkedProjectsCompletes() { + stubProjectsCacheGetBookmarkedProjects(Observable.just(listOf( + ProjectFactory.makeProjectEntity()))) + val testObserver = store.getBookmarkedProjects().test() + testObserver.assertComplete() + } + + @Test + fun getBookmarkedProjectsCallsCacheStore() { + stubProjectsCacheGetBookmarkedProjects(Observable.just(listOf( + ProjectFactory.makeProjectEntity()))) + store.getBookmarkedProjects().test() + verify(cache).getBookmarkedProjects() + } + + @Test + fun getBookmarkedProjectsReturnsData() { + val data = listOf(ProjectFactory.makeProjectEntity()) + stubProjectsCacheGetBookmarkedProjects(Observable.just(data)) + val testObserver = store.getBookmarkedProjects().test() + testObserver.assertValue(data) + } + + @Test + fun setProjectAsBookmarkedCompletes() { + stubProjectsCacheSetProjectAsBookmarked(Completable.complete()) + val testObserver = store.setProjectAsBookmarked(DataFactory.randomString()).test() + testObserver.assertComplete() + } + + @Test + fun setProjectAsBookmarkedCallsCacheStore() { + val projectId = DataFactory.randomString() + stubProjectsCacheSetProjectAsBookmarked(Completable.complete()) + store.setProjectAsBookmarked(projectId).test() + verify(cache).setProjectAsBookmarked(projectId) + } + + @Test + fun setProjectAsNotBookmarkedCompletes() { + stubProjectsCacheSetProjectAsNotBookmarked(Completable.complete()) + val testObserver = store.setProjectAsNotBookmarked(DataFactory.randomString()).test() + testObserver.assertComplete() + } + + @Test + fun setProjectAsNotBookmarkedCallsCacheStore() { + val projectId = DataFactory.randomString() + stubProjectsCacheSetProjectAsNotBookmarked(Completable.complete()) + store.setProjectAsNotBookmarked(projectId).test() + verify(cache).setProjectAsNotBookmarked(projectId) + } + + private fun stubProjectsCacheGetProjects(observable: Observable>) { + whenever(cache.getProjects()) + .thenReturn(observable) + } + + private fun stubProjectsCacheSaveProjects(completable: Completable) { + whenever(cache.saveProjects(any())) + .thenReturn(completable) + } + + private fun stubProjectsCacheSetLastCacheTime(completable: Completable) { + whenever(cache.setLastCacheTime(any())) + .thenReturn(completable) + } + + private fun stubProjectsClearProjects(completable: Completable) { + whenever(cache.clearProjects()) + .thenReturn(completable) + } + + private fun stubProjectsCacheGetBookmarkedProjects(observable: Observable>) { + whenever(cache.getBookmarkedProjects()) + .thenReturn(observable) + } + + private fun stubProjectsCacheSetProjectAsBookmarked(completable: Completable) { + whenever(cache.setProjectAsBookmarked(any())) + .thenReturn(completable) + } + + private fun stubProjectsCacheSetProjectAsNotBookmarked(completable: Completable) { + whenever(cache.setProjectAsNotBookmarked(any())) + .thenReturn(completable) + } + +} \ No newline at end of file diff --git a/Data/src/test/java/co/joebirch/data/store/ProjectsDataStoreFactoryTest.kt b/Data/src/test/java/co/joebirch/data/store/ProjectsDataStoreFactoryTest.kt new file mode 100644 index 0000000..3067b36 --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/store/ProjectsDataStoreFactoryTest.kt @@ -0,0 +1,57 @@ +package co.joebirch.data.store + +import co.joebirch.data.repository.ProjectsCache +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ProjectsDataStoreFactoryTest { + + private val cache = mock() + private val cacheStore = mock() + private val remoteStore = mock() + private val factory = ProjectsDataStoreFactory(cache, + cacheStore, remoteStore) + + @Test + fun getRemoteStoreRetrievesRemoteSource() { + assert(factory.getRemoteDataStore() is ProjectsRemoteDataStore) + } + + @Test + fun getCacheStoreRetrievesCacheSource() { + assert(factory.getCacheDataStore() is ProjectsCacheDataStore) + } + + @Test + fun getDataStoreReturnsRemoteSourceWhenNoCachedData() { + assert(factory.getDataStore() is ProjectsRemoteDataStore) + } + + @Test + fun getDataStoreReturnsRemoteSourceWhenCacheExpired() { + assert(factory.getDataStore() is ProjectsRemoteDataStore) + } + + @Test + fun getDataStoreReturnsCacheSourceWhenDataIsCached() { + stubProjectsCacheAreProjectsCached(true) + stubProjectsCacheIsProjectsCachedExpired(false) + + assert(factory.getDataStore() is ProjectsCacheDataStore) + } + + private fun stubProjectsCacheAreProjectsCached(areCached: Boolean) { + whenever(cache.areProjectsCached()) + .thenReturn(areCached) + } + + private fun stubProjectsCacheIsProjectsCachedExpired(expired: Boolean) { + whenever(cache.isProjectsCacheExpired()) + .thenReturn(expired) + } + +} \ No newline at end of file diff --git a/Data/src/test/java/co/joebirch/data/store/ProjectsRemoteDataStoreTest.kt b/Data/src/test/java/co/joebirch/data/store/ProjectsRemoteDataStoreTest.kt new file mode 100644 index 0000000..224d3b7 --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/store/ProjectsRemoteDataStoreTest.kt @@ -0,0 +1,64 @@ +package co.joebirch.data.store + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsRemote +import co.joebirch.data.test.factory.DataFactory +import co.joebirch.data.test.factory.ProjectFactory +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import io.reactivex.Observable +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ProjectsRemoteDataStoreTest { + + private val remote = mock() + private val store = ProjectsRemoteDataStore(remote) + + @Test + fun getProjectsCompletes() { + stubRemoteGetProjects(Observable.just(listOf(ProjectFactory.makeProjectEntity()))) + val testObserver = store.getProjects().test() + testObserver.assertComplete() + } + + @Test + fun getProjectsReturnsData() { + val response = listOf(ProjectFactory.makeProjectEntity()) + stubRemoteGetProjects(Observable.just(response)) + val testObserver = store.getProjects().test() + testObserver.assertValue(response) + } + + @Test(expected = UnsupportedOperationException::class) + fun saveProjectsThrowsException() { + store.saveProjects(listOf()).test() + } + + @Test(expected = UnsupportedOperationException::class) + fun clearProjectsThrowsException() { + store.clearProjects().test() + } + + @Test(expected = UnsupportedOperationException::class) + fun getBookmarkedProjectsThrowsException() { + store.getBookmarkedProjects().test() + } + + @Test(expected = UnsupportedOperationException::class) + fun setProjectAsBookmarkedThrowsException() { + store.setProjectAsBookmarked(DataFactory.randomString()).test() + } + + @Test(expected = UnsupportedOperationException::class) + fun setProjectAsNotBookmarkedThrowsException() { + store.setProjectAsNotBookmarked(DataFactory.randomString()).test() + } + + private fun stubRemoteGetProjects(observable: Observable>) { + whenever(remote.getProjects()) + .thenReturn(observable) + } +} \ No newline at end of file diff --git a/Data/src/test/java/co/joebirch/data/test/factory/DataFactory.kt b/Data/src/test/java/co/joebirch/data/test/factory/DataFactory.kt new file mode 100644 index 0000000..2c541d3 --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/test/factory/DataFactory.kt @@ -0,0 +1,24 @@ +package co.joebirch.data.test.factory + +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +object DataFactory { + + fun randomString(): String { + return UUID.randomUUID().toString() + } + + fun randomInt(): Int { + return ThreadLocalRandom.current().nextInt(0, 1000 + 1) + } + + fun randomLong(): Long { + return randomInt().toLong() + } + + fun randomBoolean(): Boolean { + return Math.random() < 0.5 + } + +} \ No newline at end of file diff --git a/Data/src/test/java/co/joebirch/data/test/factory/ProjectFactory.kt b/Data/src/test/java/co/joebirch/data/test/factory/ProjectFactory.kt new file mode 100644 index 0000000..e544c50 --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/test/factory/ProjectFactory.kt @@ -0,0 +1,22 @@ +package co.joebirch.data.test.factory + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.domain.model.Project + +object ProjectFactory { + + fun makeProjectEntity(): ProjectEntity { + return ProjectEntity(DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString()) + } + + fun makeProject(): Project { + return Project(DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString()) + } + +} \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt b/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt index 2de7bf6..bc9648f 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt @@ -13,6 +13,6 @@ open class GetBookmarkedProjects @Inject constructor( : SingleUseCase, Nothing>(postExecutionThread) { public override fun buildUseCaseSingle(params: Nothing?): Single> { - return projectsRepository.getBookmarkedProjects() + return Single.just(null) } } \ No newline at end of file diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt b/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt index d9fa62f..f9ed298 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/browse/GetProjects.kt @@ -12,6 +12,6 @@ class GetProjects @Inject constructor(private val projectsRepository: ProjectsRe : SingleUseCase, Nothing>(postExecutionThread) { public override fun buildUseCaseSingle(params: Nothing?): Single> { - return projectsRepository.getProjects() + return Single.just(null) } } \ No newline at end of file From 1eb7f302140f0d01475f389addf4c8232e8a7fcf Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Mon, 2 Apr 2018 18:56:36 +0100 Subject: [PATCH 17/42] Implement the view model layer --- .../interactor/browse/BookmarkProject.kt | 1 - .../interactor/browse/UnBookmarkProject.kt | 26 +++++++ Presentation/.gitignore | 1 + Presentation/build.gradle | 45 +++++++++++ Presentation/proguard-rules.pro | 21 +++++ .../presentation/ExampleInstrumentedTest.java | 26 +++++++ Presentation/src/main/AndroidManifest.xml | 2 + .../BrowseBookmarkedProjectsViewModel.kt | 51 ++++++++++++ .../presentation/BrowseProjectsViewModel.kt | 77 +++++++++++++++++++ .../co/joebirch/presentation/mapper/Mapper.kt | 7 ++ .../presentation/mapper/ProjectViewMapper.kt | 14 ++++ .../presentation/model/ProjectView.kt | 5 ++ .../joebirch/presentation/state/Resource.kt | 19 +++++ .../presentation/state/ResourceState.kt | 5 ++ Presentation/src/main/res/values/strings.xml | 3 + .../android/presentation/ExampleUnitTest.java | 17 ++++ dependencies.gradle | 35 +++++++-- settings.gradle | 2 +- 18 files changed, 349 insertions(+), 8 deletions(-) create mode 100644 Domain/src/main/java/co/joebirch/domain/interactor/browse/UnBookmarkProject.kt create mode 100644 Presentation/.gitignore create mode 100644 Presentation/build.gradle create mode 100644 Presentation/proguard-rules.pro create mode 100644 Presentation/src/androidTest/java/org/buffer/android/presentation/ExampleInstrumentedTest.java create mode 100644 Presentation/src/main/AndroidManifest.xml create mode 100644 Presentation/src/main/java/co/joebirch/presentation/BrowseBookmarkedProjectsViewModel.kt create mode 100644 Presentation/src/main/java/co/joebirch/presentation/BrowseProjectsViewModel.kt create mode 100644 Presentation/src/main/java/co/joebirch/presentation/mapper/Mapper.kt create mode 100644 Presentation/src/main/java/co/joebirch/presentation/mapper/ProjectViewMapper.kt create mode 100644 Presentation/src/main/java/co/joebirch/presentation/model/ProjectView.kt create mode 100644 Presentation/src/main/java/co/joebirch/presentation/state/Resource.kt create mode 100644 Presentation/src/main/java/co/joebirch/presentation/state/ResourceState.kt create mode 100644 Presentation/src/main/res/values/strings.xml create mode 100644 Presentation/src/test/java/org/buffer/android/presentation/ExampleUnitTest.java diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt b/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt index 73048d5..8491b92 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt @@ -13,7 +13,6 @@ class BookmarkProject @Inject constructor(private val projectsRepository: Projec public override fun buildUseCaseCompletable(params: Params?): Completable { if (params == null) throw IllegalArgumentException("Params can't be null!") return projectsRepository.bookmarkProject(params.projectId) - return projectsRepository.unbookmarkProject(params.projectId) } data class Params constructor(val projectId: String) { diff --git a/Domain/src/main/java/co/joebirch/domain/interactor/browse/UnBookmarkProject.kt b/Domain/src/main/java/co/joebirch/domain/interactor/browse/UnBookmarkProject.kt new file mode 100644 index 0000000..543a139 --- /dev/null +++ b/Domain/src/main/java/co/joebirch/domain/interactor/browse/UnBookmarkProject.kt @@ -0,0 +1,26 @@ +package co.joebirch.domain.interactor.browse + +import co.joebirch.domain.executor.PostExecutionThread +import co.joebirch.domain.interactor.CompletableUseCase +import co.joebirch.domain.repository.ProjectsRepository +import io.reactivex.Completable +import javax.inject.Inject + +class UnBookmarkProject @Inject constructor(private val projectsRepository: ProjectsRepository, + postExecutionThread: PostExecutionThread) + : CompletableUseCase(postExecutionThread) { + + public override fun buildUseCaseCompletable(params: Params?): Completable { + if (params == null) throw IllegalArgumentException("Params can't be null!") + return projectsRepository.unbookmarkProject(params.projectId) + } + + data class Params constructor(val projectId: String) { + companion object { + fun forProject(projectId: String): Params { + return Params(projectId) + } + } + } + +} \ No newline at end of file diff --git a/Presentation/.gitignore b/Presentation/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/Presentation/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Presentation/build.gradle b/Presentation/build.gradle new file mode 100644 index 0000000..f16f7ed --- /dev/null +++ b/Presentation/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + def globalConfiguration = rootProject.extensions.getByName("ext") + + compileSdkVersion globalConfiguration["androidCompileSdkVersion"] + buildToolsVersion globalConfiguration["androidBuildToolsVersion"] + + + defaultConfig { + minSdkVersion globalConfiguration["androidMinSdkVersion"] + targetSdkVersion globalConfiguration["androidTargetSdkVersion"] + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + + def presentationDependencies = rootProject.ext.presentationDependencies + def presentationTestDependencies = rootProject.ext.presentationTestDependencies + + compile project(':Domain') + + compile presentationDependencies.kotlin + compile presentationDependencies.javaxInject + compile presentationDependencies.rxKotlin + compile presentationDependencies.archRuntime + compile presentationDependencies.archExtensions + kapt presentationDependencies.archCompiler + + testCompile presentationTestDependencies.junit + testCompile presentationTestDependencies.mockito + testCompile presentationTestDependencies.assertj + testCompile presentationTestDependencies.robolectric + +} diff --git a/Presentation/proguard-rules.pro b/Presentation/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/Presentation/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/Presentation/src/androidTest/java/org/buffer/android/presentation/ExampleInstrumentedTest.java b/Presentation/src/androidTest/java/org/buffer/android/presentation/ExampleInstrumentedTest.java new file mode 100644 index 0000000..54e3479 --- /dev/null +++ b/Presentation/src/androidTest/java/org/buffer/android/presentation/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package org.buffer.android.presentation; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("org.buffer.android.presentation.test", appContext.getPackageName()); + } +} diff --git a/Presentation/src/main/AndroidManifest.xml b/Presentation/src/main/AndroidManifest.xml new file mode 100644 index 0000000..171ef4b --- /dev/null +++ b/Presentation/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/Presentation/src/main/java/co/joebirch/presentation/BrowseBookmarkedProjectsViewModel.kt b/Presentation/src/main/java/co/joebirch/presentation/BrowseBookmarkedProjectsViewModel.kt new file mode 100644 index 0000000..81f3eea --- /dev/null +++ b/Presentation/src/main/java/co/joebirch/presentation/BrowseBookmarkedProjectsViewModel.kt @@ -0,0 +1,51 @@ +package co.joebirch.presentation + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.arch.lifecycle.ViewModel +import co.joebirch.domain.interactor.bookmarked.GetBookmarkedProjects +import co.joebirch.domain.model.Project +import co.joebirch.presentation.mapper.ProjectViewMapper +import co.joebirch.presentation.model.ProjectView +import co.joebirch.presentation.state.Resource +import co.joebirch.presentation.state.ResourceState +import io.reactivex.observers.DisposableObserver +import javax.inject.Inject + +class BrowseBookmarkedProjectsViewModel @Inject constructor( + private val getBookmarkedProjects: GetBookmarkedProjects, + private val mapper: ProjectViewMapper): ViewModel() { + + private val liveData: MutableLiveData>> = + MutableLiveData() + + override fun onCleared() { + getBookmarkedProjects.dispose() + super.onCleared() + } + + fun getProjects(): LiveData>> { + return liveData + } + + fun fetchProjects() { + liveData.postValue(Resource(ResourceState.LOADING, null, null)) + return getBookmarkedProjects.execute(ProjectsSubscriber()) + } + + inner class ProjectsSubscriber: DisposableObserver>() { + override fun onNext(t: List) { + liveData.postValue(Resource(ResourceState.SUCCESS, + t.map { mapper.mapToView(it) }, null)) + } + + override fun onError(e: Throwable) { + liveData.postValue(Resource(ResourceState.ERROR, null, + e.localizedMessage)) + } + + override fun onComplete() { } + + } + +} \ No newline at end of file diff --git a/Presentation/src/main/java/co/joebirch/presentation/BrowseProjectsViewModel.kt b/Presentation/src/main/java/co/joebirch/presentation/BrowseProjectsViewModel.kt new file mode 100644 index 0000000..69ce5e5 --- /dev/null +++ b/Presentation/src/main/java/co/joebirch/presentation/BrowseProjectsViewModel.kt @@ -0,0 +1,77 @@ +package co.joebirch.presentation + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.arch.lifecycle.ViewModel +import co.joebirch.domain.interactor.browse.BookmarkProject +import co.joebirch.domain.interactor.browse.GetProjects +import co.joebirch.domain.interactor.browse.UnBookmarkProject +import co.joebirch.domain.model.Project +import co.joebirch.presentation.mapper.ProjectViewMapper +import co.joebirch.presentation.model.ProjectView +import co.joebirch.presentation.state.Resource +import co.joebirch.presentation.state.ResourceState +import io.reactivex.observers.DisposableCompletableObserver +import io.reactivex.observers.DisposableObserver +import javax.inject.Inject + +class BrowseProjectsViewModel @Inject constructor( + private val getProjects: GetProjects, + private val bookmarkProject: BookmarkProject, + private val unBookmarkProject: UnBookmarkProject, + private val mapper: ProjectViewMapper): ViewModel() { + + private val liveData: MutableLiveData>> = MutableLiveData() + + override fun onCleared() { + getProjects.dispose() + super.onCleared() + } + + fun getProjects(): LiveData>> { + return liveData + } + + fun fetchProjects() { + liveData.postValue(Resource(ResourceState.LOADING, null, null)) + return getProjects.execute(ProjectsSubscriber()) + } + + fun bookmarkProject(projectId: String) { + liveData.postValue(Resource(ResourceState.LOADING, null, null)) + return bookmarkProject.execute(BookmarkProjectsSubscriber(), + BookmarkProject.Params.forProject(projectId)) + } + + fun unbookmarkProject(projectId: String) { + liveData.postValue(Resource(ResourceState.LOADING, null, null)) + return unBookmarkProject.execute(BookmarkProjectsSubscriber(), + UnBookmarkProject.Params.forProject(projectId)) + } + + inner class ProjectsSubscriber: DisposableObserver>() { + override fun onNext(t: List) { + liveData.postValue(Resource(ResourceState.SUCCESS, + t.map { mapper.mapToView(it) }, null)) + } + + override fun onComplete() { } + + override fun onError(e: Throwable) { + liveData.postValue(Resource(ResourceState.ERROR, null, e.localizedMessage)) + } + + } + + inner class BookmarkProjectsSubscriber: DisposableCompletableObserver() { + override fun onComplete() { + liveData.postValue(Resource(ResourceState.SUCCESS, liveData.value?.data, null)) + } + + override fun onError(e: Throwable) { + liveData.postValue(Resource(ResourceState.ERROR, liveData.value?.data, + e.localizedMessage)) + } + + } +} \ No newline at end of file diff --git a/Presentation/src/main/java/co/joebirch/presentation/mapper/Mapper.kt b/Presentation/src/main/java/co/joebirch/presentation/mapper/Mapper.kt new file mode 100644 index 0000000..e8fb8d7 --- /dev/null +++ b/Presentation/src/main/java/co/joebirch/presentation/mapper/Mapper.kt @@ -0,0 +1,7 @@ +package co.joebirch.presentation.mapper + +interface Mapper { + + fun mapToView(type: D): V + +} \ No newline at end of file diff --git a/Presentation/src/main/java/co/joebirch/presentation/mapper/ProjectViewMapper.kt b/Presentation/src/main/java/co/joebirch/presentation/mapper/ProjectViewMapper.kt new file mode 100644 index 0000000..81affa1 --- /dev/null +++ b/Presentation/src/main/java/co/joebirch/presentation/mapper/ProjectViewMapper.kt @@ -0,0 +1,14 @@ +package co.joebirch.presentation.mapper + +import co.joebirch.domain.model.Project +import co.joebirch.presentation.model.ProjectView +import javax.inject.Inject + +class ProjectViewMapper @Inject constructor(): Mapper { + + override fun mapToView(type: Project): ProjectView { + return ProjectView(type.id, type.name, type.fullName, + type.starCount, type.dateCreated, type.ownerName, + type.ownerAvatar) + } +} \ No newline at end of file diff --git a/Presentation/src/main/java/co/joebirch/presentation/model/ProjectView.kt b/Presentation/src/main/java/co/joebirch/presentation/model/ProjectView.kt new file mode 100644 index 0000000..306aa34 --- /dev/null +++ b/Presentation/src/main/java/co/joebirch/presentation/model/ProjectView.kt @@ -0,0 +1,5 @@ +package co.joebirch.presentation.model + +class ProjectView(val id: String, val name: String, val fullName: String, + val starCount: String, val dateCreated: String, + val ownerName: String, val ownerAvatar: String) \ No newline at end of file diff --git a/Presentation/src/main/java/co/joebirch/presentation/state/Resource.kt b/Presentation/src/main/java/co/joebirch/presentation/state/Resource.kt new file mode 100644 index 0000000..19f6787 --- /dev/null +++ b/Presentation/src/main/java/co/joebirch/presentation/state/Resource.kt @@ -0,0 +1,19 @@ +package co.joebirch.presentation.state + +class Resource constructor(val status: ResourceState, + val data: T?, + val message: String?) { + + fun success(data: T): Resource { + return Resource(ResourceState.SUCCESS, data, null) + } + + fun error(message: String?): Resource { + return Resource(ResourceState.ERROR, null, message) + } + + fun loading(): Resource { + return Resource(ResourceState.LOADING, null, null) + } + +} \ No newline at end of file diff --git a/Presentation/src/main/java/co/joebirch/presentation/state/ResourceState.kt b/Presentation/src/main/java/co/joebirch/presentation/state/ResourceState.kt new file mode 100644 index 0000000..1edfee8 --- /dev/null +++ b/Presentation/src/main/java/co/joebirch/presentation/state/ResourceState.kt @@ -0,0 +1,5 @@ +package co.joebirch.presentation.state + +enum class ResourceState { + LOADING, SUCCESS, ERROR +} \ No newline at end of file diff --git a/Presentation/src/main/res/values/strings.xml b/Presentation/src/main/res/values/strings.xml new file mode 100644 index 0000000..598ee60 --- /dev/null +++ b/Presentation/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Presentation + diff --git a/Presentation/src/test/java/org/buffer/android/presentation/ExampleUnitTest.java b/Presentation/src/test/java/org/buffer/android/presentation/ExampleUnitTest.java new file mode 100644 index 0000000..8c21615 --- /dev/null +++ b/Presentation/src/test/java/org/buffer/android/presentation/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package org.buffer.android.presentation; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 4b54750..8868a19 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -14,12 +14,13 @@ ext { javaxInjectVersion = '1' rxJavaVersion = '2.0.2' rxKotlinVersion = '2.1.0' + rxAndroidVersion = '2.0.1' androidAnnotationsVersion = '21.0.3' daggerVersion = '2.11' gsonVersion = '2.8.1' okHttpVersion = '3.8.1' retrofitVersion = '2.3.0' - roomVersion = '1.0.0' + archCompVersion = '1.0.0' //Testing jUnitVersion = '4.12' @@ -66,9 +67,9 @@ ext { javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", - roomRuntime: "android.arch.persistence.room:runtime:${roomVersion}", - roomCompiler: "android.arch.persistence.room:compiler:${roomVersion}", - roomRxJava: "android.arch.persistence.room:rxjava2:${roomVersion}" + roomRuntime: "android.arch.persistence.room:runtime:${archCompVersion}", + roomCompiler: "android.arch.persistence.room:compiler:${archCompVersion}", + roomRxJava: "android.arch.persistence.room:rxjava2:${archCompVersion}" ] cacheTestDependencies = [ @@ -76,8 +77,8 @@ ext { kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", assertj: "org.assertj:assertj-core:${assertJVersion}", mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", - roomTesting: "android.arch.persistence.room:testing:${roomVersion}", - archTesting: "android.arch.core:core-testing:${roomVersion}", + roomTesting: "android.arch.persistence.room:testing:${archCompVersion}", + archTesting: "android.arch.core:core-testing:${archCompVersion}", supportRunner: "com.android.support.test:runner:${androidSupportRunnerVersion}", supportRules: "com.android.support.test:rules:${androidSupportRulesVersion}" ] @@ -103,4 +104,26 @@ ext { mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}" ] + presentationDependencies = [ + daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", + dagger: "com.google.dagger:dagger:${daggerVersion}", + rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", + kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", + rxAndroid: "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", + javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", + javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", + androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", + archRuntime: "android.arch.lifecycle:runtime:${archCompVersion}", + archExtensions: "android.arch.lifecycle:extensions:${archCompVersion}", + archCompiler: "android.arch.lifecycle:compiler:${archCompVersion}", + ] + + presentationTestDependencies = [ + junit: "junit:junit:${jUnitVersion}", + kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", + assertj: "org.assertj:assertj-core:${assertJVersion}", + mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", + robolectric: "org.robolectric:robolectric:${robolectricVersion}" + ] + } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index ee3d8b0..c2247e1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':Domain', ':Data', ':Remote', ':cache' +include ':Domain', ':Data', ':Remote', ':cache', ':Presentation' From 46fb608b4cd2504cb79ed053a769eb220aef4308 Mon Sep 17 00:00:00 2001 From: Joe Birch Date: Tue, 24 Apr 2018 13:33:03 +0100 Subject: [PATCH 18/42] Create ui layer --- .../BrowseBookmarkedProjectsViewModel.kt | 51 ------ .../presentation/BrowseProjectsViewModel.kt | 77 -------- .../mapper/ProjectsResponseModelMapper.kt | 2 +- build.gradle | 2 +- cache/build.gradle | 2 +- dependencies.gradle | 49 ++++- gradle/wrapper/gradle-wrapper.properties | 4 +- mobile-ui/.gitignore | 1 + mobile-ui/build.gradle | 94 ++++++++++ mobile-ui/proguard-rules.pro | 21 +++ .../mobile_ui/ExampleInstrumentedTest.java | 26 +++ mobile-ui/src/main/AndroidManifest.xml | 27 +++ .../co/joebirch/mobile_ui/BrowseActivity.kt | 21 +++ .../mobile_ui/GithubTrendingApplication.kt | 35 ++++ .../java/co/joebirch/mobile_ui/UiThread.kt | 12 ++ .../injection/ApplicationComponent.kt | 26 +++ .../injection/module/ApplicationModule.kt | 8 + .../mobile_ui/mapper/ProjectViewMapper.kt | 16 ++ .../joebirch/mobile_ui/mapper/ViewMapper.kt | 7 + .../co/joebirch/mobile_ui/model/Project.kt | 5 + .../drawable-v24/ic_launcher_foreground.xml | 34 ++++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++++ .../src/main/res/layout/activity_browse.xml | 21 +++ .../src/main/res/layout/content_main.xml | 11 ++ .../src/main/res/layout/item_project.xml | 22 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes mobile-ui/src/main/res/values/colors.xml | 6 + mobile-ui/src/main/res/values/dimens.xml | 3 + mobile-ui/src/main/res/values/strings.xml | 4 + mobile-ui/src/main/res/values/styles.xml | 20 +++ .../joebirch/mobile_ui}/ExampleUnitTest.java | 4 +- settings.gradle | 2 +- 43 files changed, 656 insertions(+), 137 deletions(-) delete mode 100644 Presentation/src/main/java/co/joebirch/presentation/BrowseBookmarkedProjectsViewModel.kt delete mode 100644 Presentation/src/main/java/co/joebirch/presentation/BrowseProjectsViewModel.kt create mode 100644 mobile-ui/.gitignore create mode 100644 mobile-ui/build.gradle create mode 100644 mobile-ui/proguard-rules.pro create mode 100644 mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/ExampleInstrumentedTest.java create mode 100644 mobile-ui/src/main/AndroidManifest.xml create mode 100644 mobile-ui/src/main/java/co/joebirch/mobile_ui/BrowseActivity.kt create mode 100644 mobile-ui/src/main/java/co/joebirch/mobile_ui/GithubTrendingApplication.kt create mode 100644 mobile-ui/src/main/java/co/joebirch/mobile_ui/UiThread.kt create mode 100644 mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/ApplicationComponent.kt create mode 100644 mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/ApplicationModule.kt create mode 100644 mobile-ui/src/main/java/co/joebirch/mobile_ui/mapper/ProjectViewMapper.kt create mode 100644 mobile-ui/src/main/java/co/joebirch/mobile_ui/mapper/ViewMapper.kt create mode 100644 mobile-ui/src/main/java/co/joebirch/mobile_ui/model/Project.kt create mode 100644 mobile-ui/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 mobile-ui/src/main/res/drawable/ic_launcher_background.xml create mode 100644 mobile-ui/src/main/res/layout/activity_browse.xml create mode 100644 mobile-ui/src/main/res/layout/content_main.xml create mode 100644 mobile-ui/src/main/res/layout/item_project.xml create mode 100644 mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile-ui/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile-ui/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile-ui/src/main/res/values/colors.xml create mode 100644 mobile-ui/src/main/res/values/dimens.xml create mode 100644 mobile-ui/src/main/res/values/strings.xml create mode 100644 mobile-ui/src/main/res/values/styles.xml rename {Presentation/src/test/java/org/buffer/android/presentation => mobile-ui/src/test/java/co/joebirch/mobile_ui}/ExampleUnitTest.java (76%) diff --git a/Presentation/src/main/java/co/joebirch/presentation/BrowseBookmarkedProjectsViewModel.kt b/Presentation/src/main/java/co/joebirch/presentation/BrowseBookmarkedProjectsViewModel.kt deleted file mode 100644 index 81f3eea..0000000 --- a/Presentation/src/main/java/co/joebirch/presentation/BrowseBookmarkedProjectsViewModel.kt +++ /dev/null @@ -1,51 +0,0 @@ -package co.joebirch.presentation - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.ViewModel -import co.joebirch.domain.interactor.bookmarked.GetBookmarkedProjects -import co.joebirch.domain.model.Project -import co.joebirch.presentation.mapper.ProjectViewMapper -import co.joebirch.presentation.model.ProjectView -import co.joebirch.presentation.state.Resource -import co.joebirch.presentation.state.ResourceState -import io.reactivex.observers.DisposableObserver -import javax.inject.Inject - -class BrowseBookmarkedProjectsViewModel @Inject constructor( - private val getBookmarkedProjects: GetBookmarkedProjects, - private val mapper: ProjectViewMapper): ViewModel() { - - private val liveData: MutableLiveData>> = - MutableLiveData() - - override fun onCleared() { - getBookmarkedProjects.dispose() - super.onCleared() - } - - fun getProjects(): LiveData>> { - return liveData - } - - fun fetchProjects() { - liveData.postValue(Resource(ResourceState.LOADING, null, null)) - return getBookmarkedProjects.execute(ProjectsSubscriber()) - } - - inner class ProjectsSubscriber: DisposableObserver>() { - override fun onNext(t: List) { - liveData.postValue(Resource(ResourceState.SUCCESS, - t.map { mapper.mapToView(it) }, null)) - } - - override fun onError(e: Throwable) { - liveData.postValue(Resource(ResourceState.ERROR, null, - e.localizedMessage)) - } - - override fun onComplete() { } - - } - -} \ No newline at end of file diff --git a/Presentation/src/main/java/co/joebirch/presentation/BrowseProjectsViewModel.kt b/Presentation/src/main/java/co/joebirch/presentation/BrowseProjectsViewModel.kt deleted file mode 100644 index 69ce5e5..0000000 --- a/Presentation/src/main/java/co/joebirch/presentation/BrowseProjectsViewModel.kt +++ /dev/null @@ -1,77 +0,0 @@ -package co.joebirch.presentation - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.ViewModel -import co.joebirch.domain.interactor.browse.BookmarkProject -import co.joebirch.domain.interactor.browse.GetProjects -import co.joebirch.domain.interactor.browse.UnBookmarkProject -import co.joebirch.domain.model.Project -import co.joebirch.presentation.mapper.ProjectViewMapper -import co.joebirch.presentation.model.ProjectView -import co.joebirch.presentation.state.Resource -import co.joebirch.presentation.state.ResourceState -import io.reactivex.observers.DisposableCompletableObserver -import io.reactivex.observers.DisposableObserver -import javax.inject.Inject - -class BrowseProjectsViewModel @Inject constructor( - private val getProjects: GetProjects, - private val bookmarkProject: BookmarkProject, - private val unBookmarkProject: UnBookmarkProject, - private val mapper: ProjectViewMapper): ViewModel() { - - private val liveData: MutableLiveData>> = MutableLiveData() - - override fun onCleared() { - getProjects.dispose() - super.onCleared() - } - - fun getProjects(): LiveData>> { - return liveData - } - - fun fetchProjects() { - liveData.postValue(Resource(ResourceState.LOADING, null, null)) - return getProjects.execute(ProjectsSubscriber()) - } - - fun bookmarkProject(projectId: String) { - liveData.postValue(Resource(ResourceState.LOADING, null, null)) - return bookmarkProject.execute(BookmarkProjectsSubscriber(), - BookmarkProject.Params.forProject(projectId)) - } - - fun unbookmarkProject(projectId: String) { - liveData.postValue(Resource(ResourceState.LOADING, null, null)) - return unBookmarkProject.execute(BookmarkProjectsSubscriber(), - UnBookmarkProject.Params.forProject(projectId)) - } - - inner class ProjectsSubscriber: DisposableObserver>() { - override fun onNext(t: List) { - liveData.postValue(Resource(ResourceState.SUCCESS, - t.map { mapper.mapToView(it) }, null)) - } - - override fun onComplete() { } - - override fun onError(e: Throwable) { - liveData.postValue(Resource(ResourceState.ERROR, null, e.localizedMessage)) - } - - } - - inner class BookmarkProjectsSubscriber: DisposableCompletableObserver() { - override fun onComplete() { - liveData.postValue(Resource(ResourceState.SUCCESS, liveData.value?.data, null)) - } - - override fun onError(e: Throwable) { - liveData.postValue(Resource(ResourceState.ERROR, liveData.value?.data, - e.localizedMessage)) - } - - } -} \ No newline at end of file diff --git a/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt index 75259e6..fca5d1a 100644 --- a/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt +++ b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt @@ -7,7 +7,7 @@ class ProjectsResponseModelMapper: ModelMapper { override fun mapFromModel(model: ProjectModel): ProjectEntity { return ProjectEntity(model.id, model.name, model.fullName, model.starCount.toString(), - model.dateCreated, model.owner.ownerName, model.owner.ownerAvatar) + model.dateCreated, model.owner.ownerName, model.owner.ownerAvatar, true) } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index a1e0904..4e04ca8 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/cache/build.gradle b/cache/build.gradle index 434051c..4246e19 100644 --- a/cache/build.gradle +++ b/cache/build.gradle @@ -20,7 +20,7 @@ android { } } - buildToolsVersion '26.0.2' + buildToolsVersion '27.0.3' } dependencies { diff --git a/dependencies.gradle b/dependencies.gradle index 8868a19..7db38cb 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,7 +1,7 @@ ext { //Android - androidBuildToolsVersion = "26.0.0" + androidBuildToolsVersion = "27.0.3" androidMinSdkVersion = 21 androidTargetSdkVersion = 26 androidCompileSdkVersion = 26 @@ -21,15 +21,24 @@ ext { okHttpVersion = '3.8.1' retrofitVersion = '2.3.0' archCompVersion = '1.0.0' + supportLibraryVersion = '26.1.0' + timberVersion = '4.5.1' + glideVersion = '4.0.0' + daggerVersion = '2.11' + glassfishAnnotationVersion = '10.0-b28' //Testing jUnitVersion = '4.12' assertJVersion = '3.8.0' mockitoKotlinVersion = '1.5.0' + espressoVersion = '3.0.0' robolectricVersion = '3.4.2' mockitoVersion = '1.9.5' + mockitoAndroidVersion = '2.8.47' androidSupportRunnerVersion = '1.0.0' androidSupportRulesVersion = '1.0.0' + dexmakerMockitoversion = '2.2.0' + runnerVersion = '0.5' domainDependencies = [ javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", @@ -126,4 +135,42 @@ ext { robolectric: "org.robolectric:robolectric:${robolectricVersion}" ] + mobileUiDependencies = [ + daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", + dagger: "com.google.dagger:dagger:${daggerVersion}", + rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", + rxAndroid: "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", + glide: "com.github.bumptech.glide:glide:${glideVersion}", + kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", + javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", + javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", + androidAnnotations: "com.android.support:support-annotations:${supportLibraryVersion}", + androidSupportV4: "com.android.support:support-v4:${supportLibraryVersion}", + androidSupportV13: "com.android.support:support-v13:${supportLibraryVersion}", + appCompatV7: "com.android.support:appcompat-v7:${supportLibraryVersion}", + supportRecyclerView:"com.android.support:recyclerview-v7:${supportLibraryVersion}", + supportDesign: "com.android.support:design:${supportLibraryVersion}", + timber: "com.jakewharton.timber:timber:${timberVersion}", + daggerSupport: "com.google.dagger:dagger-android-support:${daggerVersion}", + daggerProcessor: "com.google.dagger:dagger-android-processor:${daggerVersion}", + glassfishAnnotation: "org.glassfish:javax.annotation:${glassfishAnnotationVersion}", + roomRuntime: "android.arch.persistence.room:runtime:${archCompVersion}", + roomCompiler: "android.arch.persistence.room:compiler:${archCompVersion}", + roomRxJava: "android.arch.persistence.room:rxjava2:${archCompVersion}", + ] + + mobileUiTestDependencies = [ + junit: "junit:junit:${jUnitVersion}", + kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", + assertj: "org.assertj:assertj-core:${assertJVersion}", + mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", + supportRunner: "com.android.support.test:runner:${androidSupportRunnerVersion}", + supportRules: "com.android.support.test:rules:${androidSupportRulesVersion}", + mockitoAndroid: "org.mockito:mockito-android:${mockitoAndroidVersion}", + espressoCore: "com.android.support.test.espresso:espresso-core:${espressoVersion}", + espressoIntents: "com.android.support.test.espresso:espresso-intents:${espressoVersion}", + espressoContrib: "com.android.support.test.espresso:espresso-contrib:${espressoVersion}", + androidRunner: "com.android.support.test:runner:${runnerVersion}", + androidRules: "com.android.support.test:rules:${runnerVersion}" + ] } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a18941a..5110321 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Nov 28 12:35:12 GMT 2017 +#Sun Apr 08 15:03:52 BST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/mobile-ui/.gitignore b/mobile-ui/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/mobile-ui/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mobile-ui/build.gradle b/mobile-ui/build.gradle new file mode 100644 index 0000000..acea1f5 --- /dev/null +++ b/mobile-ui/build.gradle @@ -0,0 +1,94 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + def globalConfiguration = rootProject.extensions.getByName("ext") + + compileSdkVersion globalConfiguration["androidCompileSdkVersion"] + buildToolsVersion globalConfiguration["androidBuildToolsVersion"] + + defaultConfig { + minSdkVersion globalConfiguration["androidMinSdkVersion"] + targetSdkVersion globalConfiguration["androidTargetSdkVersion"] + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + def mobileUiDependencies = rootProject.ext.mobileUiDependencies + def mobileUiTestDependencies = rootProject.ext.mobileUiTestDependencies + + implementation 'com.android.support.constraint:constraint-layout:1.1.0' + compile project(':Presentation') + compile project(':Data') + compile project(':Remote') + compile project(':cache') + + implementation mobileUiDependencies.javaxAnnotation + + implementation mobileUiDependencies.kotlin + implementation mobileUiDependencies.javaxInject + implementation mobileUiDependencies.rxKotlin + implementation mobileUiDependencies.androidAnnotations + implementation mobileUiDependencies.androidSupportV4 + implementation mobileUiDependencies.androidSupportV13 + implementation mobileUiDependencies.appCompatV7 + implementation mobileUiDependencies.supportRecyclerView + implementation mobileUiDependencies.supportDesign + implementation mobileUiDependencies.timber + implementation mobileUiDependencies.rxAndroid + implementation mobileUiDependencies.glide + implementation mobileUiDependencies.dagger + implementation mobileUiDependencies.daggerSupport + + compile presentationDependencies.archRuntime + compile presentationDependencies.archExtensions + compile "android.arch.persistence.room:rxjava2:1.0.0" + kapt presentationDependencies.archCompiler + + testImplementation mobileUiTestDependencies.kotlinJUnit + + kapt mobileUiDependencies.daggerCompiler + kapt mobileUiDependencies.daggerProcessor + compileOnly mobileUiDependencies.glassfishAnnotation + + // Instrumentation test dependencies + androidTestImplementation mobileUiTestDependencies.junit + androidTestImplementation mobileUiTestDependencies.mockito + androidTestImplementation mobileUiTestDependencies.mockitoAndroid + androidTestImplementation(mobileUiTestDependencies.espressoCore) { + exclude group: 'com.android.support', module: 'support-annotations' + } + androidTestImplementation(mobileUiTestDependencies.androidRunner) { + exclude group: 'com.android.support', module: 'support-annotations' + } + androidTestImplementation(mobileUiTestDependencies.androidRules) { + exclude group: 'com.android.support', module: 'support-annotations' + } + androidTestImplementation(mobileUiTestDependencies.espressoIntents) { + exclude group: 'com.android.support', module: 'support-annotations' + } + androidTestImplementation(mobileUiTestDependencies.espressoContrib) { + exclude module: 'appcompat' + exclude module: 'appcompat-v7' + exclude module: 'support-v4' + exclude module: 'support-v13' + exclude module: 'support-annotations' + exclude module: 'recyclerview-v7' + exclude module: 'design' + } + + kaptTest mobileUiDependencies.daggerCompiler + kaptAndroidTest mobileUiDependencies.daggerCompiler +} + +apply plugin: 'kotlin-android-extensions' diff --git a/mobile-ui/proguard-rules.pro b/mobile-ui/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/mobile-ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/ExampleInstrumentedTest.java b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/ExampleInstrumentedTest.java new file mode 100644 index 0000000..d176b54 --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package co.joebirch.mobile_ui; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("co.joebirch.mobile_ui", appContext.getPackageName()); + } +} diff --git a/mobile-ui/src/main/AndroidManifest.xml b/mobile-ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ad9c28b --- /dev/null +++ b/mobile-ui/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/BrowseActivity.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/BrowseActivity.kt new file mode 100644 index 0000000..8cb16da --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/BrowseActivity.kt @@ -0,0 +1,21 @@ +package co.joebirch.mobile_ui + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.LinearLayoutManager +import kotlinx.android.synthetic.main.activity_browse.* + +class BrowseActivity: AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_browse) + + setupBrowseRecycler() + } + + private fun setupBrowseRecycler() { + recycler_projects.layoutManager = LinearLayoutManager(this) + } + +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/GithubTrendingApplication.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/GithubTrendingApplication.kt new file mode 100644 index 0000000..8cfd9ad --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/GithubTrendingApplication.kt @@ -0,0 +1,35 @@ +package co.joebirch.mobile_ui + +import android.app.Activity +import android.app.Application +import co.joebirch.mobile_ui.injection.DaggerApplicationComponent +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasActivityInjector +import timber.log.Timber +import javax.inject.Inject + +class GithubTrendingApplication: Application(), HasActivityInjector { + + @Inject lateinit var androidInjector: DispatchingAndroidInjector + + override fun activityInjector(): AndroidInjector { + return androidInjector + } + + override fun onCreate() { + super.onCreate() + setupTimber() + + DaggerApplicationComponent + .builder() + .application(this) + .build() + .inject(this) + } + + private fun setupTimber() { + Timber.plant(Timber.DebugTree()) + } + +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/UiThread.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/UiThread.kt new file mode 100644 index 0000000..58ae389 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/UiThread.kt @@ -0,0 +1,12 @@ +package co.joebirch.mobile_ui + +import co.joebirch.domain.executor.PostExecutionThread +import io.reactivex.Scheduler +import io.reactivex.android.schedulers.AndroidSchedulers +import javax.inject.Inject + +class UiThread @Inject constructor(): PostExecutionThread { + + override val scheduler: Scheduler + get() = AndroidSchedulers.mainThread() +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/ApplicationComponent.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/ApplicationComponent.kt new file mode 100644 index 0000000..883dccc --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/ApplicationComponent.kt @@ -0,0 +1,26 @@ +package co.joebirch.mobile_ui.injection + +import android.app.Application +import co.joebirch.mobile_ui.GithubTrendingApplication +import co.joebirch.mobile_ui.injection.module.ApplicationModule +import dagger.BindsInstance +import dagger.Component +import dagger.android.AndroidInjectionModule +import javax.inject.Singleton + +@Singleton +@Component(modules = arrayOf(AndroidInjectionModule::class, + ApplicationModule::class)) +interface ApplicationComponent { + + @Component.Builder + interface Builder { + @BindsInstance + fun application(application: Application): Builder + + fun build(): ApplicationComponent + } + + fun inject(app: GithubTrendingApplication) + +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/ApplicationModule.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/ApplicationModule.kt new file mode 100644 index 0000000..c75a3ff --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/ApplicationModule.kt @@ -0,0 +1,8 @@ +package co.joebirch.mobile_ui.injection.module + +import dagger.Module + +@Module +class ApplicationModule { + +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/mapper/ProjectViewMapper.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/mapper/ProjectViewMapper.kt new file mode 100644 index 0000000..456e5c9 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/mapper/ProjectViewMapper.kt @@ -0,0 +1,16 @@ +package co.joebirch.mobile_ui.mapper + +import co.joebirch.mobile_ui.model.Project +import co.joebirch.presentation.model.ProjectView +import javax.inject.Inject + +class ProjectViewMapper @Inject constructor(): ViewMapper { + + override fun mapToView(presentation: ProjectView): Project { + return Project(presentation.id, presentation.name, + presentation.fullName, presentation.starCount, + presentation.dateCreated, presentation.ownerName, + presentation.ownerAvatar) + } + +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/mapper/ViewMapper.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/mapper/ViewMapper.kt new file mode 100644 index 0000000..9256c59 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/mapper/ViewMapper.kt @@ -0,0 +1,7 @@ +package co.joebirch.mobile_ui.mapper + +interface ViewMapper { + + fun mapToView(presentation: P): V + +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/model/Project.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/model/Project.kt new file mode 100644 index 0000000..b492342 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/model/Project.kt @@ -0,0 +1,5 @@ +package co.joebirch.mobile_ui.model + +class Project(val id: String, val name: String, val fullName: String, + val starCount: String, val dateCreated: String, + val ownerName: String, val ownerAvatar: String) \ No newline at end of file diff --git a/mobile-ui/src/main/res/drawable-v24/ic_launcher_foreground.xml b/mobile-ui/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/mobile-ui/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/mobile-ui/src/main/res/drawable/ic_launcher_background.xml b/mobile-ui/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/mobile-ui/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-ui/src/main/res/layout/activity_browse.xml b/mobile-ui/src/main/res/layout/activity_browse.xml new file mode 100644 index 0000000..1a9a1e2 --- /dev/null +++ b/mobile-ui/src/main/res/layout/activity_browse.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/mobile-ui/src/main/res/layout/content_main.xml b/mobile-ui/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..c649451 --- /dev/null +++ b/mobile-ui/src/main/res/layout/content_main.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/mobile-ui/src/main/res/layout/item_project.xml b/mobile-ui/src/main/res/layout/item_project.xml new file mode 100644 index 0000000..d7ca6cb --- /dev/null +++ b/mobile-ui/src/main/res/layout/item_project.xml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mobile-ui/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile-ui/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f5908281d070150700378b64a84c7db1f97aa1 GIT binary patch literal 3056 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 axPAxe%7rDusX+w*00006jj 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⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* literal 0 HcmV?d00001 diff --git a/mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..459ca609d3ae0d3943ab44cdc27feef9256dc6d7 GIT binary patch literal 7098 zcmV;r8%5-aP)U(QdAI7f)tS=AhH53iU?Q%B}x&gA$2B`o|*LCD1jhW zSQpS0{*?u3iXtkY?&2<)$@#zc%$?qDlF1T~d7k&lWaiv^&wbx>zVm(GIrof<%iY)A zm%|rhEg~Z$Te<*wd9Cb1SB{RkOI$-=MBtc%k*xtvYC~Uito}R@3fRUqJvco z|Bt2r9pSOcJocAEd)UN^Tz-82GUZlqsU;wb|2Q_1!4Rms&HO1Xyquft~#6lJoR z`$|}VSy@{k6U652FJ~bnD9(X%>CS6Wp6U>sn;f}te}%WL`rg)qE4Q=4OOhk^@ykw( ziKr^LHnAd4M?#&SQhw8zaC05q#Mc66K^mxY!dZ=W+#Bq1B}cQ6Y8FWd(n>#%{8Di_8$CHibtvP z-x#-g;~Q?y0vJA*8TW>ZxF?fAy1DuFy7%O1ylLF(t=ah7LjZ$=p!;8(ZLjXAhwEkCR{wF`L=hwm>|vLK2=gR&KM1ZEG9R~53yNCZdabQoQ%VsolX zS#WlesPcpJ)7XLo6>Ly$im38oxyiizP&&>***e@KqUk3q3y+LQN^-v?ZmO>9O{Oq@ z{{He$*Z=Kf_FPR>El3iB*FULYFMnLa#Fl^l&|bFg$Omlh{xVVJ7uHm=4WE6)NflH6 z=>z4w{GV&8#MNnEY3*B7pXU!$9v-tZvdjO}9O=9r{3Wxq2QB}(n%%YI$)pS~NEd}U z)n#nv-V)K}kz9M0$hogDLsa<(OS0Hf5^WUKO-%WbR1W1ID$NpAegxHH;em?U$Eyn1 zU{&J2@WqSUn0tav=jR&&taR9XbV+Izb*PwFn|?cv0mksBdOWeGxNb~oR;`~>#w3bp zrOrEQ+BiW_*f&GARyW|nE}~oh0R>>AOH^>NHNKe%%sXLgWRu1Sy3yW0Q#L{8Y6=3d zKd=By=Nb8?#W6|LrpZm>8Ro)`@cLmU;D`d64nKT~6Z!aLOS{m`@oYwD`9yily@}%yr0A>P!6O4G|ImNbBzI`LJ0@=TfLt^f`M07vw_PvXvN{nx%4 zD8vS>8*2N}`lD>M{`v?2!nYnf%+`GRK3`_i+yq#1a1Yx~_1o~-$2@{=r~q11r0oR* zqBhFFVZFx!U0!2CcItqLs)C;|hZ|9zt3k^(2g32!KB-|(RhKbq-vh|uT>jT@tX8dN zH`TT5iytrZT#&8u=9qt=oV`NjC)2gWl%KJ;n63WwAe%-)iz&bK{k`lTSAP`hr)H$Q`Yq8-A4PBBuP*-G#hSKrnmduy6}G zrc+mcVrrxM0WZ__Y#*1$mVa2y=2I`TQ%3Vhk&=y!-?<4~iq8`XxeRG!q?@l&cG8;X zQ(qH=@6{T$$qk~l?Z0@I4HGeTG?fWL67KN#-&&CWpW0fUm}{sBGUm)Xe#=*#W{h_i zohQ=S{=n3jDc1b{h6oTy=gI!(N%ni~O$!nBUig}9u1b^uI8SJ9GS7L#s!j;Xy*CO>N(o6z){ND5WTew%1lr? znp&*SAdJb5{L}y7q#NHbY;N_1vn!a^3TGRzCKjw?i_%$0d2%AR73CwHf z`h4QFmE-7G=psYnw)B!_Cw^{=!UNZeR{(s47|V$`3;-*gneX=;O+eN@+Efd_Zt=@H3T@v&o^%H z7QgDF8g>X~$4t9pv35G{a_8Io>#>uGRHV{2PSk#Ea~^V8!n@9C)ZH#87~ z#{~PUaRR~4K*m4*PI16)rvzdaP|7sE8SyMQYI6!t(%JNebR%?lc$={$s?VBI0Qk!A zvrE4|#asTZA|5tB{>!7BcxOezR?QIo4U_LU?&9Im-liGSc|TrJ>;1=;W?gG)0pQaw z|6o7&I&PH!*Z=c7pNPkp)1(4W`9Z01*QKv44FkvF^2Kdz3gDNpV=A6R;Q}~V-_sZY zB9DB)F8%iFEjK?Gf4$Cwu_hA$98&pkrJM!7{l+}osR_aU2PEx!1CRCKsS`0v$LlKq z{Pg#ZeoBMv@6BcmK$-*|S9nv50or*2&EV`L7PfW$2J7R1!9Q(1SSe42eSWZ5sYU?g z2v{_QB^^jfh$)L?+|M`u-E7D=Hb?7@9O89!bRUSI7uD?Mxh63j5!4e(v)Kc&TUEqy z8;f`#(hwrIeW);FA0CK%YHz6;(WfJz^<&W#y0N3O2&Qh_yxHu?*8z1y9Ua}rECL!5 z7L1AEXx83h^}+)cY*Ko{`^0g3GtTuMP>b$kq;Aqo+2d&+48mc#DP;Sv z*UL^nR*K7J968xR0_eTaZ`N`u_c#9bFUjTj-}0+_57(gtEJT|7PA12W=2Z>#_a z&Wg@_b=$d~wonN3h~?)gS`qxx<4J&`dI*rH9!mTSiQj(0rF-{YoNJRnOqd5IbP7p} ztDaPu$A;#osxf=z2zVe4>tpa(knS_Mp67nKcE<>Cj$G2orP(Z$Oc4;4DPwbXYZsS^ z;b>59s(LgYmx|tkRD?U{+9VZ$T}{S}L6>lQNR^a|&5joAFXtOrI07Do!vk(e$mu@Y zNdN!djB`Hq1*T8mrC@S)MLwZ`&8aM8YYtVj7i)IY{g&D1sJaY`3e=1DSFnjO+jEHH zj+|@r$$4RtpuJ!8=C`n5X;5BjU2slP9VV&m0gr+{O(I}9pYF32AMU?n$k$=x;X^E# zOb-x}p1_`@IOXAj3>HFxnmvBV9M^^9CfD7UlfuH*y^aOD?X6D82p_r*c>DF)m=9>o zgv_SDeSF6WkoVOI<_mX};FlW9rk3WgQP|vr-eVo8!wH!TiX)aiw+I|dBWJX=H6zxx z_tSI2$ChOM+?XlJwEz3!juYU6Z_b+vP-Y|m1!|ahw>Kpjrii-M_wmO@f@7;aK(I;p zqWgn+X^onc-*f)V9Vfu?AHLHHK!p2|M`R&@4H0x4hD5#l1##Plb8KsgqGZ{`d+1Ns zQ7N(V#t49wYIm9drzw`;WSa|+W+VW8Zbbx*Z+aXHSoa!c!@3F_yVww58NPH2->~Ls z2++`lSrKF(rBZLZ5_ts6_LbZG-W-3fDq^qI>|rzbc@21?)H>!?7O*!D?dKlL z6J@yulp7;Yk6Bdytq*J1JaR1!pXZz4aXQ{qfLu0;TyPWebr3|*EzCk5%ImpjUI4cP z7A$bJvo4(n2km-2JTfRKBjI9$mnJG@)LjjE9dnG&O=S;fC)@nq9K&eUHAL%yAPX7OFuD$pb_H9nhd{iE0OiI4#F-);A|&YT z|A3tvFLfR`5NYUkE?Rfr&PyUeFX-VHzcss2i*w06vn4{k1R%1_1+Ygx2oFt*HwfT> zd=PFdfFtrP1+YRs0AVr{YVp4Bnw2HQX-|P$M^9&P7pY6XSC-8;O2Ia4c{=t{NRD=z z0DeYUO3n;p%k zNEmBntbNac&5o#&fkY1QSYA4tKqBb=w~c6yktzjyk_Po)A|?nn8>HdA31amaOf7jX z2qillM8t8V#qv5>19Cg_X`mlU*O5|C#X-kfAXAHAD*q%6+z%IK(*H6olm-N4%Ic)5 zL`?wQgXfD&qQRxWskoO^Ylb>`jelq;*~ZIwKw|#BQjOSLkgc2uy7|oFEVhC?pcnU+ z^7qz}Z2%F!WOp%JO3y*&_7t;uRfU>)drR1q)c7lX?;A1-TuLTR zyr(`7O19`eW{ev;L%`;BvOzh?m|)Rh?W8&I$KVvUTo?@f@K!du&vf=o6kKb?hA z%e6$T0jWS7doVkN%^_k3QOksfV?aC$Ge$a)z(!C@UVs*@qzDw*OFd*JfX#>5LCXjE z_vfUrLF7D`K$U2Ld#OCnh9U!;r7%GlKo$e__Il-oba06ER{H&f#J&W@x^^5j;y$0` zs2`m6pf+{UiDb{Mjsb$rH+MCM6G_wX92so96`ODFYKD>!Xz^0y@U7Tc1uON4L<>2f-oPe%FRPEZ@S#-yd7Md-i?v z)$Kgtq;%4g@>Kap3Nl2I&jnCIfGmRmcF4CXfF1H}3SfhLg8=!a0ucGaUk&c3*Ykgl z2X_L84cs+FD#cjf-nMJkVDH%XzOoh5!X-Q$K5VZx-hGF7MQ=XKBjhZZQ@1Sh zO^vY`WQ`zi21z-+01na%<^niMFIWm-n|!?hm4X2HEHkba4YS|+HRoIR=`#Xck@PFXaPjnP z=hC4A*0lumS+gpK=TUN!G;{WqICbMz-V=-lTP^@a#C|E!qH;T00SZh7u#?+?08g0< zV1s%-U-`T@8wGh!3pO^`zUIY{nAED7kBqg!qi&GfOp>57f2PGTV19m z0qU@1PYkf%4z_%;Sq4IY94rS+ie~pwT@O3+tg?#k_=5PIk6tV@< zwLoqM0wBVLkI#`|1w=eYMnc^aRR!t?lnUng>WekR#X!!9mYXL3g^gC7`)S7mmo{y} z9*N!d$s32Nu{cZp#O|UxEZK7eY<7hGcI=lc;HrSVL|HA|S$rhhu_DBT&l+`75d`Sj3LaM~H)P zZuk2&jor6yipafklSsPL-vMo?0yAYXpH3=LveBhkno-3{4VLWL16I-@!RM$Po>&}} zm&PX3-$i>$*yx-THZmvK2q`8Qm7B`(NMR;>VSgoGw}W|G6Xd6v04Zf;HIZ0DZU?@- z39vPe0N8w(9kl$2?eG4T?tLgY5V&aFl%~g;2)aSpi!dl?{hDgsz|3<-M(gPtwP_!n z2aB4tV?d0k+>X`+(HMYfK@qtfDK|mIJeg+A<_i-n+5wkrexFs#V0N&~+{+qJ(wggC*52o2daaRwcu7r;S!!KwguB3!Ei7?IEY ze4V$m{8B4Q^(VK4~Ea!V@@}Gs0HGbR5 zy~WI*21hZuoiK`=O$2a|Uce-Zi2%A*pB|?{gv)n8+_B+i&u8Ys)ePY+UwhBDlzbC& z+N00*-?a8DTC26*(3pKgeMO`fOau^-+c6Qqq}3-dpTsEEH}ds! zT^}8XAWO>c5%+qF%#M8#x_0gC+N%q8h6-%w;qidS%gai<T)vpfYuCHXRx6O-TbC|fnj87X zBESvn(9XlXFMj6%{&BaNQ&;xixaKP)+jJ|%u&?HXvYficY}{%hf?0rNDS-X-0_Jcr zjfj~n?T;~RL#sd4ZED2Jf{*Vj+*1eP9-H+~8X^#Jb?HHabLY)EH{QD@Yh-$M`XXt@3_f-L8nBo~*C?L4~n6M92PCuzX=KFgM*j!B66er$F! z+*M(Wkk`UI@uhrL#IUz-C{K@@xtd&n-PQz%kc}7YeE{{&$?}-*yW$eG*E4jp>B_U!2`2oZuvvitN& z%RN>tE$+Yhtqb1q+xQHbp=W4uKSiIj_LZppR0=hEiVj>P0^Vcr^hu2+#Hqum+}zzo znqZ|M4oD|qd=y&JX-qob`=uqt?o%FJPIVY2w0M7BH>#sx>s#OM#9JF1(3LxMAe-vi ztJeU*G)aksP`5sP9_%|~>Pp{NmMMcay>&D+cI%H}$uSx{Su(yz$)2e$*pS%*+!Zo>DNp(P7 zI%w^D2ceEFUGCtQPKfsKr`x%^dy;Rh>lMKuhA^btz=071W=vV`_xz&m;cvd0`|!3+ z2M6uga6CNvy)%Pjw_X}5+xf###jc+?=>6chZI{BMH=haH^7ipT>(?9{weF3apk<4; z_nZFsi`@oFBXCZE^k9B1x+cH2)~9d(MnfEm;GJxG*IB zU@ly{cOTWk*K1ryX+T7m!6A>VwB-*qfH;b>`AUP19lLSA9HbfppW!={L0K)??SymOCA^V>=tOBLn2c5e ksm9QK-qMKdW>5J419kFO%DdQj-T(jq07*qoM6N<$f+5oB`~Uy| literal 0 HcmV?d00001 diff --git a/mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca12fe024be86e868d14e91120a6902f8e88ac6 GIT binary patch literal 6464 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 B0*L?s literal 0 HcmV?d00001 diff --git a/mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8e19b410a1b15ff180f3dacac19395fe3046cdec GIT binary patch literal 10676 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 literal 0 HcmV?d00001 diff --git a/mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4c19a13c239cb67b8a2134ddd5f325db1d2d5bee GIT binary patch literal 15523 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 BIP(Ai literal 0 HcmV?d00001 diff --git a/mobile-ui/src/main/res/values/colors.xml b/mobile-ui/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/mobile-ui/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/mobile-ui/src/main/res/values/dimens.xml b/mobile-ui/src/main/res/values/dimens.xml new file mode 100644 index 0000000..59a0b0c --- /dev/null +++ b/mobile-ui/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 16dp + diff --git a/mobile-ui/src/main/res/values/strings.xml b/mobile-ui/src/main/res/values/strings.xml new file mode 100644 index 0000000..b5c010d --- /dev/null +++ b/mobile-ui/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + Mobile-UI + MainActivity + diff --git a/mobile-ui/src/main/res/values/styles.xml b/mobile-ui/src/main/res/values/styles.xml new file mode 100644 index 0000000..545b9c6 --- /dev/null +++ b/mobile-ui/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + +