├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── functionalkotlin │ │ │ └── bandhookkotlin │ │ │ ├── App.kt │ │ │ ├── data │ │ │ ├── CloudAlbumDataSet.kt │ │ │ ├── CloudArtistDataSet.kt │ │ │ ├── Util.kt │ │ │ ├── lastfm │ │ │ │ ├── LastFmRequestInterceptor.kt │ │ │ │ ├── LastFmService.kt │ │ │ │ └── model │ │ │ │ │ ├── LastFmAlbum.kt │ │ │ │ │ ├── LastFmAlbumDetail.kt │ │ │ │ │ ├── LastFmArtist.kt │ │ │ │ │ ├── LastFmArtistList.kt │ │ │ │ │ ├── LastFmArtistMatches.kt │ │ │ │ │ ├── LastFmBio.kt │ │ │ │ │ ├── LastFmImage.kt │ │ │ │ │ ├── LastFmImageType.kt │ │ │ │ │ ├── LastFmResponse.kt │ │ │ │ │ ├── LastFmResult.kt │ │ │ │ │ ├── LastFmSimilar.kt │ │ │ │ │ ├── LastFmTopAlbums.kt │ │ │ │ │ ├── LastFmTrack.kt │ │ │ │ │ └── LastFmTracklist.kt │ │ │ └── mapper │ │ │ │ ├── album │ │ │ │ └── AlbumMapper.kt │ │ │ │ ├── artist │ │ │ │ └── ArtistMapper.kt │ │ │ │ ├── image │ │ │ │ └── ImageMapper.kt │ │ │ │ └── track │ │ │ │ └── TrackMapper.kt │ │ │ ├── di │ │ │ ├── ActivityModule.kt │ │ │ ├── ApplicationComponent.kt │ │ │ ├── ApplicationModule.kt │ │ │ ├── DataModule.kt │ │ │ ├── DomainModule.kt │ │ │ ├── RepositoryModule.kt │ │ │ ├── qualifier │ │ │ │ ├── ApiKey.kt │ │ │ │ ├── ApplicationQualifier.kt │ │ │ │ ├── CacheDuration.kt │ │ │ │ └── LanguageSelection.kt │ │ │ ├── scope │ │ │ │ └── ActivityScope.kt │ │ │ └── subcomponent │ │ │ │ ├── album │ │ │ │ ├── AlbumActivityComponent.kt │ │ │ │ └── AlbumActivityModule.kt │ │ │ │ ├── detail │ │ │ │ ├── ArtistActivityComponent.kt │ │ │ │ └── ArtistActivityModule.kt │ │ │ │ └── main │ │ │ │ ├── MainActivityComponent.kt │ │ │ │ └── MainActivityModule.kt │ │ │ ├── domain │ │ │ ├── entity │ │ │ │ ├── Album.kt │ │ │ │ ├── Artist.kt │ │ │ │ ├── Exceptions.kt │ │ │ │ └── Track.kt │ │ │ ├── interactor │ │ │ │ ├── GetAlbumDetailInteractor.kt │ │ │ │ ├── GetArtistDetailInteractor.kt │ │ │ │ ├── GetRecommendedArtistsInteractor.kt │ │ │ │ └── GetTopAlbumsInteractor.kt │ │ │ └── repository │ │ │ │ ├── AlbumRepository.kt │ │ │ │ └── ArtistRepository.kt │ │ │ ├── functional │ │ │ ├── AsyncResult.kt │ │ │ ├── Future.kt │ │ │ ├── List.kt │ │ │ ├── Option.kt │ │ │ └── Result.kt │ │ │ ├── repository │ │ │ ├── AlbumRepositoryImpl.kt │ │ │ ├── ArtistRepositoryImpl.kt │ │ │ └── dataset │ │ │ │ ├── AlbumDataSet.kt │ │ │ │ └── ArtistDataSet.kt │ │ │ └── ui │ │ │ ├── activity │ │ │ ├── ActivityAnkoComponent.kt │ │ │ ├── BaseActivity.kt │ │ │ └── ViewGroupComponent.kt │ │ │ ├── adapter │ │ │ ├── ArtistDetailPagerAdapter.kt │ │ │ ├── BaseAdapter.kt │ │ │ ├── ImageTitleAdapter.kt │ │ │ ├── SingleClickListener.kt │ │ │ └── TracksAdapter.kt │ │ │ ├── custom │ │ │ ├── AutofitRecyclerView.kt │ │ │ ├── PaddingItemDecoration.kt │ │ │ └── SquareImageView.kt │ │ │ ├── entity │ │ │ ├── AlbumDetail.kt │ │ │ ├── ArtistDetail.kt │ │ │ ├── ImageTitle.kt │ │ │ ├── TrackDetail.kt │ │ │ └── mapper │ │ │ │ ├── album │ │ │ │ └── detail │ │ │ │ │ └── AlbumDetailDataMapper.kt │ │ │ │ ├── artist │ │ │ │ └── detail │ │ │ │ │ └── ArtistDetailDataMapper.kt │ │ │ │ ├── image │ │ │ │ └── title │ │ │ │ │ └── ImageTitleDataMapper.kt │ │ │ │ └── track │ │ │ │ └── TrackDataMapper.kt │ │ │ ├── fragment │ │ │ └── AlbumsFragmentContainer.kt │ │ │ ├── presenter │ │ │ ├── AlbumPresenter.kt │ │ │ ├── ArtistPresenter.kt │ │ │ ├── MainPresenter.kt │ │ │ └── base │ │ │ │ ├── AlbumsPresenter.kt │ │ │ │ └── Presenter.kt │ │ │ ├── screens │ │ │ ├── Styles.kt │ │ │ ├── album │ │ │ │ ├── AlbumActivity.kt │ │ │ │ └── AlbumLayout.kt │ │ │ ├── detail │ │ │ │ ├── AlbumsFragment.kt │ │ │ │ ├── ArtistActivity.kt │ │ │ │ ├── ArtistLayout.kt │ │ │ │ └── BiographyFragment.kt │ │ │ └── main │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainLayout.kt │ │ │ ├── util │ │ │ ├── ContextExtensions.kt │ │ │ ├── PicassoExtensions.kt │ │ │ ├── VersionsSupport.kt │ │ │ └── ViewExtensions.kt │ │ │ └── view │ │ │ ├── AlbumView.kt │ │ │ ├── ArtistView.kt │ │ │ ├── MainView.kt │ │ │ └── PresentationView.kt │ └── res │ │ ├── drawable │ │ └── gradient_shadow.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── values-sw720dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── config.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ ├── java │ └── com │ │ └── functionalkotlin │ │ └── bandhookkotlin │ │ ├── data │ │ ├── CloudAlbumDataSetTest.kt │ │ ├── CloudArtistDataSetTest.kt │ │ ├── Constants.kt │ │ ├── mapper │ │ │ ├── album │ │ │ │ └── AlbumMapperTest.kt │ │ │ ├── artist │ │ │ │ └── ArtistMapperTest.kt │ │ │ ├── image │ │ │ │ └── ImageMapperTest.kt │ │ │ └── track │ │ │ │ └── TrackMapperTest.kt │ │ └── mock │ │ │ └── FakeCall.kt │ │ ├── domain │ │ └── interactor │ │ │ ├── Constants.kt │ │ │ ├── GetAlbumDetailInteractorTest.kt │ │ │ └── GetTopAlbumsInteractorTest.kt │ │ ├── repository │ │ ├── AlbumRepositoryImplTest.kt │ │ └── Constants.kt │ │ ├── ui │ │ ├── entity │ │ │ ├── ImageTitleTest.kt │ │ │ └── mapper │ │ │ │ ├── AlbumDetailDataMapperTest.kt │ │ │ │ ├── ArtistDetailDataMapperTest.kt │ │ │ │ ├── ImageTitleDataMapperTest.kt │ │ │ │ └── TrackDataMapperTest.kt │ │ └── presenter │ │ │ ├── ArtistPresenterTest.kt │ │ │ └── Constants.kt │ │ └── util │ │ └── Functional.kt │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── art ├── bandhook.gif └── logo.png ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── ProjectConfiguration.kt ├── detekt.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | bin/ 3 | gen/ 4 | 5 | # Gradle files 6 | .gradle/ 7 | build/ 8 | 9 | # Local configuration file (sdk path, etc) 10 | local.properties 11 | 12 | # Intellij project files 13 | *.iws 14 | .idea/tasks.xml 15 | .idea 16 | *.iml 17 | 18 | # OS 19 | .DS_Store 20 | 21 | # Api key 22 | app/src/main/res/values/api_key.xml -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © FunctionalKotlin.com 2017. All rights reserved. 2 | 3 | It is strictly prohibited to copy, play, transmit, publish or modify any material included in the website FunctionalKotlin.com or in any other related platform except with express written permission of the authors. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Functional Hub 3 |

4 | 5 | # Final App: Bandhook 6 | 7 | ## How to use this project 8 | 9 | You can use Android Studio or Intellij to work with this repository. 10 | 11 | First thing you will need to compile this project is to get an [API Key from Last.fm](http://www.lastfm.es/api). It will be used to connect to the service that will provide artists info. Then create a resource file `app/src/main/res/values/api_key.xml` (this path is ignored by git) with the following content: 12 | 13 | ```xml 14 | YOUR_KEY 15 | ``` 16 | 17 | The `Kotlin` plugin for Android Studio is also required. 18 | 19 | ## Acknowledgements 20 | 21 | This project was originally a fork from [Antonio Leiva - Bandhook-Kotlin](https://github.com/antoniolg/Bandhook-Kotlin) 22 | 23 | ## License 24 | 25 | Copyright © [FunctionalHub.com](http://functionalhub.com) 2018. All rights reserved. 26 | 27 |

28 | Screenshot 29 |

