├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── build.gradle ├── cache ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── tayfuncesur │ │ │ └── cache │ │ │ ├── Constants.kt │ │ │ ├── ProjectsCacheImpl.kt │ │ │ ├── dao │ │ │ └── CachedProjectsDao.kt │ │ │ ├── db │ │ │ └── ProjectsDatabase.kt │ │ │ ├── mapper │ │ │ ├── CacheMapper.kt │ │ │ └── CachedProjectMapper.kt │ │ │ └── model │ │ │ └── CachedProject.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── tayfuncesur │ └── cache │ ├── MockData.kt │ ├── ProjectsCacheImplTest.kt │ ├── RunAll.kt │ ├── dao │ └── CachedProjectsDaoTest.kt │ └── mapper │ └── CachedProjectMapperTest.kt ├── data ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── tayfuncesur │ │ └── data │ │ ├── ProjectsDataRepository.kt │ │ ├── mapper │ │ ├── Mapper.kt │ │ └── ProjectMapper.kt │ │ ├── model │ │ └── ProjectEntity.kt │ │ ├── repository │ │ ├── ProjectsCache.kt │ │ ├── ProjectsDataStore.kt │ │ └── ProjectsRemote.kt │ │ └── store │ │ ├── ProjectsCacheDataStore.kt │ │ ├── ProjectsDataStoreFactory.kt │ │ └── ProjectsRemoteDataStore.kt │ └── test │ └── java │ └── com │ └── tayfuncesur │ └── data │ ├── ProjectDataRepositoryTest.kt │ ├── RunAll.kt │ ├── data │ └── MockData.kt │ ├── mapper │ └── ProjectMapperTest.kt │ └── store │ ├── ProjectsCacheDataStoreTest.kt │ └── ProjectsRemoteDataStoreTest.kt ├── dependencies.gradle ├── domain ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── tayfuncesur │ │ └── domain │ │ ├── CompletableUseCase.kt │ │ ├── ObservableUseCase.kt │ │ ├── bookmark │ │ ├── BookmarkProject.kt │ │ ├── GetBookmarkedProjects.kt │ │ └── UnbookmarkProject.kt │ │ ├── executor │ │ └── PostExecutionThread.kt │ │ ├── getProjects │ │ └── GetProjects.kt │ │ ├── model │ │ └── Project.kt │ │ └── repository │ │ └── ProjectsRepository.kt │ └── test │ └── java │ └── com │ └── tayfuncesur │ └── data │ ├── MockData.kt │ ├── RunAll.kt │ ├── bookmark │ ├── BookmarkProjectTest.kt │ ├── GetBookmarkedProjectsTest.kt │ └── UnbookmarkProject.kt │ └── getProjects │ └── GetProjectsTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mobile ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tayfuncesur │ │ └── mobile │ │ ├── RunAllUITests.kt │ │ ├── di │ │ ├── TestAppComponent.kt │ │ └── module │ │ │ ├── TestApplicationModule.kt │ │ │ ├── TestCacheModule.kt │ │ │ ├── TestDataModule.kt │ │ │ └── TestRemoteModule.kt │ │ ├── test │ │ ├── Helper.kt │ │ ├── TestApp.kt │ │ └── TestRunner.kt │ │ └── ui │ │ ├── data │ │ └── MockData.kt │ │ ├── detail │ │ └── DetailActivityTest.kt │ │ └── main │ │ └── MainActivityTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── anims │ │ │ ├── bookmark.json │ │ │ ├── error.json │ │ │ ├── loading.json │ │ │ └── no_connection.json │ ├── java │ │ └── com │ │ │ └── tayfuncesur │ │ │ └── mobile │ │ │ ├── App.kt │ │ │ ├── UIThread.kt │ │ │ ├── base │ │ │ ├── ActivityExtensions.kt │ │ │ ├── BaseDaggerActivity.kt │ │ │ └── UIConstants.kt │ │ │ ├── di │ │ │ ├── AppComponent.kt │ │ │ ├── ViewModelFactory.kt │ │ │ └── module │ │ │ │ ├── ApplicationModule.kt │ │ │ │ ├── CacheModule.kt │ │ │ │ ├── DataModule.kt │ │ │ │ ├── PresentationModule.kt │ │ │ │ ├── RemoteModule.kt │ │ │ │ └── UIModule.kt │ │ │ ├── mapper │ │ │ ├── ProjectMapper.kt │ │ │ └── ViewMapper.kt │ │ │ ├── model │ │ │ └── Project.kt │ │ │ └── ui │ │ │ ├── detail │ │ │ └── MainDetailActivity.kt │ │ │ ├── main │ │ │ ├── MainActivity.kt │ │ │ └── MainAdapter.kt │ │ │ ├── noConnection │ │ │ └── NoConnectionFragment.kt │ │ │ └── splash │ │ │ └── SplashActivity.kt │ └── res │ │ ├── drawable │ │ ├── appicon.png │ │ ├── appiconblack.png │ │ ├── ic_bookmarked.xml │ │ ├── ic_favorite.xml │ │ ├── ic_favorite_outlined.xml │ │ └── ic_star.xml │ │ ├── font │ │ ├── manropemedium.ttf │ │ └── manroperegular.ttf │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── main_detail.xml │ │ ├── no_connection.xml │ │ ├── single_row.xml │ │ └── splash_screen.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── tayfuncesur │ └── mobile │ ├── MockData.kt │ └── ProjectMapperTest.kt ├── presentation ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── tayfuncesur │ │ │ └── presentation │ │ │ ├── BookmarkViewModel.kt │ │ │ ├── ProjectsViewModel.kt │ │ │ ├── SplashScreenViewModel.kt │ │ │ ├── mapper │ │ │ ├── Mapper.kt │ │ │ └── ProjectViewMapper.kt │ │ │ ├── model │ │ │ └── ProjectView.kt │ │ │ └── state │ │ │ └── Resource.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── tayfuncesur │ └── presentation │ ├── MockData.kt │ ├── RunAll.kt │ ├── mapper │ └── ProjectViewMapperTest.kt │ └── viewmodel │ ├── BookmarkViewModelTest.kt │ └── ProjectsViewModelTest.kt ├── remote ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── tayfuncesur │ │ └── remote │ │ ├── ProjectsRemoteImpl.kt │ │ ├── mapper │ │ ├── Mapper.kt │ │ └── ProjectResponseMapper.kt │ │ ├── model │ │ └── ProjectModel.kt │ │ └── service │ │ ├── GithubService.kt │ │ └── GithubServiceFactory.kt │ └── test │ └── java │ └── com │ └── tayfuncesur │ └── data │ ├── mapper │ └── ProjectModelMapperTest.kt │ └── remote │ └── MockData.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Project Browser 2 |       This is a sample Android Project that is based on [Uncle Bob's Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) .It contains six different layers but the count of layers may change with respect to your project, you may have more or less layers. All of them have own mission but none of them interests with the others. That is the main idea actually. I will not explain the Clean Architecture because this is already well documented. I will just explain how I implemented. Here is the preview of project for just motivation before diving 3 | 4 | 5 | 6 | ### Layers 7 | When you do an action in your application, the flow will be like below respectively 8 | - **UI Layer** is the layer that displays your data. You may use multi UI layer in your project. (*For instance you have already an app that runs in phones and if you want to expand your targets for new platforms such as watches or TVs. There is no problem for you anymore.*) 9 | - **Presentation Layer** is the layer like a gateway between your UI and Business. You can use any design pattern in here(MVP,MVVM,MVI etc.) 10 | (***MVVM*** is used in this project) 11 | - **Domain Layer,** is the core layer that represents the use cases of project. You can think this layer as a middleman. 12 | - **Data Layer**, is the layer that determines where the data will come from. 13 | It has two sublayers. 14 | - **Remote Layer** is the layer that fetches the data from network. (***Retrofit2*** is used in this project) 15 | - **Cache Layer** is the layer that is responsible for getting data from sqlite 16 | (***Room*** is used in this project) 17 | 18 | ## UI Layer 19 |       As I mentioned above, this is the layer that displays your data. It may use your presenters or your ViewModels. (ViewModels are used in this projects.) This layer works as usual way. I mean, observes your livedatas and displays the user. 20 | ## Presentation 21 |       This is the layer that is the easiest one in my opinion. It's mission is so simple. It has ViewModels that are just executing your command like below.( *For instance, getProjects*) 22 | ``` 23 | fun getProjects() { 24 | remoteProjectsLiveData.postValue(Resource.Loading()) 25 | getProjects.execute(ProjectsSubscriber())//Here, the flow navigates through the domain layer 26 | } 27 | ``` 28 | The ViewModel's main functions are so simple like above. 29 | 30 | ## Domain 31 |       This is the core layer. You should determine your projects use cases. For instance, in this project, I needed four use cases. These are, (*in my **ProjectsRepository** interface*) 32 | - getProjects(): Observable> 33 | - getBookmarkedProjects(): Observable> 34 | - bookmarkProject(projectId: String): Completable 35 | - unbookmarkProject(projectId: String) 36 | 37 |       But of course that depends on your projects needs. And you can imagine this interface like a *bridge* that navigates your flow to the data layer. So if you want to display projects, your class *(GetProjects)* calls the getProjects() function of the *ProjectsRepository* and then the flow navigates through the data layer because data layer implements your bridge. 38 | 39 | ## Data 40 |       The core class of Data layer is ***ProjectsDataRepository*** class in this project because that class implements your use cases and takes care of them. 41 | 42 | ``` 43 | class ProjectsDataRepository @Inject constructor( 44 | private val mapper: ProjectMapper, 45 | private val dataStore: ProjectsDataStoreFactory 46 | ) : ProjectsRepository { // This is the interface that represents your use cases 47 | // You defined it in the Domain Layer 48 | override fun getProjects(): Observable> { 49 | return dataStore.getRemoteDataStore().getProjects().map { 50 | it.map { 51 | mapper.mapFromEntity(it) 52 | } 53 | } 54 | } 55 | } 56 | ``` 57 |       You can think this layer responsible for getting and serving data through the Domain layer but it doesn't care about where or how the data comes. It just a navigator that navigates your flow through the Remote or Cache. 58 |       In this project, Data layer navigates the flow if getProjects() calls, because projects must be fetched from network, but if the other use cases are intended for example *bookmarkProject* or *getBookmarkedProjects*, these data must be fetched from local storage.So data layer navigates your flow though the cache layer. 59 | ## Remote Layer 60 |       This layer is also easy layer because it has only one responsibility. It just making a network request via *Retrofit2* 61 | It has only one class that is called by ***ProjectsRemoteImpl*** 62 | 63 | ## Cache Layer 64 |       This layer is responsible for managing the local storage, getting and serving data through the Data Layer. It uses Room which is an awesome library. It just calls the DAO functions. 65 | 66 | # Keynotes & Bonuses 67 | 68 | ### Mapper 69 |       This project have many mapper classes. This was my decision actually not a requirement but this is good way because for this, each layer can work with different model and anyone don't depends on some other model. I have mapper interface like below,(For example CacheMapper, because I can cache some additional data like creationDate etc which doesn't concerns the other layers.) 70 | ``` 71 | interface CacheMapper { //Like I: Input, O:Output 72 | 73 | fun mapFromCache(cache: I): O 74 | 75 | fun mapToCache(dataToCache: O): I 76 | } 77 | ``` 78 | 79 | ### Dependencies 80 |       Many of these layers above uses same dependencies like RX, JavaxAnnotation, or mockito for testing. So it would be really good to manage them all in one file. For this reason, this project uses the dependencies.gradle file and it applies in main build.gradle file 81 | ``` 82 | apply from: 'dependencies.gradle' 83 | ``` 84 | ### Bonus 1: Tests 85 |        This project contains all its units and UI tests and I strongly recommmend you to have a look cache layer unit tests because there are Room and local storage tests. 86 | 87 | ### Bonus 2: [ReactiveNetwork](https://github.com/pwittchen/ReactiveNetwork) 88 |        You can learn the implementation of the ReactiveNetwork and how to broadcast the network connection with Rx from this project. It is really useful because there is a behaviour changes between API levels. 89 | 90 | # Core Libraries 91 | - [Android Architecture Components](https://developer.android.com/topic/libraries/architecture/) 92 | - [Dagger2](https://google.github.io/dagger/) 93 | - [Rx](https://github.com/ReactiveX/RxJava) 94 | - [Lottie](https://github.com/airbnb/lottie-android) 95 | - [Room](https://developer.android.com/topic/libraries/architecture/room) 96 | - [Retrofit2](http://square.github.io/retrofit/) 97 | - [Mockito](https://github.com/mockito/mockito) 98 | - [Espresso](https://developer.android.com/training/testing/espresso) 99 | 100 | # Summary 101 |       To use Clean Architecture or not, this is not requirement actually this is up to you and your project. But if you want to ***[seperate your concerns](https://www.slideshare.net/outware/a-separation-of-concerns-clean-architecture-on-android)*** for example testing, maintaining. I found it really useful. 102 | 103 | ### Thanks 104 |       This project is based on [Joe Birch's Github Trending](https://github.com/hitherejoe/GithubTrending) 105 | If you have any questions, hit me on [Twitter](https://twitter.com/CesurTayfun35) 106 | 107 | ## Licence 108 | ``` 109 | Copyright 2019 Tayfun CESUR 110 | 111 | Licensed under the Apache License, Version 2.0 (the "License"); 112 | you may not use this file except in compliance with the License. 113 | You may obtain a copy of the License at 114 | 115 | http://www.apache.org/licenses/LICENSE-2.0 116 | 117 | Unless required by applicable law or agreed to in writing, software 118 | distributed under the License is distributed on an "AS IS" BASIS, 119 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 120 | See the License for the specific language governing permissions and 121 | limitations under the License. 122 | ``` 123 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.21' 5 | repositories { 6 | google() 7 | jcenter() 8 | 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.3.2' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | apply from: 'dependencies.gradle' 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /cache/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /cache/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin:'kotlin-android' 3 | apply plugin:'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 28 7 | 8 | defaultConfig { 9 | minSdkVersion 16 10 | targetSdkVersion 28 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | 20 | } 21 | 22 | dependencies { 23 | def cacheDependencies = rootProject.ext.cacheDependencies 24 | def cacheTestDependencies = rootProject.ext.cacheTestDependencies 25 | 26 | compileOnly cacheDependencies.javaxAnnotation 27 | 28 | implementation project(':data') 29 | 30 | implementation cacheDependencies.kotlin 31 | implementation cacheDependencies.javaxInject 32 | implementation cacheDependencies.rxKotlin 33 | implementation cacheDependencies.roomRuntime 34 | implementation cacheDependencies.roomRxJava 35 | kapt cacheDependencies.roomCompiler 36 | 37 | testImplementation cacheTestDependencies.junit 38 | testImplementation cacheTestDependencies.kotlinJUnit 39 | testImplementation cacheTestDependencies.mockito 40 | testImplementation cacheTestDependencies.robolectric 41 | testImplementation cacheTestDependencies.archTesting 42 | testImplementation cacheTestDependencies.roomTesting 43 | androidTestImplementation cacheTestDependencies.supportAnnotations 44 | androidTestImplementation cacheTestDependencies.junit 45 | } 46 | -------------------------------------------------------------------------------- /cache/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /cache/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /cache/src/main/java/com/tayfuncesur/cache/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache 2 | 3 | object Constants { 4 | 5 | const val dbName= "TayfunCesurTask" 6 | 7 | const val tableName = "Projects" 8 | 9 | const val selectQuery = "SELECT * from $tableName" 10 | 11 | } -------------------------------------------------------------------------------- /cache/src/main/java/com/tayfuncesur/cache/ProjectsCacheImpl.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache 2 | 3 | import com.tayfuncesur.cache.db.ProjectsDatabase 4 | import com.tayfuncesur.cache.mapper.CachedProjectMapper 5 | import com.tayfuncesur.cache.model.CachedProject 6 | import com.tayfuncesur.data.repository.ProjectsCache 7 | import io.reactivex.Completable 8 | import io.reactivex.Observable 9 | import javax.inject.Inject 10 | 11 | class ProjectsCacheImpl @Inject constructor( 12 | private val projectsDatabase: ProjectsDatabase, 13 | private val mapper: CachedProjectMapper 14 | ) : ProjectsCache { 15 | 16 | override fun getBookmarkedProjects(): Observable> { 17 | return projectsDatabase.cachedProjectsDao().getBookmarkedProjects().toObservable().map { 18 | it.map { 19 | mapper.mapFromCache(it) 20 | } 21 | } 22 | } 23 | 24 | override fun setProjectBookmarked(projectId: String): Completable { 25 | return Completable.defer { 26 | projectsDatabase.cachedProjectsDao().setBookmarkedProject(CachedProject(projectId, true)) 27 | Completable.complete() 28 | } 29 | 30 | } 31 | 32 | override fun setProjectUnbookmarked(projectId: String): Completable { 33 | return Completable.defer { 34 | projectsDatabase.cachedProjectsDao().setUnBookmarkedProject(projectId) 35 | Completable.complete() 36 | } 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /cache/src/main/java/com/tayfuncesur/cache/dao/CachedProjectsDao.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache.dao 2 | 3 | import android.arch.persistence.room.* 4 | import com.tayfuncesur.cache.Constants 5 | import com.tayfuncesur.cache.model.CachedProject 6 | import io.reactivex.Flowable 7 | 8 | @Dao 9 | interface CachedProjectsDao { 10 | 11 | @Query(Constants.selectQuery) 12 | fun getBookmarkedProjects(): Flowable> 13 | 14 | @Insert(onConflict = OnConflictStrategy.REPLACE) 15 | fun setBookmarkedProject(cachedProject: CachedProject) 16 | 17 | @Query("DELETE FROM ${Constants.tableName} where id = :projectId") 18 | fun setUnBookmarkedProject(projectId: String) 19 | 20 | } -------------------------------------------------------------------------------- /cache/src/main/java/com/tayfuncesur/cache/db/ProjectsDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache.db 2 | 3 | import android.arch.persistence.room.Database 4 | import android.arch.persistence.room.Room 5 | import android.arch.persistence.room.RoomDatabase 6 | import android.content.Context 7 | import com.tayfuncesur.cache.Constants 8 | import com.tayfuncesur.cache.dao.CachedProjectsDao 9 | import com.tayfuncesur.cache.model.CachedProject 10 | import javax.inject.Inject 11 | 12 | @Database(version = 1, entities = [CachedProject::class]) 13 | abstract class ProjectsDatabase @Inject constructor() : RoomDatabase() { 14 | 15 | abstract fun cachedProjectsDao(): CachedProjectsDao 16 | 17 | companion object { 18 | 19 | @Volatile 20 | private var Instance: ProjectsDatabase? = null 21 | 22 | fun getInstance(context: Context): ProjectsDatabase { 23 | return Instance ?: synchronized(this) { 24 | Instance ?: buildDatabase(context).also { Instance = it } 25 | } 26 | } 27 | 28 | private fun buildDatabase(context: Context): ProjectsDatabase { 29 | return Room.databaseBuilder(context.applicationContext, ProjectsDatabase::class.java, Constants.dbName) 30 | .build() 31 | } 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /cache/src/main/java/com/tayfuncesur/cache/mapper/CacheMapper.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache.mapper 2 | 3 | interface CacheMapper { 4 | 5 | fun mapFromCache(cache: I): O 6 | 7 | fun mapToCache(cache: O): I 8 | 9 | } -------------------------------------------------------------------------------- /cache/src/main/java/com/tayfuncesur/cache/mapper/CachedProjectMapper.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache.mapper 2 | 3 | import com.tayfuncesur.cache.model.CachedProject 4 | 5 | class CachedProjectMapper : CacheMapper { 6 | 7 | override fun mapFromCache(cache: CachedProject): String { 8 | return cache.id 9 | } 10 | 11 | override fun mapToCache(cache: String): CachedProject { 12 | return CachedProject(cache, true) 13 | } 14 | } -------------------------------------------------------------------------------- /cache/src/main/java/com/tayfuncesur/cache/model/CachedProject.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache.model 2 | 3 | import android.arch.persistence.room.Entity 4 | import android.arch.persistence.room.PrimaryKey 5 | import com.tayfuncesur.cache.Constants 6 | 7 | @Entity(tableName = Constants.tableName) 8 | class CachedProject(@PrimaryKey var id: String, var isBookmarked: Boolean) -------------------------------------------------------------------------------- /cache/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | cache 3 | 4 | -------------------------------------------------------------------------------- /cache/src/test/java/com/tayfuncesur/cache/MockData.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache 2 | 3 | import com.tayfuncesur.cache.model.CachedProject 4 | import com.tayfuncesur.data.model.ProjectEntity 5 | import java.util.* 6 | 7 | object MockData { 8 | 9 | fun generateRandomProjectEntity(isBookmarked : Boolean = false): ProjectEntity { 10 | return ProjectEntity(UUID.randomUUID().toString(), UUID.randomUUID().toString(), Math.random().toInt(), isBookmarked) 11 | } 12 | 13 | fun generateRandomCachedProject(): CachedProject { 14 | return CachedProject(UUID.randomUUID().toString(), Math.random() < 0.5) 15 | } 16 | 17 | fun generateRandomCachedProjectList(count: Int = 20): List { 18 | val list = mutableListOf() 19 | repeat(count) { 20 | list.add(generateRandomCachedProject()) 21 | } 22 | return list 23 | } 24 | 25 | 26 | } -------------------------------------------------------------------------------- /cache/src/test/java/com/tayfuncesur/cache/ProjectsCacheImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache 2 | 3 | import android.arch.core.executor.testing.InstantTaskExecutorRule 4 | import android.arch.persistence.room.Room 5 | import com.tayfuncesur.cache.db.ProjectsDatabase 6 | import com.tayfuncesur.cache.mapper.CachedProjectMapper 7 | import org.junit.BeforeClass 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.robolectric.RobolectricTestRunner 12 | import org.robolectric.RuntimeEnvironment 13 | import kotlin.test.assertEquals 14 | 15 | @RunWith(RobolectricTestRunner::class) 16 | class ProjectsCacheImplTest { 17 | 18 | 19 | @get:Rule 20 | var instantTaskExecutorRule = InstantTaskExecutorRule() 21 | 22 | private val database = Room.inMemoryDatabaseBuilder( 23 | RuntimeEnvironment.application.applicationContext, 24 | ProjectsDatabase::class.java 25 | ).allowMainThreadQueries() 26 | .build() 27 | 28 | private val cacheMapper = CachedProjectMapper() 29 | 30 | private val projectsCacheImpl = ProjectsCacheImpl(database, cacheMapper) 31 | 32 | @Test 33 | fun shouldGetBookmarkedProjectsReturnsData() { 34 | val randomCachedProject = MockData.generateRandomCachedProject() 35 | projectsCacheImpl.setProjectBookmarked(randomCachedProject.id).test() 36 | val result = projectsCacheImpl.getBookmarkedProjects().test() 37 | assertEquals(result.values()[0].isNotEmpty(), true) 38 | } 39 | 40 | @Test 41 | fun shouldSetBookmarkProjectReturnsExpectedData() { 42 | val randomCachedProject = MockData.generateRandomCachedProject() 43 | randomCachedProject.isBookmarked = true 44 | projectsCacheImpl.setProjectBookmarked(randomCachedProject.id).test() 45 | val data = projectsCacheImpl.getBookmarkedProjects().test() 46 | assertEquals(data.values()[0][0], randomCachedProject.id) 47 | } 48 | 49 | @Test 50 | fun shouldUnbookmarkProjectWork() { 51 | val randomCachedProject = MockData.generateRandomCachedProject() 52 | randomCachedProject.isBookmarked = true 53 | projectsCacheImpl.setProjectBookmarked(randomCachedProject.id).test() 54 | 55 | 56 | projectsCacheImpl.setProjectUnbookmarked(randomCachedProject.id).test() 57 | 58 | 59 | val result = projectsCacheImpl.getBookmarkedProjects().test() 60 | assertEquals(result.values()[0].size, 0) 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /cache/src/test/java/com/tayfuncesur/cache/RunAll.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache 2 | 3 | import com.tayfuncesur.cache.dao.CachedProjectsDaoTest 4 | import com.tayfuncesur.cache.mapper.CachedProjectMapperTest 5 | import org.junit.runner.RunWith 6 | import org.junit.runners.Suite 7 | 8 | @RunWith(Suite::class) 9 | @Suite.SuiteClasses(CachedProjectsDaoTest::class, CachedProjectMapperTest::class, ProjectsCacheImplTest::class) 10 | class RunAll -------------------------------------------------------------------------------- /cache/src/test/java/com/tayfuncesur/cache/dao/CachedProjectsDaoTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache.dao 2 | 3 | import android.arch.core.executor.testing.InstantTaskExecutorRule 4 | import android.arch.persistence.room.Room 5 | import com.tayfuncesur.cache.MockData 6 | import com.tayfuncesur.cache.db.ProjectsDatabase 7 | import org.junit.* 8 | import org.junit.runner.RunWith 9 | import org.mockito.Mockito 10 | import org.robolectric.RobolectricTestRunner 11 | import org.robolectric.RuntimeEnvironment 12 | import kotlin.test.assertEquals 13 | 14 | @RunWith(RobolectricTestRunner::class) 15 | class CachedProjectsDaoTest { 16 | 17 | @Rule 18 | @JvmField 19 | var instantTaskExecutorRule = InstantTaskExecutorRule() 20 | 21 | lateinit var database: ProjectsDatabase 22 | 23 | @Before 24 | fun setup() { 25 | database = Room.inMemoryDatabaseBuilder( 26 | RuntimeEnvironment.application.applicationContext, 27 | ProjectsDatabase::class.java 28 | ).allowMainThreadQueries() 29 | .build() 30 | } 31 | 32 | @Test 33 | fun shouldGetBookmarkedProjectsReturnsExpectedData() { 34 | val cachedProject = MockData.generateRandomCachedProject() 35 | database.cachedProjectsDao().setBookmarkedProject(cachedProject) 36 | 37 | val list = database.cachedProjectsDao().getBookmarkedProjects().test() 38 | assertEquals(list.values()[0][0].id, cachedProject.id) 39 | assertEquals(list.values()[0][0].isBookmarked, cachedProject.isBookmarked) 40 | } 41 | 42 | @Test 43 | fun shouldSetBookmarkedProjectReturnsExpectedData() { 44 | val cachedProject = MockData.generateRandomCachedProject() 45 | cachedProject.isBookmarked = true 46 | database.cachedProjectsDao().setBookmarkedProject(cachedProject) 47 | 48 | val list = database.cachedProjectsDao().getBookmarkedProjects().test() 49 | assertEquals(list.values()[0][0].id, cachedProject.id) 50 | assertEquals(list.values()[0][0].isBookmarked, cachedProject.isBookmarked) 51 | } 52 | 53 | @Test 54 | fun shouldUnbookmarkedProjectReturnsExpectedData() { 55 | val cachedProject = MockData.generateRandomCachedProject() 56 | cachedProject.isBookmarked = true 57 | database.cachedProjectsDao().setBookmarkedProject(cachedProject) 58 | database.cachedProjectsDao().setUnBookmarkedProject(cachedProject.id) 59 | 60 | val list = database.cachedProjectsDao().getBookmarkedProjects().test() 61 | assertEquals(list.values()[0].size, 0) 62 | } 63 | 64 | 65 | @After 66 | fun close() { 67 | database.close() 68 | } 69 | 70 | 71 | } -------------------------------------------------------------------------------- /cache/src/test/java/com/tayfuncesur/cache/mapper/CachedProjectMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.cache.mapper 2 | 3 | import com.tayfuncesur.cache.MockData 4 | import org.junit.Test 5 | import org.junit.runner.RunWith 6 | import org.junit.runners.JUnit4 7 | import kotlin.test.assertEquals 8 | 9 | @RunWith(JUnit4::class) 10 | class CachedProjectMapperTest { 11 | 12 | private val mapper = CachedProjectMapper() 13 | 14 | @Test 15 | fun shouldMapFromCache() { 16 | val cached = MockData.generateRandomCachedProject() 17 | val mapped = mapper.mapFromCache(cached) 18 | assertEquals(cached.id, mapped) 19 | } 20 | 21 | @Test 22 | fun shouldMapToCache() { 23 | val entity = MockData.generateRandomProjectEntity(true) 24 | val cached = mapper.mapToCache(entity.id) 25 | assertEquals(cached.id, entity.id) 26 | assertEquals(cached.isBookmarked, entity.isBookmarked) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /data/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | dependencies { 4 | def dataDependencies = rootProject.ext.dataDependencies 5 | def dataTestDependencies = rootProject.ext.dataTestDependencies 6 | 7 | implementation project(':domain') 8 | 9 | implementation dataDependencies.javaxAnnotation 10 | implementation dataDependencies.javaxInject 11 | implementation dataDependencies.rxKotlin 12 | 13 | testImplementation dataTestDependencies.junit 14 | testImplementation dataTestDependencies.kotlinJUnit 15 | testImplementation dataTestDependencies.mockito 16 | } 17 | 18 | sourceCompatibility = "1.6" 19 | targetCompatibility = "1.6" 20 | -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/ProjectsDataRepository.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data 2 | 3 | import com.tayfuncesur.data.mapper.ProjectMapper 4 | import com.tayfuncesur.data.store.ProjectsDataStoreFactory 5 | import com.tayfuncesur.domain.model.Project 6 | import com.tayfuncesur.domain.repository.ProjectsRepository 7 | import io.reactivex.Completable 8 | import io.reactivex.Observable 9 | import javax.inject.Inject 10 | 11 | class ProjectsDataRepository @Inject constructor( 12 | private val mapper: ProjectMapper, 13 | private val dataStore: ProjectsDataStoreFactory 14 | ) : ProjectsRepository { 15 | override fun getProjects(): Observable> { 16 | return dataStore.getRemoteDataStore().getProjects().map { 17 | it.map { 18 | mapper.mapFromEntity(it) 19 | } 20 | } 21 | } 22 | 23 | override fun bookmarkProject(projectId: String): Completable { 24 | return dataStore.getCacheDataStore().setProjectBookmarked(projectId) 25 | } 26 | 27 | override fun unbookmarkProject(projectId: String): Completable { 28 | return dataStore.getCacheDataStore().setProjectUnbookmarked(projectId) 29 | } 30 | 31 | override fun getBookmarkedProjects(): Observable> { 32 | return dataStore.getCacheDataStore().getBookmarkedProjects() 33 | } 34 | } -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.mapper 2 | 3 | interface Mapper { 4 | 5 | fun mapFromEntity(entity: E): D 6 | 7 | fun mapToEntity(domain: D): E 8 | 9 | } -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/mapper/ProjectMapper.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.mapper 2 | 3 | import com.tayfuncesur.data.model.ProjectEntity 4 | import com.tayfuncesur.domain.model.Project 5 | 6 | class ProjectMapper : Mapper { 7 | override fun mapFromEntity(entity: ProjectEntity): Project { 8 | return Project(entity.id, entity.fullName, entity.starCount, entity.isBookmarked) 9 | } 10 | 11 | override fun mapToEntity(domain: Project): ProjectEntity { 12 | return ProjectEntity(domain.id, domain.fullName, domain.starCount, domain.isBookmarked) 13 | } 14 | } -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/model/ProjectEntity.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.model 2 | 3 | data class ProjectEntity( 4 | val id: String, val fullName: String, 5 | val starCount: Int, val isBookmarked: Boolean 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/repository/ProjectsCache.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.repository 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Observable 5 | 6 | interface ProjectsCache { 7 | 8 | fun getBookmarkedProjects(): Observable> 9 | 10 | fun setProjectBookmarked(projectId: String): Completable 11 | 12 | fun setProjectUnbookmarked(projectId: String): Completable 13 | 14 | } -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/repository/ProjectsDataStore.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.repository 2 | 3 | import com.tayfuncesur.data.model.ProjectEntity 4 | import io.reactivex.Completable 5 | import io.reactivex.Observable 6 | 7 | interface ProjectsDataStore { 8 | 9 | fun getBookmarkedProjects(): Observable> 10 | 11 | fun setProjectBookmarked(projectId: String): Completable 12 | 13 | fun setProjectUnbookmarked(projectId: String): Completable 14 | 15 | fun getProjects(): Observable> 16 | } -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/repository/ProjectsRemote.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.repository 2 | 3 | import com.tayfuncesur.data.model.ProjectEntity 4 | import io.reactivex.Observable 5 | 6 | interface ProjectsRemote { 7 | 8 | fun getProjects(): Observable> 9 | } -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/store/ProjectsCacheDataStore.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.store 2 | 3 | import com.tayfuncesur.data.model.ProjectEntity 4 | import com.tayfuncesur.data.repository.ProjectsCache 5 | import com.tayfuncesur.data.repository.ProjectsDataStore 6 | import io.reactivex.Completable 7 | import io.reactivex.Observable 8 | import javax.inject.Inject 9 | 10 | class ProjectsCacheDataStore @Inject constructor( 11 | private val projectsCache: ProjectsCache 12 | ) : ProjectsDataStore { 13 | 14 | override fun getBookmarkedProjects(): Observable> { 15 | return projectsCache.getBookmarkedProjects() 16 | } 17 | 18 | override fun setProjectBookmarked(projectId: String): Completable { 19 | return projectsCache.setProjectBookmarked(projectId) 20 | } 21 | 22 | override fun setProjectUnbookmarked(projectId: String): Completable { 23 | return projectsCache.setProjectUnbookmarked(projectId) 24 | } 25 | 26 | override fun getProjects(): Observable> { 27 | throw UnsupportedOperationException("Unsupported operation") 28 | } 29 | } -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/store/ProjectsDataStoreFactory.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.store 2 | 3 | import javax.inject.Inject 4 | 5 | class ProjectsDataStoreFactory @Inject constructor( 6 | private val projectsCacheDataStore: ProjectsCacheDataStore, 7 | private val projectsRemoteDataStore: ProjectsRemoteDataStore 8 | ) { 9 | fun getCacheDataStore(): ProjectsCacheDataStore { 10 | return projectsCacheDataStore 11 | } 12 | 13 | fun getRemoteDataStore(): ProjectsRemoteDataStore { 14 | return projectsRemoteDataStore 15 | } 16 | } -------------------------------------------------------------------------------- /data/src/main/java/com/tayfuncesur/data/store/ProjectsRemoteDataStore.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.store 2 | 3 | import com.tayfuncesur.data.model.ProjectEntity 4 | import com.tayfuncesur.data.repository.ProjectsDataStore 5 | import com.tayfuncesur.data.repository.ProjectsRemote 6 | import io.reactivex.Completable 7 | import io.reactivex.Observable 8 | import java.lang.UnsupportedOperationException 9 | import javax.inject.Inject 10 | 11 | class ProjectsRemoteDataStore @Inject constructor( 12 | private val projectsRemote: ProjectsRemote 13 | ) : ProjectsDataStore { 14 | override fun getBookmarkedProjects(): Observable> { 15 | throw UnsupportedOperationException("Not supported") 16 | } 17 | 18 | override fun setProjectBookmarked(projectId: String): Completable { 19 | throw UnsupportedOperationException("Not supported") 20 | } 21 | 22 | override fun setProjectUnbookmarked(projectId: String): Completable { 23 | throw UnsupportedOperationException("Not supported") 24 | } 25 | 26 | override fun getProjects(): Observable> { 27 | return projectsRemote.getProjects() 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /data/src/test/java/com/tayfuncesur/data/ProjectDataRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data 2 | 3 | import com.tayfuncesur.data.mapper.ProjectMapper 4 | import com.tayfuncesur.data.store.ProjectsCacheDataStore 5 | import com.tayfuncesur.data.store.ProjectsDataStoreFactory 6 | import com.tayfuncesur.data.store.ProjectsRemoteDataStore 7 | import com.tayfuncesur.data.data.MockData 8 | import io.reactivex.Completable 9 | import io.reactivex.Observable 10 | import org.junit.Before 11 | import org.junit.Test 12 | import org.junit.runner.RunWith 13 | import org.mockito.Mock 14 | import org.mockito.Mockito 15 | import org.mockito.junit.MockitoJUnitRunner 16 | import java.util.* 17 | 18 | @RunWith(MockitoJUnitRunner::class) 19 | class ProjectDataRepositoryTest { 20 | 21 | lateinit var projectsDataRepository: ProjectsDataRepository 22 | 23 | @Mock 24 | lateinit var mapper: ProjectMapper 25 | 26 | @Mock 27 | lateinit var projectsRemoteDataStore: ProjectsRemoteDataStore 28 | 29 | @Mock 30 | lateinit var projectsCacheDataStore: ProjectsCacheDataStore 31 | 32 | lateinit var projectsDataStoreFactory: ProjectsDataStoreFactory 33 | 34 | @Before 35 | fun setup() { 36 | projectsDataStoreFactory = ProjectsDataStoreFactory(projectsCacheDataStore, projectsRemoteDataStore) 37 | projectsDataRepository = ProjectsDataRepository(mapper, projectsDataStoreFactory) 38 | } 39 | 40 | @Test 41 | fun shouldGetProjectsCompletes() { 42 | Mockito.`when`(projectsDataStoreFactory.getRemoteDataStore().getProjects()) 43 | .thenReturn(Observable.just(MockData.generateRandomProjectEntityList())) 44 | projectsDataRepository.getProjects().test().assertComplete() 45 | } 46 | 47 | @Test 48 | fun shouldGetProjectsReturnsExpectedData() { 49 | val list = MockData.generateRandomProjectList(1) 50 | val entityList = MockData.generateRandomProjectEntityList(1) 51 | 52 | Mockito.`when`(projectsDataStoreFactory.getRemoteDataStore().getProjects()) 53 | .thenReturn(Observable.just(entityList)) 54 | 55 | Mockito.`when`(mapper.mapFromEntity(entityList[0])) 56 | .thenReturn(list[0]) 57 | 58 | projectsDataRepository.getProjects().test().assertValue(list) 59 | } 60 | 61 | @Test 62 | fun shouldSetProjectBookmarkedCompletes() { 63 | val projectId = UUID.randomUUID().toString() 64 | Mockito.`when`(projectsDataStoreFactory.getCacheDataStore().setProjectBookmarked(projectId)).thenReturn( 65 | Completable.complete() 66 | ) 67 | projectsDataRepository.bookmarkProject(projectId).test().assertComplete() 68 | } 69 | 70 | @Test 71 | fun shouldSetProjectUnbookmarkedCompletes() { 72 | val projectId = UUID.randomUUID().toString() 73 | Mockito.`when`(projectsDataStoreFactory.getCacheDataStore().setProjectUnbookmarked(projectId)).thenReturn( 74 | Completable.complete() 75 | ) 76 | projectsDataRepository.unbookmarkProject(projectId).test().assertComplete() 77 | } 78 | 79 | @Test 80 | fun shouldGetBookmarkedProjectCompletes(){ 81 | Mockito.`when`(projectsDataStoreFactory.getCacheDataStore().getBookmarkedProjects()) 82 | .thenReturn(Observable.just(MockData.generateRandomIdList())) 83 | projectsDataRepository.getBookmarkedProjects().test().assertComplete() 84 | } 85 | 86 | 87 | 88 | } -------------------------------------------------------------------------------- /data/src/test/java/com/tayfuncesur/data/RunAll.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data 2 | 3 | import com.tayfuncesur.data.mapper.ProjectMapperTest 4 | import com.tayfuncesur.data.store.ProjectsCacheDataStoreTest 5 | import com.tayfuncesur.data.store.ProjectsRemoteDataStoreTest 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.Suite 8 | 9 | 10 | @RunWith(Suite::class) 11 | @Suite.SuiteClasses( 12 | ProjectDataRepositoryTest::class, 13 | ProjectMapperTest::class, 14 | ProjectsCacheDataStoreTest::class, 15 | ProjectsRemoteDataStoreTest::class 16 | ) 17 | class RunAll -------------------------------------------------------------------------------- /data/src/test/java/com/tayfuncesur/data/data/MockData.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.data 2 | 3 | import com.tayfuncesur.data.model.ProjectEntity 4 | import com.tayfuncesur.domain.model.Project 5 | import java.util.* 6 | 7 | object MockData { 8 | 9 | fun randomProjectEntity() = ProjectEntity( 10 | UUID.randomUUID().toString(), 11 | UUID.randomUUID().toString(), 12 | Math.random().toInt(), 13 | false 14 | ) 15 | 16 | 17 | fun randomProject() = Project( 18 | UUID.randomUUID().toString(), 19 | UUID.randomUUID().toString(), 20 | Math.random().toInt(), 21 | false 22 | ) 23 | 24 | fun generateRandomProjectEntity(): ProjectEntity { 25 | return ProjectEntity(UUID.randomUUID().toString(), UUID.randomUUID().toString(), Math.random().toInt(), false) 26 | } 27 | 28 | fun generateRandomProjectEntityList(count : Int = 20): List { 29 | val list = mutableListOf() 30 | repeat(count) { 31 | list.add(generateRandomProjectEntity()) 32 | } 33 | return list 34 | } 35 | 36 | fun generateRandomIdList(): List { 37 | val list = mutableListOf() 38 | repeat(20) { 39 | list.add(UUID.randomUUID().toString()) 40 | } 41 | return list 42 | } 43 | 44 | fun generateRandomProject(): Project { 45 | return Project(UUID.randomUUID().toString(), UUID.randomUUID().toString(), Math.random().toInt(), false) 46 | } 47 | 48 | fun generateRandomProjectList(count : Int = 20): List { 49 | val list = mutableListOf() 50 | repeat(count) { 51 | list.add(generateRandomProject()) 52 | } 53 | return list 54 | } 55 | 56 | 57 | } -------------------------------------------------------------------------------- /data/src/test/java/com/tayfuncesur/data/mapper/ProjectMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.mapper 2 | 3 | import com.tayfuncesur.data.data.MockData 4 | import org.junit.Test 5 | import org.junit.runner.RunWith 6 | import org.mockito.junit.MockitoJUnitRunner 7 | import kotlin.test.assertEquals 8 | 9 | 10 | @RunWith(MockitoJUnitRunner::class) 11 | class ProjectMapperTest { 12 | 13 | private val projectMapper = ProjectMapper() 14 | 15 | @Test 16 | fun mapFromEntityTest() { 17 | val entity = MockData.randomProjectEntity() 18 | val domain = projectMapper.mapFromEntity(entity) 19 | assertEquals(entity.id, domain.id) 20 | assertEquals(entity.fullName, domain.fullName) 21 | assertEquals(entity.starCount, domain.starCount) 22 | assertEquals(entity.isBookmarked, domain.isBookmarked) 23 | } 24 | 25 | @Test 26 | fun mapToEntityTest() { 27 | val domain = MockData.randomProject() 28 | val entity = projectMapper.mapToEntity(domain) 29 | assertEquals(entity.id, domain.id) 30 | assertEquals(entity.fullName, domain.fullName) 31 | assertEquals(entity.starCount, domain.starCount) 32 | assertEquals(entity.isBookmarked, domain.isBookmarked) 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /data/src/test/java/com/tayfuncesur/data/store/ProjectsCacheDataStoreTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.store 2 | 3 | import com.tayfuncesur.data.repository.ProjectsCache 4 | import com.tayfuncesur.data.data.MockData 5 | import io.reactivex.Completable 6 | import io.reactivex.Observable 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.mockito.ArgumentMatchers.anyString 11 | import org.mockito.Mock 12 | import org.mockito.Mockito 13 | import org.mockito.junit.MockitoJUnitRunner 14 | import java.lang.UnsupportedOperationException 15 | import java.util.* 16 | 17 | @RunWith(MockitoJUnitRunner::class) 18 | class ProjectsCacheDataStoreTest { 19 | 20 | lateinit var projectsCacheDataStore: ProjectsCacheDataStore 21 | 22 | @Mock 23 | lateinit var projectesCache: ProjectsCache 24 | 25 | @Before 26 | fun setup() { 27 | projectsCacheDataStore = ProjectsCacheDataStore(projectesCache) 28 | } 29 | 30 | @Test 31 | fun shouldGetBookmarkedProjectCompletes() { 32 | Mockito.`when`(projectesCache.getBookmarkedProjects()) 33 | .thenReturn(Observable.just(MockData.generateRandomIdList())) 34 | projectsCacheDataStore.getBookmarkedProjects().test().assertComplete() 35 | } 36 | 37 | @Test 38 | fun shouldGetBookmarkedProjectReturnsExpectedData() { 39 | val list = MockData.generateRandomIdList() 40 | Mockito.`when`(projectesCache.getBookmarkedProjects()).thenReturn(Observable.just(list)) 41 | projectsCacheDataStore.getBookmarkedProjects().test().assertValue(list) 42 | } 43 | 44 | @Test 45 | fun shouldBookmarkProjectsCompletes() { 46 | val projectId = UUID.randomUUID().toString() 47 | Mockito.`when`(projectesCache.setProjectBookmarked(anyString())).thenReturn(Completable.complete()) 48 | projectsCacheDataStore.setProjectBookmarked(projectId).test().assertComplete() 49 | } 50 | 51 | @Test 52 | fun shouldBookmarkProjectCallsCacheStore() { 53 | val projectId = UUID.randomUUID().toString() 54 | Mockito.`when`(projectesCache.setProjectBookmarked(anyString())).thenReturn(Completable.complete()) 55 | projectsCacheDataStore.setProjectBookmarked(projectId).test() 56 | Mockito.verify(projectesCache).setProjectBookmarked(projectId) 57 | } 58 | 59 | @Test 60 | fun shouldUnbookmarkProjectsCompletes() { 61 | val projectId = UUID.randomUUID().toString() 62 | Mockito.`when`(projectesCache.setProjectUnbookmarked(anyString())).thenReturn(Completable.complete()) 63 | projectsCacheDataStore.setProjectUnbookmarked(projectId).test().assertComplete() 64 | } 65 | 66 | @Test 67 | fun shouldUnbookmarkProjectCallsCacheStore() { 68 | val projectId = UUID.randomUUID().toString() 69 | Mockito.`when`(projectesCache.setProjectUnbookmarked(anyString())).thenReturn(Completable.complete()) 70 | projectsCacheDataStore.setProjectUnbookmarked(projectId).test() 71 | Mockito.verify(projectesCache).setProjectUnbookmarked(projectId) 72 | } 73 | 74 | @Test(expected = UnsupportedOperationException::class) 75 | fun shouldThrowExceptionForGetProjectsRemote() { 76 | projectsCacheDataStore.getProjects() 77 | } 78 | 79 | 80 | } -------------------------------------------------------------------------------- /data/src/test/java/com/tayfuncesur/data/store/ProjectsRemoteDataStoreTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.store 2 | 3 | import com.tayfuncesur.data.repository.ProjectsRemote 4 | import com.tayfuncesur.data.data.MockData 5 | import io.reactivex.Observable 6 | import org.junit.Before 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.mockito.Mock 10 | import org.mockito.Mockito 11 | import org.mockito.junit.MockitoJUnitRunner 12 | import java.lang.UnsupportedOperationException 13 | import java.util.* 14 | 15 | @RunWith(MockitoJUnitRunner::class) 16 | class ProjectsRemoteDataStoreTest { 17 | 18 | lateinit var projectsRemoteDataStore: ProjectsRemoteDataStore 19 | 20 | @Mock 21 | lateinit var projectsRemote: ProjectsRemote 22 | 23 | @Before 24 | fun setup() { 25 | projectsRemoteDataStore = ProjectsRemoteDataStore(projectsRemote) 26 | } 27 | 28 | @Test 29 | fun shouldGetProjectsCompletes() { 30 | Mockito.`when`(projectsRemote.getProjects()).thenReturn(Observable.just(MockData.generateRandomProjectEntityList())) 31 | projectsRemoteDataStore.getProjects().test().assertComplete() 32 | } 33 | 34 | @Test 35 | fun shouldGetProjectsReturnsExpectedData() { 36 | val list = MockData.generateRandomProjectEntityList() 37 | Mockito.`when`(projectsRemote.getProjects()).thenReturn(Observable.just(list)) 38 | projectsRemoteDataStore.getProjects().test().assertValue(list) 39 | } 40 | 41 | @Test(expected = UnsupportedOperationException::class) 42 | fun shouldThrowExceptionForBookmarkProject() { 43 | projectsRemoteDataStore.setProjectBookmarked(UUID.randomUUID().toString()) 44 | } 45 | 46 | @Test(expected = UnsupportedOperationException::class) 47 | fun shouldThrowExceptionForUnbookmarkProject() { 48 | projectsRemoteDataStore.setProjectUnbookmarked(UUID.randomUUID().toString()) 49 | } 50 | 51 | 52 | @Test(expected = UnsupportedOperationException::class) 53 | fun shouldThrowExceptionForGetBookmarkedProject() { 54 | projectsRemoteDataStore.getBookmarkedProjects() 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | //Android 3 | androidBuildToolsVersion = "28.0.0" 4 | androidMinSdkVersion = 16 5 | androidTargetSdkVersion = 28 6 | androidCompileSdkVersion = 28 7 | supportLibraryVersion = '28.0.0' 8 | 9 | 10 | //Layer Dependencies 11 | javaxAnnotationVersion = '1.0' 12 | javaxInjectVersion = '1' 13 | rxKotlinVersion = '2.2.0' 14 | 15 | gsonVersion = '2.8.5' 16 | retrofitVersion = '2.4.0' 17 | okHttpVersion = '3.9.1' 18 | 19 | daggerVersion = '2.19' 20 | kotlinVersion = '1.3.21' 21 | roomVersion = '1.1.1' 22 | 23 | androidAnnotationsVersion = '26.0.0' 24 | rxAndroidVersion = '2.1.1' 25 | 26 | glideVersion = '4.9.0' 27 | glassfishAnnotationVersion = '3.1.1' 28 | espressoVersion = '3.0.2' 29 | runnerVersion = '0.5' 30 | constraintLayoutVersion = '1.1.3' 31 | lottieVersion = '2.7.0' 32 | pushDownAnimVersion = '1.1.1' 33 | rxNetworkVersion = '3.0.1' 34 | 35 | //Test Dependencies 36 | jUnitVersion = '4.12' 37 | mockitoVersion = '2.22.0' 38 | kotlinJUnitVersion = '1.3.21' 39 | 40 | archCompVersion = '1.1.1' 41 | androidSupportRunnerVersion = '1.0.0' 42 | androidSupportRulesVersion = '1.0.0' 43 | androidSupportRulesVersion = '28.0.0' 44 | robolectricVersion = '3.4.2' 45 | 46 | mockitoKotlinVersion = '1.5.0' 47 | mockitoAndroidVersion = '2.8.9' 48 | 49 | // -------------------------------- Domain -------------------------------- 50 | domainDependencies = [ 51 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 52 | javaxInject : "javax.inject:javax.inject:${javaxInjectVersion}", 53 | rxJava : "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}" 54 | ] 55 | 56 | // -------------------------------- Domain Test -------------------------------- 57 | domainTestDependencies = [ 58 | junit : "junit:junit:${jUnitVersion}", 59 | mockito: "org.mockito:mockito-inline:${mockitoVersion}", 60 | ] 61 | //------------------------------------------------------------- 62 | //------------------------------------------------------------- 63 | //------------------------------------------------------------- 64 | 65 | 66 | // -------------------------------- Data -------------------------------- 67 | dataDependencies = [ 68 | rxKotlin : "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 69 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 70 | javaxInject : "javax.inject:javax.inject:${javaxInjectVersion}", 71 | ] 72 | 73 | // -------------------------------- Data Test -------------------------------- 74 | dataTestDependencies = [ 75 | junit : "junit:junit:${jUnitVersion}", 76 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlinJUnitVersion}", 77 | mockito : "org.mockito:mockito-inline:${mockitoVersion}", 78 | ] 79 | //------------------------------------------------------------- 80 | //------------------------------------------------------------- 81 | //------------------------------------------------------------- 82 | 83 | 84 | // -------------------------------- Remote -------------------------------- 85 | remoteDependencies = [ 86 | rxKotlin : "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 87 | javaxAnnotation : "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 88 | javaxInject : "javax.inject:javax.inject:${javaxInjectVersion}", 89 | gson : "com.google.code.gson:gson:${gsonVersion}", 90 | okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}", 91 | okHttpLogger : "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}", 92 | retrofit : "com.squareup.retrofit2:retrofit:${retrofitVersion}", 93 | retrofitConverter: "com.squareup.retrofit2:converter-gson:${retrofitVersion}", 94 | retrofitAdapter : "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}" 95 | ] 96 | 97 | // -------------------------------- Remote Test -------------------------------- 98 | remoteTestDependencies = [ 99 | junit : "junit:junit:${jUnitVersion}", 100 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlinJUnitVersion}", 101 | mockito : "org.mockito:mockito-inline:${mockitoVersion}", 102 | ] 103 | 104 | //------------------------------------------------------------- 105 | //------------------------------------------------------------- 106 | //------------------------------------------------------------- 107 | 108 | 109 | // -------------------------------- Cache -------------------------------- 110 | cacheDependencies = [ 111 | rxKotlin : "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 112 | kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion}", 113 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 114 | javaxInject : "javax.inject:javax.inject:${javaxInjectVersion}", 115 | roomRuntime : "android.arch.persistence.room:runtime:${roomVersion}", 116 | roomCompiler : "android.arch.persistence.room:compiler:${roomVersion}", 117 | roomRxJava : "android.arch.persistence.room:rxjava2:${roomVersion}" 118 | ] 119 | 120 | // -------------------------------- Cache Test -------------------------------- 121 | cacheTestDependencies = [ 122 | junit : "junit:junit:${jUnitVersion}", 123 | kotlinJUnit : "org.jetbrains.kotlin:kotlin-test-junit:${kotlinJUnitVersion}", 124 | mockito : "org.mockito:mockito-inline:${mockitoVersion}", 125 | roomTesting : "android.arch.persistence.room:testing:${roomVersion}", 126 | archTesting : "android.arch.core:core-testing:${archCompVersion}", 127 | supportRunner : "com.android.support.test:runner:${androidSupportRunnerVersion}", 128 | supportRules : "com.android.support.test:rules:${androidSupportRulesVersion}", 129 | supportAnnotations: "com.android.support:support-annotations:${androidSupportRulesVersion}", 130 | robolectric : "org.robolectric:robolectric:${robolectricVersion}" 131 | ] 132 | 133 | //------------------------------------------------------------- 134 | //------------------------------------------------------------- 135 | //------------------------------------------------------------- 136 | 137 | 138 | // -------------------------------- Presentation -------------------------------- 139 | presentationDependencies = [ 140 | daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}", 141 | dagger : "com.google.dagger:dagger:${daggerVersion}", 142 | rxKotlin : "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 143 | kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion}", 144 | rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", 145 | javaxAnnotation : "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 146 | javaxInject : "javax.inject:javax.inject:${javaxInjectVersion}", 147 | androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", 148 | archRuntime : "android.arch.lifecycle:runtime:${archCompVersion}", 149 | archExtensions : "android.arch.lifecycle:extensions:${archCompVersion}", 150 | archCompiler : "android.arch.lifecycle:compiler:${archCompVersion}", 151 | ] 152 | 153 | // -------------------------------- Presentation Test -------------------------------- 154 | presentationTestDependencies = [ 155 | junit : "junit:junit:${jUnitVersion}", 156 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlinJUnitVersion}", 157 | mockito : "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 158 | robolectric: "org.robolectric:robolectric:${robolectricVersion}", 159 | archTesting: "android.arch.core:core-testing:${archCompVersion}", 160 | ] 161 | //------------------------------------------------------------- 162 | //------------------------------------------------------------- 163 | //------------------------------------------------------------- 164 | 165 | 166 | // -------------------------------- Mobile -------------------------------- 167 | mobileDependencies = [ 168 | daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}", 169 | dagger : "com.google.dagger:dagger:${daggerVersion}", 170 | rxKotlin : "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 171 | rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", 172 | glide : "com.github.bumptech.glide:glide:${glideVersion}", 173 | kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion}", 174 | javaxAnnotation : "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 175 | javaxInject : "javax.inject:javax.inject:${javaxInjectVersion}", 176 | androidAnnotations : "com.android.support:support-annotations:${supportLibraryVersion}", 177 | supportDesign : "com.android.support:design:${supportLibraryVersion}", 178 | supportRecyclerView: "com.android.support:recyclerview-v7:${supportLibraryVersion}", 179 | daggerSupport : "com.google.dagger:dagger-android-support:${daggerVersion}", 180 | daggerProcessor : "com.google.dagger:dagger-android-processor:${daggerVersion}", 181 | glassfishAnnotation: "org.glassfish:javax.annotation:${glassfishAnnotationVersion}", 182 | roomRxJava : "android.arch.persistence.room:rxjava2:${roomVersion}", 183 | constraintLayout : "com.android.support.constraint:constraint-layout:${constraintLayoutVersion}", 184 | lottie : "com.airbnb.android:lottie:${lottieVersion}", 185 | pushDownAnim : "com.github.thekhaeng:pushdown-anim-click:${pushDownAnimVersion}", 186 | reactivenetwork : "com.github.pwittchen:reactivenetwork-rx2:${rxNetworkVersion}" 187 | 188 | ] 189 | 190 | 191 | // -------------------------------- Mobile Test -------------------------------- 192 | 193 | mobileUiTestDependencies = [ 194 | junit : "junit:junit:${jUnitVersion}", 195 | kotlinJUnit : "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 196 | supportRunner : "com.android.support.test:runner:${androidSupportRunnerVersion}", 197 | supportRules : "com.android.support.test:rules:${androidSupportRulesVersion}", 198 | mockitoAndroid : "org.mockito:mockito-android:${mockitoAndroidVersion}", 199 | espressoCore : "com.android.support.test.espresso:espresso-core:${espressoVersion}", 200 | espressoContrib: "com.android.support.test.espresso:espresso-contrib:${espressoVersion}", 201 | androidRunner : "com.android.support.test:runner:${runnerVersion}", 202 | androidRules : "com.android.support.test:rules:${runnerVersion}" 203 | ] 204 | 205 | 206 | } -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | dependencies { 4 | testImplementation domainTestDependencies.junit 5 | def domainDependencies = rootProject.ext.domainDependencies 6 | def domainTestDependencies = rootProject.ext.domainTestDependencies 7 | implementation domainDependencies.javaxAnnotation 8 | implementation domainDependencies.javaxInject 9 | implementation domainDependencies.rxJava 10 | testImplementation domainTestDependencies.mockito 11 | } 12 | 13 | sourceCompatibility = "1.6" 14 | targetCompatibility = "1.6" 15 | 16 | -------------------------------------------------------------------------------- /domain/src/main/java/com/tayfuncesur/domain/CompletableUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.domain 2 | 3 | import com.tayfuncesur.domain.executor.PostExecutionThread 4 | import io.reactivex.Completable 5 | import io.reactivex.disposables.CompositeDisposable 6 | import io.reactivex.disposables.Disposable 7 | import io.reactivex.observers.DisposableCompletableObserver 8 | import io.reactivex.schedulers.Schedulers 9 | 10 | abstract class CompletableUseCase constructor( 11 | private val postExecutionThread: PostExecutionThread 12 | ) { 13 | 14 | private val disposables = CompositeDisposable() 15 | 16 | protected abstract fun doWork(params: Params? = null): Completable 17 | 18 | open fun execute(observer: DisposableCompletableObserver, params: Params? = null) { 19 | val completable = this.doWork(params) 20 | .subscribeOn(Schedulers.io()) 21 | .observeOn(postExecutionThread.scheduler) 22 | addDisposable(completable.subscribeWith(observer)) 23 | } 24 | 25 | fun addDisposable(disposable: Disposable) { 26 | disposables.add(disposable) 27 | } 28 | 29 | fun clear() { 30 | if (!disposables.isDisposed) disposables.dispose() 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/tayfuncesur/domain/ObservableUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.domain 2 | 3 | import com.tayfuncesur.domain.executor.PostExecutionThread 4 | import io.reactivex.Observable 5 | import io.reactivex.disposables.CompositeDisposable 6 | import io.reactivex.disposables.Disposable 7 | import io.reactivex.observers.DisposableObserver 8 | import io.reactivex.schedulers.Schedulers 9 | 10 | abstract class ObservableUseCase constructor( 11 | private val postExecutionThread: PostExecutionThread 12 | ) { 13 | 14 | private val disposables = CompositeDisposable() 15 | 16 | protected abstract fun doWork(params: Params? = null): Observable 17 | 18 | open fun execute(singleObserver: DisposableObserver, params: Params? = null) { 19 | val single = this.doWork(params) 20 | .subscribeOn(Schedulers.io()) 21 | .observeOn(postExecutionThread.scheduler) 22 | addDisposable(single.subscribeWith(singleObserver)) 23 | } 24 | 25 | fun addDisposable(disposable: Disposable) { 26 | disposables.add(disposable) 27 | } 28 | 29 | fun clear() { 30 | if (!disposables.isDisposed) disposables.dispose() 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/tayfuncesur/domain/bookmark/BookmarkProject.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.domain.bookmark 2 | 3 | import com.tayfuncesur.domain.CompletableUseCase 4 | import com.tayfuncesur.domain.executor.PostExecutionThread 5 | import com.tayfuncesur.domain.repository.ProjectsRepository 6 | import io.reactivex.Completable 7 | import java.lang.IllegalArgumentException 8 | import javax.inject.Inject 9 | 10 | class BookmarkProject @Inject constructor( 11 | private val projectsRepository: ProjectsRepository, 12 | postExecutionThread: PostExecutionThread 13 | ) : 14 | CompletableUseCase(postExecutionThread) { 15 | public override fun doWork(params: Params?): Completable { 16 | if (params == null) throw IllegalArgumentException("ProjectId is required to bookmark") 17 | return projectsRepository.bookmarkProject(params.projectId) 18 | } 19 | 20 | 21 | data class Params constructor(val projectId: String) { 22 | companion object { 23 | fun projectId(projectId: String): Params = Params(projectId) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/tayfuncesur/domain/bookmark/GetBookmarkedProjects.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.domain.bookmark 2 | 3 | import com.tayfuncesur.domain.ObservableUseCase 4 | import com.tayfuncesur.domain.executor.PostExecutionThread 5 | import com.tayfuncesur.domain.repository.ProjectsRepository 6 | import io.reactivex.Observable 7 | import javax.inject.Inject 8 | 9 | class GetBookmarkedProjects @Inject constructor( 10 | private val projectsRepository: ProjectsRepository, 11 | postExecutionThread: PostExecutionThread 12 | ) : ObservableUseCase, Nothing?>(postExecutionThread) { 13 | public override fun doWork(params: Nothing?): Observable> { 14 | return projectsRepository.getBookmarkedProjects() 15 | } 16 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/tayfuncesur/domain/bookmark/UnbookmarkProject.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.domain.bookmark 2 | 3 | import com.tayfuncesur.domain.CompletableUseCase 4 | import com.tayfuncesur.domain.executor.PostExecutionThread 5 | import com.tayfuncesur.domain.repository.ProjectsRepository 6 | import io.reactivex.Completable 7 | import java.lang.IllegalArgumentException 8 | import javax.inject.Inject 9 | 10 | class UnbookmarkProject @Inject constructor( 11 | private val projectsRepository: ProjectsRepository, 12 | postExecutionThread: PostExecutionThread 13 | ) : 14 | CompletableUseCase(postExecutionThread) { 15 | public override fun doWork(params: Params?): Completable { 16 | if (params == null) throw IllegalArgumentException("ProjectId is required to unbookmark") 17 | return projectsRepository.unbookmarkProject(params.projectId) 18 | } 19 | 20 | 21 | class Params constructor(val projectId: String) { 22 | companion object { 23 | fun projectId(projectId: String): Params = Params(projectId) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/tayfuncesur/domain/executor/PostExecutionThread.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.domain.executor 2 | 3 | import io.reactivex.Scheduler 4 | 5 | interface PostExecutionThread { 6 | 7 | val scheduler: Scheduler 8 | 9 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/tayfuncesur/domain/getProjects/GetProjects.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.domain.getProjects 2 | 3 | import com.tayfuncesur.domain.ObservableUseCase 4 | import com.tayfuncesur.domain.executor.PostExecutionThread 5 | import com.tayfuncesur.domain.model.Project 6 | import com.tayfuncesur.domain.repository.ProjectsRepository 7 | import io.reactivex.Observable 8 | import javax.inject.Inject 9 | 10 | open class GetProjects @Inject constructor( 11 | private val projectsRepository: ProjectsRepository, 12 | postExecutionThread: PostExecutionThread 13 | ) : ObservableUseCase, Nothing?>(postExecutionThread) { 14 | public override fun doWork(params: Nothing?): Observable> { 15 | return projectsRepository.getProjects() 16 | } 17 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/tayfuncesur/domain/model/Project.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.domain.model 2 | 3 | class Project( 4 | val id: String, val fullName: String, 5 | val starCount: Int, val isBookmarked: Boolean 6 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/tayfuncesur/domain/repository/ProjectsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.domain.repository 2 | 3 | import com.tayfuncesur.domain.model.Project 4 | import io.reactivex.Completable 5 | import io.reactivex.Observable 6 | 7 | interface ProjectsRepository { 8 | 9 | fun getProjects(): Observable> 10 | 11 | fun bookmarkProject(projectId: String): Completable 12 | 13 | fun unbookmarkProject(projectId: String): Completable 14 | 15 | fun getBookmarkedProjects(): Observable> 16 | 17 | 18 | } -------------------------------------------------------------------------------- /domain/src/test/java/com/tayfuncesur/data/MockData.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data 2 | 3 | import com.tayfuncesur.domain.model.Project 4 | import java.util.* 5 | 6 | object MockData { 7 | 8 | fun generateRandomProject(): Project { 9 | return Project(UUID.randomUUID().toString(), UUID.randomUUID().toString(), Math.random().toInt(), false) 10 | } 11 | 12 | fun generateRandomProjectList(): List { 13 | val list = mutableListOf() 14 | repeat(20) { 15 | list.add(generateRandomProject()) 16 | } 17 | return list 18 | } 19 | 20 | fun generateRandomIdList(): List { 21 | val list = mutableListOf() 22 | repeat(20) { 23 | list.add(UUID.randomUUID().toString()) 24 | } 25 | return list 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /domain/src/test/java/com/tayfuncesur/data/RunAll.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data 2 | 3 | import com.tayfuncesur.data.bookmark.BookmarkProjectTest 4 | import com.tayfuncesur.data.bookmark.GetBookmarkedProjectsTest 5 | import com.tayfuncesur.data.bookmark.UnbookmarkProjectTest 6 | import com.tayfuncesur.data.getProjects.GetProjectsTest 7 | import org.junit.runner.RunWith 8 | import org.junit.runners.Suite 9 | 10 | 11 | @RunWith(Suite::class) 12 | @Suite.SuiteClasses( 13 | BookmarkProjectTest::class, 14 | GetBookmarkedProjectsTest::class, 15 | UnbookmarkProjectTest::class, 16 | GetProjectsTest::class 17 | ) 18 | class RunAll -------------------------------------------------------------------------------- /domain/src/test/java/com/tayfuncesur/data/bookmark/BookmarkProjectTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.bookmark 2 | 3 | import com.tayfuncesur.domain.bookmark.BookmarkProject 4 | import com.tayfuncesur.domain.executor.PostExecutionThread 5 | import com.tayfuncesur.domain.repository.ProjectsRepository 6 | import io.reactivex.Completable 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.mockito.ArgumentMatchers.anyString 11 | import org.mockito.Mock 12 | import org.mockito.Mockito 13 | import org.mockito.junit.MockitoJUnitRunner 14 | import java.lang.IllegalArgumentException 15 | import java.util.* 16 | 17 | @RunWith(MockitoJUnitRunner::class) 18 | class BookmarkProjectTest { 19 | 20 | lateinit var bookmarkProject: BookmarkProject 21 | 22 | @Mock 23 | lateinit var projectsRepository: ProjectsRepository 24 | @Mock 25 | lateinit var postExecutionThread: PostExecutionThread 26 | 27 | @Before 28 | fun setup() { 29 | bookmarkProject = BookmarkProject(projectsRepository, postExecutionThread) 30 | } 31 | 32 | @Test 33 | fun shouldBookmarkProjectCompletes() { 34 | Mockito.`when`(projectsRepository.bookmarkProject(anyString())).thenReturn(Completable.complete()) 35 | bookmarkProject.doWork(BookmarkProject.Params.projectId(UUID.randomUUID().toString())).test().assertComplete() 36 | } 37 | 38 | @Test(expected = IllegalArgumentException::class) 39 | fun shouldThrowException() { 40 | bookmarkProject.doWork().test() 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /domain/src/test/java/com/tayfuncesur/data/bookmark/GetBookmarkedProjectsTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.bookmark 2 | 3 | import com.tayfuncesur.domain.bookmark.GetBookmarkedProjects 4 | import com.tayfuncesur.domain.executor.PostExecutionThread 5 | import com.tayfuncesur.domain.repository.ProjectsRepository 6 | import com.tayfuncesur.data.MockData 7 | import io.reactivex.Observable 8 | import org.junit.Before 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.mockito.Mock 12 | import org.mockito.Mockito 13 | import org.mockito.junit.MockitoJUnitRunner 14 | 15 | @RunWith(MockitoJUnitRunner::class) 16 | class GetBookmarkedProjectsTest { 17 | 18 | lateinit var getBookmarkedProjects: GetBookmarkedProjects 19 | 20 | @Mock 21 | lateinit var projectsRepository: ProjectsRepository 22 | 23 | @Mock 24 | lateinit var postExecutionThread: PostExecutionThread 25 | 26 | @Before 27 | fun setup() { 28 | getBookmarkedProjects = GetBookmarkedProjects(projectsRepository, postExecutionThread) 29 | } 30 | 31 | @Test 32 | fun shouldGetBookmarkedProjectsCompletes() { 33 | Mockito.`when`(projectsRepository.getBookmarkedProjects()) 34 | .thenReturn(Observable.just(MockData.generateRandomIdList())) 35 | getBookmarkedProjects.doWork().test().assertComplete() 36 | } 37 | 38 | @Test 39 | fun shouldGetBookmarkedProjectsReturnsExpectedData(){ 40 | val list = MockData.generateRandomIdList() 41 | Mockito.`when`(projectsRepository.getBookmarkedProjects()).thenReturn(Observable.just(list)) 42 | getBookmarkedProjects.doWork().test().assertValue(list) 43 | } 44 | 45 | 46 | } -------------------------------------------------------------------------------- /domain/src/test/java/com/tayfuncesur/data/bookmark/UnbookmarkProject.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.bookmark 2 | 3 | import com.tayfuncesur.domain.bookmark.UnbookmarkProject 4 | import com.tayfuncesur.domain.executor.PostExecutionThread 5 | import com.tayfuncesur.domain.repository.ProjectsRepository 6 | import io.reactivex.Completable 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.mockito.ArgumentMatchers.anyString 11 | import org.mockito.Mock 12 | import org.mockito.Mockito 13 | import org.mockito.junit.MockitoJUnitRunner 14 | import java.util.* 15 | 16 | @RunWith(MockitoJUnitRunner::class) 17 | class UnbookmarkProjectTest { 18 | 19 | lateinit var unbookmarkProject: UnbookmarkProject 20 | 21 | @Mock 22 | lateinit var projectsRepository: ProjectsRepository 23 | 24 | @Mock 25 | lateinit var postExecutionThread: PostExecutionThread 26 | 27 | @Before 28 | fun setup() { 29 | unbookmarkProject = UnbookmarkProject(projectsRepository, postExecutionThread) 30 | } 31 | 32 | @Test 33 | fun shouldUnbookmarkProjectCompletes() { 34 | Mockito.`when`(projectsRepository.unbookmarkProject(anyString())).thenReturn(Completable.complete()) 35 | unbookmarkProject.doWork(UnbookmarkProject.Params.projectId(UUID.randomUUID().toString())).test() 36 | .assertComplete() 37 | 38 | } 39 | 40 | 41 | } -------------------------------------------------------------------------------- /domain/src/test/java/com/tayfuncesur/data/getProjects/GetProjectsTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.data.getProjects 2 | 3 | import com.tayfuncesur.domain.executor.PostExecutionThread 4 | import com.tayfuncesur.domain.getProjects.GetProjects 5 | import com.tayfuncesur.domain.repository.ProjectsRepository 6 | import com.tayfuncesur.data.MockData 7 | import io.reactivex.Observable 8 | import org.junit.Before 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.mockito.Mock 12 | import org.mockito.Mockito 13 | import org.mockito.Mockito.`when` 14 | import org.mockito.junit.MockitoJUnitRunner 15 | 16 | @RunWith(MockitoJUnitRunner::class) 17 | class GetProjectsTest { 18 | 19 | private lateinit var getProjects: GetProjects 20 | @Mock 21 | lateinit var projectRepository: ProjectsRepository 22 | @Mock 23 | lateinit var postExecutionThread: PostExecutionThread 24 | 25 | @Before 26 | fun setup() { 27 | getProjects = GetProjects(projectRepository, postExecutionThread) 28 | } 29 | 30 | @Test 31 | fun shouldGetProjectsCompletes() { 32 | `when`(projectRepository.getProjects()).thenReturn(Observable.just(MockData.generateRandomProjectList())) 33 | 34 | val testObserver = getProjects.doWork().test() 35 | testObserver.assertComplete() 36 | } 37 | 38 | @Test 39 | fun shouldGetProjectsReturnsData() { 40 | val list = MockData.generateRandomProjectList() 41 | Mockito.`when`(projectRepository.getProjects()).thenReturn(Observable.just(list)) 42 | val testObserver = getProjects.doWork().test() 43 | testObserver.assertValue(list) 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # Kotlin code style for this project: "official" or "obsolete": 15 | kotlin.code.style=official 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TayfunCesur/GithubProjectBrowser/8aa6705cf42651422ec677c5e48c9cab12e54b9f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu May 02 18:57:34 EET 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /mobile/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'org.jetbrains.kotlin.android.extensions' 5 | 6 | 7 | android { 8 | compileSdkVersion 28 9 | 10 | defaultConfig { 11 | minSdkVersion 16 12 | targetSdkVersion 28 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "com.tayfuncesur.mobile.test.TestRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | androidExtensions { 29 | experimental = true 30 | } 31 | } 32 | 33 | dependencies { 34 | def mobileDependencies = rootProject.ext.mobileDependencies 35 | def mobileUiTestDependencies = rootProject.ext.mobileUiTestDependencies 36 | 37 | implementation project(':domain') 38 | implementation project(':data') 39 | implementation project(':cache') 40 | implementation project(':remote') 41 | implementation project(':presentation') 42 | 43 | //Structure 44 | implementation mobileDependencies.javaxAnnotation 45 | implementation mobileDependencies.kotlin 46 | implementation mobileDependencies.javaxInject 47 | implementation presentationDependencies.archRuntime 48 | implementation presentationDependencies.archExtensions 49 | kapt presentationDependencies.archCompiler 50 | 51 | //Design 52 | implementation mobileDependencies.supportRecyclerView 53 | implementation mobileDependencies.supportDesign 54 | implementation mobileDependencies.constraintLayout 55 | implementation mobileDependencies.lottie 56 | implementation (mobileDependencies.pushDownAnim) { 57 | exclude group: 'com.android.support' 58 | } 59 | implementation mobileDependencies.glide 60 | implementation mobileDependencies.reactivenetwork 61 | 62 | //Dagger 63 | implementation mobileDependencies.dagger 64 | implementation mobileDependencies.daggerSupport 65 | implementation mobileDependencies.androidAnnotations 66 | kapt mobileDependencies.daggerCompiler 67 | kapt mobileDependencies.daggerProcessor 68 | compileOnly mobileDependencies.glassfishAnnotation 69 | 70 | //Rx 71 | implementation mobileDependencies.rxKotlin 72 | implementation mobileDependencies.rxAndroid 73 | 74 | 75 | //Room 76 | implementation mobileDependencies.roomRxJava 77 | 78 | 79 | //Testing 80 | testImplementation mobileUiTestDependencies.kotlinJUnit 81 | // Instrumentation test dependencies 82 | androidTestImplementation mobileUiTestDependencies.junit 83 | androidTestImplementation mobileUiTestDependencies.mockitoAndroid 84 | androidTestImplementation(mobileUiTestDependencies.espressoCore) { 85 | exclude group: 'com.android.support', module: 'support-annotations' 86 | } 87 | androidTestImplementation(mobileUiTestDependencies.androidRunner) { 88 | exclude group: 'com.android.support', module: 'support-annotations' 89 | } 90 | androidTestImplementation(mobileUiTestDependencies.androidRules) { 91 | exclude group: 'com.android.support', module: 'support-annotations' 92 | } 93 | androidTestImplementation(mobileUiTestDependencies.espressoContrib) { 94 | exclude module: 'appcompat' 95 | exclude module: 'appcompat-v7' 96 | exclude module: 'support-v4' 97 | exclude module: 'support-v13' 98 | exclude module: 'support-annotations' 99 | exclude module: 'recyclerview-v7' 100 | exclude module: 'design' 101 | } 102 | 103 | kaptTest mobileDependencies.daggerCompiler 104 | kaptAndroidTest mobileDependencies.daggerCompiler 105 | implementation 'com.android.support:cardview-v7:28.0.0' 106 | } 107 | 108 | apply plugin: 'kotlin-android-extensions' -------------------------------------------------------------------------------- /mobile/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/RunAllUITests.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile 2 | 3 | import com.tayfuncesur.mobile.ui.detail.DetailActivityTest 4 | import com.tayfuncesur.mobile.ui.main.MainActivityTest 5 | import org.junit.runners.Suite 6 | import org.junit.runner.RunWith 7 | 8 | 9 | @RunWith(Suite::class) 10 | @Suite.SuiteClasses(MainActivityTest::class, DetailActivityTest::class) 11 | class RunAllUITests -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/di/TestAppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di 2 | 3 | import android.app.Application 4 | import com.tayfuncesur.domain.repository.ProjectsRepository 5 | import com.tayfuncesur.mobile.test.TestApp 6 | import com.tayfuncesur.mobile.di.module.* 7 | import dagger.BindsInstance 8 | import dagger.Component 9 | import dagger.android.AndroidInjectionModule 10 | import javax.inject.Singleton 11 | 12 | @Singleton 13 | @Component( 14 | modules = [AndroidInjectionModule::class, 15 | TestApplicationModule::class, 16 | TestCacheModule::class, 17 | TestDataModule::class, 18 | PresentationModule::class, 19 | UIModule::class, 20 | TestRemoteModule::class 21 | ] 22 | ) 23 | interface TestAppComponent { 24 | 25 | 26 | fun projectsRepository(): ProjectsRepository 27 | 28 | @Component.Builder 29 | interface Builder { 30 | 31 | @BindsInstance 32 | fun application(application: Application): TestAppComponent.Builder 33 | 34 | fun build(): TestAppComponent 35 | } 36 | 37 | fun inject(app: TestApp) 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/di/module/TestApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import dagger.Binds 6 | import dagger.Module 7 | 8 | @Module 9 | abstract class TestApplicationModule { 10 | 11 | @Binds 12 | abstract fun bindContext(application: Application): Context 13 | } -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/di/module/TestCacheModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import android.app.Application 4 | import com.tayfuncesur.cache.ProjectsCacheImpl 5 | import com.tayfuncesur.cache.db.ProjectsDatabase 6 | import com.tayfuncesur.cache.mapper.CachedProjectMapper 7 | import com.tayfuncesur.data.repository.ProjectsCache 8 | import dagger.Binds 9 | import dagger.Module 10 | import dagger.Provides 11 | import org.mockito.Mockito 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | object TestCacheModule { 16 | 17 | @Provides 18 | @Singleton 19 | @JvmStatic 20 | fun providesDatabase(application: Application): ProjectsDatabase { 21 | return ProjectsDatabase.getInstance(application) 22 | } 23 | 24 | @Provides 25 | @Singleton 26 | @JvmStatic 27 | fun providesCachedProjectMapper(): CachedProjectMapper { 28 | return Mockito.mock(CachedProjectMapper::class.java) 29 | } 30 | 31 | @Provides 32 | @Singleton 33 | @JvmStatic 34 | fun providesProjectsCache(): ProjectsCache { 35 | return Mockito.mock(ProjectsCache::class.java) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/di/module/TestDataModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import com.tayfuncesur.data.mapper.ProjectMapper 4 | import com.tayfuncesur.domain.repository.ProjectsRepository 5 | import dagger.Module 6 | import dagger.Provides 7 | import org.mockito.Mockito 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | object TestDataModule { 12 | 13 | @Provides 14 | @Singleton 15 | @JvmStatic 16 | fun providesProjectMapper(): ProjectMapper { 17 | return Mockito.mock(ProjectMapper::class.java) 18 | } 19 | 20 | @Provides 21 | @Singleton 22 | @JvmStatic 23 | fun providesDataRepository(): ProjectsRepository { 24 | return Mockito.mock(ProjectsRepository::class.java) 25 | } 26 | } -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/di/module/TestRemoteModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import com.tayfuncesur.data.repository.ProjectsRemote 4 | import com.tayfuncesur.remote.mapper.ProjectResponseMapper 5 | import com.tayfuncesur.remote.service.GithubService 6 | import dagger.Module 7 | import dagger.Provides 8 | import org.mockito.Mockito 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | object TestRemoteModule { 13 | 14 | @Provides 15 | @Singleton 16 | @JvmStatic 17 | fun providesGithubService(): GithubService { 18 | return Mockito.mock(GithubService::class.java) 19 | } 20 | 21 | @Provides 22 | @Singleton 23 | @JvmStatic 24 | fun providesProjectResponseMapper(): ProjectResponseMapper { 25 | return Mockito.mock(ProjectResponseMapper::class.java) 26 | } 27 | 28 | 29 | @Provides 30 | @Singleton 31 | @JvmStatic 32 | fun providesProjectsRemote(): ProjectsRemote { 33 | return Mockito.mock(ProjectsRemote::class.java) 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/test/Helper.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.test 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.support.test.espresso.matcher.BoundedMatcher 5 | import android.view.View 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | 9 | 10 | fun withViewAtPosition(position: Int, itemMatcher: Matcher): Matcher { 11 | return object : BoundedMatcher(RecyclerView::class.java) { 12 | override fun describeTo(description: Description) { 13 | itemMatcher.describeTo(description) 14 | } 15 | 16 | override fun matchesSafely(recyclerView: RecyclerView): Boolean { 17 | val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) 18 | return viewHolder != null && itemMatcher.matches(viewHolder.itemView) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/test/TestApp.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.test 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.support.test.InstrumentationRegistry 6 | import com.tayfuncesur.mobile.di.DaggerTestAppComponent 7 | import com.tayfuncesur.mobile.di.TestAppComponent 8 | import dagger.android.AndroidInjector 9 | import dagger.android.DispatchingAndroidInjector 10 | import dagger.android.HasActivityInjector 11 | import javax.inject.Inject 12 | 13 | class TestApp : Application(), HasActivityInjector { 14 | 15 | @Inject 16 | lateinit var injector: DispatchingAndroidInjector 17 | 18 | private lateinit var appComponent: TestAppComponent 19 | 20 | 21 | companion object { 22 | fun appComponent(): TestAppComponent { 23 | return (InstrumentationRegistry.getTargetContext().applicationContext as TestApp).appComponent 24 | } 25 | } 26 | 27 | override fun activityInjector(): AndroidInjector { 28 | return injector 29 | } 30 | 31 | override fun onCreate() { 32 | super.onCreate() 33 | appComponent = DaggerTestAppComponent.builder().application(this).build() 34 | appComponent.inject(this) 35 | 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/test/TestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.test 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.os.Bundle 6 | import android.support.test.runner.AndroidJUnitRunner 7 | import io.reactivex.plugins.RxJavaPlugins 8 | import io.reactivex.schedulers.Schedulers 9 | 10 | class TestRunner : AndroidJUnitRunner() { 11 | 12 | override fun onCreate(arguments: Bundle) { 13 | super.onCreate(arguments) 14 | RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } 15 | } 16 | 17 | @Throws(InstantiationException::class, IllegalAccessException::class, ClassNotFoundException::class) 18 | override fun newApplication(cl: ClassLoader, className: String, context: Context): Application { 19 | return super.newApplication(cl, TestApp::class.java.name, context) 20 | } 21 | } -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/ui/data/MockData.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.ui.data 2 | 3 | import com.tayfuncesur.domain.model.Project 4 | import java.util.* 5 | 6 | object MockData { 7 | 8 | fun generateRandomProjectView(isBookmarked: Boolean = false): Project { 9 | return Project( 10 | UUID.randomUUID().toString(), 11 | UUID.randomUUID().toString(), 12 | Math.random().toInt(), 13 | isBookmarked 14 | ) 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/ui/detail/DetailActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.ui.detail 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.support.test.espresso.Espresso.onView 6 | import android.support.test.espresso.action.ViewActions.click 7 | import android.support.test.espresso.assertion.ViewAssertions.matches 8 | import android.support.test.espresso.matcher.ViewMatchers.withId 9 | import android.support.test.espresso.matcher.ViewMatchers.withText 10 | import android.support.test.rule.ActivityTestRule 11 | import android.support.test.runner.AndroidJUnit4 12 | import com.tayfuncesur.mobile.R 13 | import com.tayfuncesur.mobile.base.UIConstants.Selected_Item_Key 14 | import com.tayfuncesur.mobile.mapper.ProjectMapper 15 | import com.tayfuncesur.mobile.model.Project 16 | import com.tayfuncesur.mobile.test.TestApp 17 | import com.tayfuncesur.mobile.ui.data.MockData 18 | import com.tayfuncesur.presentation.mapper.ProjectViewMapper 19 | import io.reactivex.Completable 20 | import kotlinx.android.synthetic.main.main_detail.* 21 | import org.junit.Before 22 | import org.junit.Rule 23 | import org.junit.Test 24 | import org.junit.runner.RunWith 25 | import org.mockito.Mockito 26 | import org.mockito.Mockito.verify 27 | 28 | @RunWith(AndroidJUnit4::class) 29 | class DetailActivityTest { 30 | 31 | @Rule 32 | @JvmField 33 | val activityRule = ActivityTestRule(MainDetailActivity::class.java, false, false) 34 | 35 | @Test 36 | fun shouldStarCountDisplays() { 37 | val randomProject = launchWith(true) 38 | onView(withId(R.id.starCount)).check(matches(withText(randomProject.starCount.toString() + " Stars"))) 39 | } 40 | 41 | @Test 42 | fun shouldBookmarkActionCallsCorrectAction() { 43 | val randomProject = launchWith(false) 44 | Mockito.`when`(TestApp.appComponent().projectsRepository().bookmarkProject(randomProject.id)).thenReturn( 45 | Completable.complete() 46 | ) 47 | onView(withId(R.id.bookmarkProjectButton)).perform(click()) 48 | verify(TestApp.appComponent().projectsRepository()).bookmarkProject(randomProject.id) 49 | } 50 | 51 | 52 | @Test 53 | fun shouldUnBookmarkActionCallsCorrectAction() { 54 | val randomProject = launchWith(true) 55 | Mockito.`when`(TestApp.appComponent().projectsRepository().unbookmarkProject(randomProject.id)).thenReturn( 56 | Completable.complete() 57 | ) 58 | onView(withId(R.id.bookmarkProjectButton)).perform(click()) 59 | verify(TestApp.appComponent().projectsRepository()).unbookmarkProject(randomProject.id) 60 | } 61 | 62 | 63 | private fun launchWith(isBookmarked: Boolean = true): Project { 64 | val randomProject = MockData.generateRandomProjectView(isBookmarked) 65 | val mapper = ProjectViewMapper() 66 | val project = mapper.mapToView(randomProject) 67 | 68 | val anotherMapper = ProjectMapper() 69 | val realProjec = anotherMapper.mapToView(project) 70 | 71 | val intent = Intent() 72 | val bundle = Bundle(1) 73 | bundle.putParcelable(Selected_Item_Key, realProjec) 74 | intent.putExtras(bundle) 75 | 76 | activityRule.launchActivity(intent) 77 | return realProjec 78 | } 79 | } -------------------------------------------------------------------------------- /mobile/src/androidTest/java/com/tayfuncesur/mobile/ui/main/MainActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.ui.main 2 | 3 | import android.support.test.espresso.Espresso.onView 4 | import android.support.test.espresso.assertion.ViewAssertions.matches 5 | import android.support.test.espresso.contrib.RecyclerViewActions 6 | import android.support.test.espresso.matcher.ViewMatchers.* 7 | import android.support.test.rule.ActivityTestRule 8 | import android.support.test.runner.AndroidJUnit4 9 | import com.tayfuncesur.mobile.R 10 | import com.tayfuncesur.mobile.test.TestApp 11 | import com.tayfuncesur.mobile.test.withViewAtPosition 12 | import com.tayfuncesur.mobile.ui.data.MockData 13 | import io.reactivex.Observable 14 | import org.hamcrest.Matchers.allOf 15 | import org.hamcrest.Matchers.not 16 | import org.junit.Rule 17 | import org.junit.Test 18 | import org.junit.runner.RunWith 19 | import org.mockito.Mockito 20 | import java.util.* 21 | 22 | 23 | @RunWith(AndroidJUnit4::class) 24 | class MainActivityTest { 25 | 26 | @get:Rule 27 | val activity = ActivityTestRule(MainActivity::class.java, false, false) 28 | 29 | @Test 30 | fun shouldActivityLaunchesSuccessfully() { 31 | Mockito.`when`(TestApp.appComponent().projectsRepository().getProjects()).thenReturn( 32 | Observable.just(listOf(MockData.generateRandomProjectView())) 33 | ) 34 | Mockito.`when`(TestApp.appComponent().projectsRepository().getBookmarkedProjects()).thenReturn( 35 | Observable.just(listOf(UUID.randomUUID().toString())) 36 | ) 37 | activity.launchActivity(null) 38 | } 39 | 40 | @Test 41 | fun shouldProjectsShow() { 42 | val projects = listOf( 43 | MockData.generateRandomProjectView(), 44 | MockData.generateRandomProjectView(), 45 | MockData.generateRandomProjectView(), 46 | MockData.generateRandomProjectView(), 47 | MockData.generateRandomProjectView() 48 | ) 49 | Mockito.`when`(TestApp.appComponent().projectsRepository().getProjects()).thenReturn( 50 | Observable.just(projects) 51 | ) 52 | Mockito.`when`(TestApp.appComponent().projectsRepository().getBookmarkedProjects()).thenReturn( 53 | Observable.just(listOf(UUID.randomUUID().toString())) 54 | ) 55 | activity.launchActivity(null) 56 | 57 | projects.forEachIndexed { index, project -> 58 | onView(withId(R.id.mainRecycler)).perform(RecyclerViewActions.scrollToPosition(index)) 59 | 60 | onView(withId(R.id.mainRecycler)).check(matches(hasDescendant(withText(project.fullName)))) 61 | } 62 | } 63 | 64 | @Test 65 | fun shouldBookmarksShow() { 66 | val projects = listOf(MockData.generateRandomProjectView(true), MockData.generateRandomProjectView(false)) 67 | Mockito.`when`(TestApp.appComponent().projectsRepository().getProjects()).thenReturn( 68 | Observable.just(projects) 69 | ) 70 | Mockito.`when`(TestApp.appComponent().projectsRepository().getBookmarkedProjects()).thenReturn( 71 | Observable.just(listOf(projects[0].id)) 72 | ) 73 | activity.launchActivity(null) 74 | 75 | projects.forEachIndexed { index, project -> 76 | onView(withId(R.id.mainRecycler)).perform(RecyclerViewActions.scrollToPosition(index)) 77 | 78 | onView(withId(R.id.mainRecycler)) 79 | .check( 80 | matches( 81 | withViewAtPosition( 82 | index, 83 | hasDescendant( 84 | allOf( 85 | withId(R.id.favImage), 86 | if (project.isBookmarked) isDisplayed() else not(isDisplayed()) 87 | ) 88 | ) 89 | ) 90 | ) 91 | ) 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /mobile/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /mobile/src/main/assets/anims/error.json: -------------------------------------------------------------------------------- 1 | {"v":"5.2.1","fr":60,"ip":0,"op":361,"w":500,"h":500,"nm":"Artboard 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"?","sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[211,193,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.54,0.95],[-1.35,1.22],[-0.36,0.54],[0,0.7],[0.61,0.59],[1,0],[0.67,-0.64],[0.29,-0.95],[0,0],[-1.47,1.26],[-2.21,0],[-1.29,-0.68],[-0.72,-1.18],[0,-1.43],[0.58,-1],[1.13,-1.11],[0.33,-0.63],[0,-0.95],[0,0],[0,0]],"o":[[0,-1.3],[0.54,-0.95],[0.79,-0.73],[0.35,-0.54],[0,-0.84],[-0.61,-0.58],[-1.06,0],[-0.67,0.63],[0,0],[0.5,-1.73],[1.46,-1.26],[1.66,0],[1.29,0.67],[0.73,1.18],[0,1.41],[-0.58,1],[-0.9,0.87],[-0.33,0.62],[0,0],[0,0],[0,0]],"v":[[6.01,18.41],[6.82,15.04],[9.65,11.79],[11.37,9.88],[11.9,8.01],[10.99,5.87],[8.58,5],[5.99,5.96],[4.55,8.33],[0,6.38],[2.95,1.89],[8.46,0],[12.89,1.02],[15.91,3.8],[17,7.72],[16.13,11.34],[13.56,14.51],[11.72,16.75],[11.23,19.11],[11.23,19.76],[6.01,19.76]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":1,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.95,0],[0.67,0.69],[0,0.97],[-0.67,0.67],[-0.95,0],[-0.66,-0.68],[0,-0.98],[0.66,-0.7]],"o":[[-0.95,0],[-0.67,-0.7],[0,-0.98],[0.67,-0.68],[0.95,0],[0.66,0.67],[0,0.97],[-0.66,0.69]],"v":[[3.44,7.03],[1.01,6],[0,3.5],[1.01,1.02],[3.44,0],[5.85,1.02],[6.84,3.5],[5.85,6]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[5.14,22.97],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":1,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933332979679,0.933332979679,0.941175997257,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"?","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"ic_search","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.631],"y":[0.996]},"o":{"x":[0.289],"y":[-0.007]},"n":["0p631_0p996_0p289_-0p007"],"t":0,"s":[100],"e":[172]},{"i":{"x":[0.654],"y":[1.022]},"o":{"x":[0.437],"y":[-0.003]},"n":["0p654_1p022_0p437_-0p003"],"t":49,"s":[172],"e":[50]},{"i":{"x":[0.89],"y":[0.588]},"o":{"x":[0.24],"y":[0.043]},"n":["0p89_0p588_0p24_0p043"],"t":103,"s":[50],"e":[110.505]},{"i":{"x":[0.563],"y":[-5.665]},"o":{"x":[0.208],"y":[-5.168]},"n":["0p563_-5p665_0p208_-5p168"],"t":177,"s":[110.505],"e":[100.029]},{"i":{"x":[0.661],"y":[61.353]},"o":{"x":[0.199],"y":[1213.655]},"n":["0p661_61p353_0p199_1213p655"],"t":262,"s":[100.029],"e":[100]},{"t":357}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.744],"y":[1.016]},"o":{"x":[0.349],"y":[-0.014]},"n":["0p744_1p016_0p349_-0p014"],"t":0,"s":[91],"e":[49]},{"i":{"x":[0.669],"y":[0.846]},"o":{"x":[0.393],"y":[0.013]},"n":["0p669_0p846_0p393_0p013"],"t":49,"s":[49],"e":[135]},{"i":{"x":[0.744],"y":[1.144]},"o":{"x":[0.146],"y":[0.624]},"n":["0p744_1p144_0p146_0p624"],"t":103,"s":[135],"e":[147.777]},{"i":{"x":[0.351],"y":[0.993]},"o":{"x":[0.274],"y":[0.019]},"n":["0p351_0p993_0p274_0p019"],"t":177,"s":[147.777],"e":[25.549]},{"i":{"x":[0.661],"y":[0.973]},"o":{"x":[0.688],"y":[-0.015]},"n":["0p661_0p973_0p688_-0p015"],"t":262,"s":[25.549],"e":[91]},{"t":357}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,19.3],[-19.3,0],[0,-19.3],[19.3,0]],"o":[[-19.3,0],[0,-19.3],[19.3,0],[0,19.3],[0,0]],"v":[[35,70],[0,35],[35,0],[70,35],[35,70]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[12,12],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":1,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,10.78],[25.77,0],[0,-25.78],[-25.78,0],[-7.9,6.14],[0,0],[-1.5,0],[-1.14,1.14],[2.28,2.28]],"o":[[0,0],[6.14,-7.9],[0,-25.78],[-25.78,0],[0,25.77],[10.78,0],[0,0],[1.14,1.14],[1.49,0],[2.28,-2.28],[0,0]],"v":[[103.29,95.04],[83.5,75.25],[93.33,46.67],[46.67,0],[0,46.67],[46.67,93.33],[75.25,83.5],[95.04,103.29],[99.17,105],[103.29,103.29],[103.29,95.04]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":1,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933332979679,0.933332979679,0.941175997257,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"ic_search","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /mobile/src/main/assets/anims/loading.json: -------------------------------------------------------------------------------- 1 | {"v":"5.1.16","fr":29.9700012207031,"ip":0,"op":31.0000012626559,"w":320,"h":320,"nm":"ball stretch","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"right","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[191.25,160,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":6,"s":[25.258,25.258],"e":[25.258,25.258]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":12,"s":[25.258,25.258],"e":[25.258,70.258]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":18,"s":[25.258,70.258],"e":[25.258,25.258]},{"t":24.00000097754}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.086274995991,0.580391977348,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":12,"s":[0.829825008617,0.829825008617,0.829825008617,1],"e":[0.152941182256,0.396078437567,0.811764717102,1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":18,"s":[0.152941182256,0.396078437567,0.811764717102,1],"e":[0.829825008617,0.829825008617,0.829825008617,1]},{"t":24.00000097754}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.016,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120.0000048877,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"middle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,160,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":6,"s":[25.258,25.258],"e":[25.258,70.258]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":12,"s":[25.258,70.258],"e":[25.258,25.258]},{"t":18.000000733155}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.086274995991,0.580391977348,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.829825008617,0.829825008617,0.829825008617,1],"e":[0.901960790157,0.007843137719,0.470588237047,1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":12,"s":[0.901960790157,0.007843137719,0.470588237047,1],"e":[0.829825008617,0.829825008617,0.829825008617,1]},{"t":18.000000733155}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.016,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.901960790157,0.007843137719,0.470588237047,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120.0000048877,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"left","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[129,160,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[25.258,25.258],"e":[25.258,70.258]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":6,"s":[25.258,70.258],"e":[25.258,25.258]},{"t":12.00000048877}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.086274995991,0.580391977348,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0.829825008617,0.829825008617,0.829825008617,1],"e":[0.050980392843,0.219607844949,0.501960813999,1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.050980392843,0.219607844949,0.501960813999,1],"e":[0.829825008617,0.829825008617,0.829825008617,1]},{"t":12.00000048877}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.016,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120.0000048877,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /mobile/src/main/assets/anims/no_connection.json: -------------------------------------------------------------------------------- 1 | {"v":"5.1.1","fr":60,"ip":0,"op":360,"w":400,"h":400,"nm":"Composição 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[217,184.5,0],"ix":2},"a":{"a":0,"k":[127,158.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[253.5,0.5],[0.5,316.5]],"c":false},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":190,"s":[0],"e":[100]},{"t":210}],"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Aparar caminhos 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.912683844566,0.301613509655,0.301613509655,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"XMLID_359_","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199.9,105.883,0],"ix":2},"a":{"a":0,"k":[194.9,58.173,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.75,8.77],[71.15,0],[50.3,-50.32],[-8.76,-8.76],[-5.74,0],[-4.37,4.38],[-59.15,0],[-41.82,-41.84],[-8.77,8.75]],"o":[[-50.3,-50.32],[-71.16,0],[-8.76,8.77],[4.39,4.38],[5.75,0],[41.82,-41.84],[59.15,0],[8.76,8.77],[8.77,-8.77]],"v":[[383.24,78.02],[194.91,0],[6.57,78.02],[6.57,109.77],[22.45,116.34],[38.32,109.77],[194.91,44.9],[351.48,109.77],[383.22,109.78]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":90,"s":[0.870588243008,0.870588243008,0.870588243008,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":120,"s":[0,0,0,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":150,"s":[0,0,0,1],"e":[0.870588243008,0.870588243008,0.870588243008,1]},{"t":160}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":368,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"XMLID_360_","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199.904,169.12,0],"ix":2},"a":{"a":0,"k":[142.514,47.32,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[51.36,0],[36.31,-36.31],[-8.76,-8.76],[-8.76,8.77],[-39.36,0],[-27.82,-27.83],[-5.72,0],[-4.38,4.39],[8.77,8.77]],"o":[[-51.36,0],[-8.76,8.77],[8.78,8.77],[27.83,-27.83],[39.35,0],[4.39,4.39],[5.76,0],[8.77,-8.76],[-36.31,-36.31]],"v":[[142.52,0],[6.57,56.31],[6.57,88.06],[38.32,88.06],[142.52,44.9],[246.7,88.06],[262.57,94.64],[278.45,88.06],[278.45,56.31]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":60,"s":[0.870588243008,0.870588243008,0.870588243008,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":90,"s":[0,0,0,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":150,"s":[0,0,0,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":160,"s":[0,0,0,1],"e":[0.870588243008,0.870588243008,0.870588243008,1]},{"t":170}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":368,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"XMLID_361_","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199.914,232.215,0],"ix":2},"a":{"a":0,"k":[90.244,36.495,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[31.6,0],[22.33,-22.35],[-8.77,-8.76],[-8.78,8.77],[-19.62,0],[-13.86,-13.87],[-5.72,0],[-4.38,4.39],[8.78,8.77]],"o":[[-31.6,0],[-8.77,8.77],[8.77,8.77],[13.85,-13.87],[19.62,0],[4.39,4.39],[5.76,0],[8.78,-8.76],[-22.33,-22.35]],"v":[[90.24,0],[6.58,34.66],[6.58,66.41],[38.33,66.41],[90.24,44.9],[142.16,66.41],[158.03,72.99],[173.9,66.41],[173.9,34.66]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":30,"s":[0.870588243008,0.870588243008,0.870588243008,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":60,"s":[0,0,0,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":150,"s":[0,0,0,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":170,"s":[0,0,0,1],"e":[0.870588243008,0.870588243008,0.870588243008,1]},{"t":180}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":368,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"XMLID_362_","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199.905,314.064,0],"ix":2},"a":{"a":0,"k":[38.355,38.354,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[14.98,-14.97],[-14.97,-14.97],[-14.98,14.98],[14.97,14.98]],"o":[[-14.97,14.98],[14.98,14.98],[14.97,-14.97],[-14.98,-14.97]],"v":[[11.23,11.23],[11.23,65.47],[65.48,65.47],[65.48,11.23]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0.870588243008,0.870588243008,0.870588243008,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":30,"s":[0,0,0,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":150,"s":[0,0,0,1],"e":[0,0,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":180,"s":[0,0,0,1],"e":[0.870588243008,0.870588243008,0.870588243008,1]},{"t":190}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":368,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/App.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import com.tayfuncesur.mobile.di.DaggerAppComponent 6 | import dagger.android.AndroidInjector 7 | import dagger.android.DispatchingAndroidInjector 8 | import dagger.android.HasActivityInjector 9 | import javax.inject.Inject 10 | 11 | class App : Application(), HasActivityInjector { 12 | 13 | @Inject 14 | lateinit var androidInjector: DispatchingAndroidInjector 15 | 16 | override fun activityInjector(): AndroidInjector { 17 | return androidInjector 18 | } 19 | 20 | 21 | override fun onCreate() { 22 | super.onCreate() 23 | 24 | DaggerAppComponent 25 | .builder() 26 | .application(this) 27 | .build() 28 | .inject(this) 29 | 30 | } 31 | 32 | 33 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/UIThread.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile 2 | 3 | import com.tayfuncesur.domain.executor.PostExecutionThread 4 | import io.reactivex.Scheduler 5 | import io.reactivex.android.schedulers.AndroidSchedulers 6 | import javax.inject.Inject 7 | 8 | class UIThread @Inject constructor() : PostExecutionThread { 9 | override val scheduler: Scheduler 10 | get() = AndroidSchedulers.mainThread() 11 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/base/ActivityExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.base 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.Bundle 8 | 9 | /** 10 | * Extensions for simpler launching of Activities 11 | */ 12 | 13 | inline fun Activity.launchActivity( 14 | requestCode: Int = -1, 15 | options: Bundle? = null, 16 | noinline init: Intent.() -> Unit = {} 17 | ) { 18 | 19 | val intent = newIntent(this) 20 | intent.init() 21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 22 | startActivityForResult(intent, requestCode, options) 23 | } else { 24 | startActivityForResult(intent, requestCode) 25 | } 26 | } 27 | 28 | inline fun Context.launchActivity( 29 | options: Bundle? = null, 30 | noinline init: Intent.() -> Unit = {} 31 | ) { 32 | val intent = newIntent(this) 33 | intent.init() 34 | if (options != null) 35 | intent.putExtras(options) 36 | startActivity(intent) 37 | } 38 | 39 | inline fun newIntent(context: Context): Intent = 40 | Intent(context, T::class.java) -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/base/BaseDaggerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.base 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork 6 | import com.tayfuncesur.mobile.R 7 | import com.tayfuncesur.mobile.ui.noConnection.NoConnectionFragment 8 | import dagger.android.AndroidInjection 9 | import io.reactivex.android.schedulers.AndroidSchedulers 10 | import io.reactivex.disposables.CompositeDisposable 11 | import io.reactivex.schedulers.Schedulers 12 | 13 | open class BaseDaggerActivity : AppCompatActivity() { 14 | 15 | 16 | private var compositeDisposable = CompositeDisposable() 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | AndroidInjection.inject(this) 21 | 22 | 23 | compositeDisposable.add( 24 | ReactiveNetwork 25 | .observeInternetConnectivity() 26 | .subscribeOn(Schedulers.io()) 27 | .observeOn(AndroidSchedulers.mainThread()) 28 | .subscribe({ isConnectedToInternet -> 29 | val noConnectionFragment = NoConnectionFragment.newInstance() 30 | if (!isConnectedToInternet) { 31 | supportFragmentManager.beginTransaction().setCustomAnimations( 32 | android.R.anim.fade_in, 33 | android.R.anim.fade_out, 34 | android.R.anim.fade_in, 35 | android.R.anim.fade_out 36 | ) 37 | .add(android.R.id.content, noConnectionFragment, getString(R.string.noConnectionStr)) 38 | .addToBackStack(getString(R.string.noConnectionStr)).commit() 39 | } else { 40 | supportFragmentManager.popBackStack() 41 | supportFragmentManager.executePendingTransactions() 42 | } 43 | }, { }) 44 | ) 45 | } 46 | 47 | override fun onDestroy() { 48 | compositeDisposable.clear() 49 | super.onDestroy() 50 | } 51 | 52 | 53 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/base/UIConstants.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.base 2 | 3 | object UIConstants { 4 | 5 | const val Selected_Item_Key="SELECTED_ITEM" 6 | 7 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di 2 | 3 | import android.app.Application 4 | import com.tayfuncesur.mobile.App 5 | import com.tayfuncesur.mobile.di.module.* 6 | import dagger.BindsInstance 7 | import dagger.Component 8 | import dagger.android.AndroidInjectionModule 9 | import javax.inject.Singleton 10 | 11 | @Singleton 12 | @Component( 13 | modules = [AndroidInjectionModule::class, 14 | ApplicationModule::class, 15 | CacheModule::class, 16 | DataModule::class, 17 | PresentationModule::class, 18 | RemoteModule::class, 19 | UIModule::class] 20 | ) 21 | interface AppComponent { 22 | 23 | 24 | @Component.Builder 25 | interface Builder { 26 | 27 | @BindsInstance 28 | fun application(application: Application): Builder 29 | 30 | fun build(): AppComponent 31 | } 32 | 33 | fun inject(app: App) 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/di/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di 2 | 3 | import android.arch.lifecycle.ViewModel 4 | import android.arch.lifecycle.ViewModelProvider 5 | import dagger.MapKey 6 | import javax.inject.Inject 7 | import javax.inject.Provider 8 | import javax.inject.Singleton 9 | import kotlin.reflect.KClass 10 | 11 | @Singleton 12 | open class ViewModelFactory : ViewModelProvider.Factory { 13 | 14 | private val creators: Map, Provider> 15 | 16 | @Inject constructor(creators: Map, 17 | @JvmSuppressWildcards Provider>) { 18 | this.creators = creators 19 | } 20 | 21 | override fun create(modelClass: Class): T { 22 | var creator: Provider? = creators[modelClass] 23 | if (creator == null) { 24 | for ((key, value) in creators) { 25 | if (modelClass.isAssignableFrom(key)) { 26 | creator = value 27 | break 28 | } 29 | } 30 | } 31 | if (creator == null) { 32 | throw IllegalStateException("Unknown model class: " + modelClass) 33 | } 34 | try { 35 | return creator.get() as T 36 | } catch (e: Exception) { 37 | throw RuntimeException(e) 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/di/module/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import dagger.Binds 6 | import dagger.Module 7 | 8 | @Module 9 | abstract class ApplicationModule { 10 | 11 | @Binds 12 | abstract fun bindContext(application: Application): Context 13 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/di/module/CacheModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import android.app.Application 4 | import com.tayfuncesur.cache.ProjectsCacheImpl 5 | import com.tayfuncesur.cache.db.ProjectsDatabase 6 | import com.tayfuncesur.cache.mapper.CachedProjectMapper 7 | import com.tayfuncesur.data.repository.ProjectsCache 8 | import dagger.Binds 9 | import dagger.Module 10 | import dagger.Provides 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | abstract class CacheModule { 15 | 16 | @Module 17 | companion object { 18 | @Provides 19 | @Singleton 20 | @JvmStatic 21 | fun providesDatabase(application: Application): ProjectsDatabase { 22 | return ProjectsDatabase.getInstance(application) 23 | } 24 | 25 | @Provides 26 | @Singleton 27 | @JvmStatic 28 | fun providesCachedProjectMapper(): CachedProjectMapper { 29 | return CachedProjectMapper() 30 | } 31 | } 32 | 33 | @Binds 34 | abstract fun bindProjectsCache(projectsCacheImp: ProjectsCacheImpl): ProjectsCache 35 | 36 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/di/module/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import com.tayfuncesur.data.ProjectsDataRepository 4 | import com.tayfuncesur.data.mapper.ProjectMapper 5 | import com.tayfuncesur.domain.repository.ProjectsRepository 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.Provides 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | abstract class DataModule { 13 | 14 | @Module 15 | companion object { 16 | @Provides 17 | @Singleton 18 | @JvmStatic 19 | fun providesProjectMapper(): ProjectMapper { 20 | return ProjectMapper() 21 | } 22 | } 23 | 24 | @Binds 25 | abstract fun bindDataRepository(dataRepository: ProjectsDataRepository): ProjectsRepository 26 | 27 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/di/module/PresentationModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import android.arch.lifecycle.ViewModel 4 | import android.arch.lifecycle.ViewModelProvider 5 | import com.tayfuncesur.mobile.di.ViewModelFactory 6 | import com.tayfuncesur.presentation.BookmarkViewModel 7 | import com.tayfuncesur.presentation.ProjectsViewModel 8 | import com.tayfuncesur.presentation.SplashScreenViewModel 9 | import dagger.Binds 10 | import dagger.MapKey 11 | import dagger.Module 12 | import dagger.multibindings.IntoMap 13 | import kotlin.reflect.KClass 14 | 15 | @Module 16 | abstract class PresentationModule { 17 | 18 | @Binds 19 | @IntoMap 20 | @ViewModelKey(SplashScreenViewModel::class) 21 | abstract fun bindSplashScreenViewModel( 22 | splashViewModel: SplashScreenViewModel 23 | ): ViewModel 24 | 25 | @Binds 26 | @IntoMap 27 | @ViewModelKey(ProjectsViewModel::class) 28 | abstract fun bindProjectsViewModel( 29 | projectsViewModel: ProjectsViewModel 30 | ): ViewModel 31 | 32 | @Binds 33 | @IntoMap 34 | @ViewModelKey(BookmarkViewModel::class) 35 | abstract fun bindBookmarkViewModel( 36 | bookmarkViewModel: BookmarkViewModel 37 | ): ViewModel 38 | 39 | 40 | @Binds 41 | abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory 42 | } 43 | 44 | 45 | @Target(AnnotationTarget.FUNCTION) 46 | @Retention(AnnotationRetention.RUNTIME) 47 | @MapKey 48 | annotation class ViewModelKey(val value: KClass) -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/di/module/RemoteModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import com.tayfuncesur.data.repository.ProjectsRemote 4 | import com.tayfuncesur.mobile.BuildConfig 5 | import com.tayfuncesur.remote.ProjectsRemoteImpl 6 | import com.tayfuncesur.remote.mapper.ProjectResponseMapper 7 | import com.tayfuncesur.remote.service.GithubService 8 | import com.tayfuncesur.remote.service.GithubServiceFactory 9 | import dagger.Binds 10 | import dagger.Module 11 | import dagger.Provides 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | abstract class RemoteModule { 16 | 17 | @Module 18 | companion object { 19 | @Provides 20 | @Singleton 21 | @JvmStatic 22 | fun providesGithubService(): GithubService { 23 | return GithubServiceFactory.create(BuildConfig.DEBUG) 24 | } 25 | 26 | @Provides 27 | @Singleton 28 | @JvmStatic 29 | fun providesProjectResponseMapper(): ProjectResponseMapper { 30 | return ProjectResponseMapper() 31 | } 32 | } 33 | 34 | 35 | @Binds 36 | abstract fun bindProjectsRemote(projectsRemoteImpl: ProjectsRemoteImpl): ProjectsRemote 37 | 38 | 39 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/di/module/UIModule.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.di.module 2 | 3 | import com.tayfuncesur.domain.executor.PostExecutionThread 4 | import com.tayfuncesur.mobile.UIThread 5 | import com.tayfuncesur.mobile.mapper.ProjectMapper 6 | import com.tayfuncesur.mobile.ui.detail.MainDetailActivity 7 | import com.tayfuncesur.mobile.ui.main.MainActivity 8 | import com.tayfuncesur.mobile.ui.splash.SplashActivity 9 | import dagger.Binds 10 | import dagger.Module 11 | import dagger.Provides 12 | import dagger.android.ContributesAndroidInjector 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | abstract class UIModule { 17 | 18 | @Module 19 | companion object{ 20 | @Provides 21 | @Singleton 22 | @JvmStatic 23 | fun providesProjectViewMapper() : ProjectMapper{ 24 | return ProjectMapper() 25 | } 26 | } 27 | 28 | @Binds 29 | abstract fun bindPostExecutionThread(uiThread: UIThread): PostExecutionThread 30 | 31 | @ContributesAndroidInjector 32 | abstract fun contributesSplashScreenActivity(): SplashActivity 33 | 34 | @ContributesAndroidInjector 35 | abstract fun contributesMainActivity(): MainActivity 36 | 37 | @ContributesAndroidInjector 38 | abstract fun contributesMainDetailActivity(): MainDetailActivity 39 | 40 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/mapper/ProjectMapper.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.mapper 2 | 3 | import com.tayfuncesur.mobile.model.Project 4 | import com.tayfuncesur.presentation.model.ProjectView 5 | 6 | class ProjectMapper : ViewMapper { 7 | 8 | override fun mapToView(view: ProjectView): Project { 9 | return Project(view.id, view.fullName, view.starCount, view.isBookmarked) 10 | } 11 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/mapper/ViewMapper.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.mapper 2 | 3 | interface ViewMapper { 4 | 5 | fun mapToView(view: V): P 6 | 7 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/model/Project.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | class Project( 8 | val id: String, val fullName: String, 9 | val starCount: Int, var isBookmarked: Boolean 10 | ) : Parcelable -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/ui/detail/MainDetailActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.ui.detail 2 | 3 | import android.animation.Animator 4 | import android.annotation.SuppressLint 5 | import android.arch.lifecycle.ViewModelProviders 6 | import android.os.Bundle 7 | import com.tayfuncesur.mobile.R 8 | import com.tayfuncesur.mobile.base.BaseDaggerActivity 9 | import com.tayfuncesur.mobile.base.UIConstants.Selected_Item_Key 10 | import com.tayfuncesur.mobile.di.ViewModelFactory 11 | import com.tayfuncesur.mobile.model.Project 12 | import com.tayfuncesur.presentation.BookmarkViewModel 13 | import com.thekhaeng.pushdownanim.PushDownAnim 14 | import kotlinx.android.synthetic.main.main_detail.* 15 | import javax.inject.Inject 16 | 17 | class MainDetailActivity : BaseDaggerActivity() { 18 | 19 | @Inject 20 | lateinit var viewModelFactory: ViewModelFactory 21 | 22 | lateinit var bookmarkViewModel: BookmarkViewModel 23 | 24 | @SuppressLint("SetTextI18n") 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | super.onCreate(savedInstanceState) 27 | setContentView(R.layout.main_detail) 28 | val selectedProject = intent?.extras?.getParcelable(Selected_Item_Key) 29 | 30 | toolbar_detail_title.text = selectedProject?.fullName 31 | starCount.text = "${selectedProject?.starCount} Stars" 32 | 33 | val defaultSpeed = bookmarkAnim.speed 34 | 35 | bookmarkViewModel = ViewModelProviders.of(this, viewModelFactory).get(BookmarkViewModel::class.java) 36 | if (selectedProject?.isBookmarked!!) { 37 | bookmarkAnim.speed = defaultSpeed * 3 38 | bookmarkAnim.playAnimation() 39 | bookmarkLabel.text = getString(R.string.unbookmark_project) 40 | } 41 | 42 | 43 | bookmarkAnim.addAnimatorListener(object : Animator.AnimatorListener { 44 | override fun onAnimationRepeat(animation: Animator?) {} 45 | 46 | override fun onAnimationEnd(animation: Animator?) { 47 | if (!selectedProject.isBookmarked) 48 | bookmarkLabel.text = getString(R.string.bookmark_project) 49 | } 50 | 51 | override fun onAnimationCancel(animation: Animator?) {} 52 | 53 | override fun onAnimationStart(animation: Animator?) {} 54 | }) 55 | 56 | PushDownAnim.setPushDownAnimTo(bookmarkProjectButton).setScale(PushDownAnim.MODE_STATIC_DP, 5F).setOnClickListener { 57 | bookmarkAction(selectedProject, defaultSpeed) 58 | } 59 | } 60 | 61 | 62 | private fun bookmarkAction(selectedProject: Project, defaultSpeed: Float) { 63 | if (selectedProject.isBookmarked) { 64 | bookmarkAnim.speed = -2 * defaultSpeed 65 | bookmarkAnim.playAnimation() 66 | bookmarkViewModel.unbookmarkProject(selectedProject.id) 67 | } else { 68 | bookmarkViewModel.bookmarkProject(selectedProject.id) 69 | bookmarkAnim.speed = defaultSpeed 70 | bookmarkAnim.playAnimation() 71 | bookmarkLabel.text = getString(R.string.unbookmark_project) 72 | } 73 | selectedProject.isBookmarked = !selectedProject.isBookmarked 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.ui.main 2 | 3 | import android.arch.lifecycle.Observer 4 | import android.arch.lifecycle.ViewModelProviders 5 | import android.os.Bundle 6 | import android.view.View 7 | import com.tayfuncesur.mobile.R 8 | import com.tayfuncesur.mobile.base.BaseDaggerActivity 9 | import com.tayfuncesur.mobile.base.UIConstants.Selected_Item_Key 10 | import com.tayfuncesur.mobile.base.launchActivity 11 | import com.tayfuncesur.mobile.di.ViewModelFactory 12 | import com.tayfuncesur.mobile.mapper.ProjectMapper 13 | import com.tayfuncesur.mobile.model.Project 14 | import com.tayfuncesur.mobile.ui.detail.MainDetailActivity 15 | import com.tayfuncesur.mobile.ui.noConnection.NoConnectionFragment 16 | import com.tayfuncesur.presentation.ProjectsViewModel 17 | import com.tayfuncesur.presentation.state.Resource 18 | import com.thekhaeng.pushdownanim.PushDownAnim 19 | import kotlinx.android.synthetic.main.activity_main.* 20 | import javax.inject.Inject 21 | 22 | class MainActivity : BaseDaggerActivity() { 23 | 24 | @Inject 25 | lateinit var viewModelFactory: ViewModelFactory 26 | @Inject 27 | lateinit var mapper: ProjectMapper 28 | 29 | lateinit var projectsViewModel: ProjectsViewModel 30 | 31 | private var mainAdapter: MainAdapter? = null 32 | 33 | private var dataSource: List? = emptyList() 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | setContentView(R.layout.activity_main) 38 | projectsViewModel = ViewModelProviders.of(this, viewModelFactory).get(ProjectsViewModel::class.java) 39 | 40 | projectsViewModel.getRemoteProjectsLiveData().observe(this, Observer { it -> 41 | if (it is Resource.Success) { 42 | dataSource = it.data?.map { mapper.mapToView(it) } 43 | 44 | mainAdapter = dataSource?.let { it1 -> 45 | MainAdapter(it1) { 46 | val bundle = Bundle(1) 47 | bundle.putParcelable(Selected_Item_Key, it) 48 | launchActivity(bundle) 49 | } 50 | } 51 | mainRecycler.adapter = mainAdapter 52 | } 53 | loadingLayout.visibility = if (it is Resource.Loading) View.VISIBLE else View.GONE 54 | errorLayout.visibility = if (it is Resource.Failure) View.VISIBLE else View.GONE 55 | }) 56 | 57 | projectsViewModel.getBookmarkedProjectsLiveData().observe(this, Observer { it -> 58 | if (it is Resource.Success) { 59 | if (it.data?.isNotEmpty()!!) { 60 | dataSource?.map { project -> 61 | for (id in it.data!!) { 62 | if (project.id == id) { 63 | project.isBookmarked = true 64 | break 65 | } else { 66 | project.isBookmarked = false 67 | } 68 | } 69 | } 70 | } else { 71 | dataSource?.map { 72 | it.isBookmarked = false 73 | } 74 | } 75 | dataSource?.let { it1 -> mainAdapter?.updateData(it1) } 76 | } 77 | }) 78 | 79 | 80 | PushDownAnim.setPushDownAnimTo(tryAgainLayout).setScale(PushDownAnim.MODE_STATIC_DP, 5F).setOnClickListener { 81 | projectsViewModel.loadProjects() 82 | } 83 | } 84 | 85 | override fun onBackPressed() { 86 | if (supportFragmentManager.backStackEntryCount > 0) { 87 | var founded = false 88 | for (item in supportFragmentManager.fragments) { 89 | if (item is NoConnectionFragment) 90 | founded = true 91 | } 92 | if (founded) finishAffinity() 93 | else super.onBackPressed() 94 | } else { 95 | super.onBackPressed() 96 | } 97 | 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/ui/main/MainAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.ui.main 2 | 3 | import android.annotation.SuppressLint 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import com.tayfuncesur.mobile.R 9 | import com.tayfuncesur.mobile.model.Project 10 | import com.thekhaeng.pushdownanim.PushDownAnim 11 | import kotlinx.android.synthetic.main.single_row.view.* 12 | 13 | class MainAdapter(private var list: List, private val onClick: (Project) -> Unit) : 14 | RecyclerView.Adapter() { 15 | 16 | fun updateData(_list: List) { 17 | this@MainAdapter.list = _list 18 | notifyDataSetChanged() 19 | } 20 | 21 | override fun onCreateViewHolder(p0: ViewGroup, p1: Int): SingleRow { 22 | return SingleRow(LayoutInflater.from(p0.context).inflate(R.layout.single_row, p0, false)) 23 | } 24 | 25 | override fun getItemCount(): Int { 26 | return list.size 27 | } 28 | 29 | override fun onBindViewHolder(p0: SingleRow, p1: Int) { 30 | p0.bind(p1) 31 | } 32 | 33 | 34 | inner class SingleRow(private val view: View) : RecyclerView.ViewHolder(view) { 35 | 36 | @SuppressLint("SetTextI18n") 37 | fun bind(pos: Int) { 38 | view.repoName.text = list[pos].fullName 39 | view.starCount.text = "${list[pos].starCount} Stars" 40 | view.favImage.visibility = if (list[pos].isBookmarked) View.VISIBLE else View.GONE 41 | PushDownAnim.setPushDownAnimTo(view.singleRowItem).setScale(PushDownAnim.MODE_STATIC_DP, 5F) 42 | .setOnClickListener { 43 | onClick.invoke(list[pos]) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/ui/noConnection/NoConnectionFragment.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.ui.noConnection 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import com.tayfuncesur.mobile.R 9 | 10 | class NoConnectionFragment : Fragment() { 11 | 12 | companion object { 13 | fun newInstance() = NoConnectionFragment() 14 | } 15 | 16 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 17 | return inflater.inflate(R.layout.no_connection, container, false) 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /mobile/src/main/java/com/tayfuncesur/mobile/ui/splash/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tayfuncesur.mobile.ui.splash 2 | 3 | import android.arch.lifecycle.Observer 4 | import android.arch.lifecycle.ViewModelProvider 5 | import android.arch.lifecycle.ViewModelProviders 6 | import android.content.Intent 7 | import android.os.Bundle 8 | import android.support.v7.app.AppCompatActivity 9 | import android.view.Window 10 | import android.view.WindowManager 11 | import com.tayfuncesur.mobile.R 12 | import com.tayfuncesur.mobile.base.BaseDaggerActivity 13 | import com.tayfuncesur.mobile.base.launchActivity 14 | import com.tayfuncesur.mobile.di.ViewModelFactory 15 | import com.tayfuncesur.mobile.ui.main.MainActivity 16 | import com.tayfuncesur.presentation.SplashScreenViewModel 17 | import dagger.android.AndroidInjection 18 | import javax.inject.Inject 19 | 20 | class SplashActivity : AppCompatActivity() { 21 | 22 | private lateinit var splashScreenViewModel: SplashScreenViewModel 23 | 24 | @Inject 25 | lateinit var viewModelFactory: ViewModelFactory 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | AndroidInjection.inject(this) 30 | window.requestFeature(Window.FEATURE_NO_TITLE) 31 | window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) 32 | setContentView(R.layout.splash_screen) 33 | 34 | splashScreenViewModel = ViewModelProviders.of(this, viewModelFactory).get(SplashScreenViewModel::class.java) 35 | 36 | splashScreenViewModel.isFinished.observe(this, Observer { 37 | launchActivity { } 38 | finish() 39 | }) 40 | } 41 | 42 | 43 | } -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TayfunCesur/GithubProjectBrowser/8aa6705cf42651422ec677c5e48c9cab12e54b9f/mobile/src/main/res/drawable/appicon.png -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/appiconblack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TayfunCesur/GithubProjectBrowser/8aa6705cf42651422ec677c5e48c9cab12e54b9f/mobile/src/main/res/drawable/appiconblack.png -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_bookmarked.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_favorite.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_favorite_outlined.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/font/manropemedium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TayfunCesur/GithubProjectBrowser/8aa6705cf42651422ec677c5e48c9cab12e54b9f/mobile/src/main/res/font/manropemedium.ttf -------------------------------------------------------------------------------- /mobile/src/main/res/font/manroperegular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TayfunCesur/GithubProjectBrowser/8aa6705cf42651422ec677c5e48c9cab12e54b9f/mobile/src/main/res/font/manroperegular.ttf -------------------------------------------------------------------------------- /mobile/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 21 | 22 | 31 | 32 | 33 | 34 | 35 | 44 | 45 | 46 | 57 | 58 | 67 | 68 | 79 | 80 | 81 | 82 | 95 | 96 | 105 | 106 | 119 | 120 | 136 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/main_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 30 | 31 | 32 | 33 | 34 | 43 | 44 | 50 | 51 | 65 | 66 | 67 | 83 | 87 | 88 | 94 | 95 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/no_connection.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 23 | 24 | 36 | 37 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/single_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 22 | 23 | 24 | 34 | 35 | 36 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 57 | 69 | 70 | 71 | 72 | 73 | 74 | 77 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/splash_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 17 | 18 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /mobile/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #000000 5 | #D81B60 6 | #222327 7 | #FFFFFF 8 | #f44336 9 | #1f2022 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16sp 4 | 18sp 5 | 14sp 6 | 150dp 7 | 12sp 8 | 8sp 9 | 4sp 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Tayfun Cesur Freeletics 3 | Square Projects 4 | Loading... 5 | Bookmark Project 6 | Unbookmark Project 7 | An error occurred while getting repos. 8 | You have no internet connection 9 | NoConnectionFragmetn 10 | Try Again 11 | 12 | -------------------------------------------------------------------------------- /mobile/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 |