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/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..5d610ac --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/ProjectsDataRepository.kt @@ -0,0 +1,54 @@ +package co.joebirch.data + +import co.joebirch.data.mapper.ProjectMapper +import co.joebirch.data.repository.ProjectsCache +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 io.reactivex.functions.BiFunction +import javax.inject.Inject + +class ProjectsDataRepository @Inject constructor( + private val mapper: ProjectMapper, + private val cache: ProjectsCache, + private val factory: ProjectsDataStoreFactory) + : ProjectsRepository { + + override fun getProjects(): Observable> { + return Observable.zip(cache.areProjectsCached().toObservable(), + cache.isProjectsCacheExpired().toObservable(), + BiFunction> { areCached, isExpired -> + Pair(areCached, isExpired) + }) + .flatMap { + factory.getDataStore(it.first, it.second).getProjects().toObservable() + .distinctUntilChanged() + } + .flatMap { projects -> + factory.getCacheDataStore() + .saveProjects(projects) + .andThen(Observable.just(projects)) + } + .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().toObservable() + .map { it.map { mapper.mapFromEntity(it) } } + } + +} \ No newline at end of file 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..77821f4 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/mapper/ProjectMapper.kt @@ -0,0 +1,20 @@ +package co.joebirch.data.mapper + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.domain.model.Project +import javax.inject.Inject + +open 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, entity.isBookmarked) + } + + override fun mapToEntity(domain: Project): ProjectEntity { + return ProjectEntity(domain.id, domain.name, domain.fullName, + domain.starCount, domain.dateCreated, domain.ownerName, + domain.ownerAvatar, domain.isBookmarked) + } + +} \ 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..35fc9a7 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/model/ProjectEntity.kt @@ -0,0 +1,6 @@ +package co.joebirch.data.model + +data class ProjectEntity(val id: String, val name: String, val fullName: String, + val starCount: String, val dateCreated: String, + val ownerName: String, val ownerAvatar: String, + val isBookmarked: Boolean) \ 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..8e89f71 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/repository/ProjectsCache.kt @@ -0,0 +1,28 @@ +package co.joebirch.data.repository + +import co.joebirch.data.model.ProjectEntity +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Single + +interface ProjectsCache { + + fun clearProjects(): Completable + + fun saveProjects(projects: List): Completable + + fun getProjects(): Flowable> + + fun getBookmarkedProjects(): Flowable> + + fun setProjectAsBookmarked(projectId: String): Completable + + fun setProjectAsNotBookmarked(projectId: String): Completable + + fun areProjectsCached(): Single + + fun setLastCacheTime(lastCache: Long): Completable + + fun isProjectsCacheExpired(): Flowable + +} \ 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..8f95b83 --- /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.Flowable + +interface ProjectsDataStore { + + fun getProjects(): Flowable> + + fun saveProjects(projects: List): Completable + + fun clearProjects(): Completable + + fun getBookmarkedProjects(): Flowable> + + 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/repository/ProjectsRemote.kt b/Data/src/main/java/co/joebirch/data/repository/ProjectsRemote.kt new file mode 100644 index 0000000..cda29d4 --- /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.Flowable + +interface ProjectsRemote { + + fun getProjects(): Flowable> + +} \ 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..bd2d379 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsCacheDataStore.kt @@ -0,0 +1,40 @@ +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.Flowable +import io.reactivex.Observable +import javax.inject.Inject + +open class ProjectsCacheDataStore @Inject constructor( + private val projectsCache: ProjectsCache) + : ProjectsDataStore { + + override fun getProjects(): Flowable> { + 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(): Flowable> { + 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..9ff8525 --- /dev/null +++ b/Data/src/main/java/co/joebirch/data/store/ProjectsDataStoreFactory.kt @@ -0,0 +1,27 @@ +package co.joebirch.data.store + +import co.joebirch.data.repository.ProjectsDataStore +import javax.inject.Inject + +open class ProjectsDataStoreFactory @Inject constructor( + private val projectsCacheDataStore: ProjectsCacheDataStore, + private val projectsRemoteDataStore: ProjectsRemoteDataStore) { + + open fun getDataStore(projectsCached: Boolean, + cacheExpired: Boolean): ProjectsDataStore { + return if (projectsCached && !cacheExpired) { + projectsCacheDataStore + } else { + projectsRemoteDataStore + } + } + + open 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..a9fb012 --- /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.Flowable +import javax.inject.Inject + +open class ProjectsRemoteDataStore @Inject constructor( + private val projectsRemote: ProjectsRemote) + : ProjectsDataStore { + + override fun getProjects(): Flowable> { + 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(): Flowable> { + 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 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..37ce330 --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/ProjectsDataRepositoryTest.kt @@ -0,0 +1,146 @@ +package co.joebirch.data + +import co.joebirch.data.mapper.ProjectMapper +import co.joebirch.data.model.ProjectEntity +import co.joebirch.data.repository.ProjectsCache +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.whenever +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +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 cache = mock() + private val repository = ProjectsDataRepository(mapper, cache, factory) + + @Before + fun setup() { + stubFactoryGetDataStore() + stubFactoryGetCacheDataStore() + stubIsCacheExpired(Single.just(false)) + stubAreProjectsCached(Single.just(false)) + stubSaveProjects(Completable.complete()) + } + + @Test + fun getProjectsCompletes() { + stubGetProjects(Observable.just(listOf(ProjectFactory.makeProjectEntity()))) + stubMapper(ProjectFactory.makeProject(), any()) + + val testObserver = repository.getProjects().test() + testObserver.assertComplete() + } + + @Test + fun getProjectsReturnsData() { + val projectEntity = ProjectFactory.makeProjectEntity() + val project = ProjectFactory.makeProject() + stubGetProjects(Observable.just(listOf(projectEntity))) + stubMapper(project, projectEntity) + + val testObserver = repository.getProjects().test() + testObserver.assertValue(listOf(project)) + } + + @Test + fun getBookmarkedProjectsCompletes() { + stubGetBookmarkedProjects(Observable.just(listOf(ProjectFactory.makeProjectEntity()))) + stubMapper(ProjectFactory.makeProject(), any()) + + val testObserver = repository.getBookmarkedProjects().test() + testObserver.assertComplete() + } + + @Test + fun getBookmarkedProjectsReturnsData() { + val projectEntity = ProjectFactory.makeProjectEntity() + val project = ProjectFactory.makeProject() + stubGetBookmarkedProjects(Observable.just(listOf(projectEntity))) + stubMapper(project, projectEntity) + + val testObserver = repository.getBookmarkedProjects().test() + testObserver.assertValue(listOf(project)) + } + + @Test + fun bookmarkProjectCompletes() { + stubBookmarkProject(Completable.complete()) + + val testObserver = repository.bookmarkProject(DataFactory.randomString()).test() + testObserver.assertComplete() + } + + @Test + fun unbookmarkProjectCompletes() { + stubUnBookmarkProject(Completable.complete()) + + val testObserver = repository.unbookmarkProject(DataFactory.randomString()).test() + testObserver.assertComplete() + } + + private fun stubBookmarkProject(completable: Completable) { + whenever(cache.setProjectAsBookmarked(any())) + .thenReturn(completable) + } + + private fun stubUnBookmarkProject(completable: Completable) { + whenever(cache.setProjectAsNotBookmarked(any())) + .thenReturn(completable) + } + + private fun stubIsCacheExpired(single: Single) { + whenever(cache.isProjectsCacheExpired()) + .thenReturn(single) + } + + private fun stubAreProjectsCached(single: Single) { + whenever(cache.areProjectsCached()) + .thenReturn(single) + } + + private fun stubMapper(model: Project, entity: ProjectEntity) { + whenever(mapper.mapFromEntity(entity)) + .thenReturn(model) + } + + private fun stubGetProjects(observable: Observable>) { + whenever(store.getProjects()) + .thenReturn(observable) + } + + private fun stubGetBookmarkedProjects(observable: Observable>) { + whenever(store.getBookmarkedProjects()) + .thenReturn(observable) + } + + private fun stubFactoryGetDataStore() { + whenever(factory.getDataStore(any(), any())) + .thenReturn(store) + } + + private fun stubFactoryGetCacheDataStore() { + whenever(factory.getCacheDataStore()) + .thenReturn(store) + } + + private fun stubSaveProjects(completable: Completable) { + whenever(store.saveProjects(any())) + .thenReturn(completable) + } + +} \ 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..eb31622 --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/mapper/ProjectMapperTest.kt @@ -0,0 +1,44 @@ +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) + assertEquals(entity.isBookmarked, model.isBookmarked) + } + +} \ 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..6098d5f --- /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 io.reactivex.Single +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(cacheStore, remoteStore) + + @Test + fun getRemoteStoreRetrievesRemoteSource() { + assert(factory.getRemoteDataStore() is ProjectsRemoteDataStore) + } + + @Test + fun getCacheStoreRetrievesCacheSource() { + assert(factory.getCacheDataStore() is ProjectsCacheDataStore) + } + + @Test + fun getDataStoreReturnsRemoteSourceWhenNoCachedData() { + assert(factory.getDataStore(false, false) is ProjectsRemoteDataStore) + } + + @Test + fun getDataStoreReturnsRemoteSourceWhenCacheExpired() { + assert(factory.getDataStore(false, false) is ProjectsRemoteDataStore) + } + + @Test + fun getDataStoreReturnsCacheSourceWhenDataIsCached() { + stubProjectsCacheAreProjectsCached(true) + stubProjectsCacheIsProjectsCachedExpired(false) + + assert(factory.getDataStore(true, false) is ProjectsCacheDataStore) + } + + private fun stubProjectsCacheAreProjectsCached(areCached: Boolean) { + whenever(cache.areProjectsCached()) + .thenReturn(Single.just(areCached)) + } + + private fun stubProjectsCacheIsProjectsCachedExpired(expired: Boolean) { + whenever(cache.isProjectsCacheExpired()) + .thenReturn(Single.just(expired)) + } + +} \ 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..238234a --- /dev/null +++ b/Data/src/test/java/co/joebirch/data/test/factory/ProjectFactory.kt @@ -0,0 +1,24 @@ +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(), + DataFactory.randomBoolean()) + } + + fun makeProject(): Project { + return Project(DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomBoolean()) + } + +} \ No newline at end of file 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/CompletableUseCase.kt b/Domain/src/main/java/co/joebirch/domain/CompletableUseCase.kt similarity index 96% rename from Domain/src/main/java/co/joebirch/domain/interactor/CompletableUseCase.kt rename to Domain/src/main/java/co/joebirch/domain/CompletableUseCase.kt index 74839e1..0b8e64c 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/CompletableUseCase.kt +++ b/Domain/src/main/java/co/joebirch/domain/CompletableUseCase.kt @@ -1,4 +1,4 @@ -package co.joebirch.domain.interactor +package co.joebirch.domain import co.joebirch.domain.executor.PostExecutionThread import io.reactivex.Completable diff --git a/Domain/src/main/java/co/joebirch/domain/ObservableUseCase.kt b/Domain/src/main/java/co/joebirch/domain/ObservableUseCase.kt new file mode 100644 index 0000000..be8924a --- /dev/null +++ b/Domain/src/main/java/co/joebirch/domain/ObservableUseCase.kt @@ -0,0 +1,32 @@ +package co.joebirch.domain + +import co.joebirch.domain.executor.PostExecutionThread +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.observers.DisposableObserver +import io.reactivex.schedulers.Schedulers + +abstract class ObservableUseCase constructor( + private val postExecutionThread: PostExecutionThread) { + + private val disposables = CompositeDisposable() + + protected abstract fun buildUseCaseObservable(params: Params? = null): Observable + + open fun execute(singleObserver: DisposableObserver, params: Params? = null) { + val single = this.buildUseCaseObservable(params) + .subscribeOn(Schedulers.io()) + .observeOn(postExecutionThread.scheduler) + addDisposable(single.subscribeWith(singleObserver)) + } + + fun addDisposable(disposable: Disposable) { + disposables.add(disposable) + } + + fun dispose() { + if (!disposables.isDisposed) disposables.dispose() + } + +} \ 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/bookmark/BookmarkProject.kt similarity index 71% rename from Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt rename to Domain/src/main/java/co/joebirch/domain/bookmark/BookmarkProject.kt index 8491b92..adfa1b8 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/browse/BookmarkProject.kt +++ b/Domain/src/main/java/co/joebirch/domain/bookmark/BookmarkProject.kt @@ -1,13 +1,14 @@ -package co.joebirch.domain.interactor.browse +package co.joebirch.domain.interactor.bookmark import co.joebirch.domain.executor.PostExecutionThread -import co.joebirch.domain.interactor.CompletableUseCase +import co.joebirch.domain.CompletableUseCase import co.joebirch.domain.repository.ProjectsRepository import io.reactivex.Completable import javax.inject.Inject -class BookmarkProject @Inject constructor(private val projectsRepository: ProjectsRepository, - postExecutionThread: PostExecutionThread) +open class BookmarkProject @Inject constructor( + private val projectsRepository: ProjectsRepository, + postExecutionThread: PostExecutionThread) : CompletableUseCase(postExecutionThread) { public override fun buildUseCaseCompletable(params: Params?): Completable { @@ -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/bookmark/GetBookmarkedProjects.kt similarity index 58% rename from Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt rename to Domain/src/main/java/co/joebirch/domain/bookmark/GetBookmarkedProjects.kt index 2de7bf6..bd34737 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/bookmarked/GetBookmarkedProjects.kt +++ b/Domain/src/main/java/co/joebirch/domain/bookmark/GetBookmarkedProjects.kt @@ -1,18 +1,19 @@ -package co.joebirch.domain.interactor.bookmarked +package co.joebirch.domain.interactor.bookmark +import co.joebirch.domain.ObservableUseCase 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( 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/bookmark/UnbookmarkProject.kt b/Domain/src/main/java/co/joebirch/domain/bookmark/UnbookmarkProject.kt new file mode 100644 index 0000000..a2eb956 --- /dev/null +++ b/Domain/src/main/java/co/joebirch/domain/bookmark/UnbookmarkProject.kt @@ -0,0 +1,27 @@ +package co.joebirch.domain.interactor.bookmark + +import co.joebirch.domain.executor.PostExecutionThread +import co.joebirch.domain.CompletableUseCase +import co.joebirch.domain.repository.ProjectsRepository +import io.reactivex.Completable +import javax.inject.Inject + +open 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/Domain/src/main/java/co/joebirch/domain/browse/GetProjects.kt b/Domain/src/main/java/co/joebirch/domain/browse/GetProjects.kt new file mode 100644 index 0000000..122d933 --- /dev/null +++ b/Domain/src/main/java/co/joebirch/domain/browse/GetProjects.kt @@ -0,0 +1,19 @@ +package co.joebirch.domain.interactor.browse + +import co.joebirch.domain.ObservableUseCase +import co.joebirch.domain.executor.PostExecutionThread +import co.joebirch.domain.model.Project +import co.joebirch.domain.repository.ProjectsRepository +import io.reactivex.Observable +import javax.inject.Inject + +open class GetProjects @Inject constructor( + private val projectsRepository: ProjectsRepository, + postExecutionThread: PostExecutionThread) + : ObservableUseCase, Nothing?>(postExecutionThread) { + + public 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/interactor/SingleUseCase.kt b/Domain/src/main/java/co/joebirch/domain/interactor/FlowableUseCase.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/FlowableUseCase.kt index 4c9a205..d678d29 100644 --- a/Domain/src/main/java/co/joebirch/domain/interactor/SingleUseCase.kt +++ b/Domain/src/main/java/co/joebirch/domain/interactor/FlowableUseCase.kt @@ -1,21 +1,21 @@ package co.joebirch.domain.interactor import co.joebirch.domain.executor.PostExecutionThread -import io.reactivex.Single +import io.reactivex.Flowable import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable -import io.reactivex.observers.DisposableSingleObserver import io.reactivex.schedulers.Schedulers +import io.reactivex.subscribers.DisposableSubscriber -abstract class SingleUseCase constructor( +abstract class FlowableUseCase constructor( private val postExecutionThread: PostExecutionThread) { private val disposables = CompositeDisposable() - protected abstract fun buildUseCaseSingle(params: Params? = null): Single + protected abstract fun buildUseCaseObservable(params: Params? = null): Flowable - open fun execute(singleObserver: DisposableSingleObserver, params: Params? = null) { - val single = this.buildUseCaseSingle(params) + open fun execute(singleObserver: DisposableSubscriber, 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/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 index 5ba47ee..fc8f8b6 100644 --- a/Domain/src/main/java/co/joebirch/domain/model/Project.kt +++ b/Domain/src/main/java/co/joebirch/domain/model/Project.kt @@ -2,4 +2,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 + val ownerName: String, val ownerAvatar: String, + val isBookmarked: Boolean) \ 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..d901a5b 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,16 @@ 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 unbookmarkProject(projectId: String): Completable + + fun getBookmarkedProjects(): Observable> } \ 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..c8bdb84 100644 --- a/Domain/src/test/java/co/joebirch/githubtrending/bookmarked/GetBookmarkedProjectsTest.kt +++ b/Domain/src/test/java/co/joebirch/githubtrending/bookmarked/GetBookmarkedProjectsTest.kt @@ -1,13 +1,13 @@ package co.joebirch.githubtrending.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 com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever -import io.reactivex.Single +import io.reactivex.Observable import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -31,22 +31,22 @@ class GetBookmarkedProjectsTest { @Test fun getBookmarkedProjectsCompletes() { stubProjectsRepositoryGetBookmarkedProjects( - Single.just(ProjectDataFactory.makeProjectList(2))) + Observable.just(ProjectDataFactory.makeProjectList(2))) - val testObserver = getBookmarkedProjects.buildUseCaseSingle().test() + val testObserver = getBookmarkedProjects.buildUseCaseObservable().test() testObserver.assertComplete() } @Test fun getBookmarkProjectsCallsRepository() { stubProjectsRepositoryGetBookmarkedProjects( - Single.just(ProjectDataFactory.makeProjectList(2))) + Observable.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/githubtrending/browse/GetProjectsTest.kt index 7af8046..67ba15e 100644 --- a/Domain/src/test/java/co/joebirch/githubtrending/browse/GetProjectsTest.kt +++ b/Domain/src/test/java/co/joebirch/githubtrending/browse/GetProjectsTest.kt @@ -7,7 +7,7 @@ import co.joebirch.domain.repository.ProjectsRepository import co.joebirch.githubtrending.test.ProjectDataFactory import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever -import io.reactivex.Single +import io.reactivex.Observable import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -31,34 +31,30 @@ class GetProjectsTest { @Test fun getProjectsCompletes() { stubProjectsRepositoryGetProjects( - Single.just(ProjectDataFactory.makeProjectList(2))) - - val testObserver = getProjects.buildUseCaseSingle().test() + Observable.just(ProjectDataFactory.makeProjectList(2))) + val testObserver = getProjects.buildUseCaseObservable().test() testObserver.assertComplete() } @Test fun getProjectsCallsRepository() { stubProjectsRepositoryGetProjects( - Single.just(ProjectDataFactory.makeProjectList(2))) - - getProjects.buildUseCaseSingle().test() + Observable.just(ProjectDataFactory.makeProjectList(2))) + getProjects.buildUseCaseObservable().test() verify(projectsRepository).getProjects() } @Test fun getProjectsReturnsData() { val projects = ProjectDataFactory.makeProjectList(2) - stubProjectsRepositoryGetProjects( - Single.just(projects)) - - val testObserver = getProjects.buildUseCaseSingle().test() + stubProjectsRepositoryGetProjects(Observable.just(projects)) + val testObserver = getProjects.buildUseCaseObservable().test() testObserver.assertValue(projects) } - private fun stubProjectsRepositoryGetProjects(single: Single>) { + private fun stubProjectsRepositoryGetProjects(observable: Observable>) { whenever(projectsRepository.getProjects()) - .thenReturn(single) + .thenReturn(observable) } } \ No newline at end of file diff --git a/Domain/src/test/java/co/joebirch/githubtrending/test/ProjectDataFactory.kt b/Domain/src/test/java/co/joebirch/githubtrending/test/ProjectDataFactory.kt index 309603d..07addb8 100644 --- a/Domain/src/test/java/co/joebirch/githubtrending/test/ProjectDataFactory.kt +++ b/Domain/src/test/java/co/joebirch/githubtrending/test/ProjectDataFactory.kt @@ -2,6 +2,8 @@ package co.joebirch.githubtrending.test import co.joebirch.domain.model.Project +import co.joebirch.domain.model.Project + object ProjectDataFactory { fun randomUuid(): String { @@ -10,7 +12,7 @@ object ProjectDataFactory { fun makeProject(): Project { return Project(randomUuid(), randomUuid(), randomUuid(), randomUuid(), - randomUuid(), randomUuid(), randomUuid()) + randomUuid(), randomUuid(), randomUuid(), false) } fun makeProjectList(count: Int) : List { 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..f8a755a --- /dev/null +++ b/Presentation/build.gradle @@ -0,0 +1,79 @@ +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"] + multiDexEnabled = true + } + + dexOptions { + preDexLibraries = false + dexInProcess = false + javaMaxHeapSize "4g" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + packagingOptions { + exclude 'LICENSE.txt' + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/ASL2.0' + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + } + + lintOptions { + quiet true + abortOnError false + ignoreWarnings true + disable 'InvalidPackage' //Some libraries have issues with this. + disable 'OldTargetApi' //Lint gives this warning but SDK 20 would be Android L Beta. + disable 'IconDensities' //For testing purpose. This is safe to remove. + disable 'IconMissingDensityFolder' //For testing purpose. This is safe to remove. + } + + testOptions { + unitTests.all { + jvmArgs '-noverify' + } + } +} + +kapt { + correctErrorTypes = true + generateStubs = true +} + +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 + testCompile presentationTestDependencies.archTesting + +} 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..03a8571 --- /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.bookmark.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..b70cafe --- /dev/null +++ b/Presentation/src/main/java/co/joebirch/presentation/BrowseProjectsViewModel.kt @@ -0,0 +1,79 @@ +package co.joebirch.presentation + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.arch.lifecycle.ViewModel +import co.joebirch.domain.interactor.bookmark.BookmarkProject +import co.joebirch.domain.interactor.bookmark.UnbookmarkProject +import co.joebirch.domain.interactor.browse.GetProjects +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 + +open class BrowseProjectsViewModel @Inject internal constructor( + private val getProjects: GetProjects?, + private val bookmarkProject: BookmarkProject, + private val unBookmarkProject: UnbookmarkProject, + private val mapper: ProjectViewMapper): ViewModel() { + + private val liveData: MutableLiveData>> = MutableLiveData() + + init { + fetchProjects() + } + + override fun onCleared() { + getProjects?.dispose() + super.onCleared() + } + + fun getProjects(): LiveData>> { + return liveData + } + + fun fetchProjects() { + liveData.postValue(Resource(ResourceState.LOADING, null, null)) + getProjects?.execute(ProjectsSubscriber()) + } + + fun bookmarkProject(projectId: String) { + return bookmarkProject.execute(BookmarkProjectsSubscriber(), + BookmarkProject.Params.forProject(projectId)) + } + + fun unbookmarkProject(projectId: String) { + 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..37e5658 --- /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 + +open 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, type.isBookmarked) + } +} \ 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..123b29f --- /dev/null +++ b/Presentation/src/main/java/co/joebirch/presentation/model/ProjectView.kt @@ -0,0 +1,6 @@ +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, + val isBookmarked: Boolean) \ 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/co/joebirch/presentation/bookmarked/BrowseBookmarkedProjectsViewModelTest.kt b/Presentation/src/test/java/co/joebirch/presentation/bookmarked/BrowseBookmarkedProjectsViewModelTest.kt new file mode 100644 index 0000000..3000b62 --- /dev/null +++ b/Presentation/src/test/java/co/joebirch/presentation/bookmarked/BrowseBookmarkedProjectsViewModelTest.kt @@ -0,0 +1,108 @@ +package co.joebirch.presentation.bookmarked + +import android.arch.core.executor.testing.InstantTaskExecutorRule +import co.joebirch.domain.interactor.bookmark.GetBookmarkedProjects +import co.joebirch.domain.model.Project +import co.joebirch.presentation.BrowseBookmarkedProjectsViewModel +import co.joebirch.presentation.mapper.ProjectViewMapper +import co.joebirch.presentation.model.ProjectView +import co.joebirch.presentation.state.ResourceState +import co.joebirch.presentation.test.factory.DataFactory +import co.joebirch.presentation.test.factory.ProjectFactory +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockito_kotlin.eq +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever +import io.reactivex.observers.DisposableObserver +import junit.framework.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Captor + +@RunWith(JUnit4::class) +class BrowseBookmarkedProjectsViewModelTest { + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + var getBookmarkedProjects = mock() + var mapper = mock() + var projectViewModel = BrowseBookmarkedProjectsViewModel( + getBookmarkedProjects, mapper) + + @Captor + val captor = argumentCaptor>>() + + @Test + fun fetchProjectsExecutesUseCase() { + projectViewModel.fetchProjects() + + verify(getBookmarkedProjects, times(1)).execute(any(), eq(null)) + } + + @Test + fun fetchProjectsReturnsSuccess() { + val projects = ProjectFactory.makeProjectList(2) + val projectViews = ProjectFactory.makeProjectViewList(2) + stubProjectMapperMapToView(projectViews[0], projects[0]) + stubProjectMapperMapToView(projectViews[1], projects[1]) + + projectViewModel.fetchProjects() + + verify(getBookmarkedProjects).execute(captor.capture(), eq(null)) + captor.firstValue.onNext(projects) + + assertEquals(ResourceState.SUCCESS, + projectViewModel.getProjects().value?.status) + } + + @Test + fun fetchProjectsReturnsData() { + val projects = ProjectFactory.makeProjectList(2) + val projectViews = ProjectFactory.makeProjectViewList(2) + stubProjectMapperMapToView(projectViews[0], projects[0]) + stubProjectMapperMapToView(projectViews[1], projects[1]) + + projectViewModel.fetchProjects() + + verify(getBookmarkedProjects).execute(captor.capture(), eq(null)) + captor.firstValue.onNext(projects) + + assertEquals(projectViews, + projectViewModel.getProjects().value?.data) + } + + @Test + fun fetchProjectsReturnsError() { + projectViewModel.fetchProjects() + + verify(getBookmarkedProjects).execute(captor.capture(), eq(null)) + captor.firstValue.onError(RuntimeException()) + + assertEquals(ResourceState.ERROR, + projectViewModel.getProjects().value?.status) + } + + @Test + fun fetchProjectsReturnsMessageForError() { + val errorMessage = DataFactory.randomString() + projectViewModel.fetchProjects() + + verify(getBookmarkedProjects).execute(captor.capture(), eq(null)) + captor.firstValue.onError(RuntimeException(errorMessage)) + + assertEquals(errorMessage, + projectViewModel.getProjects().value?.message) + } + + private fun stubProjectMapperMapToView(projectView: ProjectView, + project: Project) { + whenever(mapper.mapToView(project)) + .thenReturn(projectView) + } + +} \ No newline at end of file diff --git a/Presentation/src/test/java/co/joebirch/presentation/browse/BrowseProjectsViewModelTest.kt b/Presentation/src/test/java/co/joebirch/presentation/browse/BrowseProjectsViewModelTest.kt new file mode 100644 index 0000000..0a5097e --- /dev/null +++ b/Presentation/src/test/java/co/joebirch/presentation/browse/BrowseProjectsViewModelTest.kt @@ -0,0 +1,111 @@ +package co.joebirch.presentation.browse + +import android.arch.core.executor.testing.InstantTaskExecutorRule +import co.joebirch.domain.interactor.bookmark.BookmarkProject +import co.joebirch.domain.interactor.bookmark.UnbookmarkProject +import co.joebirch.domain.interactor.browse.GetProjects +import co.joebirch.domain.model.Project +import co.joebirch.presentation.BrowseProjectsViewModel +import co.joebirch.presentation.mapper.ProjectViewMapper +import co.joebirch.presentation.model.ProjectView +import co.joebirch.presentation.state.ResourceState +import co.joebirch.presentation.test.factory.DataFactory +import co.joebirch.presentation.test.factory.ProjectFactory +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockito_kotlin.eq +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever +import io.reactivex.observers.DisposableObserver +import junit.framework.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Captor + +@RunWith(JUnit4::class) +class BrowseProjectsViewModelTest { + + @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() + var getProjects = mock() + var bookmarkProject = mock() + var unbookmarkProject = mock() + var projectMapper = mock() + var projectViewModel = BrowseProjectsViewModel(getProjects, + bookmarkProject, unbookmarkProject, projectMapper) + + @Captor + val captor = argumentCaptor>>() + + @Test + fun fetchProjectsExecutesUseCase() { + projectViewModel.fetchProjects() + + verify(getProjects, times(1)).execute(any(), eq(null)) + } + + @Test + fun fetchProjectsReturnsSuccess() { + val projects = ProjectFactory.makeProjectList(2) + val projectViews = ProjectFactory.makeProjectViewList(2) + stubProjectMapperMapToView(projectViews[0], projects[0]) + stubProjectMapperMapToView(projectViews[1], projects[1]) + + projectViewModel.fetchProjects() + + verify(getProjects).execute(captor.capture(), eq(null)) + captor.firstValue.onNext(projects) + + assertEquals(ResourceState.SUCCESS, + projectViewModel.getProjects().value?.status) + } + + @Test + fun fetchProjectsReturnsData() { + val projects = ProjectFactory.makeProjectList(2) + val projectViews = ProjectFactory.makeProjectViewList(2) + stubProjectMapperMapToView(projectViews[0], projects[0]) + stubProjectMapperMapToView(projectViews[1], projects[1]) + + projectViewModel.fetchProjects() + + verify(getProjects).execute(captor.capture(), eq(null)) + captor.firstValue.onNext(projects) + + assertEquals(projectViews, + projectViewModel.getProjects().value?.data) + } + + @Test + fun fetchProjectsReturnsError() { + projectViewModel.fetchProjects() + + verify(getProjects).execute(captor.capture(), eq(null)) + captor.firstValue.onError(RuntimeException()) + + assertEquals(ResourceState.ERROR, + projectViewModel.getProjects().value?.status) + } + + @Test + fun fetchProjectsReturnsMessageForError() { + val errorMessage = DataFactory.randomString() + projectViewModel.fetchProjects() + + verify(getProjects).execute(captor.capture(), eq(null)) + captor.firstValue.onError(RuntimeException(errorMessage)) + + assertEquals(errorMessage, + projectViewModel.getProjects().value?.message) + } + + private fun stubProjectMapperMapToView(projectView: ProjectView, + project: Project) { + whenever(projectMapper.mapToView(project)) + .thenReturn(projectView) + } + +} \ No newline at end of file diff --git a/Presentation/src/test/java/co/joebirch/presentation/mapper/ProjectViewMapperTest.kt b/Presentation/src/test/java/co/joebirch/presentation/mapper/ProjectViewMapperTest.kt new file mode 100644 index 0000000..043dd27 --- /dev/null +++ b/Presentation/src/test/java/co/joebirch/presentation/mapper/ProjectViewMapperTest.kt @@ -0,0 +1,29 @@ +package co.joebirch.presentation.mapper + +import co.joebirch.presentation.test.factory.ProjectFactory +import junit.framework.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ProjectViewMapperTest { + + private val mapper = ProjectViewMapper() + + @Test + fun mapToViewMapsData() { + val project = ProjectFactory.makeProject() + val projectView = mapper.mapToView(project) + + assertEquals(project.id, projectView.id) + assertEquals(project.name, projectView.name) + assertEquals(project.fullName, projectView.fullName) + assertEquals(project.starCount, projectView.starCount) + assertEquals(project.dateCreated, projectView.dateCreated) + assertEquals(project.ownerName, projectView.ownerName) + assertEquals(project.ownerAvatar, projectView.ownerAvatar) + assertEquals(project.isBookmarked, projectView.isBookmarked) + } + +} \ No newline at end of file diff --git a/Presentation/src/test/java/co/joebirch/presentation/test/factory/DataFactory.kt b/Presentation/src/test/java/co/joebirch/presentation/test/factory/DataFactory.kt new file mode 100644 index 0000000..2e2c8f7 --- /dev/null +++ b/Presentation/src/test/java/co/joebirch/presentation/test/factory/DataFactory.kt @@ -0,0 +1,24 @@ +package co.joebirch.presentation.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/Presentation/src/test/java/co/joebirch/presentation/test/factory/ProjectFactory.kt b/Presentation/src/test/java/co/joebirch/presentation/test/factory/ProjectFactory.kt new file mode 100644 index 0000000..ea3f1ba --- /dev/null +++ b/Presentation/src/test/java/co/joebirch/presentation/test/factory/ProjectFactory.kt @@ -0,0 +1,39 @@ +package co.joebirch.presentation.test.factory + +import co.joebirch.domain.model.Project +import co.joebirch.presentation.model.ProjectView + +object ProjectFactory { + + fun makeProjectView(): ProjectView { + return ProjectView(DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomBoolean()) + } + + fun makeProject(): Project { + return Project(DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomString(), DataFactory.randomString(), + DataFactory.randomBoolean()) + } + + fun makeProjectViewList(count: Int): List { + val projects = mutableListOf() + repeat(count) { + projects.add(makeProjectView()) + } + return projects + } + + fun makeProjectList(count: Int): List { + val projects = mutableListOf() + repeat(count) { + projects.add(makeProject()) + } + return projects + } +} \ No newline at end of file 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/ProjectsRemoteImpl.kt b/Remote/src/main/java/co/joebirch/remote/ProjectsRemoteImpl.kt new file mode 100644 index 0000000..421d898 --- /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.Flowable +import javax.inject.Inject + +class ProjectsRemoteImpl @Inject constructor( + private val service: GithubTrendingService, + private val mapper: ProjectsResponseModelMapper) + : ProjectsRemote { + + override fun getProjects(): Flowable> { + 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/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..ee0bf87 --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/mapper/ProjectsResponseModelMapper.kt @@ -0,0 +1,14 @@ +package co.joebirch.remote.mapper + +import co.joebirch.data.model.ProjectEntity +import co.joebirch.remote.model.ProjectModel +import javax.inject.Inject + +class ProjectsResponseModelMapper @Inject constructor(): 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, false) + } + +} \ No newline at end of file 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..133921e --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/model/ProjectModel.kt @@ -0,0 +1,9 @@ +package co.joebirch.remote.model + +import com.google.gson.annotations.SerializedName + +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, + val owner: OwnerModel) \ 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..a07f5df --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingService.kt @@ -0,0 +1,16 @@ +package co.joebirch.remote.service + +import co.joebirch.remote.model.ProjectsResponseModel +import io.reactivex.Flowable +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) + : Flowable + +} \ 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..29fb6a0 --- /dev/null +++ b/Remote/src/main/java/co/joebirch/remote/service/GithubTrendingServiceFactory.kt @@ -0,0 +1,47 @@ +package co.joebirch.remote.service + +import com.google.gson.Gson +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +object GithubTrendingServiceFactory { + + open fun makeGithubTrendingService(isDebug: Boolean): GithubTrendingService { + val okHttpClient = makeOkHttpClient( + makeLoggingInterceptor((isDebug))) + return makeGithubTrendingService(okHttpClient, Gson()) + } + + private fun makeGithubTrendingService(okHttpClient: OkHttpClient, gson: Gson): GithubTrendingService { + val retrofit = Retrofit.Builder() + .baseUrl("https://api.github.com/") + .client(okHttpClient) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) + .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/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/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..9c2e1a4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ // 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.30' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong 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..deee30e --- /dev/null +++ b/cache/build.gradle @@ -0,0 +1,50 @@ +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 '27.0.3' +} + +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 + implementation cacheDependencies.roomRxJava + kapt cacheDependencies.roomCompiler + + testImplementation cacheTestDependencies.junit + testImplementation cacheTestDependencies.kotlinJUnit + testImplementation cacheTestDependencies.mockito + testImplementation cacheTestDependencies.assertj + testImplementation cacheTestDependencies.robolectric + testImplementation cacheTestDependencies.archTesting + testImplementation cacheTestDependencies.roomTesting + androidTestImplementation 'com.android.support:support-annotations:27.1.0' + androidTestCompile 'junit:junit:4.12' +} 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/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..53190d3 --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/ProjectsCacheImpl.kt @@ -0,0 +1,85 @@ +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 +import io.reactivex.Flowable +import io.reactivex.Single +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 { + projectsDatabase.cachedProjectsDao().insertProjects( + projects.map { mapper.mapToCached(it) }) + Completable.complete() + } + } + + override fun getProjects(): Flowable> { + return projectsDatabase.cachedProjectsDao().getProjects() + .map { + it.map { mapper.mapFromCached(it) } + } + } + + override fun getBookmarkedProjects(): Flowable> { + return projectsDatabase.cachedProjectsDao().getBookmarkedProjects() + .map { + it.map { mapper.mapFromCached(it) } + } + } + + override fun setProjectAsBookmarked(projectId: String): Completable { + return Completable.defer { + projectsDatabase.cachedProjectsDao().setBookmarkStatus(true, projectId) + Completable.complete() + } + } + + override fun setProjectAsNotBookmarked(projectId: String): Completable { + return Completable.defer { + projectsDatabase.cachedProjectsDao().setBookmarkStatus(false, projectId) + Completable.complete() + } + } + + override fun areProjectsCached(): Single { + return projectsDatabase.cachedProjectsDao().getProjects().isEmpty + .map { + !it + } + } + + override fun setLastCacheTime(lastCache: Long): Completable { + return Completable.defer { + projectsDatabase.configDao().insertConfig(Config(lastCacheTime = lastCache)) + Completable.complete() + } + } + + override fun isProjectsCacheExpired(): Flowable { + val currentTime = System.currentTimeMillis() + val expirationTime = (60 * 10 * 1000).toLong() + return projectsDatabase.configDao().getConfig() + .onErrorReturn { Config(lastCacheTime = 0) } + .map { + currentTime - it.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 new file mode 100644 index 0000000..c9354d1 --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/dao/CachedProjectsDao.kt @@ -0,0 +1,35 @@ +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_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 +import io.reactivex.Flowable + +@Dao +abstract class CachedProjectsDao { + + @Query(QUERY_PROJECTS) + @JvmSuppressWildcards + abstract fun getProjects(): Flowable> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + @JvmSuppressWildcards + abstract fun insertProjects(projects: List) + + @Query(DELETE_PROJECTS) + abstract fun deleteProjects() + + @Query(QUERY_BOOKMARKED_PROJECTS) + abstract fun getBookmarkedProjects(): Flowable> + + @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..540acfc --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/dao/ConfigDao.kt @@ -0,0 +1,20 @@ +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 +import io.reactivex.Flowable + +@Dao +abstract class ConfigDao { + + @Query(ConfigConstants.QUERY_CONFIG) + abstract fun getConfig(): Flowable + + @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 new file mode 100644 index 0000000..a45b832 --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/db/ProjectConstants.kt @@ -0,0 +1,22 @@ +package co.joebirch.cache.db + +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 new file mode 100644 index 0000000..aa0770c --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/db/ProjectsDatabase.kt @@ -0,0 +1,41 @@ +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.dao.ConfigDao +import co.joebirch.cache.model.CachedProject +import co.joebirch.cache.model.Config +import javax.inject.Inject + +@Database(entities = arrayOf(CachedProject::class, + Config::class), version = 1) +abstract class ProjectsDatabase @Inject constructor(): RoomDatabase() { + + abstract fun cachedProjectsDao(): CachedProjectsDao + + abstract fun configDao(): ConfigDao + + companion object { + + 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..6086115 --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/mapper/CachedProjectMapper.kt @@ -0,0 +1,21 @@ +package co.joebirch.cache.mapper + +import co.joebirch.cache.model.CachedProject +import co.joebirch.data.model.ProjectEntity +import javax.inject.Inject + +class CachedProjectMapper @Inject constructor(): CacheMapper { + + override fun mapFromCached(type: CachedProject): ProjectEntity { + return ProjectEntity(type.id, type.name, type.fullName, type.starCount, + 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.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 new file mode 100644 index 0000000..c3a4b02 --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/model/CachedProject.kt @@ -0,0 +1,21 @@ +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 + +@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, + @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..07ab93b --- /dev/null +++ b/cache/src/main/java/co/joebirch/cache/model/Config.kt @@ -0,0 +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) +data class Config( + @PrimaryKey(autoGenerate = true) + var id: Int = -1, + var lastCacheTime: Long) \ 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/ProjectsCacheImplTest.kt b/cache/src/test/java/co/joebirch/cache/ProjectsCacheImplTest.kt new file mode 100644 index 0000000..a1910a8 --- /dev/null +++ b/cache/src/test/java/co/joebirch/cache/ProjectsCacheImplTest.kt @@ -0,0 +1,101 @@ +package co.joebirch.cache + +import android.arch.core.executor.testing.InstantTaskExecutorRule +import android.arch.persistence.room.Room +import co.joebirch.cache.db.ProjectsDatabase +import co.joebirch.cache.mapper.CachedProjectMapper +import co.joebirch.cache.test.factory.ProjectDataFactory +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class ProjectsCacheImplTest { + + @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() + + private val database = Room.inMemoryDatabaseBuilder( + RuntimeEnvironment.application.applicationContext, + ProjectsDatabase::class.java) + .allowMainThreadQueries() + .build() + private val entityMapper = CachedProjectMapper() + private val cache = ProjectsCacheImpl(database, entityMapper) + + @Test + fun clearTablesCompletes() { + val testObserver = cache.clearProjects().test() + testObserver.assertComplete() + } + + @Test + fun saveProjectsCompletes() { + val projects = listOf(ProjectDataFactory.makeProjectEntity()) + + val testObserver = cache.saveProjects(projects).test() + testObserver.assertComplete() + } + + @Test + fun getProjectsReturnsData() { + val projects = listOf(ProjectDataFactory.makeProjectEntity()) + cache.saveProjects(projects).test() + + val testObserver = cache.getProjects().test() + testObserver.assertValue(projects) + } + + @Test + fun getBookmarkedProjectsReturnsData() { + val bookmarkedProject = ProjectDataFactory.makeBookmarkedProjectEntity() + val projects = listOf(ProjectDataFactory.makeProjectEntity(), + bookmarkedProject) + cache.saveProjects(projects).test() + + val testObserver = cache.getBookmarkedProjects().test() + testObserver.assertValue(listOf(bookmarkedProject)) + } + + @Test + fun setProjectAsBookmarkedCompletes() { + val projects = listOf(ProjectDataFactory.makeProjectEntity()) + cache.saveProjects(projects).test() + + val testObserver = cache.setProjectAsBookmarked(projects[0].id).test() + testObserver.assertComplete() + } + + @Test + fun setProjectAsNotBookmarkedCompletes() { + val projects = listOf(ProjectDataFactory.makeBookmarkedProjectEntity()) + cache.saveProjects(projects).test() + + val testObserver = cache.setProjectAsNotBookmarked(projects[0].id).test() + testObserver.assertComplete() + } + + @Test + fun areProjectsCacheReturnsData() { + val projects = listOf(ProjectDataFactory.makeProjectEntity()) + cache.saveProjects(projects).test() + + val testObserver = cache.areProjectsCached().test() + testObserver.assertValue(true) + } + + @Test + fun setLastCacheTimeCompletes() { + val testObserver = cache.setLastCacheTime(1000L).test() + testObserver.assertComplete() + } + + @Test + fun isProjectsCacheExpiredReturnsNotExpired() { + cache.setLastCacheTime(System.currentTimeMillis() - 1000).test() + val testObserver = cache.isProjectsCacheExpired().test() + testObserver.assertValue(false) + } + +} \ No newline at end of file diff --git a/cache/src/test/java/co/joebirch/cache/dao/CachedProjectsDaoTest.kt b/cache/src/test/java/co/joebirch/cache/dao/CachedProjectsDaoTest.kt new file mode 100644 index 0000000..0332270 --- /dev/null +++ b/cache/src/test/java/co/joebirch/cache/dao/CachedProjectsDaoTest.kt @@ -0,0 +1,81 @@ +package co.joebirch.cache.dao + +import android.arch.core.executor.testing.InstantTaskExecutorRule +import android.arch.persistence.room.Room +import co.joebirch.cache.db.ProjectsDatabase +import co.joebirch.cache.test.factory.ProjectDataFactory +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class CachedProjectsDaoTest { + + @Rule + @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule() + + private val database = Room.inMemoryDatabaseBuilder( + RuntimeEnvironment.application.applicationContext, + ProjectsDatabase::class.java) + .allowMainThreadQueries() + .build() + + @After + fun closeDb() { + database.close() + } + + @Test + fun getProjectsReturnsData() { + val project = ProjectDataFactory.makeCachedProject() + database.cachedProjectsDao().insertProjects(listOf(project)) + + val testObserver = database.cachedProjectsDao().getProjects().test() + testObserver.assertValue(listOf(project)) + } + + @Test + fun deleteProjectsClearsData() { + val project = ProjectDataFactory.makeCachedProject() + database.cachedProjectsDao().insertProjects(listOf(project)) + database.cachedProjectsDao().deleteProjects() + + val testObserver = database.cachedProjectsDao().getProjects().test() + testObserver.assertValue(emptyList()) + } + + @Test + fun getBookmarkedProjectsReturnsData() { + val project = ProjectDataFactory.makeCachedProject() + val bookmarkedProject = ProjectDataFactory.makeBookmarkedCachedProject() + database.cachedProjectsDao().insertProjects(listOf(project, bookmarkedProject)) + + val testObserver = database.cachedProjectsDao().getBookmarkedProjects().test() + testObserver.assertValue(listOf(bookmarkedProject)) + } + + @Test + fun setProjectAsBookmarkedSavesData() { + val project = ProjectDataFactory.makeCachedProject() + database.cachedProjectsDao().insertProjects(listOf(project)) + database.cachedProjectsDao().setBookmarkStatus(true, project.id) + project.isBookmarked = true + + val testObserver = database.cachedProjectsDao().getBookmarkedProjects().test() + testObserver.assertValue(listOf(project)) + } + + @Test + fun setProjectAsNotBookmarkedSavesData() { + val project = ProjectDataFactory.makeBookmarkedCachedProject() + database.cachedProjectsDao().insertProjects(listOf(project)) + database.cachedProjectsDao().setBookmarkStatus(false, project.id) + project.isBookmarked = false + + val testObserver = database.cachedProjectsDao().getBookmarkedProjects().test() + testObserver.assertValue(emptyList()) + } +} \ No newline at end of file diff --git a/cache/src/test/java/co/joebirch/cache/dao/ConfigDaoTest.kt b/cache/src/test/java/co/joebirch/cache/dao/ConfigDaoTest.kt new file mode 100644 index 0000000..edf92fb --- /dev/null +++ b/cache/src/test/java/co/joebirch/cache/dao/ConfigDaoTest.kt @@ -0,0 +1,40 @@ +package co.joebirch.cache.dao + +import android.arch.core.executor.testing.InstantTaskExecutorRule +import android.arch.persistence.room.Room +import co.joebirch.cache.db.ProjectsDatabase +import co.joebirch.cache.test.factory.ConfigDataFactory +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class ConfigDaoTest { + + @Rule + @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule() + + private val database = Room.inMemoryDatabaseBuilder( + RuntimeEnvironment.application.applicationContext, + ProjectsDatabase::class.java) + .allowMainThreadQueries() + .build() + + @After + fun clearDb() { + database.close() + } + + @Test + fun saveConfigurationSavesData() { + val config = ConfigDataFactory.makeCachedConfig() + database.configDao().insertConfig(config) + + val testObserver = database.configDao().getConfig().test() + testObserver.assertValue(config) + } + +} \ 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..2c8dc69 --- /dev/null +++ b/cache/src/test/java/co/joebirch/cache/test/factory/ProjectDataFactory.kt @@ -0,0 +1,39 @@ +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(), + false) + } + + fun makeBookmarkedCachedProject(): CachedProject { + return CachedProject(DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + true) + } + + fun makeProjectEntity(): ProjectEntity { + return ProjectEntity(DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomBoolean()) + } + + fun makeBookmarkedProjectEntity(): ProjectEntity { + return ProjectEntity(DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + DataFactory.randomUuid(), DataFactory.randomUuid(), + true) + } +} \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index bf3ff15..af01233 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,22 +1,45 @@ ext { //Android - androidBuildToolsVersion = "26.0.0" + androidBuildToolsVersion = "27.0.3" androidMinSdkVersion = 21 androidTargetSdkVersion = 26 androidCompileSdkVersion = 26 - kotlinVersion = '1.1.3-2' + kotlinVersion = '1.2.10' //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' + rxAndroidVersion = '2.0.1' + androidAnnotationsVersion = '21.0.3' + daggerVersion = '2.11' + gsonVersion = '2.8.1' + okHttpVersion = '3.8.1' + retrofitVersion = '2.3.0' + supportLibraryVersion = '26.1.0' + timberVersion = '4.5.1' + glideVersion = '4.0.0' + daggerVersion = '2.11' + glassfishAnnotationVersion = '10.0-b28' + archCompVersion = '1.1.1' + roomVersion = '1.0.0' //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.9' + androidSupportRunnerVersion = '1.0.0' + androidSupportRulesVersion = '1.0.0' + dexmakerMockitoversion = '2.2.0' + runnerVersion = '0.5' domainDependencies = [ javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", @@ -30,4 +53,127 @@ ext { assertj: "org.assertj:assertj-core:${assertJVersion}" ] + dataDependencies = [ + rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", + kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${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}" + ] + + 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-jdk7:${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}", + robolectric: "org.robolectric:robolectric:${robolectricVersion}", + 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:${archCompVersion}", + 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}", + kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${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}" + ] + + 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-jdk7:${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}", + archTesting: "android.arch.core:core-testing:${archCompVersion}", + ] + + 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.properties b/gradle.properties index aac7c9b..d045703 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,3 +15,4 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +kotlin.incremental=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a18941a..6cd66e7 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 Jun 03 10:38:26 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..aba9c9f --- /dev/null +++ b/mobile-ui/build.gradle @@ -0,0 +1,99 @@ +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 "co.joebirch.mobile_ui.test.TestRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + +} + +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/bookmarked/BrowseBookmarkedProjectsActivityTest.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/bookmarked/BrowseBookmarkedProjectsActivityTest.kt new file mode 100644 index 0000000..7df038c --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/bookmarked/BrowseBookmarkedProjectsActivityTest.kt @@ -0,0 +1,58 @@ +package co.joebirch.mobile_ui.bookmarked + +import android.support.test.espresso.Espresso.onView +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.contrib.RecyclerViewActions +import android.support.test.espresso.matcher.ViewMatchers.hasDescendant +import android.support.test.espresso.matcher.ViewMatchers.withId +import android.support.test.espresso.matcher.ViewMatchers.withText +import android.support.test.rule.ActivityTestRule +import android.support.test.runner.AndroidJUnit4 +import co.joebirch.domain.model.Project +import co.joebirch.mobile_ui.R +import co.joebirch.mobile_ui.test.TestApplication +import co.joebirch.mobile_ui.test.factory.ProjectDataFactory +import com.nhaarman.mockito_kotlin.whenever +import io.reactivex.Observable +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BrowseBookmarkedProjectsActivityTest { + + @Rule @JvmField + val activity = ActivityTestRule(BookmarkedActivity::class.java, false, + false) + + @Test + fun activityLaunches() { + stubProjectsRepositoryGetBookmarkedProjects(Observable.just(listOf( + ProjectDataFactory.makeProject()))) + activity.launchActivity(null) + } + + @Test + fun bookmarkedProjectsDisplay() { + val projects = listOf(ProjectDataFactory.makeProject(), ProjectDataFactory.makeProject(), + ProjectDataFactory.makeProject(), ProjectDataFactory.makeProject()) + stubProjectsRepositoryGetBookmarkedProjects(Observable.just(projects)) + + activity.launchActivity(null) + + projects.forEachIndexed { index, project -> + onView(withId(R.id.recycler_projects)) + .perform(RecyclerViewActions.scrollToPosition( + index)) + + onView(withId(R.id.recycler_projects)) + .check(matches(hasDescendant(withText(project.fullName)))) + } + } + + private fun stubProjectsRepositoryGetBookmarkedProjects(observable: Observable>) { + whenever(TestApplication.appComponent().projectsRepository().getBookmarkedProjects()) + .thenReturn(observable) + } + +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/browse/BrowseProjectsActivityTest.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/browse/BrowseProjectsActivityTest.kt new file mode 100644 index 0000000..aadc306 --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/browse/BrowseProjectsActivityTest.kt @@ -0,0 +1,53 @@ +package co.joebirch.mobile_ui.browse + +import android.support.test.espresso.Espresso.onView +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.contrib.RecyclerViewActions +import android.support.test.espresso.matcher.ViewMatchers.hasDescendant +import android.support.test.espresso.matcher.ViewMatchers.withId +import android.support.test.espresso.matcher.ViewMatchers.withText +import android.support.test.rule.ActivityTestRule +import android.support.test.runner.AndroidJUnit4 +import co.joebirch.domain.model.Project +import co.joebirch.mobile_ui.R +import co.joebirch.mobile_ui.test.TestApplication +import co.joebirch.mobile_ui.test.factory.ProjectDataFactory +import com.nhaarman.mockito_kotlin.whenever +import io.reactivex.Observable +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BrowseProjectsActivityTest { + + @Rule @JvmField + val activity = ActivityTestRule(BrowseActivity::class.java, false, false) + + @Test + fun activityLaunches() { + stubProjectsRepositoryGetProjects(Observable.just(listOf(ProjectDataFactory.makeProject()))) + activity.launchActivity(null) + } + + @Test + fun projectsDisplay() { + val projects = listOf(ProjectDataFactory.makeProject(), + ProjectDataFactory.makeProject(), ProjectDataFactory.makeProject()) + stubProjectsRepositoryGetProjects(Observable.just(projects)) + activity.launchActivity(null) + + projects.forEachIndexed { index, project -> + onView(withId(R.id.recycler_projects)) + .perform(RecyclerViewActions.scrollToPosition(index)) + + onView(withId(R.id.recycler_projects)) + .check(matches(hasDescendant(withText(project.fullName)))) + } + } + + private fun stubProjectsRepositoryGetProjects(observable: Observable>) { + whenever(TestApplication.appComponent().projectsRepository().getProjects()) + .thenReturn(observable) + } +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestApplicationComponent.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestApplicationComponent.kt new file mode 100644 index 0000000..af966f7 --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestApplicationComponent.kt @@ -0,0 +1,35 @@ +package co.joebirch.mobile_ui.injection + +import android.app.Application +import co.joebirch.domain.repository.ProjectsRepository +import co.joebirch.mobile_ui.injection.module.PresentationModule +import co.joebirch.mobile_ui.injection.module.UiModule +import co.joebirch.mobile_ui.test.TestApplication +import dagger.BindsInstance +import dagger.Component +import dagger.android.support.AndroidSupportInjectionModule +import javax.inject.Singleton + +@Singleton +@Component(modules = arrayOf(AndroidSupportInjectionModule::class, + TestApplicationModule::class, + TestCacheModule::class, + TestDataModule::class, + PresentationModule::class, + UiModule::class, + TestRemoteModule::class)) +interface TestApplicationComponent { + + fun projectsRepository(): ProjectsRepository + + @Component.Builder + interface Builder { + @BindsInstance + fun application(application: Application): TestApplicationComponent.Builder + + fun build(): TestApplicationComponent + } + + fun inject(application: TestApplication) + +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestApplicationModule.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestApplicationModule.kt new file mode 100644 index 0000000..521ca0a --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestApplicationModule.kt @@ -0,0 +1,13 @@ +package co.joebirch.mobile_ui.injection + +import android.app.Application +import android.content.Context +import dagger.Binds +import dagger.Module + +@Module +abstract class TestApplicationModule { + + @Binds + abstract fun bindContext(application: Application): Context +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestCacheModule.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestCacheModule.kt new file mode 100644 index 0000000..b834511 --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestCacheModule.kt @@ -0,0 +1,25 @@ +package co.joebirch.mobile_ui.injection + +import android.app.Application +import co.joebirch.cache.db.ProjectsDatabase +import co.joebirch.data.repository.ProjectsCache +import com.nhaarman.mockito_kotlin.mock +import dagger.Module +import dagger.Provides + +@Module +object TestCacheModule { + + @Provides + @JvmStatic + fun provideDatabase(application: Application): ProjectsDatabase { + return ProjectsDatabase.getInstance(application) + } + + @Provides + @JvmStatic + fun provideProjectsCache(): ProjectsCache { + return mock() + } + +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestDataModule.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestDataModule.kt new file mode 100644 index 0000000..8265f9b --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestDataModule.kt @@ -0,0 +1,19 @@ +package co.joebirch.mobile_ui.injection + +import co.joebirch.domain.repository.ProjectsRepository +import com.nhaarman.mockito_kotlin.mock +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +object TestDataModule { + + @Provides + @JvmStatic + @Singleton + fun provideDataRepository(): ProjectsRepository { + return mock() + } + +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestRemoteModule.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestRemoteModule.kt new file mode 100644 index 0000000..9a371a8 --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/injection/TestRemoteModule.kt @@ -0,0 +1,24 @@ +package co.joebirch.mobile_ui.injection + +import co.joebirch.data.repository.ProjectsRemote +import co.joebirch.remote.service.GithubTrendingService +import com.nhaarman.mockito_kotlin.mock +import dagger.Module +import dagger.Provides + +@Module +object TestRemoteModule { + + @Provides + @JvmStatic + fun provideGithubService(): GithubTrendingService { + return mock() + } + + @Provides + @JvmStatic + fun provideProjectsRemote(): ProjectsRemote { + return mock() + } + +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/TestApplication.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/TestApplication.kt new file mode 100644 index 0000000..d3d53a4 --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/TestApplication.kt @@ -0,0 +1,34 @@ +package co.joebirch.mobile_ui.test + +import android.app.Activity +import android.app.Application +import android.support.test.InstrumentationRegistry +import co.joebirch.mobile_ui.injection.DaggerTestApplicationComponent +import co.joebirch.mobile_ui.injection.TestApplicationComponent +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasActivityInjector +import javax.inject.Inject + +class TestApplication: Application(), HasActivityInjector { + + @Inject lateinit var injector: DispatchingAndroidInjector + private lateinit var appComponent: TestApplicationComponent + + companion object { + fun appComponent(): TestApplicationComponent { + return (InstrumentationRegistry.getTargetContext().applicationContext + as TestApplication).appComponent + } + } + + override fun onCreate() { + super.onCreate() + appComponent = DaggerTestApplicationComponent.builder().application(this).build() + appComponent.inject(this) + } + + override fun activityInjector(): AndroidInjector { + return injector + } +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/TestRunner.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/TestRunner.kt new file mode 100644 index 0000000..73341ae --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/TestRunner.kt @@ -0,0 +1,21 @@ +package co.joebirch.mobile_ui.test + +import android.app.Application +import android.content.Context +import android.os.Bundle +import android.support.test.runner.AndroidJUnitRunner +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers + +class TestRunner : AndroidJUnitRunner() { + + override fun onCreate(arguments: Bundle) { + super.onCreate(arguments) + RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } + } + + @Throws(InstantiationException::class, IllegalAccessException::class, ClassNotFoundException::class) + override fun newApplication(cl: ClassLoader, className: String, context: Context): Application { + return super.newApplication(cl, TestApplication::class.java.name, context) + } +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/factory/ProjectDataFactory.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/factory/ProjectDataFactory.kt new file mode 100644 index 0000000..a1715d4 --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/factory/ProjectDataFactory.kt @@ -0,0 +1,13 @@ +package co.joebirch.mobile_ui.test.factory + +import co.joebirch.domain.model.Project + +object ProjectDataFactory { + + fun makeProject(): Project { + return Project(TestDataFactory.randomUuid(), TestDataFactory.randomUuid(), + TestDataFactory.randomUuid(), TestDataFactory.randomUuid(), TestDataFactory.randomUuid(), + TestDataFactory.randomUuid(), TestDataFactory.randomUuid(), TestDataFactory.randomBoolean()) + } + +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/factory/TestDataFactory.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/factory/TestDataFactory.kt new file mode 100644 index 0000000..3f088a0 --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/factory/TestDataFactory.kt @@ -0,0 +1,24 @@ +package co.joebirch.mobile_ui.test.factory + +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +object TestDataFactory { + + 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 + } + +} \ No newline at end of file diff --git a/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/factory/TestProjectFactory.kt b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/factory/TestProjectFactory.kt new file mode 100644 index 0000000..3f000ae --- /dev/null +++ b/mobile-ui/src/androidTest/java/co/joebirch/mobile_ui/test/factory/TestProjectFactory.kt @@ -0,0 +1,13 @@ +package co.joebirch.mobile_ui.test.factory + +import co.joebirch.presentation.model.ProjectView + +object TestProjectFactory { + + fun makeProjectView(): ProjectView { + return ProjectView(TestDataFactory.randomUuid(), TestDataFactory.randomUuid(), + TestDataFactory.randomUuid(), TestDataFactory.randomUuid(), TestDataFactory.randomUuid(), + TestDataFactory.randomUuid(), TestDataFactory.randomUuid(), TestDataFactory.randomBoolean()) + } + +} \ No newline at end of file diff --git a/mobile-ui/src/main/AndroidManifest.xml b/mobile-ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b46bea2 --- /dev/null +++ b/mobile-ui/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + \ 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/bookmarked/BookmarkedActivity.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/bookmarked/BookmarkedActivity.kt new file mode 100644 index 0000000..0d9df4c --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/bookmarked/BookmarkedActivity.kt @@ -0,0 +1,78 @@ +package co.joebirch.mobile_ui.bookmarked + +import android.arch.lifecycle.Observer +import android.arch.lifecycle.ViewModelProviders +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.LinearLayoutManager +import android.view.View +import co.joebirch.mobile_ui.R +import co.joebirch.mobile_ui.injection.ViewModelFactory +import co.joebirch.mobile_ui.mapper.ProjectViewMapper +import co.joebirch.presentation.BrowseBookmarkedProjectsViewModel +import co.joebirch.presentation.model.ProjectView +import co.joebirch.presentation.state.Resource +import co.joebirch.presentation.state.ResourceState +import dagger.android.AndroidInjection +import kotlinx.android.synthetic.main.activity_bookmarked.* +import javax.inject.Inject + +class BookmarkedActivity: AppCompatActivity() { + + @Inject lateinit var adapter: BookmarkedAdapter + @Inject lateinit var mapper: ProjectViewMapper + @Inject lateinit var viewModelFactory: ViewModelFactory + lateinit var browseViewModel: BrowseBookmarkedProjectsViewModel + + companion object { + fun getStartIntent(context: Context): Intent { + return Intent(context, BookmarkedActivity::class.java) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_bookmarked) + AndroidInjection.inject(this) + + browseViewModel = ViewModelProviders.of(this, viewModelFactory) + .get(BrowseBookmarkedProjectsViewModel::class.java) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupBrowseRecycler() + } + + override fun onStart() { + super.onStart() + browseViewModel.getProjects().observe(this, + Observer>> { + it?.let { + handleDataState(it) + } + }) + browseViewModel.fetchProjects() + } + + private fun setupBrowseRecycler() { + recycler_projects.layoutManager = LinearLayoutManager(this) + recycler_projects.adapter = adapter + } + + private fun handleDataState(resource: Resource>) { + when (resource.status) { + ResourceState.SUCCESS -> { + progress.visibility = View.GONE + recycler_projects.visibility = View.VISIBLE + resource.data?.let { + adapter.projects = it.map { mapper.mapToView(it) } + adapter.notifyDataSetChanged() + } + } + ResourceState.LOADING -> { + progress.visibility = View.VISIBLE + recycler_projects.visibility = View.GONE + } + } + } +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/bookmarked/BookmarkedAdapter.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/bookmarked/BookmarkedAdapter.kt new file mode 100644 index 0000000..4c726a8 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/bookmarked/BookmarkedAdapter.kt @@ -0,0 +1,52 @@ +package co.joebirch.mobile_ui.bookmarked + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import co.joebirch.mobile_ui.R +import co.joebirch.mobile_ui.model.Project +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import javax.inject.Inject + +class BookmarkedAdapter @Inject constructor(): RecyclerView.Adapter() { + + var projects: List = arrayListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val itemView = LayoutInflater + .from(parent.context) + .inflate(R.layout.item_bookmarked_project, parent, false) + return ViewHolder(itemView) + } + + override fun getItemCount(): Int { + return projects.count() + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val project = projects[position] + holder.ownerNameText.text = project.ownerName + holder.projectNameText.text = project.fullName + + Glide.with(holder.itemView.context) + .load(project.ownerAvatar) + .apply(RequestOptions.circleCropTransform()) + .into(holder.avatarImage) + } + + inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) { + var avatarImage: ImageView + var ownerNameText: TextView + var projectNameText: TextView + + init { + avatarImage = view.findViewById(R.id.image_owner_avatar) + ownerNameText = view.findViewById(R.id.text_owner_name) + projectNameText = view.findViewById(R.id.text_project_name) + } + } +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/browse/BrowseActivity.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/browse/BrowseActivity.kt new file mode 100644 index 0000000..f34aa9b --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/browse/BrowseActivity.kt @@ -0,0 +1,109 @@ +package co.joebirch.mobile_ui.browse + +import android.arch.lifecycle.Observer +import android.arch.lifecycle.ViewModelProviders +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.LinearLayoutManager +import android.view.Menu +import android.view.MenuItem +import android.view.View +import co.joebirch.mobile_ui.R +import co.joebirch.mobile_ui.bookmarked.BookmarkedActivity +import co.joebirch.mobile_ui.injection.ViewModelFactory +import co.joebirch.mobile_ui.mapper.ProjectViewMapper +import co.joebirch.mobile_ui.model.Project +import co.joebirch.presentation.BrowseProjectsViewModel +import co.joebirch.presentation.model.ProjectView +import co.joebirch.presentation.state.Resource +import co.joebirch.presentation.state.ResourceState +import dagger.android.AndroidInjection +import kotlinx.android.synthetic.main.activity_browse.* +import javax.inject.Inject + +class BrowseActivity : AppCompatActivity() { + + @Inject lateinit var browseAdapter: BrowseAdapter + @Inject lateinit var mapper: ProjectViewMapper + @Inject lateinit var viewModelFactory: ViewModelFactory + private lateinit var browseViewModel: BrowseProjectsViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_browse) + AndroidInjection.inject(this) + + browseViewModel = ViewModelProviders.of(this, viewModelFactory) + .get(BrowseProjectsViewModel::class.java) + + setupBrowseRecycler() + } + + override fun onStart() { + super.onStart() + browseViewModel.getProjects().observe(this, + Observer>> { + it?.let { + handleDataState(it) + } + }) + browseViewModel.fetchProjects() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_bookmarked -> { + startActivity(BookmarkedActivity.getStartIntent(this)) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun setupBrowseRecycler() { + browseAdapter.projectListener = projectListener + recycler_projects.layoutManager = LinearLayoutManager(this) + recycler_projects.adapter = browseAdapter + } + + private fun handleDataState(resource: Resource>) { + when (resource.status) { + ResourceState.SUCCESS -> { + setupScreenForSuccess(resource.data?.map { + mapper.mapToView(it) + }) + } + ResourceState.LOADING -> { + progress.visibility = View.VISIBLE + recycler_projects.visibility = View.GONE + } + } + } + + private fun setupScreenForSuccess(projects: List?) { + progress.visibility = View.GONE + projects?.let { + browseAdapter.projects = it + browseAdapter.notifyDataSetChanged() + recycler_projects.visibility = View.VISIBLE + } ?: run { + + } + } + + private val projectListener = object : ProjectListener { + override fun onBookmarkedProjectClicked(projectId: String) { + browseViewModel.unbookmarkProject(projectId) + } + + override fun onProjectClicked(projectId: String) { + browseViewModel.bookmarkProject(projectId) + } + } + +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/browse/BrowseAdapter.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/browse/BrowseAdapter.kt new file mode 100644 index 0000000..daa9eda --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/browse/BrowseAdapter.kt @@ -0,0 +1,71 @@ +package co.joebirch.mobile_ui.browse + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import co.joebirch.mobile_ui.R +import co.joebirch.mobile_ui.model.Project +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import javax.inject.Inject + +class BrowseAdapter @Inject constructor(): RecyclerView.Adapter() { + + var projects: List = arrayListOf() + var projectListener: ProjectListener? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val itemView = LayoutInflater + .from(parent.context) + .inflate(R.layout.item_project, parent, false) + return ViewHolder(itemView) + } + + override fun getItemCount(): Int { + return projects.count() + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val project = projects[position] + holder.ownerNameText.text = project.ownerName + holder.projectNameText.text = project.fullName + + Glide.with(holder.itemView.context) + .load(project.ownerAvatar) + .apply(RequestOptions.circleCropTransform()) + .into(holder.avatarImage) + + val starResource = if (project.isBookmarked) { + R.drawable.ic_star_black_24dp + } else { + R.drawable.ic_star_border_black_24dp + } + holder.bookmarkedImage.setImageResource(starResource) + + holder.itemView.setOnClickListener { + if (project.isBookmarked) { + projectListener?.onBookmarkedProjectClicked(project.id) + } else { + projectListener?.onProjectClicked(project.id) + } + } + } + + inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) { + var avatarImage: ImageView + var ownerNameText: TextView + var projectNameText: TextView + var bookmarkedImage: ImageView + + init { + avatarImage = view.findViewById(R.id.image_owner_avatar) + ownerNameText = view.findViewById(R.id.text_owner_name) + projectNameText = view.findViewById(R.id.text_project_name) + bookmarkedImage = view.findViewById(R.id.image_bookmarked) + } + } + +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/browse/ProjectListener.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/browse/ProjectListener.kt new file mode 100644 index 0000000..ef83385 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/browse/ProjectListener.kt @@ -0,0 +1,9 @@ +package co.joebirch.mobile_ui.browse + +interface ProjectListener { + + fun onBookmarkedProjectClicked(projectId: String) + + fun onProjectClicked(projectId: String) + +} \ 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..df7595b --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/ApplicationComponent.kt @@ -0,0 +1,36 @@ +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 co.joebirch.mobile_ui.injection.module.CacheModule +import co.joebirch.mobile_ui.injection.module.DataModule +import co.joebirch.mobile_ui.injection.module.PresentationModule +import co.joebirch.mobile_ui.injection.module.RemoteModule +import co.joebirch.mobile_ui.injection.module.UiModule +import dagger.BindsInstance +import dagger.Component +import dagger.android.AndroidInjectionModule +import javax.inject.Singleton + +@Singleton +@Component(modules = arrayOf(AndroidInjectionModule::class, + ApplicationModule::class, + UiModule::class, + PresentationModule::class, + DataModule::class, + CacheModule::class, + RemoteModule::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/ViewModelFactory.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/ViewModelFactory.kt new file mode 100644 index 0000000..6cdca45 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/ViewModelFactory.kt @@ -0,0 +1,39 @@ +package co.joebirch.mobile_ui.injection + +import android.arch.lifecycle.ViewModel +import android.arch.lifecycle.ViewModelProvider +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +@Singleton +open class ViewModelFactory : ViewModelProvider.Factory { + + private val creators: Map, Provider> + + @Inject constructor(creators: Map, + @JvmSuppressWildcards Provider>) { + this.creators = creators + } + + override fun create(modelClass: Class): T { + var creator: Provider? = creators[modelClass] + if (creator == null) { + for ((key, value) in creators) { + if (modelClass.isAssignableFrom(key)) { + creator = value + break + } + } + } + if (creator == null) { + throw IllegalStateException("Unknown model class: " + modelClass) + } + try { + return creator.get() as T + } catch (e: Exception) { + throw RuntimeException(e) + } + } + +} \ 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..e719e3b --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/ApplicationModule.kt @@ -0,0 +1,13 @@ +package co.joebirch.mobile_ui.injection.module + +import android.app.Application +import android.content.Context +import dagger.Binds +import dagger.Module + +@Module +abstract class ApplicationModule { + + @Binds + abstract fun bindContext(application: Application): Context +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/CacheModule.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/CacheModule.kt new file mode 100644 index 0000000..c7c2a66 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/CacheModule.kt @@ -0,0 +1,25 @@ +package co.joebirch.mobile_ui.injection.module + +import android.app.Application +import co.joebirch.cache.ProjectsCacheImpl +import co.joebirch.cache.db.ProjectsDatabase +import co.joebirch.data.repository.ProjectsCache +import dagger.Binds +import dagger.Module +import dagger.Provides + +@Module +abstract class CacheModule { + + @Module + companion object { + @Provides + @JvmStatic + fun providesDataBase(application: Application): ProjectsDatabase { + return ProjectsDatabase.getInstance(application) + } + } + + @Binds + abstract fun bindProjectsCache(projectsCache: ProjectsCacheImpl): ProjectsCache +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/DataModule.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/DataModule.kt new file mode 100644 index 0000000..d662a5a --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/DataModule.kt @@ -0,0 +1,13 @@ +package co.joebirch.mobile_ui.injection.module + +import co.joebirch.data.ProjectsDataRepository +import co.joebirch.domain.repository.ProjectsRepository +import dagger.Binds +import dagger.Module + +@Module +abstract class DataModule { + + @Binds + abstract fun bindDataRepository(dataRepository: ProjectsDataRepository): ProjectsRepository +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/PresentationModule.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/PresentationModule.kt new file mode 100644 index 0000000..c66a103 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/PresentationModule.kt @@ -0,0 +1,37 @@ +package co.joebirch.mobile_ui.injection.module + +import android.arch.lifecycle.ViewModel +import android.arch.lifecycle.ViewModelProvider +import co.joebirch.mobile_ui.injection.ViewModelFactory +import co.joebirch.presentation.BrowseBookmarkedProjectsViewModel +import co.joebirch.presentation.BrowseProjectsViewModel +import dagger.Binds +import dagger.MapKey +import dagger.Module +import dagger.multibindings.IntoMap +import kotlin.reflect.KClass + +@Module +abstract class PresentationModule { + + @Binds + @IntoMap + @ViewModelKey(BrowseProjectsViewModel::class) + abstract fun bindBrowseProjectsViewModel(viewModel: BrowseProjectsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(BrowseBookmarkedProjectsViewModel::class) + abstract fun bindBrowseBookmarkedProjectsViewModel( + viewModel: BrowseBookmarkedProjectsViewModel): ViewModel + + + @Binds + abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory +} + +@MustBeDocumented +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MapKey +annotation class ViewModelKey(val value: KClass) \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/RemoteModule.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/RemoteModule.kt new file mode 100644 index 0000000..8b55626 --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/RemoteModule.kt @@ -0,0 +1,26 @@ +package co.joebirch.mobile_ui.injection.module + +import co.joebirch.data.repository.ProjectsRemote +import co.joebirch.mobile_ui.BuildConfig +import co.joebirch.remote.ProjectsRemoteImpl +import co.joebirch.remote.service.GithubTrendingService +import co.joebirch.remote.service.GithubTrendingServiceFactory +import dagger.Binds +import dagger.Module +import dagger.Provides + +@Module +abstract class RemoteModule { + + @Module + companion object { + @Provides + @JvmStatic + fun provideGithubService(): GithubTrendingService { + return GithubTrendingServiceFactory.makeGithubTrendingService(BuildConfig.DEBUG) + } + } + + @Binds + abstract fun bindProjectsRemote(projectsRemote: ProjectsRemoteImpl): ProjectsRemote +} \ No newline at end of file diff --git a/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/UiModule.kt b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/UiModule.kt new file mode 100644 index 0000000..b5ba46d --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/injection/module/UiModule.kt @@ -0,0 +1,22 @@ +package co.joebirch.mobile_ui.injection.module + +import co.joebirch.domain.executor.PostExecutionThread +import co.joebirch.mobile_ui.UiThread +import co.joebirch.mobile_ui.bookmarked.BookmarkedActivity +import co.joebirch.mobile_ui.browse.BrowseActivity +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class UiModule { + + @Binds + abstract fun bindPostExecutionThread(uiThread: UiThread): PostExecutionThread + + @ContributesAndroidInjector + abstract fun contributesBrowseActivity(): BrowseActivity + + @ContributesAndroidInjector + abstract fun contributesBookmarkedActivity(): BookmarkedActivity +} \ 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..406ff29 --- /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, presentation.isBookmarked) + } + +} \ 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..3aebd4a --- /dev/null +++ b/mobile-ui/src/main/java/co/joebirch/mobile_ui/model/Project.kt @@ -0,0 +1,6 @@ +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, + val isBookmarked: Boolean) \ 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_collections_bookmark_black_24dp.xml b/mobile-ui/src/main/res/drawable/ic_collections_bookmark_black_24dp.xml new file mode 100644 index 0000000..49e6c06 --- /dev/null +++ b/mobile-ui/src/main/res/drawable/ic_collections_bookmark_black_24dp.xml @@ -0,0 +1,5 @@ + + + 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/drawable/ic_star_black_24dp.xml b/mobile-ui/src/main/res/drawable/ic_star_black_24dp.xml new file mode 100644 index 0000000..a87ca09 --- /dev/null +++ b/mobile-ui/src/main/res/drawable/ic_star_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/mobile-ui/src/main/res/drawable/ic_star_border_black_24dp.xml b/mobile-ui/src/main/res/drawable/ic_star_border_black_24dp.xml new file mode 100644 index 0000000..b36536b --- /dev/null +++ b/mobile-ui/src/main/res/drawable/ic_star_border_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/mobile-ui/src/main/res/layout/activity_bookmarked.xml b/mobile-ui/src/main/res/layout/activity_bookmarked.xml new file mode 100644 index 0000000..1a9a1e2 --- /dev/null +++ b/mobile-ui/src/main/res/layout/activity_bookmarked.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file 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/item_bookmarked_project.xml b/mobile-ui/src/main/res/layout/item_bookmarked_project.xml new file mode 100644 index 0000000..fc62343 --- /dev/null +++ b/mobile-ui/src/main/res/layout/item_bookmarked_project.xml @@ -0,0 +1,50 @@ + + + + + + + + + + \ 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..cf961fc --- /dev/null +++ b/mobile-ui/src/main/res/layout/item_project.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile-ui/src/main/res/menu/main.xml b/mobile-ui/src/main/res/menu/main.xml new file mode 100644 index 0000000..f5465f1 --- /dev/null +++ b/mobile-ui/src/main/res/menu/main.xml @@ -0,0 +1,9 @@ + + + + \ 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 0000000..a2f5908 Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..1b52399 Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/mobile-ui/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile-ui/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..ff10afd Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..115a4c7 Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..dcd3cd8 Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher.png differ 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 0000000..459ca60 Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ 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 0000000..8ca12fe Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 0000000..8e19b41 Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b824ebd Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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 0000000..4c19a13 Binary files /dev/null and b/mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ 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..6aa229d --- /dev/null +++ b/mobile-ui/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + Github Trending + MainActivity + Bookmarked projects + Bookmarked + 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 @@ + + + + + + + +