-------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | import org.gradle.kotlin.dsl.extra 4 | import org.gradle.kotlin.dsl.getValue 5 | import org.gradle.kotlin.dsl.kotlin 6 | import org.gradle.kotlin.dsl.setValue 7 | import org.jetbrains.kotlin.gradle.dsl.Coroutines 8 | 9 | plugins { 10 | id("com.android.application") 11 | kotlin("android") 12 | kotlin("kapt") 13 | } 14 | 15 | val config = ProjectConfiguration() 16 | 17 | android { 18 | compileSdkVersion(config.android.compileSdkVersion) 19 | buildToolsVersion(config.android.buildToolsVersion) 20 | 21 | defaultConfig { 22 | applicationId = config.android.applicationId 23 | minSdkVersion(config.android.minSdkVersion) 24 | targetSdkVersion(config.android.targetSdkVersion) 25 | versionCode = config.android.versionCode 26 | versionName = config.android.versionName 27 | 28 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 29 | } 30 | 31 | buildTypes { 32 | getByName("release") { 33 | isMinifyEnabled = false 34 | proguardFiles("proguard-rules.pro") 35 | } 36 | } 37 | } 38 | 39 | kotlin { 40 | experimental.coroutines = Coroutines.ENABLE 41 | } 42 | 43 | dependencies { 44 | compile(config.libs.kotlinStdlib) 45 | compile(config.libs.appcompat) 46 | compile(config.libs.recyclerview) 47 | compile(config.libs.cardview) 48 | compile(config.libs.palette) 49 | compile(config.libs.design) 50 | compile(config.libs.eventbus) 51 | compile(config.libs.picasso) 52 | compile(config.libs.okhttp) 53 | compile(config.libs.okhttpInterceptor) 54 | compile(config.libs.retrofit) 55 | compile(config.libs.retrofitGson) 56 | compile(config.libs.jobqueue) 57 | compile(config.libs.ankoSdk15) 58 | compile(config.libs.ankoSupport) 59 | compile(config.libs.ankoAppcompat) 60 | compile(config.libs.ankoDesign) 61 | compile(config.libs.ankoCardview) 62 | compile(config.libs.ankoRecyclerview) 63 | compile(config.libs.dagger) 64 | compile(config.libs.coroutines) 65 | compile(config.libs.coroutinesAndroid) 66 | kapt(config.libs.daggerCompiler) 67 | 68 | testCompile(config.libs.kotlinStdlib) 69 | testCompile(config.testLibs.junit) 70 | testCompile(config.testLibs.mockito) 71 | testCompile(config.testLibs.kotlinTest) 72 | testCompile(config.testLibs.mockitoKotlin) 73 | 74 | androidTestCompile(config.libs.kotlinStdlib) 75 | androidTestCompile(config.testLibs.mockito) 76 | androidTestCompile(config.testLibs.dexmaker) 77 | androidTestCompile(config.testLibs.dexmakerMockito) 78 | androidTestCompile(config.testLibs.annotations) 79 | androidTestCompile(config.testLibs.espresso) 80 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunctionalKotlin/final-app/cd3d59794218f72740c2cb22a430eba26f244cea/app/proguard-rules.pro -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/App.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin 4 | 5 | import android.app.Application 6 | import com.functionalkotlin.bandhookkotlin.di.ApplicationComponent 7 | import com.functionalkotlin.bandhookkotlin.di.ApplicationModule 8 | import com.functionalkotlin.bandhookkotlin.di.DaggerApplicationComponent 9 | 10 | class App : Application() { 11 | 12 | companion object { 13 | lateinit var graph: ApplicationComponent 14 | } 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | 19 | graph = DaggerApplicationComponent.builder() 20 | .applicationModule(ApplicationModule(this)) 21 | .build() 22 | } 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/CloudAlbumDataSet.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.LastFmService 6 | import com.functionalkotlin.bandhookkotlin.data.mapper.album.transform 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 8 | import com.functionalkotlin.bandhookkotlin.domain.entity.AlbumNotFound 9 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 10 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 11 | import com.functionalkotlin.bandhookkotlin.functional.result 12 | import com.functionalkotlin.bandhookkotlin.functional.transform 13 | import com.functionalkotlin.bandhookkotlin.repository.dataset.AlbumDataSet 14 | 15 | class CloudAlbumDataSet(private val lastFmService: LastFmService) : AlbumDataSet { 16 | 17 | override fun requestAlbum(mbid: String): AsyncResult = 18 | lastFmService.requestAlbum(mbid).asyncResult { 19 | transform(album) 20 | }.orElse { 21 | AlbumNotFound(mbid) 22 | } 23 | 24 | override fun requestTopAlbums(artistId: String): AsyncResult, TopAlbumsNotFound> = 25 | artistId.takeIf { it.isNotBlank() }.result().transform { 26 | it?.let { 27 | lastFmService.requestAlbums(it, "").unwrapCall { 28 | transform(topAlbums.albums) 29 | } 30 | } 31 | }.orElse { 32 | TopAlbumsNotFound(artistId) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/CloudArtistDataSet.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.LastFmService 6 | import com.functionalkotlin.bandhookkotlin.data.mapper.artist.transform 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 8 | import com.functionalkotlin.bandhookkotlin.domain.entity.ArtistNotFound 9 | import com.functionalkotlin.bandhookkotlin.domain.entity.RecommendationNotFound 10 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 11 | import com.functionalkotlin.bandhookkotlin.repository.dataset.ArtistDataSet 12 | 13 | class CloudArtistDataSet( 14 | private val language: String, private val lastFmService: LastFmService) : ArtistDataSet { 15 | 16 | val coldplayMbid = "cc197bad-dc9c-440d-a5b5-d52ba2e14234" 17 | 18 | override fun requestArtist(id: String): AsyncResult = 19 | lastFmService.requestArtistInfo(id, language).asyncResult { 20 | transform(artist) 21 | }.orElse { 22 | ArtistNotFound(id) 23 | } 24 | 25 | override fun requestRecommendedArtists(): AsyncResult, RecommendationNotFound> = 26 | lastFmService.requestSimilar(coldplayMbid).asyncResult { 27 | transform(similarArtists.artists) 28 | }.orElse { RecommendationNotFound } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/Util.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data 4 | 5 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 6 | import com.functionalkotlin.bandhookkotlin.functional.Future 7 | import com.functionalkotlin.bandhookkotlin.functional.Result 8 | import com.functionalkotlin.bandhookkotlin.functional.asError 9 | import com.functionalkotlin.bandhookkotlin.functional.async 10 | import com.functionalkotlin.bandhookkotlin.functional.bind 11 | import com.functionalkotlin.bandhookkotlin.functional.pure 12 | import com.functionalkotlin.bandhookkotlin.functional.result 13 | import retrofit2.Call 14 | 15 | inline fun Call.unwrapCall(f: T.() -> U) = execute().body()?.f() 16 | 17 | fun Call.asyncResult(f: A.() -> B?): AsyncResult = 18 | Future.async { Result.pure(execute().body()?.f()) } 19 | 20 | fun AsyncResult.orElse(f: () -> E): AsyncResult { 21 | return bind { 22 | it?.result() ?: f().asError() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/LastFmRequestInterceptor.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm 4 | 5 | import okhttp3.Interceptor 6 | import okhttp3.Response 7 | 8 | class LastFmRequestInterceptor(val apiKey: String, val cacheDuration: Int) : Interceptor { 9 | override fun intercept(chain: Interceptor.Chain): Response { 10 | val request = chain.request() 11 | 12 | val url = request.url().newBuilder() 13 | .addQueryParameter("api_key", apiKey) 14 | .addQueryParameter("format", "json") 15 | .build() 16 | 17 | val newRequest = request.newBuilder() 18 | .url(url) 19 | .addHeader("Cache-Control", "public, max-age=$cacheDuration") 20 | .build() 21 | 22 | return chain.proceed(newRequest) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/LastFmService.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmResponse 6 | import retrofit2.Call 7 | import retrofit2.http.GET 8 | import retrofit2.http.Query 9 | 10 | interface LastFmService { 11 | 12 | @GET("/2.0/?method=artist.getinfo") 13 | fun requestArtistInfo(@Query("mbid") id: String, @Query("lang") language: String): 14 | Call 15 | 16 | @GET("/2.0/?method=artist.gettopalbums") 17 | fun requestAlbums(@Query("mbid") id: String, @Query("artist") artist: String): 18 | Call 19 | 20 | @GET("/2.0/?method=artist.getsimilar") 21 | fun requestSimilar(@Query("mbid") id: String): Call 22 | 23 | @GET("/2.0/?method=album.getInfo") 24 | fun requestAlbum(@Query("mbid") id: String): Call 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmAlbum.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmAlbum( 8 | val name: String, 9 | val mbid: String?, 10 | val url: String, 11 | val artist: LastFmArtist, 12 | @SerializedName("image") 13 | val images: List, 14 | val tracks: LastFmTracklist?) 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmAlbumDetail.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmAlbumDetail( 8 | val name: String, 9 | val mbid: String?, 10 | val url: String, 11 | val artist: String, 12 | @SerializedName("releasedate") 13 | val releaseDate: String, 14 | @SerializedName("image") 15 | val images: List, 16 | val tracks: LastFmTracklist) 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmArtist.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmArtist( 8 | val name: String, 9 | val mbid: String?, 10 | val url: String, 11 | @SerializedName("image") 12 | val images: List? = null, 13 | val similar: LastFmSimilar? = null, 14 | val bio: LastFmBio? = null) 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmArtistList.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmArtistList(@SerializedName("artist") val artists: List) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmArtistMatches.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmArtistMatches(@SerializedName("artist") val artists: List) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmBio.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | class LastFmBio(val content: String) 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmImage.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmImage(@SerializedName("#text") val url: String, val size: String) 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmImageType.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | enum class LastFmImageType(val type: String) { 6 | SMALL("small"), 7 | MEDIUM("medium"), 8 | LARGE("large"), 9 | EXTRALARGE("extralarge"), 10 | MEGA("mega") 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmResponse.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmResponse( 8 | val results: LastFmResult, 9 | val artist: LastFmArtist, 10 | @SerializedName("topalbums") 11 | val topAlbums: LastFmTopAlbums, 12 | @SerializedName("similarartists") 13 | val similarArtists: LastFmArtistList, 14 | val album: LastFmAlbumDetail) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmResult.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmResult(@SerializedName("artistmatches") val artistMatches: LastFmArtistMatches) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmSimilar.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmSimilar(@SerializedName("artist") var artists: List) 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmTopAlbums.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmTopAlbums(@SerializedName("album") val albums: List) 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmTrack.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | class LastFmTrack( 6 | val name: String, 7 | val duration: Int = 0, 8 | val mbid: String?, 9 | val url: String?, 10 | val artist: LastFmArtist) 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/lastfm/model/LastFmTracklist.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.lastfm.model 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class LastFmTracklist(@SerializedName("track") val tracks: List) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/mapper/album/AlbumMapper.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.mapper.album 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmAlbum 6 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmAlbumDetail 7 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtist 8 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImage 9 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmTrack 10 | import com.functionalkotlin.bandhookkotlin.data.mapper.artist.transform 11 | import com.functionalkotlin.bandhookkotlin.data.mapper.image.getMainImageUrl 12 | import com.functionalkotlin.bandhookkotlin.data.mapper.track.transform 13 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 14 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 15 | import com.functionalkotlin.bandhookkotlin.domain.entity.Track 16 | 17 | fun transform(albums: List): List = 18 | albums 19 | .filter { it.hasQualityInfo() } 20 | .mapNotNull { transform(it) } 21 | 22 | fun transform( 23 | album: LastFmAlbumDetail, imageMapper: ((List?) -> String?) = ::getMainImageUrl, 24 | trackMapper: ((List?) -> List) = { transform(it) }) = album.mbid?.let { 25 | Album( 26 | it, album.name, Artist("", album.artist), imageMapper(album.images), 27 | trackMapper(album.tracks.tracks)) 28 | } 29 | 30 | fun transform( 31 | album: LastFmAlbum, artistMapper: ((LastFmArtist) -> Artist?) = { transform(it) }, 32 | imageMapper: ((List?) -> String?) = ::getMainImageUrl, 33 | trackMapper: ((List?) -> List) = { transform(it) }) = 34 | album.mbid?.let { 35 | Album( 36 | it, album.name, artistMapper(album.artist), imageMapper(album.images), 37 | trackMapper(album.tracks?.tracks)) 38 | } 39 | 40 | private fun LastFmAlbum.hasQualityInfo(): Boolean = !hasEmptyMbid() && images.isNotEmpty() 41 | 42 | private fun LastFmAlbum.hasEmptyMbid(): Boolean = mbid.isNullOrEmpty() 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/mapper/artist/ArtistMapper.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.mapper.artist 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtist 6 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImage 7 | import com.functionalkotlin.bandhookkotlin.data.mapper.image.getMainImageUrl 8 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 9 | 10 | fun transform(artists: List): List = 11 | artists 12 | .filter { it.hasQualityInfo() } 13 | .mapNotNull { transform(it) } 14 | 15 | fun transform( 16 | artist: LastFmArtist, imageMapper: ((List?) -> String?) = ::getMainImageUrl) = 17 | artist.mbid 18 | ?.let { Artist(it, artist.name, imageMapper(artist.images), artist.bio?.content) } 19 | 20 | private fun LastFmArtist.hasQualityInfo(): Boolean = 21 | !hasEmptyMbid() && images != null && images.isNotEmpty() 22 | 23 | private fun LastFmArtist.hasEmptyMbid(): Boolean = mbid.isNullOrEmpty() 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/mapper/image/ImageMapper.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.mapper.image 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImage 6 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImageType 7 | 8 | fun getMainImageUrl(images: List?): String? = 9 | images 10 | .takeIf { it != null && !it.isEmpty() } 11 | ?.let { list -> 12 | list.firstOrNull { it.size == LastFmImageType.MEGA.type }?.url 13 | ?: list.last().url 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/data/mapper/track/TrackMapper.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.mapper.track 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmTrack 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.Track 7 | 8 | fun transform(tracks: List?): List = tracks?.map(::transform) ?: emptyList() 9 | 10 | fun transform(track: LastFmTrack) = Track(track.name, track.duration) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di 4 | 5 | import android.content.Context 6 | import android.support.v7.app.AppCompatActivity 7 | import com.functionalkotlin.bandhookkotlin.di.scope.ActivityScope 8 | import dagger.Module 9 | import dagger.Provides 10 | 11 | @Module 12 | abstract class ActivityModule(protected val activity: AppCompatActivity) { 13 | 14 | @Provides 15 | @ActivityScope 16 | fun provideActivity(): AppCompatActivity = activity 17 | 18 | @Provides 19 | @ActivityScope 20 | fun provideActivityContext(): Context = activity.baseContext 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di 4 | 5 | import com.functionalkotlin.bandhookkotlin.di.subcomponent.album.AlbumActivityComponent 6 | import com.functionalkotlin.bandhookkotlin.di.subcomponent.album.AlbumActivityModule 7 | import com.functionalkotlin.bandhookkotlin.di.subcomponent.detail.ArtistActivityComponent 8 | import com.functionalkotlin.bandhookkotlin.di.subcomponent.detail.ArtistActivityModule 9 | import com.functionalkotlin.bandhookkotlin.di.subcomponent.main.MainActivityComponent 10 | import com.functionalkotlin.bandhookkotlin.di.subcomponent.main.MainActivityModule 11 | import dagger.Component 12 | import javax.inject.Singleton 13 | 14 | @Singleton 15 | @Component(modules = [ 16 | (ApplicationModule::class), (DataModule::class), (RepositoryModule::class), 17 | (DomainModule::class) 18 | ]) 19 | interface ApplicationComponent { 20 | 21 | fun plus(module: MainActivityModule): MainActivityComponent 22 | fun plus(module: ArtistActivityModule): ArtistActivityComponent 23 | fun plus(module: AlbumActivityModule): AlbumActivityComponent 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di 4 | 5 | import android.content.Context 6 | import com.functionalkotlin.bandhookkotlin.App 7 | import com.functionalkotlin.bandhookkotlin.di.qualifier.ApplicationQualifier 8 | import com.functionalkotlin.bandhookkotlin.di.qualifier.LanguageSelection 9 | import com.squareup.picasso.Picasso 10 | import dagger.Module 11 | import dagger.Provides 12 | import java.util.Locale 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | class ApplicationModule(private val app: App) { 17 | 18 | @Provides 19 | @Singleton 20 | fun provideApplication(): App = app 21 | 22 | @Provides 23 | @Singleton 24 | @ApplicationQualifier 25 | fun provideApplicationContext(): Context = app 26 | 27 | @Provides 28 | @Singleton 29 | fun providePicasso(@ApplicationQualifier context: Context): Picasso = 30 | Picasso.Builder(context).build() 31 | 32 | @Provides 33 | @Singleton 34 | @LanguageSelection 35 | fun provideLanguageSelection(): String = Locale.getDefault().language 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/DataModule.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di 4 | 5 | import android.content.Context 6 | import com.functionalkotlin.bandhookkotlin.BuildConfig 7 | import com.functionalkotlin.bandhookkotlin.R 8 | import com.functionalkotlin.bandhookkotlin.data.lastfm.LastFmRequestInterceptor 9 | import com.functionalkotlin.bandhookkotlin.data.lastfm.LastFmService 10 | import com.functionalkotlin.bandhookkotlin.di.qualifier.ApiKey 11 | import com.functionalkotlin.bandhookkotlin.di.qualifier.ApplicationQualifier 12 | import com.functionalkotlin.bandhookkotlin.di.qualifier.CacheDuration 13 | import dagger.Module 14 | import dagger.Provides 15 | import okhttp3.Cache 16 | import okhttp3.OkHttpClient 17 | import okhttp3.logging.HttpLoggingInterceptor 18 | import okhttp3.logging.HttpLoggingInterceptor.Level 19 | import retrofit2.Retrofit 20 | import retrofit2.converter.gson.GsonConverterFactory 21 | import javax.inject.Singleton 22 | 23 | @Module 24 | class DataModule { 25 | 26 | @Provides 27 | @Singleton 28 | fun provideCache(@ApplicationQualifier context: Context) = 29 | Cache(context.cacheDir, 10 * 1024 * 1024.toLong()) 30 | 31 | @Provides 32 | @Singleton 33 | @ApiKey 34 | fun provideApiKey(@ApplicationQualifier context: Context): String = 35 | context.getString(R.string.last_fm_api_key) 36 | 37 | @Provides 38 | @Singleton 39 | @CacheDuration 40 | fun provideCacheDuration(@ApplicationQualifier context: Context) = 41 | context.resources.getInteger(R.integer.cache_duration) 42 | 43 | @Provides 44 | @Singleton 45 | fun provideOkHttpClient(cache: Cache, interceptor: LastFmRequestInterceptor): OkHttpClient = 46 | OkHttpClient().newBuilder() 47 | .cache(cache) 48 | .addInterceptor(interceptor) 49 | .addInterceptor(HttpLoggingInterceptor().apply { 50 | level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE 51 | }) 52 | .build() 53 | 54 | @Provides 55 | @Singleton 56 | fun provideRequestInterceptor(@ApiKey apiKey: String, @CacheDuration cacheDuration: Int) = 57 | LastFmRequestInterceptor(apiKey, cacheDuration) 58 | 59 | @Provides 60 | @Singleton 61 | fun provideRestAdapter(client: OkHttpClient): Retrofit = Retrofit.Builder() 62 | .baseUrl("http://ws.audioscrobbler.com") 63 | .client(client) 64 | .addConverterFactory(GsonConverterFactory.create()) 65 | .build() 66 | 67 | @Provides 68 | @Singleton 69 | fun providesLastFmService(retrofit: Retrofit): LastFmService = 70 | retrofit.create(LastFmService::class.java) 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/DomainModule.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetAlbumDetailInteractor 6 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetArtistDetailInteractor 7 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetRecommendedArtistsInteractor 8 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetTopAlbumsInteractor 9 | import com.functionalkotlin.bandhookkotlin.domain.repository.AlbumRepository 10 | import com.functionalkotlin.bandhookkotlin.domain.repository.ArtistRepository 11 | import dagger.Module 12 | import dagger.Provides 13 | 14 | @Module 15 | class DomainModule { 16 | 17 | @Provides 18 | fun provideRecommendedArtistsInteractor(artistRepository: ArtistRepository) = 19 | GetRecommendedArtistsInteractor(artistRepository) 20 | 21 | @Provides 22 | fun provideArtistDetailInteractor(artistRepository: ArtistRepository) = 23 | GetArtistDetailInteractor(artistRepository) 24 | 25 | @Provides 26 | fun provideTopAlbumsInteractor(albumRepository: AlbumRepository) = 27 | GetTopAlbumsInteractor(albumRepository) 28 | 29 | @Provides 30 | fun provideAlbumsDetailInteractor(albumRepository: AlbumRepository) = 31 | GetAlbumDetailInteractor(albumRepository) 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.CloudAlbumDataSet 6 | import com.functionalkotlin.bandhookkotlin.data.CloudArtistDataSet 7 | import com.functionalkotlin.bandhookkotlin.data.lastfm.LastFmService 8 | import com.functionalkotlin.bandhookkotlin.di.qualifier.LanguageSelection 9 | import com.functionalkotlin.bandhookkotlin.domain.repository.AlbumRepository 10 | import com.functionalkotlin.bandhookkotlin.domain.repository.ArtistRepository 11 | import com.functionalkotlin.bandhookkotlin.repository.AlbumRepositoryImpl 12 | import com.functionalkotlin.bandhookkotlin.repository.ArtistRepositoryImpl 13 | import dagger.Module 14 | import dagger.Provides 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | class RepositoryModule { 19 | 20 | @Provides 21 | @Singleton 22 | fun provideArtistRepo(@LanguageSelection language: String, lastFmService: LastFmService): 23 | ArtistRepository = ArtistRepositoryImpl(listOf(CloudArtistDataSet(language, lastFmService))) 24 | 25 | @Provides 26 | @Singleton 27 | fun provideAlbumRepo(lastFmService: LastFmService): AlbumRepository = 28 | AlbumRepositoryImpl(listOf(CloudAlbumDataSet(lastFmService))) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/qualifier/ApiKey.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.qualifier 4 | 5 | import javax.inject.Qualifier 6 | 7 | @Qualifier 8 | annotation class ApiKey 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/qualifier/ApplicationQualifier.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.qualifier 4 | 5 | import javax.inject.Qualifier 6 | 7 | @Qualifier 8 | annotation class ApplicationQualifier 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/qualifier/CacheDuration.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.qualifier 4 | 5 | import javax.inject.Qualifier 6 | 7 | @Qualifier 8 | annotation class CacheDuration 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/qualifier/LanguageSelection.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.qualifier 4 | 5 | import javax.inject.Qualifier 6 | 7 | @Qualifier 8 | annotation class LanguageSelection 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/scope/ActivityScope.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.scope 4 | 5 | import javax.inject.Scope 6 | 7 | @Scope 8 | annotation class ActivityScope 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/subcomponent/album/AlbumActivityComponent.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.subcomponent.album 4 | 5 | import com.functionalkotlin.bandhookkotlin.di.scope.ActivityScope 6 | import com.functionalkotlin.bandhookkotlin.ui.screens.album.AlbumActivity 7 | import dagger.Subcomponent 8 | 9 | @ActivityScope 10 | @Subcomponent(modules = [(AlbumActivityModule::class)]) 11 | interface AlbumActivityComponent { 12 | fun injectTo(activity: AlbumActivity) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/subcomponent/album/AlbumActivityModule.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.subcomponent.album 4 | 5 | import android.content.Context 6 | import android.support.v7.widget.LinearLayoutManager 7 | import com.functionalkotlin.bandhookkotlin.di.ActivityModule 8 | import com.functionalkotlin.bandhookkotlin.di.scope.ActivityScope 9 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetAlbumDetailInteractor 10 | import com.functionalkotlin.bandhookkotlin.ui.adapter.TracksAdapter 11 | import com.functionalkotlin.bandhookkotlin.ui.presenter.AlbumPresenter 12 | import com.functionalkotlin.bandhookkotlin.ui.screens.album.AlbumActivity 13 | import com.functionalkotlin.bandhookkotlin.ui.view.AlbumView 14 | import dagger.Module 15 | import dagger.Provides 16 | 17 | @Module 18 | class AlbumActivityModule(activity: AlbumActivity) : ActivityModule(activity) { 19 | 20 | @Provides 21 | @ActivityScope 22 | fun provideAlbumView(): AlbumView = activity as AlbumView 23 | 24 | @Provides 25 | @ActivityScope 26 | fun provideLinearLayoutManager(context: Context) = LinearLayoutManager(context) 27 | 28 | @Provides 29 | @ActivityScope 30 | fun provideTracksAdapter() = TracksAdapter() 31 | 32 | @Provides 33 | @ActivityScope 34 | fun provideAlbumPresenter(view: AlbumView, albumInteractor: GetAlbumDetailInteractor) = 35 | AlbumPresenter(view, albumInteractor) 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/subcomponent/detail/ArtistActivityComponent.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.subcomponent.detail 4 | 5 | import com.functionalkotlin.bandhookkotlin.di.scope.ActivityScope 6 | import com.functionalkotlin.bandhookkotlin.ui.screens.detail.ArtistActivity 7 | import dagger.Subcomponent 8 | 9 | @ActivityScope 10 | @Subcomponent(modules = [(ArtistActivityModule::class)]) 11 | interface ArtistActivityComponent { 12 | fun injectTo(activity: ArtistActivity) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/subcomponent/detail/ArtistActivityModule.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.subcomponent.detail 4 | 5 | import com.functionalkotlin.bandhookkotlin.di.ActivityModule 6 | import com.functionalkotlin.bandhookkotlin.di.scope.ActivityScope 7 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetArtistDetailInteractor 8 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetTopAlbumsInteractor 9 | import com.functionalkotlin.bandhookkotlin.ui.presenter.ArtistPresenter 10 | import com.functionalkotlin.bandhookkotlin.ui.screens.detail.AlbumsFragment 11 | import com.functionalkotlin.bandhookkotlin.ui.screens.detail.ArtistActivity 12 | import com.functionalkotlin.bandhookkotlin.ui.screens.detail.BiographyFragment 13 | import com.functionalkotlin.bandhookkotlin.ui.view.ArtistView 14 | import dagger.Module 15 | import dagger.Provides 16 | 17 | @Module 18 | class ArtistActivityModule(activity: ArtistActivity) : ActivityModule(activity) { 19 | 20 | @Provides 21 | @ActivityScope 22 | fun provideArtistView(): ArtistView = activity as ArtistView 23 | 24 | @Provides 25 | @ActivityScope 26 | fun provideActivityPresenter( 27 | view: ArtistView, artistDetailInteractor: GetArtistDetailInteractor, 28 | topAlbumsInteractor: GetTopAlbumsInteractor) = 29 | ArtistPresenter(view, artistDetailInteractor, topAlbumsInteractor) 30 | 31 | @Provides 32 | @ActivityScope 33 | fun provideAlbumsFragment() = AlbumsFragment() 34 | 35 | @Provides 36 | @ActivityScope 37 | fun provideBiographyFragment() = BiographyFragment() 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/subcomponent/main/MainActivityComponent.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.subcomponent.main 4 | 5 | import com.functionalkotlin.bandhookkotlin.di.scope.ActivityScope 6 | import com.functionalkotlin.bandhookkotlin.ui.screens.main.MainActivity 7 | import dagger.Subcomponent 8 | 9 | @ActivityScope 10 | @Subcomponent(modules = [(MainActivityModule::class)]) 11 | interface MainActivityComponent { 12 | fun injectTo(activity: MainActivity) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/di/subcomponent/main/MainActivityModule.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.di.subcomponent.main 4 | 5 | import com.functionalkotlin.bandhookkotlin.di.ActivityModule 6 | import com.functionalkotlin.bandhookkotlin.di.scope.ActivityScope 7 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetRecommendedArtistsInteractor 8 | import com.functionalkotlin.bandhookkotlin.ui.presenter.MainPresenter 9 | import com.functionalkotlin.bandhookkotlin.ui.screens.main.MainActivity 10 | import com.functionalkotlin.bandhookkotlin.ui.view.MainView 11 | import dagger.Module 12 | import dagger.Provides 13 | 14 | @Module 15 | class MainActivityModule(activity: MainActivity) : ActivityModule(activity) { 16 | 17 | @Provides 18 | @ActivityScope 19 | fun provideMainView(): MainView = activity as MainView 20 | 21 | @Provides 22 | @ActivityScope 23 | fun provideMainPresenter( 24 | view: MainView, recommendedArtistsInteractor: GetRecommendedArtistsInteractor) = 25 | MainPresenter(view, recommendedArtistsInteractor) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/entity/Album.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.entity 4 | 5 | data class Album( 6 | val id: String, val name: String, val artist: Artist? = null, val url: String? = null, 7 | val tracks: List = emptyList()) 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/entity/Artist.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.entity 4 | 5 | data class Artist( 6 | val id: String, val name: String, val url: String? = null, val bio: String? = null, 7 | val albums: List? = null) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/entity/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package com.functionalkotlin.bandhookkotlin.domain.entity 2 | 3 | sealed class APIError 4 | 5 | data class AlbumNotFound(val id: String) : APIError() 6 | 7 | data class ArtistNotFound(val id: String) : APIError() 8 | 9 | data class TopAlbumsNotFound(val id: String) : APIError() 10 | 11 | object RecommendationNotFound: APIError() 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/entity/Track.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.entity 4 | 5 | data class Track(val name: String, val duration: Int) 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/interactor/GetAlbumDetailInteractor.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.interactor 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.AlbumNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.repository.AlbumRepository 8 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 9 | 10 | class GetAlbumDetailInteractor(private val albumRepository: AlbumRepository) { 11 | 12 | fun getAlbum(albumId: String): AsyncResult = 13 | albumRepository.getAlbum(albumId) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/interactor/GetArtistDetailInteractor.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.interactor 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.ArtistNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.repository.ArtistRepository 8 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 9 | 10 | class GetArtistDetailInteractor(private val artistRepository: ArtistRepository) { 11 | 12 | fun getArtist(artistId: String): AsyncResult = 13 | artistRepository.getArtist(artistId) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/interactor/GetRecommendedArtistsInteractor.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.interactor 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.RecommendationNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.repository.ArtistRepository 8 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 9 | 10 | class GetRecommendedArtistsInteractor(private val artistRepository: ArtistRepository) { 11 | 12 | fun getRecommendedArtists(): AsyncResult, RecommendationNotFound> = 13 | artistRepository.getRecommendedArtists() 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/interactor/GetTopAlbumsInteractor.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.interactor 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.repository.AlbumRepository 8 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 9 | 10 | class GetTopAlbumsInteractor(private val albumRepository: AlbumRepository) { 11 | 12 | fun getTopAlbums(artistId: String): AsyncResult, TopAlbumsNotFound> = 13 | albumRepository.getTopAlbums(artistId) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/repository/AlbumRepository.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.repository 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.AlbumNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 8 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 9 | 10 | interface AlbumRepository { 11 | fun getTopAlbums(artistId: String): AsyncResult, TopAlbumsNotFound> 12 | fun getAlbum(id: String): AsyncResult 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/domain/repository/ArtistRepository.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.repository 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.ArtistNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.RecommendationNotFound 8 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 9 | 10 | interface ArtistRepository { 11 | fun getArtist(id: String): AsyncResult 12 | fun getRecommendedArtists(): AsyncResult, RecommendationNotFound> 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/functional/AsyncResult.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.functional 4 | 5 | typealias AsyncResult = Future> 6 | 7 | fun A.result(): AsyncResult = this 8 | .let(Result.Companion::pure) 9 | .let(Future.Companion::pure) 10 | 11 | fun E.asError(): AsyncResult = Future.pure(Failure(this)) 12 | 13 | fun AsyncResult.transform(f: (A) -> B): AsyncResult = this.map { 14 | result: Result -> result.map(f) 15 | } 16 | 17 | infix fun AsyncResult.bind( 18 | transform: (A) -> AsyncResult): AsyncResult = 19 | this.flatMap { 20 | when (it) { 21 | is Success -> transform(it.value) 22 | is Failure -> Future.pure(it) 23 | } 24 | } 25 | 26 | fun firstSuccessIn( 27 | list: List, acc: AsyncResult, f: (B) -> AsyncResult): AsyncResult = 28 | when { 29 | list.isEmpty() -> acc 30 | list.size == 1 -> list.first().let(f) 31 | else -> list.first().let(f).recoverWith { 32 | firstSuccessIn(list.tail(), acc, f) 33 | } 34 | } 35 | 36 | fun AsyncResult.recoverWith(f: () -> (AsyncResult)): AsyncResult = 37 | flatMap { 38 | when { 39 | it.isFailure() -> f() 40 | else -> this 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/functional/Future.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2017. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.functional 4 | 5 | import kotlinx.coroutines.experimental.CommonPool 6 | import kotlinx.coroutines.experimental.Deferred 7 | import kotlinx.coroutines.experimental.async 8 | import kotlinx.coroutines.experimental.runBlocking 9 | 10 | typealias FutureTask = Deferred 11 | 12 | class Future(val task: FutureTask) { 13 | companion object 14 | } 15 | 16 | fun Future.Companion.pure(a: A): Future = 17 | Future(async(CommonPool) { a }) 18 | 19 | fun Future.Companion.async(f: () -> A): Future = 20 | Future(async(CommonPool) { f() }) 21 | 22 | suspend fun Future.runAsync(onComplete: (A) -> Unit) { 23 | onComplete(task.await()) 24 | } 25 | 26 | fun Future.runSync(): A = 27 | runBlocking { this@runSync.task.await() } 28 | 29 | fun Future.map(transform: (A) -> B): Future = 30 | flatMap { 31 | Future(async(CommonPool) { 32 | transform(it) 33 | }) 34 | } 35 | 36 | fun Future.flatMap(transform: (A) -> Future): Future = 37 | Future(async(CommonPool) { 38 | transform(this@flatMap.task.await()).task.await() 39 | }) 40 | 41 | fun Future.apply(futureAB: Future<(A) -> B>): Future = 42 | Future(async(CommonPool) { 43 | val a = this@apply.task.await() 44 | val ab = futureAB.task.await() 45 | 46 | ab(a) 47 | }) 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/functional/List.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.functional 4 | 5 | fun List.tail(): List = this.drop(1) 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/functional/Option.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.functional 4 | 5 | /** 6 | * @author Alejandro Hernández 7 | */ 8 | sealed class Option 9 | 10 | object None : Option() 11 | 12 | data class Just(val value: A) : Option() 13 | 14 | fun Option.map(transform: (A) -> B): Option = 15 | flatMap { Just(transform(it)) } 16 | 17 | fun Option.flatMap(transform: (A) -> Option): Option = when (this) { 18 | None -> None 19 | is Just -> transform(this.value) 20 | } 21 | 22 | fun Option.ifPresent(execute: (A) -> Unit) { 23 | if (this is Just) execute(this.value) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/functional/Result.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2017. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.functional 4 | 5 | sealed class Result { 6 | companion object 7 | } 8 | 9 | data class Success(val value: A) : Result() 10 | 11 | data class Failure(val error: E) : Result() 12 | 13 | fun Result.Companion.pure(a: A): Result = Success(a) 14 | 15 | fun Result.map(transform: (A) -> B): Result = 16 | flatMap { transform(it).let { Success(it) } } 17 | 18 | fun Result.flatMap( 19 | transform: (A) -> Result): Result = when (this) { 20 | is Success -> transform(value) 21 | is Failure -> this 22 | } 23 | 24 | fun Result.apply( 25 | resultAB: Result<(A) -> B, E>): Result = 26 | flatMap { a -> resultAB.map { it(a) } } 27 | 28 | fun Result.ifSuccess(execute: (A) -> Unit) { 29 | if (this is Success) execute(this.value) 30 | } 31 | 32 | fun Result.ifFailure(execute: (E) -> Unit) { 33 | if (this is Failure) execute(this.error) 34 | } 35 | 36 | fun Result.fold(onSuccess: (A) -> Unit, onError: (E) -> Unit) { 37 | ifSuccess(onSuccess) 38 | ifFailure(onError) 39 | } 40 | 41 | fun Result.isFailure(): Boolean = this is Failure 42 | 43 | fun Result.isSuccess(): Boolean = this is Success 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/repository/AlbumRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.repository 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.AlbumNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 8 | import com.functionalkotlin.bandhookkotlin.domain.repository.AlbumRepository 9 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 10 | import com.functionalkotlin.bandhookkotlin.functional.asError 11 | import com.functionalkotlin.bandhookkotlin.functional.firstSuccessIn 12 | import com.functionalkotlin.bandhookkotlin.repository.dataset.AlbumDataSet 13 | 14 | class AlbumRepositoryImpl(private val albumDataSets: List) : AlbumRepository { 15 | 16 | override fun getAlbum(id: String): AsyncResult = 17 | firstSuccessIn( 18 | list = albumDataSets, 19 | f = { it.requestAlbum(id) }, 20 | acc = AlbumNotFound(id).asError()) 21 | 22 | override fun getTopAlbums(artistId: String): AsyncResult, TopAlbumsNotFound> = 23 | firstSuccessIn( 24 | list = albumDataSets, 25 | f = { it.requestTopAlbums(artistId) }, 26 | acc = TopAlbumsNotFound(artistId).asError()) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/repository/ArtistRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.repository 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.ArtistNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.RecommendationNotFound 8 | import com.functionalkotlin.bandhookkotlin.domain.repository.ArtistRepository 9 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 10 | import com.functionalkotlin.bandhookkotlin.functional.asError 11 | import com.functionalkotlin.bandhookkotlin.functional.firstSuccessIn 12 | import com.functionalkotlin.bandhookkotlin.repository.dataset.ArtistDataSet 13 | 14 | class ArtistRepositoryImpl(private val artistDataSets: List) : ArtistRepository { 15 | 16 | override fun getRecommendedArtists(): AsyncResult, RecommendationNotFound> = 17 | firstSuccessIn( 18 | list = artistDataSets, 19 | f = ArtistDataSet::requestRecommendedArtists, 20 | acc = RecommendationNotFound.asError()) 21 | 22 | override fun getArtist(id: String): AsyncResult = 23 | firstSuccessIn( 24 | list = artistDataSets, 25 | f = { it.requestArtist(id) }, 26 | acc = ArtistNotFound(id).asError()) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/repository/dataset/AlbumDataSet.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.repository.dataset 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.AlbumNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 8 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 9 | 10 | interface AlbumDataSet { 11 | fun requestTopAlbums(artistId: String): AsyncResult, TopAlbumsNotFound> 12 | fun requestAlbum(mbid: String): AsyncResult 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/repository/dataset/ArtistDataSet.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.repository.dataset 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.ArtistNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.RecommendationNotFound 8 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 9 | 10 | interface ArtistDataSet { 11 | fun requestArtist(id: String): AsyncResult 12 | fun requestRecommendedArtists(): AsyncResult, RecommendationNotFound> 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/activity/ActivityAnkoComponent.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.activity 4 | 5 | import android.support.v7.app.AppCompatActivity 6 | import android.support.v7.widget.Toolbar 7 | import org.jetbrains.anko.AnkoComponent 8 | 9 | @Suppress("AddVarianceModifier") 10 | interface ActivityAnkoComponent : AnkoComponent { 11 | val toolbar: Toolbar 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/activity/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.activity 4 | 5 | import android.os.Bundle 6 | import android.support.v7.app.AppCompatActivity 7 | import com.functionalkotlin.bandhookkotlin.App 8 | import com.functionalkotlin.bandhookkotlin.di.ApplicationComponent 9 | import org.jetbrains.anko.setContentView 10 | 11 | abstract class BaseActivity> : 12 | AppCompatActivity() { 13 | 14 | companion object { 15 | const val IMAGE_TRANSITION_NAME = "activity_image_transition" 16 | } 17 | 18 | abstract val ui: UI 19 | 20 | @Suppress("UNCHECKED_CAST") 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | injectDependencies(App.graph) 24 | (ui as ActivityAnkoComponent).setContentView(this) 25 | setSupportActionBar(ui.toolbar) 26 | } 27 | 28 | abstract fun injectDependencies(applicationComponent: ApplicationComponent) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/activity/ViewGroupComponent.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.activity 4 | 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import org.jetbrains.anko.AnkoComponent 8 | import org.jetbrains.anko.AnkoContext 9 | 10 | interface ViewAnkoComponent : AnkoComponent { 11 | 12 | val view: T 13 | 14 | fun inflate(): View { 15 | val ctx = AnkoContext.Companion.create(view.context, view) 16 | return createView(ctx) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/adapter/ArtistDetailPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.adapter 4 | 5 | import android.support.v4.app.Fragment 6 | import android.support.v4.app.FragmentManager 7 | import android.support.v4.app.FragmentPagerAdapter 8 | import java.util.LinkedHashMap 9 | 10 | class ArtistDetailPagerAdapter(fragmentManager: FragmentManager) : 11 | FragmentPagerAdapter(fragmentManager) { 12 | private val fragments = LinkedHashMap() 13 | 14 | override fun getItem(position: Int): Fragment? = fragments.keys.elementAt(position) 15 | 16 | override fun getCount(): Int = fragments.keys.size 17 | 18 | fun addFragment(fragment: Fragment, title: String) { 19 | fragments[fragment] = title 20 | } 21 | 22 | override fun getPageTitle(position: Int): CharSequence = fragments.values.elementAt(position) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/adapter/BaseAdapter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.adapter 4 | 5 | import android.support.v7.widget.RecyclerView 6 | import android.support.v7.widget.RecyclerView.Adapter 7 | import android.view.ViewGroup 8 | import com.functionalkotlin.bandhookkotlin.ui.activity.ViewAnkoComponent 9 | import com.functionalkotlin.bandhookkotlin.ui.adapter.BaseAdapter.BaseViewHolder 10 | import com.functionalkotlin.bandhookkotlin.ui.util.singleClick 11 | import kotlin.properties.Delegates 12 | 13 | abstract class BaseAdapter>( 14 | private val listener: (Item) -> Unit = {}) : Adapter>() { 15 | 16 | abstract val bind: Component.(item: Item) -> Unit 17 | 18 | var items: List by Delegates.observable(emptyList()) { _, _, _ -> 19 | notifyDataSetChanged() 20 | } 21 | 22 | abstract fun onCreateComponent(parent: RecyclerView): Component 23 | 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder = 25 | BaseViewHolder(onCreateComponent(parent as RecyclerView)) 26 | 27 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { 28 | val item = items[position] 29 | holder.itemView.singleClick { listener(item) } 30 | holder.ui.bind(item) 31 | } 32 | 33 | override fun getItemCount() = items.size 34 | 35 | class BaseViewHolder>(val ui: Component) : 36 | RecyclerView.ViewHolder(ui.inflate()) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/adapter/ImageTitleAdapter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.adapter 4 | 5 | import android.support.v7.widget.RecyclerView 6 | import android.text.TextUtils 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import com.functionalkotlin.bandhookkotlin.R 10 | import com.functionalkotlin.bandhookkotlin.ui.activity.ViewAnkoComponent 11 | import com.functionalkotlin.bandhookkotlin.ui.custom.squareImageView 12 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 13 | import com.functionalkotlin.bandhookkotlin.ui.util.loadUrl 14 | import com.functionalkotlin.bandhookkotlin.ui.util.setTextAppearanceC 15 | import org.jetbrains.anko.AnkoContext 16 | import org.jetbrains.anko.backgroundResource 17 | import org.jetbrains.anko.dip 18 | import org.jetbrains.anko.frameLayout 19 | import org.jetbrains.anko.matchParent 20 | import org.jetbrains.anko.padding 21 | import org.jetbrains.anko.textView 22 | import org.jetbrains.anko.verticalLayout 23 | 24 | class ImageTitleAdapter(listener: (ImageTitle) -> Unit) : 25 | BaseAdapter(listener) { 26 | 27 | override val bind: Component.(item: ImageTitle) -> Unit = { item -> 28 | title.text = item.name 29 | item.url?.let { image.loadUrl(it) } 30 | } 31 | 32 | override fun onCreateComponent(parent: RecyclerView) = Component(parent) 33 | 34 | fun findPositionById(id: String): Int = items.withIndex().first { it.value.id == id }.index 35 | 36 | class Component(override val view: RecyclerView) : ViewAnkoComponent { 37 | 38 | lateinit var title: TextView 39 | lateinit var image: ImageView 40 | 41 | override fun createView(ui: AnkoContext) = with(ui) { 42 | frameLayout { 43 | 44 | verticalLayout { 45 | 46 | image = squareImageView { 47 | scaleType = ImageView.ScaleType.CENTER_CROP 48 | backgroundResource = R.color.cardview_dark_background 49 | } 50 | title = textView { 51 | padding = dip(16) 52 | backgroundResource = R.color.cardview_dark_background 53 | setTextAppearanceC(R.style.TextAppearance_AppCompat_Subhead_Inverse) 54 | maxLines = 1 55 | ellipsize = TextUtils.TruncateAt.END 56 | }.lparams(width = matchParent) 57 | 58 | }.lparams(width = matchParent) 59 | 60 | } 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/adapter/SingleClickListener.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.adapter 4 | 5 | import android.view.View 6 | import android.view.ViewConfiguration 7 | 8 | /** 9 | * Prevents from double clicks on a view, which could otherwise lead to unpredictable states. Useful 10 | * while transitioning to another activity for instance. 11 | */ 12 | class SingleClickListener(private val click: (v: View) -> Unit) : View.OnClickListener { 13 | 14 | companion object { 15 | private val DOUBLE_CLICK_TIMEOUT = ViewConfiguration.getDoubleTapTimeout() 16 | } 17 | 18 | private var lastClick: Long = 0 19 | 20 | override fun onClick(v: View) { 21 | if (getLastClickTimeout() > DOUBLE_CLICK_TIMEOUT) { 22 | lastClick = System.currentTimeMillis() 23 | click(v) 24 | } 25 | } 26 | 27 | private fun getLastClickTimeout(): Long = System.currentTimeMillis() - lastClick 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/adapter/TracksAdapter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.adapter 4 | 5 | import android.support.v7.widget.RecyclerView 6 | import android.view.Gravity 7 | import android.widget.TextView 8 | import com.functionalkotlin.bandhookkotlin.ui.activity.ViewAnkoComponent 9 | import com.functionalkotlin.bandhookkotlin.ui.entity.TrackDetail 10 | import org.jetbrains.anko.AnkoContext 11 | import org.jetbrains.anko.applyRecursively 12 | import org.jetbrains.anko.dip 13 | import org.jetbrains.anko.horizontalPadding 14 | import org.jetbrains.anko.linearLayout 15 | import org.jetbrains.anko.matchParent 16 | import org.jetbrains.anko.padding 17 | import org.jetbrains.anko.textView 18 | 19 | class TracksAdapter : BaseAdapter() { 20 | 21 | private val timeStampPattern = "%d:%02d" 22 | private val timeSystemBaseNumber = 60 23 | 24 | override val bind: Component.(item: TrackDetail) -> Unit = { item -> 25 | number.text = item.number.toString() 26 | name.text = item.name 27 | length.text = secondsToTrackDurationString(item) 28 | } 29 | 30 | private fun secondsToTrackDurationString(item: TrackDetail): String { 31 | val fullMinutes = item.duration / timeSystemBaseNumber 32 | val restSeconds = item.duration % timeSystemBaseNumber 33 | return String.format(timeStampPattern, fullMinutes, restSeconds) 34 | } 35 | 36 | override fun onCreateComponent(parent: RecyclerView) = Component(parent) 37 | 38 | class Component(override val view: RecyclerView) : ViewAnkoComponent { 39 | 40 | lateinit var number: TextView 41 | lateinit var name: TextView 42 | lateinit var length: TextView 43 | 44 | override fun createView(ui: AnkoContext) = with(ui) { 45 | 46 | linearLayout { 47 | 48 | lparams(width = matchParent) 49 | 50 | padding = dip(16) 51 | weightSum = 1f 52 | 53 | number = textView { 54 | minEms = 2 55 | gravity = Gravity.END 56 | } 57 | 58 | name = textView { 59 | horizontalPadding = dip(16) 60 | }.lparams(width = 0) { 61 | weight = 1f 62 | } 63 | 64 | length = textView() 65 | 66 | }.applyRecursively { view -> 67 | when (view) { 68 | is TextView -> view.textSize = 16f 69 | } 70 | } 71 | 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/custom/AutofitRecyclerView.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.custom 4 | 5 | import android.content.Context 6 | import android.support.v7.widget.GridLayoutManager 7 | import android.support.v7.widget.RecyclerView 8 | import android.util.AttributeSet 9 | import android.view.ViewManager 10 | import org.jetbrains.anko.custom.ankoView 11 | import kotlin.properties.Delegates 12 | 13 | class AutofitRecyclerView : RecyclerView { 14 | 15 | private var manager: GridLayoutManager by Delegates.notNull() 16 | var columnWidth = -1 17 | 18 | constructor(context: Context) : super(context) { 19 | init(context) 20 | } 21 | 22 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { 23 | init(context, attrs) 24 | } 25 | 26 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( 27 | context, attrs, defStyleAttr) { 28 | 29 | init(context, attrs) 30 | } 31 | 32 | fun init(context: Context, attrs: AttributeSet? = null) { 33 | if (attrs != null) { 34 | val attrsArray = intArrayOf(android.R.attr.columnWidth) 35 | val ta = context.obtainStyledAttributes(attrs, attrsArray) 36 | columnWidth = ta.getDimensionPixelSize(0, -1) 37 | ta.recycle() 38 | } 39 | 40 | manager = GridLayoutManager(context, 1) 41 | layoutManager = manager 42 | } 43 | 44 | override fun onMeasure(widthSpec: Int, heightSpec: Int) { 45 | super.onMeasure(widthSpec, heightSpec) 46 | if (columnWidth > 0) { 47 | val spanCount = Math.max(1, measuredWidth / columnWidth) 48 | manager.spanCount = spanCount 49 | } 50 | } 51 | } 52 | 53 | fun ViewManager.autoFitRecycler(theme: Int = 0) = autoFitRecycler(theme) {} 54 | inline fun ViewManager.autoFitRecycler(theme: Int = 0, init: AutofitRecyclerView.() -> Unit) = 55 | ankoView(::AutofitRecyclerView, theme, init) 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/custom/PaddingItemDecoration.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.custom 4 | 5 | import android.graphics.Rect 6 | import android.support.v7.widget.RecyclerView 7 | import android.support.v7.widget.RecyclerView.ItemDecoration 8 | import android.support.v7.widget.RecyclerView.State 9 | import android.view.View 10 | 11 | class PaddingItemDecoration internal constructor(private val m_space: Int) : ItemDecoration() { 12 | 13 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State?) { 14 | outRect.apply { 15 | left = m_space 16 | right = m_space 17 | top = m_space 18 | bottom = m_space 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/custom/SquareImageView.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.custom 4 | 5 | import android.content.Context 6 | import android.util.AttributeSet 7 | import android.view.ViewManager 8 | import android.widget.ImageView 9 | import org.jetbrains.anko.custom.ankoView 10 | 11 | class SquareImageView : ImageView { 12 | 13 | constructor(context: Context) : super(context) 14 | 15 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 16 | 17 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( 18 | context, attrs, defStyleAttr) 19 | 20 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 21 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 22 | val width = measuredWidth 23 | setMeasuredDimension(width, width) 24 | } 25 | } 26 | 27 | inline fun ViewManager.squareImageView(theme: Int = 0, init: SquareImageView.() -> Unit) = 28 | ankoView(::SquareImageView, theme, init) 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/entity/AlbumDetail.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Track 6 | 7 | data class AlbumDetail( 8 | val id: String, val name: String, val url: String? = null, 9 | val tracks: List = emptyList()) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/entity/ArtistDetail.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity 4 | 5 | data class ArtistDetail( 6 | val id: String, val name: String, val url: String? = null, val bio: String? = null, 7 | val albums: List? = null) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/entity/ImageTitle.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity 4 | 5 | data class ImageTitle(val id: String, val name: String, private val rawUrl: String? = null) { 6 | 7 | val url: String? = rawUrl?.takeUnless(""::equals) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/entity/TrackDetail.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity 4 | 5 | data class TrackDetail(val number: Int, val name: String, val duration: Int) 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/entity/mapper/album/detail/AlbumDetailDataMapper.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity.mapper.album.detail 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.ui.entity.AlbumDetail 7 | 8 | fun transform(album: Album?): AlbumDetail? = album?.let { 9 | AlbumDetail(it.id, it.name, it.url, it.tracks) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/entity/mapper/artist/detail/ArtistDetailDataMapper.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity.mapper.artist.detail 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 6 | import com.functionalkotlin.bandhookkotlin.ui.entity.ArtistDetail 7 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.image.title.transformAlbums 8 | 9 | fun transform(artist: Artist): ArtistDetail = with(artist) { 10 | ArtistDetail(id, name, url, bio, albums?.let(::transformAlbums)) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/entity/mapper/image/title/ImageTitleDataMapper.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity.mapper.image.title 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 7 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 8 | 9 | fun transformArtists(artists: List): List = artists.map { 10 | ImageTitle(it.id, it.name, it.url) 11 | } 12 | 13 | fun transformAlbums(albums: List): List = albums.map { 14 | ImageTitle(it.id, it.name, it.url) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/entity/mapper/track/TrackDataMapper.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity.mapper.track 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Track 6 | import com.functionalkotlin.bandhookkotlin.ui.entity.TrackDetail 7 | 8 | fun transform(number: Int, track: Track): TrackDetail = with(track) { 9 | TrackDetail(number, name, duration) 10 | } 11 | 12 | fun transform(tracks: List): List = tracks.mapIndexed { i, track -> 13 | transform(i + 1, track) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/fragment/AlbumsFragmentContainer.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.fragment 4 | 5 | import com.functionalkotlin.bandhookkotlin.ui.presenter.base.AlbumsPresenter 6 | 7 | interface AlbumsFragmentContainer { 8 | fun getAlbumsPresenter(): AlbumsPresenter 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/presenter/AlbumPresenter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.presenter 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetAlbumDetailInteractor 6 | import com.functionalkotlin.bandhookkotlin.functional.fold 7 | import com.functionalkotlin.bandhookkotlin.functional.runAsync 8 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.album.detail.transform 9 | import com.functionalkotlin.bandhookkotlin.ui.presenter.base.Presenter 10 | import com.functionalkotlin.bandhookkotlin.ui.view.AlbumView 11 | 12 | open class AlbumPresenter( 13 | override val view: AlbumView, 14 | private val albumInteractor: GetAlbumDetailInteractor) : Presenter { 15 | 16 | suspend fun init(albumId: String) { 17 | albumInteractor.getAlbum(albumId).runAsync { 18 | it.fold( 19 | onSuccess = { view.showAlbum(transform(it)) }, 20 | onError = { view.showAlbumNotFound(it) }) 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/presenter/ArtistPresenter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.presenter 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetArtistDetailInteractor 6 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetTopAlbumsInteractor 7 | import com.functionalkotlin.bandhookkotlin.functional.fold 8 | import com.functionalkotlin.bandhookkotlin.functional.runAsync 9 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 10 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.artist.detail.transform 11 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.image.title.transformAlbums 12 | import com.functionalkotlin.bandhookkotlin.ui.presenter.base.AlbumsPresenter 13 | import com.functionalkotlin.bandhookkotlin.ui.presenter.base.Presenter 14 | import com.functionalkotlin.bandhookkotlin.ui.view.ArtistView 15 | 16 | open class ArtistPresenter( 17 | override val view: ArtistView, 18 | private val artistDetailInteractor: GetArtistDetailInteractor, 19 | private val topAlbumsInteractor: GetTopAlbumsInteractor) : 20 | Presenter, AlbumsPresenter { 21 | 22 | suspend fun init(artistId: String) { 23 | topAlbumsInteractor.getTopAlbums(artistId).runAsync { 24 | it.fold( 25 | onSuccess = { view.showAlbums(transformAlbums(it)) }, 26 | onError = { view.showAlbumsNotFound(it) }) 27 | } 28 | 29 | artistDetailInteractor.getArtist(artistId).runAsync { 30 | it.fold( 31 | onSuccess = { view.showArtist(transform(it)) }, 32 | onError = { view.showArtistNotFound(it) }) 33 | } 34 | } 35 | 36 | override fun onAlbumClicked(item: ImageTitle) { 37 | view.navigateToAlbum(item.id) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/presenter/MainPresenter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.presenter 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetRecommendedArtistsInteractor 6 | import com.functionalkotlin.bandhookkotlin.functional.fold 7 | import com.functionalkotlin.bandhookkotlin.functional.runAsync 8 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 9 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.image.title.transformArtists 10 | import com.functionalkotlin.bandhookkotlin.ui.presenter.base.Presenter 11 | import com.functionalkotlin.bandhookkotlin.ui.view.MainView 12 | 13 | class MainPresenter( 14 | override val view: MainView, 15 | private val recommendedArtistsInteractor: GetRecommendedArtistsInteractor) : 16 | Presenter { 17 | 18 | suspend override fun onResume() { 19 | super.onResume() 20 | 21 | recommendedArtistsInteractor.getRecommendedArtists().runAsync { 22 | it.fold( 23 | onSuccess = { view.showArtists(transformArtists(it)) }, 24 | onError = { }) 25 | } 26 | } 27 | 28 | fun onArtistClicked(item: ImageTitle) { 29 | view.navigateToDetail(item.id) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/presenter/base/AlbumsPresenter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.presenter.base 4 | 5 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 6 | 7 | interface AlbumsPresenter { 8 | fun onAlbumClicked(item: ImageTitle) 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/presenter/base/Presenter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.presenter.base 4 | 5 | interface Presenter { 6 | 7 | val view: T 8 | 9 | suspend fun onResume() { 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/screens/Styles.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.screens 4 | 5 | import android.view.View 6 | import com.functionalkotlin.bandhookkotlin.R 7 | import com.functionalkotlin.bandhookkotlin.ui.custom.AutofitRecyclerView 8 | import com.functionalkotlin.bandhookkotlin.ui.custom.PaddingItemDecoration 9 | import org.jetbrains.anko.dimen 10 | import org.jetbrains.anko.dip 11 | import org.jetbrains.anko.horizontalPadding 12 | import org.jetbrains.anko.verticalPadding 13 | 14 | fun AutofitRecyclerView.style() { 15 | clipToPadding = false 16 | columnWidth = dimen(R.dimen.column_width) 17 | scrollBarStyle = View.SCROLLBARS_OUTSIDE_OVERLAY 18 | horizontalPadding = dimen(R.dimen.recycler_spacing) 19 | verticalPadding = dip(2) 20 | addItemDecoration(PaddingItemDecoration(dip(2))) 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/screens/album/AlbumActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.screens.album 4 | 5 | import android.animation.Animator 6 | import android.animation.AnimatorListenerAdapter 7 | import android.annotation.SuppressLint 8 | import android.os.Bundle 9 | import android.support.annotation.VisibleForTesting 10 | import android.support.v7.widget.LinearLayoutManager 11 | import android.view.MenuItem 12 | import android.view.ViewPropertyAnimator 13 | import android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 14 | import com.functionalkotlin.bandhookkotlin.R 15 | import com.functionalkotlin.bandhookkotlin.di.ApplicationComponent 16 | import com.functionalkotlin.bandhookkotlin.di.subcomponent.album.AlbumActivityModule 17 | import com.functionalkotlin.bandhookkotlin.domain.entity.AlbumNotFound 18 | import com.functionalkotlin.bandhookkotlin.ui.activity.BaseActivity 19 | import com.functionalkotlin.bandhookkotlin.ui.adapter.TracksAdapter 20 | import com.functionalkotlin.bandhookkotlin.ui.entity.AlbumDetail 21 | import com.functionalkotlin.bandhookkotlin.ui.entity.TrackDetail 22 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.track.transform 23 | import com.functionalkotlin.bandhookkotlin.ui.presenter.AlbumPresenter 24 | import com.functionalkotlin.bandhookkotlin.ui.util.getNavigationId 25 | import com.functionalkotlin.bandhookkotlin.ui.util.into 26 | import com.functionalkotlin.bandhookkotlin.ui.util.supportsLollipop 27 | import com.functionalkotlin.bandhookkotlin.ui.view.AlbumView 28 | import com.squareup.picasso.Picasso 29 | import kotlinx.coroutines.experimental.android.UI 30 | import kotlinx.coroutines.experimental.launch 31 | import org.jetbrains.anko.dimen 32 | import javax.inject.Inject 33 | 34 | class AlbumActivity : BaseActivity(), AlbumView { 35 | 36 | override val ui = AlbumLayout() 37 | 38 | companion object { 39 | private const val LIST_ANIMATION_ON_START_DELAY = 500L 40 | private const val NO_TRANSLATION = 0f 41 | private const val TRANSPARENT = 0f 42 | } 43 | 44 | val albumListBreakingEdgeHeight by lazy { dimen(R.dimen.album_breaking_edge_height).toFloat() } 45 | 46 | @Inject 47 | @VisibleForTesting 48 | lateinit var presenter: AlbumPresenter 49 | 50 | @Inject 51 | lateinit var adapter: TracksAdapter 52 | 53 | @Inject 54 | lateinit var layoutManager: LinearLayoutManager 55 | 56 | @Inject 57 | lateinit var picasso: Picasso 58 | 59 | override fun onCreate(savedInstanceState: Bundle?) { 60 | super.onCreate(savedInstanceState) 61 | setUpTransition() 62 | setUpActionBar() 63 | setUpTrackList() 64 | } 65 | 66 | override fun injectDependencies(applicationComponent: ApplicationComponent) { 67 | applicationComponent.plus(AlbumActivityModule(this)) 68 | .injectTo(this) 69 | } 70 | 71 | @SuppressLint("NewApi") 72 | private fun setUpTransition() { 73 | supportPostponeEnterTransition() 74 | supportsLollipop { ui.image.transitionName = IMAGE_TRANSITION_NAME } 75 | } 76 | 77 | private fun setUpTrackList() { 78 | ui.trackList.adapter = adapter 79 | ui.trackList.layoutManager = layoutManager 80 | ui.listCard.translationY = -albumListBreakingEdgeHeight 81 | } 82 | 83 | private fun setUpActionBar() { 84 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 85 | title = null 86 | } 87 | 88 | override fun onResume() { 89 | super.onResume() 90 | 91 | launch(UI) { 92 | presenter.init(getNavigationId()) 93 | } 94 | } 95 | 96 | override fun showAlbum(albumDetail: AlbumDetail?) { 97 | 98 | albumDetail?.let(this::updateAlbumDetail) 99 | ?: postponeTransitions() 100 | } 101 | 102 | override fun showAlbumNotFound(e: AlbumNotFound) { 103 | supportStartPostponedEnterTransition() 104 | supportFinishAfterTransition() 105 | } 106 | 107 | private fun updateAlbumDetail(albumDetail: AlbumDetail) { 108 | picasso.load(albumDetail.url).fit().centerCrop().into(ui.image) { 109 | makeStatusBarTransparent() 110 | supportStartPostponedEnterTransition() 111 | populateTrackList(transform(albumDetail.tracks)) 112 | animateTrackListUp() 113 | } 114 | } 115 | 116 | private fun postponeTransitions() { 117 | supportStartPostponedEnterTransition() 118 | supportFinishAfterTransition() 119 | } 120 | 121 | private fun animateTrackListUp() { 122 | ui.listCard.animate() 123 | .setStartDelay(LIST_ANIMATION_ON_START_DELAY) 124 | .translationY(NO_TRANSLATION) 125 | } 126 | 127 | private fun populateTrackList(trackDetails: List) { 128 | adapter.items = trackDetails 129 | } 130 | 131 | @SuppressLint("InlinedApi") 132 | private fun makeStatusBarTransparent() { 133 | supportsLollipop { 134 | window.setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS) 135 | } 136 | } 137 | 138 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 139 | if (item != null && item.itemId == android.R.id.home) { 140 | onBackPressed() 141 | return true 142 | } 143 | return super.onOptionsItemSelected(item) 144 | } 145 | 146 | override fun onBackPressed() { 147 | ui.listCard.animate().alpha(TRANSPARENT) 148 | .onAnimationEnd { supportFinishAfterTransition() } 149 | } 150 | } 151 | 152 | inline fun ViewPropertyAnimator.onAnimationEnd( 153 | crossinline continuation: (Animator) -> Unit) { 154 | 155 | setListener(object : AnimatorListenerAdapter() { 156 | override fun onAnimationEnd(animation: Animator) { 157 | continuation(animation) 158 | } 159 | }) 160 | 161 | } 162 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/screens/album/AlbumLayout.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.screens.album 4 | 5 | import android.support.v7.widget.CardView 6 | import android.support.v7.widget.RecyclerView 7 | import android.support.v7.widget.Toolbar 8 | import android.view.View 9 | import android.widget.ImageView 10 | import com.functionalkotlin.bandhookkotlin.R 11 | import com.functionalkotlin.bandhookkotlin.ui.activity.ActivityAnkoComponent 12 | import com.functionalkotlin.bandhookkotlin.ui.custom.squareImageView 13 | import org.jetbrains.anko.AnkoContext 14 | import org.jetbrains.anko.appcompat.v7.themedToolbar 15 | import org.jetbrains.anko.backgroundResource 16 | import org.jetbrains.anko.below 17 | import org.jetbrains.anko.cardview.v7.cardView 18 | import org.jetbrains.anko.dimen 19 | import org.jetbrains.anko.dip 20 | import org.jetbrains.anko.horizontalMargin 21 | import org.jetbrains.anko.matchParent 22 | import org.jetbrains.anko.recyclerview.v7.recyclerView 23 | import org.jetbrains.anko.relativeLayout 24 | 25 | class AlbumLayout : ActivityAnkoComponent { 26 | 27 | lateinit var image: ImageView 28 | lateinit var trackList: RecyclerView 29 | lateinit var listCard: CardView 30 | override lateinit var toolbar: Toolbar 31 | 32 | override fun createView(ui: AnkoContext) = with(ui) { 33 | relativeLayout { 34 | 35 | image = squareImageView { 36 | id = View.generateViewId() 37 | backgroundResource = android.R.color.darker_gray 38 | }.lparams(width = matchParent) 39 | 40 | toolbar = themedToolbar(R.style.ThemeOverlay_AppCompat_Dark_ActionBar) 41 | .lparams(width = matchParent) { 42 | topMargin = dimen(R.dimen.statusbar_height) 43 | } 44 | 45 | listCard = cardView { 46 | radius = dip(2).toFloat() 47 | cardElevation = dip(5).toFloat() 48 | 49 | trackList = recyclerView() 50 | 51 | }.lparams { 52 | below(image) 53 | horizontalMargin = dip(8) 54 | topMargin = dimen(R.dimen.album_breaking_edge_height) 55 | bottomMargin = dip(-5) 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/screens/detail/AlbumsFragment.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.screens.detail 4 | 5 | import android.content.Context 6 | import android.os.Bundle 7 | import android.support.v4.app.Fragment 8 | import android.support.v7.widget.RecyclerView 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import com.functionalkotlin.bandhookkotlin.ui.activity.ViewAnkoComponent 13 | import com.functionalkotlin.bandhookkotlin.ui.adapter.BaseAdapter 14 | import com.functionalkotlin.bandhookkotlin.ui.adapter.ImageTitleAdapter 15 | import com.functionalkotlin.bandhookkotlin.ui.custom.AutofitRecyclerView 16 | import com.functionalkotlin.bandhookkotlin.ui.custom.autoFitRecycler 17 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 18 | import com.functionalkotlin.bandhookkotlin.ui.fragment.AlbumsFragmentContainer 19 | import com.functionalkotlin.bandhookkotlin.ui.screens.style 20 | import org.jetbrains.anko.AnkoContext 21 | 22 | class AlbumsFragment : Fragment() { 23 | 24 | var albumsFragmentContainer: AlbumsFragmentContainer? = null 25 | private set 26 | 27 | private var component: Component? = null 28 | var adapter: ImageTitleAdapter? = null 29 | 30 | override fun onCreateView( 31 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 32 | 33 | component = container?.let { Component(container) } 34 | return component?.inflate()?.setup() 35 | } 36 | 37 | private fun View.setup(): View { 38 | component?.recycler?.let { 39 | adapter = ImageTitleAdapter { item -> 40 | albumsFragmentContainer?.getAlbumsPresenter()?.onAlbumClicked(item) 41 | } 42 | it.adapter = adapter 43 | } 44 | return this 45 | } 46 | 47 | override fun onAttach(context: Context?) { 48 | super.onAttach(context) 49 | 50 | if (context is AlbumsFragmentContainer) { 51 | albumsFragmentContainer = context 52 | } 53 | } 54 | 55 | override fun onDetach() { 56 | super.onDetach() 57 | 58 | albumsFragmentContainer = null 59 | } 60 | 61 | private class Component(override val view: ViewGroup) : ViewAnkoComponent { 62 | 63 | lateinit var recycler: RecyclerView 64 | 65 | override fun createView(ui: AnkoContext) = with(ui) { 66 | recycler = autoFitRecycler().apply(AutofitRecyclerView::style) 67 | recycler 68 | } 69 | } 70 | 71 | fun findViewByItemId(id: String): View? = adapter?.findPositionById(id)?.let { 72 | val holder = component?.recycler?.findViewHolderForLayoutPosition(it) 73 | as BaseAdapter.BaseViewHolder 74 | return holder.ui.image 75 | } 76 | 77 | fun showAlbums(albums: List) { 78 | adapter?.items = albums 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/screens/detail/ArtistActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.screens.detail 4 | 5 | import android.annotation.SuppressLint 6 | import android.graphics.drawable.BitmapDrawable 7 | import android.os.Bundle 8 | import android.support.annotation.VisibleForTesting 9 | import android.support.v7.graphics.Palette 10 | import android.view.MenuItem 11 | import android.view.WindowManager 12 | import com.functionalkotlin.bandhookkotlin.R 13 | import com.functionalkotlin.bandhookkotlin.di.ApplicationComponent 14 | import com.functionalkotlin.bandhookkotlin.di.subcomponent.detail.ArtistActivityModule 15 | import com.functionalkotlin.bandhookkotlin.domain.entity.ArtistNotFound 16 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 17 | import com.functionalkotlin.bandhookkotlin.ui.activity.BaseActivity 18 | import com.functionalkotlin.bandhookkotlin.ui.adapter.ArtistDetailPagerAdapter 19 | import com.functionalkotlin.bandhookkotlin.ui.entity.ArtistDetail 20 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 21 | import com.functionalkotlin.bandhookkotlin.ui.fragment.AlbumsFragmentContainer 22 | import com.functionalkotlin.bandhookkotlin.ui.presenter.ArtistPresenter 23 | import com.functionalkotlin.bandhookkotlin.ui.presenter.base.AlbumsPresenter 24 | import com.functionalkotlin.bandhookkotlin.ui.screens.album.AlbumActivity 25 | import com.functionalkotlin.bandhookkotlin.ui.util.getNavigationId 26 | import com.functionalkotlin.bandhookkotlin.ui.util.into 27 | import com.functionalkotlin.bandhookkotlin.ui.util.navigate 28 | import com.functionalkotlin.bandhookkotlin.ui.util.supportsLollipop 29 | import com.functionalkotlin.bandhookkotlin.ui.view.ArtistView 30 | import com.squareup.picasso.Picasso 31 | import kotlinx.coroutines.experimental.android.UI 32 | import kotlinx.coroutines.experimental.launch 33 | import javax.inject.Inject 34 | 35 | class ArtistActivity : BaseActivity(), ArtistView, AlbumsFragmentContainer { 36 | override val ui = ArtistLayout() 37 | 38 | @Inject 39 | @VisibleForTesting 40 | lateinit var presenter: ArtistPresenter 41 | 42 | @Inject 43 | lateinit var picasso: Picasso 44 | 45 | @Inject 46 | lateinit var biographyFragment: BiographyFragment 47 | 48 | @Inject 49 | lateinit var albumsFragment: AlbumsFragment 50 | 51 | override fun onCreate(savedInstanceState: Bundle?) { 52 | super.onCreate(savedInstanceState) 53 | 54 | setUpTransition() 55 | setUpTopBar() 56 | setUpViewPager() 57 | setUpTabLayout() 58 | } 59 | 60 | override fun injectDependencies(applicationComponent: ApplicationComponent) { 61 | applicationComponent.plus(ArtistActivityModule(this)) 62 | .injectTo(this) 63 | } 64 | 65 | @SuppressLint("NewApi") 66 | private fun setUpTransition() { 67 | supportPostponeEnterTransition() 68 | supportsLollipop { ui.image.transitionName = IMAGE_TRANSITION_NAME } 69 | } 70 | 71 | private fun setUpTopBar() { 72 | title = null 73 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 74 | } 75 | 76 | private fun setUpTabLayout() { 77 | ui.tabLayout.setupWithViewPager(ui.viewPager) 78 | } 79 | 80 | private fun setUpViewPager() { 81 | val artistDetailPagerAdapter = ArtistDetailPagerAdapter(supportFragmentManager) 82 | artistDetailPagerAdapter.addFragment( 83 | biographyFragment, resources.getString(R.string.bio_fragment_title)) 84 | artistDetailPagerAdapter.addFragment( 85 | albumsFragment, resources.getString(R.string.albums_fragment_title)) 86 | ui.viewPager.adapter = artistDetailPagerAdapter 87 | } 88 | 89 | override fun onResume() { 90 | super.onResume() 91 | 92 | launch(UI) { 93 | presenter.init(getNavigationId()) 94 | } 95 | } 96 | 97 | override fun showArtist(artistDetail: ArtistDetail) { 98 | picasso.load(artistDetail.url).fit().centerCrop().into(ui.image) { 99 | makeStatusBarTransparent() 100 | supportStartPostponedEnterTransition() 101 | setActionBarTitle(artistDetail.name) 102 | biographyFragment.setBiographyText(artistDetail.bio) 103 | setActionBarPalette() 104 | } 105 | } 106 | 107 | override fun showAlbums(albums: List) { 108 | albumsFragment.showAlbums(albums) 109 | } 110 | 111 | override fun showArtistNotFound(it: ArtistNotFound) { 112 | supportStartPostponedEnterTransition() 113 | supportFinishAfterTransition() 114 | } 115 | 116 | override fun showAlbumsNotFound(e: TopAlbumsNotFound) { 117 | supportStartPostponedEnterTransition() 118 | supportFinishAfterTransition() 119 | } 120 | 121 | private fun setActionBarTitle(title: String) { 122 | supportActionBar?.title = title 123 | } 124 | 125 | private fun makeStatusBarTransparent() { 126 | supportsLollipop { 127 | window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, 128 | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) 129 | } 130 | } 131 | 132 | private fun setActionBarPalette() { 133 | val drawable = ui.image.drawable as BitmapDrawable? 134 | val bitmap = drawable?.bitmap 135 | if (bitmap != null) { 136 | Palette.from(bitmap).generate { palette -> 137 | val darkVibrantColor = palette.getDarkVibrantColor(R.attr.colorPrimary) 138 | ui.collapsingToolbarLayout.setContentScrimColor(darkVibrantColor) 139 | ui.collapsingToolbarLayout.setStatusBarScrimColor(darkVibrantColor) 140 | } 141 | } 142 | } 143 | 144 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 145 | if (item != null && item.itemId == android.R.id.home) { 146 | supportFinishAfterTransition() 147 | return true 148 | } 149 | return super.onOptionsItemSelected(item) 150 | } 151 | 152 | override fun navigateToAlbum(albumId: String) { 153 | val view = albumsFragment.findViewByItemId(albumId) 154 | navigate(albumId, view, BaseActivity.IMAGE_TRANSITION_NAME) 155 | } 156 | 157 | override fun getAlbumsPresenter(): AlbumsPresenter = presenter 158 | } 159 | 160 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/screens/detail/ArtistLayout.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.screens.detail 4 | 5 | import android.graphics.Color 6 | import android.support.design.widget.AppBarLayout 7 | import android.support.design.widget.AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED 8 | import android.support.design.widget.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL 9 | import android.support.design.widget.CollapsingToolbarLayout 10 | import android.support.design.widget.CollapsingToolbarLayout.LayoutParams 11 | import android.support.design.widget.CollapsingToolbarLayout.LayoutParams.COLLAPSE_MODE_PARALLAX 12 | import android.support.design.widget.CollapsingToolbarLayout.LayoutParams.COLLAPSE_MODE_PIN 13 | import android.support.design.widget.TabLayout 14 | import android.support.v4.view.ViewPager 15 | import android.support.v7.widget.Toolbar 16 | import android.view.Gravity 17 | import android.view.View 18 | import android.widget.ImageView 19 | import com.functionalkotlin.bandhookkotlin.R 20 | import com.functionalkotlin.bandhookkotlin.ui.activity.ActivityAnkoComponent 21 | import com.functionalkotlin.bandhookkotlin.ui.custom.squareImageView 22 | import org.jetbrains.anko.AnkoContext 23 | import org.jetbrains.anko.appcompat.v7.toolbar 24 | import org.jetbrains.anko.design.collapsingToolbarLayout 25 | import org.jetbrains.anko.design.coordinatorLayout 26 | import org.jetbrains.anko.design.tabLayout 27 | import org.jetbrains.anko.design.themedAppBarLayout 28 | import org.jetbrains.anko.dip 29 | import org.jetbrains.anko.matchParent 30 | import org.jetbrains.anko.support.v4.viewPager 31 | import org.jetbrains.anko.wrapContent 32 | 33 | class ArtistLayout : ActivityAnkoComponent { 34 | 35 | override lateinit var toolbar: Toolbar 36 | 37 | lateinit var image: ImageView 38 | lateinit var collapsingToolbarLayout: CollapsingToolbarLayout 39 | lateinit var viewPager: ViewPager 40 | lateinit var tabLayout: TabLayout 41 | 42 | override fun createView(ui: AnkoContext) = with(ui) { 43 | 44 | coordinatorLayout { 45 | 46 | themedAppBarLayout(R.style.ThemeOverlay_AppCompat_Dark_ActionBar) { 47 | fitsSystemWindows = true 48 | 49 | collapsingToolbarLayout = collapsingToolbarLayout { 50 | fitsSystemWindows = true 51 | collapsedTitleGravity = Gravity.TOP 52 | expandedTitleMarginBottom = dip(60) 53 | 54 | image = squareImageView { 55 | fitsSystemWindows = true 56 | }.lparamsC(matchParent) { 57 | collapseMode = COLLAPSE_MODE_PARALLAX 58 | } 59 | 60 | toolbar = toolbar { 61 | popupTheme = R.style.ThemeOverlay_AppCompat_Light 62 | titleMarginTop = dip(16) 63 | }.lparamsC(width = matchParent, height = dip(88)) { 64 | gravity = Gravity.TOP 65 | collapseMode = COLLAPSE_MODE_PIN 66 | } 67 | 68 | tabLayout = tabLayout { 69 | setSelectedTabIndicatorColor(Color.WHITE) 70 | }.lparamsC(width = matchParent) { 71 | gravity = Gravity.BOTTOM 72 | } 73 | 74 | }.lparams(width = matchParent) { 75 | scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_EXIT_UNTIL_COLLAPSED 76 | } 77 | 78 | }.lparams(width = matchParent) 79 | 80 | viewPager = viewPager { 81 | id = View.generateViewId() 82 | }.lparams { 83 | behavior = AppBarLayout.ScrollingViewBehavior() 84 | } 85 | } 86 | } 87 | } 88 | 89 | private fun T.lparamsC( 90 | width: Int = wrapContent, height: Int = wrapContent, init: LayoutParams.() -> Unit = {}): T { 91 | 92 | val params = LayoutParams(width, height) 93 | params.init() 94 | layoutParams = params 95 | return this 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/screens/detail/BiographyFragment.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.screens.detail 4 | 5 | import android.os.Bundle 6 | import android.support.v4.app.Fragment 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.TextView 11 | import com.functionalkotlin.bandhookkotlin.R 12 | import com.functionalkotlin.bandhookkotlin.ui.activity.ViewAnkoComponent 13 | import com.functionalkotlin.bandhookkotlin.ui.util.fromHtml 14 | import com.functionalkotlin.bandhookkotlin.ui.util.setTextAppearanceC 15 | import org.jetbrains.anko.AnkoContext 16 | import org.jetbrains.anko.backgroundResource 17 | import org.jetbrains.anko.dip 18 | import org.jetbrains.anko.padding 19 | import org.jetbrains.anko.support.v4.nestedScrollView 20 | import org.jetbrains.anko.textView 21 | 22 | class BiographyFragment : Fragment() { 23 | 24 | private var component: Component? = null 25 | 26 | override fun onCreateView( 27 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 28 | 29 | component = container?.let { Component(container) } 30 | return component?.inflate() 31 | } 32 | 33 | fun setBiographyText(biographyText: String?) { 34 | component?.textView?.text = biographyText?.fromHtml() 35 | } 36 | 37 | fun getBiographyText(): String? = component?.textView?.text?.toString() 38 | 39 | private class Component(override val view: ViewGroup) : ViewAnkoComponent { 40 | 41 | lateinit var textView: TextView 42 | 43 | override fun createView(ui: AnkoContext): View = with(ui) { 44 | nestedScrollView { 45 | textView = textView { 46 | backgroundResource = android.R.color.white 47 | padding = dip(16) 48 | setTextAppearanceC(R.style.TextAppearance_AppCompat_Body1) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/screens/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.screens.main 4 | 5 | import android.os.Bundle 6 | import android.view.View 7 | import com.functionalkotlin.bandhookkotlin.di.ApplicationComponent 8 | import com.functionalkotlin.bandhookkotlin.di.subcomponent.main.MainActivityModule 9 | import com.functionalkotlin.bandhookkotlin.ui.activity.BaseActivity 10 | import com.functionalkotlin.bandhookkotlin.ui.adapter.BaseAdapter 11 | import com.functionalkotlin.bandhookkotlin.ui.adapter.ImageTitleAdapter 12 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 13 | import com.functionalkotlin.bandhookkotlin.ui.presenter.MainPresenter 14 | import com.functionalkotlin.bandhookkotlin.ui.screens.detail.ArtistActivity 15 | import com.functionalkotlin.bandhookkotlin.ui.util.navigate 16 | import com.functionalkotlin.bandhookkotlin.ui.view.MainView 17 | import kotlinx.coroutines.experimental.android.UI 18 | import kotlinx.coroutines.experimental.launch 19 | import javax.inject.Inject 20 | 21 | class MainActivity : BaseActivity(), MainView { 22 | 23 | override val ui = MainLayout() 24 | 25 | @Inject 26 | lateinit var presenter: MainPresenter 27 | 28 | val adapter = ImageTitleAdapter { presenter.onArtistClicked(it) } 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | ui.recycler.adapter = adapter 33 | } 34 | 35 | override fun injectDependencies(applicationComponent: ApplicationComponent) { 36 | applicationComponent.plus(MainActivityModule(this)) 37 | .injectTo(this) 38 | } 39 | 40 | override fun onResume() { 41 | super.onResume() 42 | 43 | launch(UI) { 44 | presenter.onResume() 45 | } 46 | } 47 | 48 | override fun showArtists(artists: List) { 49 | adapter.items = artists 50 | } 51 | 52 | override fun navigateToDetail(id: String) { 53 | navigate(id, findItemById(id), BaseActivity.IMAGE_TRANSITION_NAME) 54 | } 55 | 56 | private fun findItemById(id: String): View { 57 | val pos = adapter.findPositionById(id) 58 | val holder = ui.recycler.findViewHolderForLayoutPosition(pos) 59 | as BaseAdapter.BaseViewHolder 60 | return holder.ui.image 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/screens/main/MainLayout.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.screens.main 4 | 5 | import android.support.design.widget.AppBarLayout 6 | import android.support.design.widget.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS 7 | import android.support.design.widget.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL 8 | import android.support.design.widget.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP 9 | import android.support.v7.widget.RecyclerView 10 | import android.support.v7.widget.Toolbar 11 | import com.functionalkotlin.bandhookkotlin.R 12 | import com.functionalkotlin.bandhookkotlin.ui.activity.ActivityAnkoComponent 13 | import com.functionalkotlin.bandhookkotlin.ui.custom.AutofitRecyclerView 14 | import com.functionalkotlin.bandhookkotlin.ui.custom.autoFitRecycler 15 | import com.functionalkotlin.bandhookkotlin.ui.screens.style 16 | import org.jetbrains.anko.AnkoContext 17 | import org.jetbrains.anko.appcompat.v7.themedToolbar 18 | import org.jetbrains.anko.backgroundResource 19 | import org.jetbrains.anko.design.appBarLayout 20 | import org.jetbrains.anko.design.coordinatorLayout 21 | import org.jetbrains.anko.matchParent 22 | 23 | class MainLayout : ActivityAnkoComponent { 24 | 25 | lateinit var recycler: RecyclerView 26 | override lateinit var toolbar: Toolbar 27 | 28 | override fun createView(ui: AnkoContext) = with(ui) { 29 | 30 | coordinatorLayout { 31 | 32 | appBarLayout { 33 | toolbar = themedToolbar(R.style.ThemeOverlay_AppCompat_Dark_ActionBar) { 34 | backgroundResource = R.color.primary 35 | }.lparams(width = matchParent) { 36 | scrollFlags = SCROLL_FLAG_SNAP or SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS 37 | } 38 | }.lparams(width = matchParent) 39 | 40 | recycler = autoFitRecycler() 41 | .apply(AutofitRecyclerView::style) 42 | .lparams(matchParent, matchParent) { 43 | behavior = AppBarLayout.ScrollingViewBehavior() 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/util/ContextExtensions.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.util 4 | 5 | import android.app.Activity 6 | import android.content.Intent 7 | import android.support.v4.app.ActivityCompat 8 | import android.support.v4.app.ActivityOptionsCompat 9 | import android.view.View 10 | 11 | inline fun Activity.navigate( 12 | id: String, sharedView: View? = null, transitionName: String? = null) { 13 | 14 | val intent = Intent(this, T::class.java) 15 | intent.putExtra("id", id) 16 | 17 | var options: ActivityOptionsCompat? = null 18 | 19 | if (sharedView != null && transitionName != null) { 20 | options = ActivityOptionsCompat.makeSceneTransitionAnimation( 21 | this, sharedView, transitionName) 22 | } 23 | 24 | ActivityCompat.startActivity(this, intent, options?.toBundle()) 25 | } 26 | 27 | fun Activity.getNavigationId(): String { 28 | val intent = intent 29 | return intent.getStringExtra("id") 30 | } 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/util/PicassoExtensions.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.util 4 | 5 | import android.widget.ImageView 6 | import com.squareup.picasso.Callback 7 | import com.squareup.picasso.RequestCreator 8 | 9 | fun RequestCreator.into(target: ImageView, callback: () -> Unit) { 10 | into(target, object : Callback.EmptyCallback() { 11 | override fun onSuccess() { 12 | callback() 13 | } 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/util/VersionsSupport.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.util 4 | 5 | import android.os.Build 6 | 7 | inline fun supportsLollipop(code: () -> Unit) { 8 | if (Build.VERSION.SDK_INT >= 21) { 9 | code() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/util/ViewExtensions.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.util 4 | 5 | import android.os.Build 6 | import android.support.annotation.StyleRes 7 | import android.support.v4.widget.TextViewCompat 8 | import android.text.Html 9 | import android.text.Spanned 10 | import android.view.View 11 | import android.widget.ImageView 12 | import android.widget.TextView 13 | import com.functionalkotlin.bandhookkotlin.ui.adapter.SingleClickListener 14 | import com.squareup.picasso.Picasso 15 | 16 | /** 17 | * Click listener setter that prevents double click on the view it's set 18 | */ 19 | fun View.singleClick(l: (android.view.View?) -> Unit) { 20 | setOnClickListener(SingleClickListener(l)) 21 | } 22 | 23 | fun ImageView.loadUrl(url: String) { 24 | Picasso.with(context).load(url).into(this) 25 | } 26 | 27 | fun TextView.setTextAppearanceC(@StyleRes textAppearance: Int) 28 | = TextViewCompat.setTextAppearance(this, textAppearance) 29 | 30 | @Suppress("DEPRECATION") 31 | fun String.fromHtml(): Spanned = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 32 | Html.fromHtml(this, Html.FROM_HTML_MODE_COMPACT) 33 | } else { 34 | Html.fromHtml(this) 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/view/AlbumView.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.view 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.AlbumNotFound 6 | import com.functionalkotlin.bandhookkotlin.ui.entity.AlbumDetail 7 | 8 | interface AlbumView : PresentationView { 9 | fun showAlbum(albumDetail: AlbumDetail?) 10 | fun showAlbumNotFound(e: AlbumNotFound) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/view/ArtistView.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.view 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.ArtistNotFound 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 7 | import com.functionalkotlin.bandhookkotlin.ui.entity.ArtistDetail 8 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 9 | 10 | interface ArtistView : PresentationView { 11 | fun showArtist(artistDetail: ArtistDetail) 12 | fun showAlbums(albums: List) 13 | fun showAlbumsNotFound(e: TopAlbumsNotFound) 14 | fun showArtistNotFound(it: ArtistNotFound) 15 | fun navigateToAlbum(albumId: String) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/view/MainView.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.view 4 | 5 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 6 | 7 | interface MainView : PresentationView { 8 | fun showArtists(artists: List) 9 | fun navigateToDetail(id: String) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/functionalkotlin/bandhookkotlin/ui/view/PresentationView.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.view 4 | 5 | interface PresentationView 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_shadow.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunctionalKotlin/final-app/cd3d59794218f72740c2cb22a430eba26f244cea/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunctionalKotlin/final-app/cd3d59794218f72740c2cb22a430eba26f244cea/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunctionalKotlin/final-app/cd3d59794218f72740c2cb22a430eba26f244cea/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunctionalKotlin/final-app/cd3d59794218f72740c2cb22a430eba26f244cea/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-sw720dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 64dp 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | #ef6c00 8 | #e65100 9 | @color/primary 10 | @color/background_material_dark 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 86400 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 300dp 7 | 24dp 8 | -110dp 9 | 180dp 10 | 2dp 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Bandhook 7 | Settings 8 | Albums 9 | Bio 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/data/CloudAlbumDataSetTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.LastFmService 6 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmAlbum 7 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmAlbumDetail 8 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtist 9 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtistList 10 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtistMatches 11 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmResponse 12 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmResult 13 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmTopAlbums 14 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmTracklist 15 | import com.functionalkotlin.bandhookkotlin.data.mapper.album.transform 16 | import com.functionalkotlin.bandhookkotlin.data.mock.FakeCall 17 | import com.functionalkotlin.bandhookkotlin.domain.entity.AlbumNotFound 18 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 19 | import com.functionalkotlin.bandhookkotlin.util.asFailure 20 | import com.functionalkotlin.bandhookkotlin.util.asSuccess 21 | import com.nhaarman.mockito_kotlin.doReturn 22 | import com.nhaarman.mockito_kotlin.mock 23 | import com.nhaarman.mockito_kotlin.never 24 | import com.nhaarman.mockito_kotlin.whenever 25 | import io.kotlintest.matchers.shouldBe 26 | import io.kotlintest.specs.StringSpec 27 | import org.mockito.Mockito.anyString 28 | import org.mockito.Mockito.verify 29 | import retrofit2.Response 30 | 31 | class CloudAlbumDataSetTest : StringSpec() { 32 | init { 33 | val knownAlbumDetail = LastFmAlbumDetail( 34 | "name", ALBUM_MBID, "url", "artist", "date", emptyList(), LastFmTracklist(emptyList())) 35 | 36 | val unknownAlbumDetail = LastFmAlbumDetail( 37 | "", null, "", "", "", emptyList(), LastFmTracklist(emptyList())) 38 | 39 | val lastFmResponse = lastFmResponse(knownAlbumDetail) 40 | 41 | val lastFmService = mock { 42 | on { requestAlbum(ALBUM_MBID) } doReturn fakeCall(lastFmResponse) 43 | on { requestAlbums(anyString(), anyString()) } doReturn fakeCall(lastFmResponse) 44 | } 45 | 46 | val cloudAlbumDataSet = CloudAlbumDataSet(lastFmService) 47 | 48 | "requestAlbum with valid mbid returns valid album" { 49 | val asyncResult = cloudAlbumDataSet.requestAlbum(ALBUM_MBID) 50 | 51 | verify(lastFmService).requestAlbum(ALBUM_MBID) 52 | asyncResult.asSuccess { shouldBe(transform(lastFmResponse.album)) } 53 | } 54 | 55 | "requestAlbum with unknown mbid returns null" { 56 | whenever(lastFmService.requestAlbum(ALBUM_MBID)) 57 | .thenReturn(fakeCall(lastFmResponse(unknownAlbumDetail))) 58 | 59 | val asyncResult = cloudAlbumDataSet.requestAlbum(ALBUM_MBID) 60 | 61 | verify(lastFmService).requestAlbum(ALBUM_MBID) 62 | asyncResult.asFailure { shouldBe(AlbumNotFound(ALBUM_MBID)) } 63 | } 64 | 65 | "requestTopAlbums with valid artist mbid returns valid list" { 66 | val asyncResult = cloudAlbumDataSet.requestTopAlbums(ARTIST_MBID) 67 | 68 | asyncResult.asSuccess { shouldBe(transform(lastFmResponse.topAlbums.albums)) } 69 | verify(lastFmService).requestAlbums(ARTIST_MBID, "") 70 | } 71 | 72 | "requestTopAlbums with invalid arguments returns error" { 73 | val asyncResult = cloudAlbumDataSet.requestTopAlbums("") 74 | 75 | verify(lastFmService, never()).requestAlbums(anyString(), anyString()) 76 | asyncResult.asFailure { shouldBe(TopAlbumsNotFound("")) } 77 | } 78 | } 79 | 80 | private fun lastFmResponse(lastFmAlbumDetail: LastFmAlbumDetail): LastFmResponse { 81 | val artist = LastFmArtist(ARTIST_NAME, ARTIST_MBID, "url", emptyList(), null, null) 82 | 83 | val topAlbums = LastFmTopAlbums(listOf(LastFmAlbum( 84 | "name", ALBUM_MBID, "url", artist, emptyList(), LastFmTracklist(emptyList())))) 85 | 86 | return LastFmResponse( 87 | LastFmResult(LastFmArtistMatches(emptyList())), artist, topAlbums, 88 | LastFmArtistList(emptyList()), lastFmAlbumDetail) 89 | } 90 | 91 | private fun fakeCall(lastFmResponse: LastFmResponse) = 92 | FakeCall(Response.success(lastFmResponse), null) 93 | 94 | } 95 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/data/CloudArtistDataSetTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.LastFmService 6 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmAlbumDetail 7 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtist 8 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtistList 9 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtistMatches 10 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmResponse 11 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmResult 12 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmTopAlbums 13 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmTracklist 14 | import com.functionalkotlin.bandhookkotlin.data.mapper.artist.transform 15 | import com.functionalkotlin.bandhookkotlin.data.mock.FakeCall 16 | import com.functionalkotlin.bandhookkotlin.domain.entity.ArtistNotFound 17 | import com.functionalkotlin.bandhookkotlin.util.asFailure 18 | import com.functionalkotlin.bandhookkotlin.util.asSuccess 19 | import com.nhaarman.mockito_kotlin.doReturn 20 | import com.nhaarman.mockito_kotlin.mock 21 | import com.nhaarman.mockito_kotlin.whenever 22 | import io.kotlintest.matchers.shouldBe 23 | import io.kotlintest.specs.StringSpec 24 | import org.mockito.Mockito.anyString 25 | import org.mockito.Mockito.verify 26 | import retrofit2.Response 27 | 28 | class CloudArtistDataSetTest : StringSpec() { 29 | 30 | init { 31 | val language = "lang" 32 | 33 | val lastFmAlbumDetail = LastFmAlbumDetail( 34 | "album name", ARTIST_MBID, "album url", "album artist", "album release", emptyList(), 35 | LastFmTracklist(emptyList())) 36 | 37 | val lastFmArtist = LastFmArtist("name", ARTIST_MBID, "url", null, null, null) 38 | 39 | val lastFmResponse = lastFmResponse(lastFmArtist, lastFmAlbumDetail) 40 | 41 | val lastFmService = mock { 42 | on { requestSimilar(anyString()) }.doReturn(fakeCall(lastFmResponse)) 43 | on { requestArtistInfo(ARTIST_MBID, language) }.doReturn(fakeCall(lastFmResponse)) 44 | } 45 | 46 | val cloudArtistDataSet = CloudArtistDataSet(language, lastFmService) 47 | 48 | "requestRecommendedArtists returns recommended artists" { 49 | val asyncResult = cloudArtistDataSet.requestRecommendedArtists() 50 | 51 | asyncResult.asSuccess { shouldBe(transform(listOf(lastFmArtist))) } 52 | verify(lastFmService).requestSimilar(cloudArtistDataSet.coldplayMbid) 53 | } 54 | 55 | "requestArtist should return artist" { 56 | val asyncResult = cloudArtistDataSet.requestArtist(ARTIST_MBID) 57 | 58 | asyncResult.asSuccess { shouldBe(transform(lastFmArtist)) } 59 | verify(lastFmService).requestArtistInfo(ARTIST_MBID, language) 60 | } 61 | 62 | "requestArtist should return null if unknown id" { 63 | val unknownMbid = "unknown" 64 | 65 | val unknownArtistResponse = lastFmResponse( 66 | LastFmArtist("unknown artist name", null, "unknown artist url"), lastFmAlbumDetail) 67 | 68 | whenever(lastFmService.requestArtistInfo(unknownMbid, language)) 69 | .thenReturn(fakeCall(unknownArtistResponse)) 70 | 71 | val asyncResult = cloudArtistDataSet.requestArtist(unknownMbid) 72 | 73 | asyncResult.asFailure { shouldBe(ArtistNotFound(unknownMbid)) } 74 | verify(lastFmService).requestArtistInfo(unknownMbid, language) 75 | } 76 | } 77 | 78 | private fun lastFmResponse( 79 | lastFmArtist: LastFmArtist, lastFmAlbumDetail: LastFmAlbumDetail): LastFmResponse = 80 | LastFmResponse( 81 | LastFmResult(LastFmArtistMatches(emptyList())), 82 | lastFmArtist, LastFmTopAlbums(emptyList()), LastFmArtistList(listOf(lastFmArtist)), 83 | lastFmAlbumDetail) 84 | 85 | private fun fakeCall(lastFmResponse: LastFmResponse) = 86 | FakeCall(Response.success(lastFmResponse), null) 87 | } 88 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/data/Constants.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data 4 | 5 | const val ALBUM_MBID = "mbid" 6 | const val ARTIST_MBID = "mbid" 7 | const val ARTIST_NAME = "name" -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/data/mapper/album/AlbumMapperTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.mapper.album 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmAlbum 6 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmAlbumDetail 7 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtist 8 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImage 9 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImageType 10 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmTrack 11 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmTracklist 12 | import io.kotlintest.matchers.beEmpty 13 | import io.kotlintest.matchers.haveSize 14 | import io.kotlintest.matchers.should 15 | import io.kotlintest.matchers.shouldBe 16 | import io.kotlintest.matchers.shouldNotBe 17 | import io.kotlintest.specs.StringSpec 18 | 19 | class AlbumMapperTest : StringSpec() { 20 | init { 21 | val lastFmArtist = LastFmArtist( 22 | "name", "artist mbid", "artist url", emptyList(), null, null) 23 | val lastFmImages = listOf( 24 | LastFmImage("image url", LastFmImageType.MEGA.type)) 25 | val lastFmTrackList = LastFmTracklist( 26 | listOf(LastFmTrack("track name", 10, null, null, lastFmArtist))) 27 | 28 | val lastFmAlbum = LastFmAlbum( 29 | "name", "mbid", "url", lastFmArtist, lastFmImages, lastFmTrackList) 30 | val lastFmAlbumWithoutId = LastFmAlbum( 31 | "name", null, "url", lastFmArtist, lastFmImages, lastFmTrackList) 32 | val lastFmAlbumWithoutImages = LastFmAlbum( 33 | "name", "mbid", "url", lastFmArtist, emptyList(), lastFmTrackList) 34 | 35 | "transform invalid albums returns empty list" { 36 | val albums = listOf(lastFmAlbumWithoutId, lastFmAlbumWithoutImages) 37 | 38 | transform(albums) should beEmpty() 39 | } 40 | 41 | "transform valid albums returns valid list" { 42 | val albums = transform(listOf(lastFmAlbum, lastFmAlbum)) 43 | 44 | albums should haveSize(2) 45 | albums[0].id shouldBe lastFmAlbum.mbid 46 | albums[1].id shouldBe lastFmAlbum.mbid 47 | } 48 | 49 | "transform empty albums returns empty list" { 50 | transform(emptyList()) should beEmpty() 51 | } 52 | 53 | "transform albums valid and invalid should only transform valid" { 54 | val albums = transform( 55 | listOf(lastFmAlbum, lastFmAlbumWithoutId, lastFmAlbumWithoutImages)) 56 | 57 | albums should haveSize(1) 58 | albums[0].id shouldBe lastFmAlbum.mbid 59 | } 60 | 61 | "transform album detail should return valid album" { 62 | val albumDetail = LastFmAlbumDetail( 63 | "name", "mbid", "url", "name", "album release date", lastFmImages, lastFmTrackList) 64 | 65 | val album = transform(albumDetail) 66 | 67 | album?.run { 68 | id shouldBe albumDetail.mbid 69 | name shouldBe albumDetail.name 70 | url shouldNotBe null 71 | artist?.name shouldBe albumDetail.artist 72 | tracks[0].name shouldBe albumDetail.tracks.tracks[0].name 73 | tracks[0].duration shouldBe albumDetail.tracks.tracks[0].duration 74 | } 75 | } 76 | 77 | "transform invalid album detail should return null album" { 78 | val albumDetail = LastFmAlbumDetail( 79 | "name", null, "url", "name", "album release date", lastFmImages, lastFmTrackList) 80 | 81 | transform(albumDetail) shouldBe null 82 | } 83 | 84 | "transform valid album should return valid album" { 85 | val album = transform(lastFmAlbum) 86 | 87 | album?.run { 88 | id shouldBe lastFmAlbum.mbid 89 | name shouldBe lastFmAlbum.name 90 | url shouldNotBe null 91 | artist?.name shouldBe lastFmAlbum.artist.name 92 | artist?.id shouldBe lastFmAlbum.artist.mbid 93 | tracks[0].name shouldBe lastFmAlbum.tracks?.tracks?.get(0)?.name 94 | tracks[0].duration shouldBe lastFmAlbum.tracks?.tracks?.get(0)?.duration 95 | } 96 | } 97 | 98 | "transform invalid album should return null" { 99 | transform(lastFmAlbumWithoutId) shouldBe null 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/data/mapper/artist/ArtistMapperTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.mapper.artist 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtist 6 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmBio 7 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImage 8 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImageType 9 | import io.kotlintest.matchers.haveSize 10 | import io.kotlintest.matchers.should 11 | import io.kotlintest.matchers.shouldBe 12 | import io.kotlintest.specs.StringSpec 13 | 14 | class ArtistMapperTest : StringSpec() { 15 | init { 16 | val coldplayBio = LastFmBio("British rock band formed in 1996") 17 | val megaImageUrl = "megaImageOneUrl" 18 | val smallImageUrl = "smallImageOneUrl" 19 | 20 | val megaLastFmImage = LastFmImage(megaImageUrl, LastFmImageType.MEGA.type) 21 | val smallLastFmImage = LastFmImage(smallImageUrl, LastFmImageType.SMALL.type) 22 | 23 | val validLastFmArtistWithMegaImage = LastFmArtist( 24 | "Coldplay", "mbid", "url", listOf(megaLastFmImage, smallLastFmImage), null, 25 | coldplayBio) 26 | val validLastFmArtistWithoutMegaImage = LastFmArtist( 27 | "Him", "mbid", "url", listOf(smallLastFmImage, smallLastFmImage), null, null) 28 | val invalidLastFmArtist = LastFmArtist( 29 | "Unknown", null, "url", listOf(megaLastFmImage, smallLastFmImage), null, null) 30 | 31 | val artistsList = listOf(validLastFmArtistWithMegaImage, 32 | invalidLastFmArtist, 33 | validLastFmArtistWithoutMegaImage) 34 | 35 | "transform valid list returns artist list" { 36 | val artists = transform(artistsList) 37 | 38 | artists should haveSize(2) 39 | artists[0].id shouldBe validLastFmArtistWithMegaImage.mbid 40 | artists[1].id shouldBe validLastFmArtistWithMegaImage.mbid 41 | } 42 | 43 | "transform artist with mega image return valid artist" { 44 | val artist = transform(validLastFmArtistWithMegaImage) 45 | 46 | artist?.run { 47 | id shouldBe validLastFmArtistWithMegaImage.mbid 48 | name shouldBe validLastFmArtistWithMegaImage.name 49 | bio shouldBe validLastFmArtistWithMegaImage.bio?.content 50 | url shouldBe megaImageUrl 51 | } 52 | } 53 | 54 | "transform artist without mega image return valid artist" { 55 | val artist = transform(validLastFmArtistWithoutMegaImage) 56 | 57 | artist?.run { 58 | id shouldBe validLastFmArtistWithoutMegaImage.mbid 59 | name shouldBe validLastFmArtistWithoutMegaImage.name 60 | bio shouldBe validLastFmArtistWithoutMegaImage.bio?.content 61 | url shouldBe smallImageUrl 62 | } 63 | } 64 | 65 | "transform invalid artist returns null" { 66 | transform(invalidLastFmArtist) shouldBe null 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/data/mapper/image/ImageMapperTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.mapper.image 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImage 6 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmImageType 7 | import io.kotlintest.matchers.shouldBe 8 | import io.kotlintest.specs.StringSpec 9 | 10 | class ImageMapperTest : StringSpec() { 11 | init { 12 | val megaImage = LastFmImage("mega", LastFmImageType.MEGA.type) 13 | val largeImage = LastFmImage("large", LastFmImageType.LARGE.type) 14 | val smallImage = LastFmImage("small", LastFmImageType.SMALL.type) 15 | 16 | val imagesWithMegaImage = listOf(smallImage, megaImage, largeImage) 17 | val imagesWithoutMegaImage = listOf(smallImage, largeImage) 18 | 19 | "getMainImageUrl from list with mega image returns mega image url" { 20 | getMainImageUrl(imagesWithMegaImage) shouldBe "mega" 21 | } 22 | 23 | "getMainImageUrl from list without mega image returns last image url" { 24 | getMainImageUrl(imagesWithoutMegaImage) shouldBe "large" 25 | } 26 | 27 | "getMainImageUrl from null list returns null" { 28 | getMainImageUrl(null) shouldBe null 29 | } 30 | 31 | "getMainImageUrl from empty list returns null" { 32 | getMainImageUrl(emptyList()) shouldBe null 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/data/mapper/track/TrackMapperTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.mapper.track 4 | 5 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmArtist 6 | import com.functionalkotlin.bandhookkotlin.data.lastfm.model.LastFmTrack 7 | import io.kotlintest.matchers.haveSize 8 | import io.kotlintest.matchers.should 9 | import io.kotlintest.matchers.shouldBe 10 | import io.kotlintest.specs.StringSpec 11 | 12 | class TrackMapperTest : StringSpec() { 13 | init { 14 | val lastFmArtist = LastFmArtist("name", "mbid", "url", emptyList(), null, null) 15 | val lastFmTrack = LastFmTrack("name", 10, null, "url", lastFmArtist) 16 | 17 | "transform valid tracks return valid list" { 18 | val tracks = transform(listOf(lastFmTrack, lastFmTrack)) 19 | 20 | tracks should haveSize(2) 21 | lastFmTrack.run { 22 | tracks[0].name shouldBe name 23 | tracks[0].duration shouldBe duration 24 | tracks[1].name shouldBe name 25 | tracks[1].duration shouldBe duration 26 | } 27 | } 28 | 29 | "transform null list returns empty list" { 30 | transform(null) should haveSize(0) 31 | } 32 | 33 | "transform empty list returns empty list" { 34 | transform(emptyList()) should haveSize(0) 35 | } 36 | 37 | "transform track returns track" { 38 | transform(lastFmTrack).run { 39 | name shouldBe lastFmTrack.name 40 | duration shouldBe lastFmTrack.duration 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/data/mock/FakeCall.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.data.mock 4 | 5 | import okhttp3.Request 6 | import retrofit2.Call 7 | import retrofit2.Callback 8 | import retrofit2.Response 9 | import java.io.IOException 10 | import java.util.concurrent.atomic.AtomicBoolean 11 | 12 | /** 13 | * Created by Mahmoud Abdurrahman (ma.abdurrahman@gmail.com) on 2/17/17. 14 | * 15 | * Pulled from: https://github.com/square/retrofit/blob/master/retrofit-mock/src/main/java/retrofit2/mock/Calls.java 16 | */ 17 | class FakeCall(private val response: Response?, private val error: IOException?) : Call { 18 | private val canceled = AtomicBoolean() 19 | private val executed = AtomicBoolean() 20 | 21 | init { 22 | if ((response == null) == (error == null)) { 23 | throw AssertionError("Only one of response or error can be set.") 24 | } 25 | } 26 | 27 | @Throws(IOException::class) 28 | override fun execute(): Response { 29 | when { 30 | !executed.compareAndSet(false, true) -> throw IllegalStateException("Already executed") 31 | canceled.get() -> throw IOException("canceled") 32 | response != null -> return response 33 | error != null -> throw error 34 | else -> throw IOException("Should either have a response or throw exception") 35 | } 36 | } 37 | 38 | override fun enqueue(callback: Callback?) { 39 | if (!executed.compareAndSet(false, true)) { 40 | throw IllegalStateException("Already executed") 41 | } 42 | callback?.let { 43 | when { 44 | canceled.get() -> callback.onFailure(this, IOException("canceled")) 45 | response != null -> callback.onResponse(this, response) 46 | else -> callback.onFailure(this, error) 47 | } 48 | } ?: throw NullPointerException("callback == null") 49 | } 50 | 51 | override fun isExecuted(): Boolean = executed.get() 52 | 53 | override fun cancel() = canceled.set(true) 54 | 55 | override fun isCanceled(): Boolean = canceled.get() 56 | 57 | override fun clone(): Call = FakeCall(response, error) 58 | 59 | override fun request(): Request = response?.raw()?.request() 60 | ?: Request.Builder().url("http://localhost").build() 61 | } 62 | 63 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/domain/interactor/Constants.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.interactor 4 | 5 | const val ALBUM_ID = "album id" -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/domain/interactor/GetAlbumDetailInteractorTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.interactor 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 7 | import com.functionalkotlin.bandhookkotlin.domain.repository.AlbumRepository 8 | import com.functionalkotlin.bandhookkotlin.functional.result 9 | import com.functionalkotlin.bandhookkotlin.util.asSuccess 10 | import com.nhaarman.mockito_kotlin.mock 11 | import io.kotlintest.matchers.shouldBe 12 | import io.kotlintest.specs.StringSpec 13 | 14 | class GetAlbumDetailInteractorTest : StringSpec() { 15 | 16 | init { 17 | val albumRepository = mock { 18 | val result = Album( 19 | ALBUM_ID, "name", Artist("id", "name", null, null, null), "url", emptyList() 20 | ).result() 21 | 22 | on { getAlbum(ALBUM_ID) }.thenReturn(result) 23 | } 24 | 25 | val interactor = GetAlbumDetailInteractor(albumRepository) 26 | 27 | "getAlbum should return valid album" { 28 | val asyncResult = interactor.getAlbum(ALBUM_ID) 29 | 30 | asyncResult.asSuccess { id shouldBe ALBUM_ID } 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/domain/interactor/GetTopAlbumsInteractorTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.domain.interactor 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 7 | import com.functionalkotlin.bandhookkotlin.domain.repository.AlbumRepository 8 | import com.functionalkotlin.bandhookkotlin.functional.result 9 | import com.functionalkotlin.bandhookkotlin.util.asSuccess 10 | import com.nhaarman.mockito_kotlin.mock 11 | import io.kotlintest.matchers.shouldBe 12 | import io.kotlintest.specs.StringSpec 13 | 14 | class GetTopAlbumsInteractorTest : StringSpec() { 15 | 16 | init { 17 | val album = Album( 18 | ALBUM_ID, "name", Artist("id", "name", null, null, null), "url", emptyList()) 19 | 20 | val albumRepository = mock { 21 | on { getTopAlbums(ALBUM_ID) }.thenReturn(listOf(album).result()) 22 | } 23 | 24 | val interactor = GetTopAlbumsInteractor(albumRepository) 25 | 26 | "getTopAlbums should return valid list" { 27 | val asyncResult = interactor.getTopAlbums(ALBUM_ID) 28 | 29 | asyncResult.asSuccess { get(0) shouldBe album } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/repository/AlbumRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.repository 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.AlbumNotFound 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 8 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 9 | import com.functionalkotlin.bandhookkotlin.functional.asError 10 | import com.functionalkotlin.bandhookkotlin.functional.result 11 | import com.functionalkotlin.bandhookkotlin.repository.dataset.AlbumDataSet 12 | import com.functionalkotlin.bandhookkotlin.util.asFailure 13 | import com.functionalkotlin.bandhookkotlin.util.asSuccess 14 | import com.nhaarman.mockito_kotlin.mock 15 | import io.kotlintest.matchers.shouldBe 16 | import io.kotlintest.specs.StringSpec 17 | 18 | class AlbumRepositoryImplTest : StringSpec() { 19 | 20 | init { 21 | val albumInBothDataSets = Album( 22 | ALBUM_ID_BOTH, "name", Artist(ARTIST_ID_BOTH, "name"), "url", 23 | emptyList()) 24 | 25 | val albumInSecondDataSet = Album( 26 | ALBUM_ID_SECOND, "name", Artist(ARTIST_ID_BOTH, "name"), "url", 27 | emptyList()) 28 | 29 | val albumsInBothDataSets = listOf(albumInBothDataSets) 30 | val albumsInSecondDataSet = listOf(albumInSecondDataSet) 31 | 32 | val firstAlbumDataSet = mock { 33 | on { requestTopAlbums("") }.thenReturn(TopAlbumsNotFound("").asError()) 34 | on { requestTopAlbums(ARTIST_ID_SECOND) }.thenReturn(TopAlbumsNotFound("").asError()) 35 | on { requestTopAlbums(ARTIST_ID_BOTH) }.thenReturn(albumsInBothDataSets.result()) 36 | on { requestAlbum(ALBUM_ID_BOTH) }.thenReturn(albumInBothDataSets.result()) 37 | on { requestAlbum(ALBUM_ID_SECOND) } 38 | .thenReturn(AlbumNotFound(ALBUM_ID_SECOND).asError()) 39 | } 40 | 41 | val secondAlbumDataSet = mock { 42 | on { requestTopAlbums("") }.thenReturn(TopAlbumsNotFound("").asError()) 43 | on { requestTopAlbums(ARTIST_ID_SECOND) }.thenReturn(albumsInSecondDataSet.result()) 44 | on { requestAlbum(ALBUM_ID_SECOND) }.thenReturn(albumInSecondDataSet.result()) 45 | } 46 | 47 | val albumRepository = AlbumRepositoryImpl(listOf(firstAlbumDataSet, secondAlbumDataSet)) 48 | 49 | "getAlbum in both data sets returns valid album" { 50 | albumRepository.getAlbum(ALBUM_ID_BOTH) 51 | .asSuccess { shouldBe(albumInBothDataSets) } 52 | } 53 | 54 | "getAlbum in second data set returns album" { 55 | albumRepository.getAlbum(ALBUM_ID_SECOND) 56 | .asSuccess { shouldBe(albumInSecondDataSet) } 57 | } 58 | 59 | "getTopAlbums in both data sets returns valid list" { 60 | albumRepository.getTopAlbums(ARTIST_ID_BOTH) 61 | .asSuccess { shouldBe(albumsInBothDataSets) } 62 | } 63 | 64 | "getTopAlbums in second data set returns valid list" { 65 | albumRepository.getTopAlbums(ARTIST_ID_SECOND) 66 | .asSuccess { shouldBe(albumsInSecondDataSet) } 67 | } 68 | 69 | "getTopAlbums with empty string returns error" { 70 | albumRepository.getTopAlbums("") 71 | .asFailure { shouldBe(TopAlbumsNotFound("")) } 72 | } 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/repository/Constants.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.repository 4 | 5 | const val ALBUM_ID_BOTH = "album id both" 6 | const val ALBUM_ID_SECOND = "album id second" 7 | const val ARTIST_ID_BOTH = "artist id both" 8 | const val ARTIST_ID_SECOND = "artist id second" -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/ui/entity/ImageTitleTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity 4 | 5 | import io.kotlintest.matchers.shouldBe 6 | import io.kotlintest.matchers.shouldNotBe 7 | import io.kotlintest.specs.StringSpec 8 | 9 | class ImageTitleTest : StringSpec() { 10 | 11 | init { 12 | "init with empty url returns null" { 13 | ImageTitle("id", "name", "").url shouldBe null 14 | } 15 | 16 | "init with null url returns null" { 17 | ImageTitle("id", "name", null).url shouldBe null 18 | } 19 | 20 | "init with non empty url returns non null url" { 21 | ImageTitle("id", "name", "url").url shouldNotBe null 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/ui/entity/mapper/AlbumDetailDataMapperTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity.mapper 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 7 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.album.detail.transform 8 | import io.kotlintest.matchers.shouldBe 9 | import io.kotlintest.matchers.shouldNotBe 10 | import io.kotlintest.specs.StringSpec 11 | 12 | class AlbumDetailDataMapperTest : StringSpec() { 13 | 14 | init { 15 | val album = Album("id", "name", Artist("artist id", "artist name"), "url", emptyList()) 16 | 17 | "transform returns valid album" { 18 | val albumDetail = transform(album) 19 | 20 | albumDetail shouldNotBe null 21 | 22 | albumDetail?.run { 23 | id shouldBe album.id 24 | name shouldBe album.name 25 | url shouldBe album.url 26 | tracks shouldBe album.tracks 27 | } 28 | } 29 | 30 | "transform null returns null" { 31 | transform(null) shouldBe null 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/ui/entity/mapper/ArtistDetailDataMapperTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity.mapper 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 7 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.artist.detail.transform 8 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.image.title.transformAlbums 9 | import io.kotlintest.matchers.shouldBe 10 | import io.kotlintest.specs.StringSpec 11 | 12 | class ArtistDetailDataMapperTest : StringSpec() { 13 | 14 | init { 15 | val artist = Artist("id", "name", "url", null, listOf(Album("album id", "album name"))) 16 | 17 | "transform returns valid album" { 18 | val artistDetail = transform(artist) 19 | 20 | artistDetail.run { 21 | id shouldBe artist.id 22 | name shouldBe artist.name 23 | url shouldBe artist.url 24 | albums shouldBe artist.albums?.let(::transformAlbums) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/ui/entity/mapper/ImageTitleDataMapperTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity.mapper 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 7 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.image.title.transformAlbums 8 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.image.title.transformArtists 9 | import io.kotlintest.matchers.shouldBe 10 | import io.kotlintest.specs.StringSpec 11 | 12 | class ImageTitleDataMapperTest : StringSpec() { 13 | 14 | init { 15 | 16 | "transformArtists return valid list" { 17 | val artist0 = Artist("artist id", "artist name", "artist url") 18 | val artist1 = Artist("artist id", "artist name") 19 | val artist2 = Artist("artist id", "artist name", "") 20 | 21 | val imageTitles = transformArtists(listOf(artist0, artist1, artist2)) 22 | 23 | imageTitles[0].run { 24 | id shouldBe artist0.id 25 | name shouldBe artist0.name 26 | url shouldBe artist0.url 27 | } 28 | 29 | imageTitles[1].run { 30 | id shouldBe artist1.id 31 | name shouldBe artist1.name 32 | url shouldBe artist1.url 33 | } 34 | 35 | imageTitles[2].run { 36 | id shouldBe artist2.id 37 | name shouldBe artist2.name 38 | url shouldBe null 39 | } 40 | } 41 | 42 | "transformAlbums return valid list" { 43 | val album0 = Album("album id", "album name", null, "album url", emptyList()) 44 | val album1 = Album("album id", "album name", null, null, emptyList()) 45 | val album2 = Album("album id", "album name", null, "", emptyList()) 46 | 47 | val imageTitles = transformAlbums(listOf(album0, album1, album2)) 48 | 49 | imageTitles[0].run { 50 | id shouldBe album0.id 51 | name shouldBe album0.name 52 | url shouldBe album0.url 53 | } 54 | 55 | imageTitles[1].run { 56 | id shouldBe album1.id 57 | name shouldBe album1.name 58 | url shouldBe album1.url 59 | } 60 | 61 | imageTitles[2].run { 62 | id shouldBe album2.id 63 | name shouldBe album2.name 64 | url shouldBe null 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/ui/entity/mapper/TrackDataMapperTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.entity.mapper 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Track 6 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.track.transform 7 | import io.kotlintest.matchers.shouldBe 8 | import io.kotlintest.specs.StringSpec 9 | 10 | class TrackDataMapperTest : StringSpec() { 11 | 12 | init { 13 | val track = Track("track name", 10) 14 | val tracks = listOf(track, track) 15 | 16 | "transform track return valid track" { 17 | transform(1, track).run { 18 | number shouldBe 1 19 | name shouldBe track.name 20 | duration shouldBe track.duration 21 | } 22 | } 23 | 24 | "transform tracks return valid list" { 25 | transform(tracks).run { 26 | size shouldBe 2 27 | get(0).run { 28 | name shouldBe track.name 29 | duration shouldBe track.duration 30 | } 31 | get(1).run { 32 | name shouldBe track.name 33 | duration shouldBe track.duration 34 | } 35 | } 36 | } 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/ui/presenter/ArtistPresenterTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.presenter 4 | 5 | import com.functionalkotlin.bandhookkotlin.domain.entity.Album 6 | import com.functionalkotlin.bandhookkotlin.domain.entity.Artist 7 | import com.functionalkotlin.bandhookkotlin.domain.entity.ArtistNotFound 8 | import com.functionalkotlin.bandhookkotlin.domain.entity.TopAlbumsNotFound 9 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetArtistDetailInteractor 10 | import com.functionalkotlin.bandhookkotlin.domain.interactor.GetTopAlbumsInteractor 11 | import com.functionalkotlin.bandhookkotlin.functional.asError 12 | import com.functionalkotlin.bandhookkotlin.functional.result 13 | import com.functionalkotlin.bandhookkotlin.ui.entity.ImageTitle 14 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.artist.detail.transform 15 | import com.functionalkotlin.bandhookkotlin.ui.entity.mapper.image.title.transformAlbums 16 | import com.functionalkotlin.bandhookkotlin.ui.view.ArtistView 17 | import com.nhaarman.mockito_kotlin.doReturn 18 | import com.nhaarman.mockito_kotlin.mock 19 | import io.kotlintest.specs.StringSpec 20 | import kotlinx.coroutines.experimental.runBlocking 21 | import org.mockito.Mockito.verify 22 | 23 | class ArtistPresenterTest : StringSpec() { 24 | 25 | init { 26 | val albums = listOf(Album("id", "name", null, null, emptyList())) 27 | val artist = Artist("id", "name", null, null, emptyList()) 28 | 29 | val artistView = mock() 30 | val artistDetailInteractor = mock { 31 | on { getArtist(ARTIST_ID) } doReturn artist.result() 32 | on { getArtist("") } doReturn ArtistNotFound("").asError() 33 | } 34 | val topAlbumsInteractor = mock { 35 | on { getTopAlbums(ARTIST_ID) } doReturn albums.result() 36 | on { getTopAlbums("") } doReturn TopAlbumsNotFound("").asError() 37 | } 38 | 39 | val artistPresenter = ArtistPresenter( 40 | artistView, artistDetailInteractor, topAlbumsInteractor) 41 | 42 | "onAlbumClicked should rely on view" { 43 | artistPresenter.onAlbumClicked(ImageTitle(IMAGE_ID, "name", null)) 44 | 45 | verify(artistView).navigateToAlbum(IMAGE_ID) 46 | } 47 | 48 | "init with valid artist ID calls interactor and view" { 49 | runBlocking { artistPresenter.init(ARTIST_ID) } 50 | 51 | verify(artistDetailInteractor).getArtist(ARTIST_ID) 52 | verify(artistView).showArtist(transform(artist)) 53 | verify(topAlbumsInteractor).getTopAlbums(ARTIST_ID) 54 | verify(artistView).showAlbums(transformAlbums(albums)) 55 | } 56 | 57 | "init with non valid artist ID calls interactor and view" { 58 | runBlocking { artistPresenter.init("") } 59 | 60 | verify(artistDetailInteractor).getArtist("") 61 | verify(artistView).showArtistNotFound(ArtistNotFound("")) 62 | verify(topAlbumsInteractor).getTopAlbums("") 63 | verify(artistView).showAlbumsNotFound(TopAlbumsNotFound("")) 64 | } 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/ui/presenter/Constants.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.ui.presenter 4 | 5 | const val IMAGE_ID = "image id" 6 | const val ARTIST_ID = "artist id" -------------------------------------------------------------------------------- /app/src/test/java/com/functionalkotlin/bandhookkotlin/util/Functional.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | package com.functionalkotlin.bandhookkotlin.util 4 | 5 | import com.functionalkotlin.bandhookkotlin.functional.AsyncResult 6 | import com.functionalkotlin.bandhookkotlin.functional.Failure 7 | import com.functionalkotlin.bandhookkotlin.functional.Success 8 | import com.functionalkotlin.bandhookkotlin.functional.isFailure 9 | import com.functionalkotlin.bandhookkotlin.functional.isSuccess 10 | import com.functionalkotlin.bandhookkotlin.functional.runSync 11 | import io.kotlintest.matchers.shouldBe 12 | 13 | fun AsyncResult.asSuccess(f: A.() -> Unit) { 14 | val result = runSync() 15 | result.isSuccess() shouldBe true 16 | f((result as Success).value) 17 | } 18 | 19 | fun AsyncResult.asFailure(f: E.() -> Unit) { 20 | val result = runSync() 21 | result.isFailure() shouldBe true 22 | f((result as Failure).error) 23 | } -------------------------------------------------------------------------------- /app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /art/bandhook.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunctionalKotlin/final-app/cd3d59794218f72740c2cb22a430eba26f244cea/art/bandhook.gif -------------------------------------------------------------------------------- /art/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunctionalKotlin/final-app/cd3d59794218f72740c2cb22a430eba26f244cea/art/logo.png -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | import org.gradle.kotlin.dsl.* 4 | 5 | plugins { 6 | id("io.gitlab.arturbosch.detekt").version("1.0.0.RC3") 7 | } 8 | 9 | buildscript { 10 | 11 | val config = ProjectConfiguration() 12 | 13 | repositories { 14 | jcenter() 15 | google() 16 | } 17 | 18 | dependencies { 19 | classpath(config.buildPlugins.androidGradle) 20 | classpath(config.buildPlugins.kotlinGradlePlugin) 21 | } 22 | } 23 | 24 | detekt { 25 | version = "1.0.0.RC3" 26 | profile("main", Action { 27 | input = "$projectDir" 28 | config = "$projectDir/detekt.yml" 29 | filters = ".*test.*,.*/resources/.*,.*/tmp/.*,.*/.idea/.*" 30 | }) 31 | } 32 | 33 | allprojects { 34 | repositories { 35 | jcenter() 36 | google() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | import org.gradle.kotlin.dsl.`kotlin-dsl` 4 | 5 | plugins { 6 | `kotlin-dsl` 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/ProjectConfiguration.kt: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | val kotlinVersion = "1.2.20" 4 | val androidGradleVersion = "2.3.3" 5 | 6 | // Compile dependencies 7 | val supportVersion = "26.1.0" 8 | val ankoVersion = "0.10.2" 9 | val daggerVersion = "2.11" 10 | val retrofitVersion = "2.3.0" 11 | val okhttpVersion = "3.9.0" 12 | val eventBusVersion = "2.4.1" 13 | val picassoVersion = "2.5.2" 14 | val priorityJobQueueVersion = "2.0.1" 15 | val kotlinxCoroutinesVersion = "0.14.1" 16 | 17 | // Unit tests 18 | val mockitoVersion = "2.8.47" 19 | val kotlinTestVersion = "2.0.7" 20 | val mockitoKoltinVersion = "1.5.0" 21 | 22 | class ProjectConfiguration { 23 | val buildPlugins = BuildPlugins() 24 | val android = Android() 25 | val libs = Libs() 26 | val testLibs = TestLibs() 27 | } 28 | 29 | class BuildPlugins { 30 | val androidGradle = "com.android.tools.build:gradle:$androidGradleVersion" 31 | val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 32 | } 33 | 34 | class Android { 35 | val buildToolsVersion = "26.0.2" 36 | val minSdkVersion = 19 37 | val targetSdkVersion = 26 38 | val compileSdkVersion = 26 39 | val applicationId = "com.functionalkotlin.bandhookkotlin" 40 | val versionCode = 1 41 | val versionName = "0.1" 42 | 43 | } 44 | 45 | class Libs { 46 | val kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 47 | val appcompat = "com.android.support:appcompat-v7:$supportVersion" 48 | val recyclerview = "com.android.support:recyclerview-v7:$supportVersion" 49 | val cardview = "com.android.support:cardview-v7:$supportVersion" 50 | val palette = "com.android.support:palette-v7:$supportVersion" 51 | val design = "com.android.support:design:$supportVersion" 52 | val eventbus = "de.greenrobot:eventbus:$eventBusVersion" 53 | val picasso = "com.squareup.picasso:picasso:$picassoVersion" 54 | val okhttp = "com.squareup.okhttp3:okhttp:$okhttpVersion" 55 | val okhttpInterceptor = "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" 56 | val retrofit = "com.squareup.retrofit2:retrofit:$retrofitVersion" 57 | val retrofitGson = "com.squareup.retrofit2:converter-gson:$retrofitVersion" 58 | val jobqueue = "com.birbit:android-priority-jobqueue:$priorityJobQueueVersion" 59 | val ankoSdk15 = "org.jetbrains.anko:anko-sdk15:$ankoVersion" 60 | val ankoSupport = "org.jetbrains.anko:anko-support-v4:$ankoVersion" 61 | val ankoAppcompat = "org.jetbrains.anko:anko-appcompat-v7:$ankoVersion" 62 | val ankoDesign = "org.jetbrains.anko:anko-design:$ankoVersion" 63 | val ankoCardview = "org.jetbrains.anko:anko-cardview-v7:$ankoVersion" 64 | val ankoRecyclerview = "org.jetbrains.anko:anko-recyclerview-v7:$ankoVersion" 65 | val daggerCompiler = "com.google.dagger:dagger-compiler:$daggerVersion" 66 | val dagger = "com.google.dagger:dagger:$daggerVersion" 67 | val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion" 68 | val coroutinesAndroid = 69 | "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion" 70 | } 71 | 72 | class TestLibs { 73 | val junit = "junit:junit:4.12" 74 | val mockito = "org.mockito:mockito-core:$mockitoVersion" 75 | val dexmaker = "com.google.dexmaker:dexmaker:1.2" 76 | val dexmakerMockito = "com.google.dexmaker:dexmaker-mockito:1.2" 77 | val annotations = "com.android.support:support-annotations:$supportVersion" 78 | val espresso = "com.android.support.test.espresso:espresso-core:2.2.2" 79 | val kotlinTest = "io.kotlintest:kotlintest:$kotlinTestVersion" 80 | val mockitoKotlin = "com.nhaarman:mockito-kotlin:$mockitoKoltinVersion" 81 | } 82 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | autoCorrect: true 2 | 3 | build: 4 | warningThreshold: 5 5 | failThreshold: 10 6 | weights: 7 | complexity: 2 8 | formatting: 0 9 | LongParameterList: 1 10 | comments: 0.5 11 | 12 | potential-bugs: 13 | active: true 14 | UnsafeCast: 15 | active: false 16 | DuplicateCaseInWhenExpression: 17 | active: true 18 | EqualsWithHashCodeExist: 19 | active: true 20 | ExplicitGarbageCollectionCall: 21 | active: true 22 | LateinitUsage: 23 | active: false 24 | 25 | exceptions: 26 | active: true 27 | 28 | empty-blocks: 29 | active: false 30 | 31 | complexity: 32 | active: true 33 | LabeledExpression: 34 | active: false 35 | LongMethod: 36 | threshold: 20 37 | LongParameterList: 38 | threshold: 7 39 | LargeClass: 40 | threshold: 150 41 | ComplexMethod: 42 | threshold: 10 43 | TooManyFunctions: 44 | threshold: 17 45 | ComplexCondition: 46 | threshold: 3 47 | 48 | code-smell: 49 | active: true 50 | FeatureEnvy: 51 | threshold: 0.5 52 | weight: 0.45 53 | base: 0.5 54 | 55 | formatting: 56 | active: true 57 | useTabs: false 58 | Indentation: 59 | active: true 60 | autoCorrect: true 61 | indentSize: 4 62 | ConsecutiveBlankLines: 63 | active: true 64 | autoCorrect: true 65 | MultipleSpaces: 66 | active: true 67 | autoCorrect: true 68 | SpacingAfterComma: 69 | active: true 70 | autoCorrect: true 71 | SpacingAfterKeyword: 72 | active: true 73 | autoCorrect: true 74 | SpacingAroundColon: 75 | active: true 76 | autoCorrect: true 77 | SpacingAroundCurlyBraces: 78 | active: true 79 | autoCorrect: true 80 | SpacingAroundOperator: 81 | active: true 82 | autoCorrect: true 83 | TrailingSpaces: 84 | active: true 85 | autoCorrect: true 86 | UnusedImports: 87 | active: true 88 | autoCorrect: true 89 | OptionalSemicolon: 90 | active: true 91 | autoCorrect: true 92 | OptionalUnit: 93 | active: true 94 | autoCorrect: true 95 | ExpressionBodySyntax: 96 | active: true 97 | autoCorrect: false 98 | ExpressionBodySyntaxLineBreaks: 99 | active: false 100 | autoCorrect: true 101 | OptionalReturnKeyword: 102 | active: true 103 | autoCorrect: true 104 | 105 | style: 106 | active: true 107 | WildcardImport: 108 | active: true 109 | NewLineAtEndOfFile: 110 | active: true 111 | autoCorrect: true 112 | MagicNumber: 113 | active: false 114 | MaxLineLength: 115 | active: true 116 | maxLineLength: 100 117 | excludePackageStatements: false 118 | excludeImportStatements: false 119 | NamingConventionViolation: 120 | active: true 121 | variablePattern: '^(_)?[a-z$][a-zA-Z$0-9]*$' 122 | constantPattern: '^([A-Z_]*|serialVersionUID)$' 123 | methodPattern: '^[a-z$][a-zA-Z$0-9]*$' 124 | classPattern: '[A-Z$][a-zA-Z$]*' 125 | enumEntryPattern: '^[A-Z$][a-zA-Z_$]*$' 126 | 127 | comments: 128 | active: true 129 | CommentOverPrivateMethod: 130 | active: true 131 | CommentOverPrivateProperty: 132 | active: true 133 | UndocumentedPublicClass: 134 | active: false 135 | UndocumentedPublicFunction: 136 | active: false 137 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunctionalKotlin/final-app/cd3d59794218f72740c2cb22a430eba26f244cea/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Nov 02 07:12:48 CET 2017 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.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright © FunctionalHub.com 2018. All rights reserved. 2 | 3 | include ':app' --------------------------------------------------------------------------------