Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add artist gallery feature module #8

Merged
merged 3 commits into from Nov 10, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -34,7 +34,8 @@ android {
'proguard-rules-retrofit.pro'
}
}
dynamicFeatures = [':painting']

dynamicFeatures = [':artist', ':painting']
}

dependencies {
@@ -7,12 +7,16 @@ import okhttp3.Interceptor
import okhttp3.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface HarvardArtMuseumApi {

@GET("object?$PAINTINGS_&$WITH_IMAGES_&$WITH_ARTIST_&$INC_FIELDS")
fun gallery(): Deferred<ApiPaintingsResponse>

@GET("object?$PAINTINGS_&$WITH_IMAGES_&$INC_FIELDS")
fun artistGallery(@Query("person") artistId: String): Deferred<ApiPaintingsResponse>

@GET("object/{object_id}?$INC_FIELDS")
fun painting(@Path("object_id") id: String): Deferred<ApiRecord>

@@ -49,7 +53,7 @@ data class ApiInfo(
@Json(name = "totalrecords") val totalRecords: Int,
@Json(name = "pages") val pages: Int,
@Json(name = "page") val page: Int,
@Json(name = "next") val next: String
@Json(name = "next") val next: String?
)

@JsonClass(generateAdapter = true)
@@ -3,20 +3,33 @@ package com.ataulm.artcollector
import android.content.Intent
import android.net.Uri

enum class Navigation(private val semiQualifiedActivityName: String) {
private const val SCHEME = "https"
private const val AUTHORITY = "art-collector.ataulm.com"
private const val ARTIST_GALLERY = "${BuildConfig.APPLICATION_ID}.artist.ui.ArtistActivity"
private const val PAINTING = "${BuildConfig.APPLICATION_ID}.painting.ui.PaintingActivity"

GALLERY("gallery.ui.GalleryActivity"),
PAINTING("painting.ui.PaintingActivity");
fun artistGalleryIntent(artistId: String): Intent {
val uri = Uri.Builder()
.scheme(SCHEME)
.authority(AUTHORITY)
.path(artistId)
.build()

fun viewIntent(artistId: String, paintingId: String): Intent {
val uri = Uri.Builder()
.scheme("https://")
.authority("art-collector.ataulm.com")
.path("$artistId/$paintingId")
.build()
return intent(uri, ARTIST_GALLERY)
}

fun paintingIntent(artistId: String, paintingId: String): Intent {
val uri = Uri.Builder()
.scheme(SCHEME)
.authority(AUTHORITY)
.path("$artistId/$paintingId")
.build()

return intent(uri, PAINTING)
}

return Intent(Intent.ACTION_VIEW)
.setClassName(BuildConfig.APPLICATION_ID, "${BuildConfig.APPLICATION_ID}.$semiQualifiedActivityName")
.setData(uri)
}
private fun intent(uri: Uri, componentName: String): Intent {
return Intent(Intent.ACTION_VIEW)
.setClassName(BuildConfig.APPLICATION_ID, componentName)
Copy link
Owner Author

@ataulm ataulm Nov 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should just be able to ditch this line when we add the correct intent filters in the manifest 🤔

.setData(uri)
}
@@ -5,10 +5,11 @@ import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.GridLayoutManager
import com.ataulm.artcollector.DataObserver
import com.ataulm.artcollector.EventObserver
import com.ataulm.artcollector.Navigation
import com.ataulm.artcollector.R
import com.ataulm.artcollector.artistGalleryIntent
import com.ataulm.artcollector.gallery.domain.Gallery
import com.ataulm.artcollector.gallery.injectDependencies
import com.ataulm.artcollector.paintingIntent
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_gallery.*
import javax.inject.Inject
@@ -26,19 +27,25 @@ class GalleryActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gallery)

val galleryAdapter = GalleryAdapter(picasso) { viewModel.onClick(it) }
recyclerView.apply {
adapter = galleryAdapter
layoutManager = GridLayoutManager(this@GalleryActivity, 2)
}
val adapter = GalleryAdapter(
picasso,
{ viewModel.onClick(it) },
{ viewModel.onClickArtist(it) }
)

recyclerView.adapter = adapter
recyclerView.layoutManager = GridLayoutManager(this@GalleryActivity, 2)

viewModel.gallery.observe(this, DataObserver<Gallery> { gallery ->
galleryAdapter.submitList(gallery)
adapter.submitList(gallery)
})

viewModel.events.observe(this, EventObserver {
val painting = it.painting
startActivity(Navigation.PAINTING.viewIntent(painting.artist.id, painting.id))
val intent = when (it) {
is NavigateToArtistGallery -> artistGalleryIntent(it.artist.id)
is NavigateToPainting -> paintingIntent(it.painting.artist.id, it.painting.id)
}
startActivity(intent)
})
}
}
@@ -7,18 +7,20 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.ataulm.artcollector.R
import com.ataulm.artcollector.gallery.domain.Artist
import com.ataulm.artcollector.gallery.domain.Painting
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.itemview_painting.view.*

internal class GalleryAdapter constructor(
private val picasso: Picasso,
private val onClick: (Painting) -> Unit
private val onClick: (Painting) -> Unit,
private val onClickArtist: (Artist) -> Unit
) : ListAdapter<Painting, GalleryAdapter.PaintingViewHolder>(PaintingDiffer) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaintingViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_painting, parent, false)
return PaintingViewHolder(picasso, onClick, view)
return PaintingViewHolder(picasso, onClick, onClickArtist, view)
}

override fun onBindViewHolder(viewHolder: PaintingViewHolder, position: Int) = viewHolder.bind(getItem(position))
@@ -31,12 +33,14 @@ internal class GalleryAdapter constructor(
internal class PaintingViewHolder(
private val picasso: Picasso,
private val onClick: (Painting) -> Unit,
private val onClickArtist: (Artist) -> Unit,
view: View
) : RecyclerView.ViewHolder(view) {

fun bind(item: Painting) {
itemView.setOnClickListener { onClick(item) }
itemView.artistTextView.text = item.artist.name
itemView.artistTextView.setOnClickListener { onClickArtist(item.artist) }
picasso.load(item.imageUrl).into(itemView.imageView)
}
}
@@ -4,6 +4,7 @@ import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel
import com.ataulm.artcollector.Event
import com.ataulm.artcollector.gallery.domain.Artist
import com.ataulm.artcollector.gallery.domain.Gallery
import com.ataulm.artcollector.gallery.domain.GetGalleryUseCase
import com.ataulm.artcollector.gallery.domain.Painting
@@ -21,8 +22,8 @@ internal class PaintingsViewModel @Inject constructor(
private val _gallery = MutableLiveData<Gallery>()
val gallery: LiveData<Gallery> = _gallery

private val _events = MutableLiveData<Event<NavigateToPainting>>()
val events: LiveData<Event<NavigateToPainting>>
private val _events = MutableLiveData<Event<NavigateCommand>>()
val events: LiveData<Event<NavigateCommand>>
get() = _events

private val parentJob = Job()
@@ -39,10 +40,16 @@ internal class PaintingsViewModel @Inject constructor(
_events.value = Event(NavigateToPainting(painting))
}

fun onClickArtist(artist: Artist) {
_events.value = Event(NavigateToArtistGallery(artist))
}

override fun onCleared() {
super.onCleared()
parentJob.cancel()
}
}

internal data class NavigateToPainting(val painting: Painting)
internal sealed class NavigateCommand
internal data class NavigateToPainting(val painting: Painting) : NavigateCommand()
internal data class NavigateToArtistGallery(val artist: Artist) : NavigateCommand()
@@ -16,6 +16,8 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:color/white"
android:gravity="center_vertical" />
android:gravity="center_vertical"
android:minHeight="48dp"
android:padding="8dp" />

</FrameLayout>
@@ -0,0 +1,14 @@
apply plugin: 'com.android.dynamic-feature'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
compileSdkVersion versions.androidSdk.compile
}

dependencies {
implementation project(':app')

kapt libraries.daggerCompiler
}
@@ -0,0 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.ataulm.artcollector.artist">

<dist:module
dist:onDemand="false"
dist:title="@string/title_artist">
<dist:fusing dist:include="true" />
</dist:module>

<application>
<activity android:name=".ui.ArtistActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
</application>

</manifest>
@@ -0,0 +1,35 @@
package com.ataulm.artcollector.artist

import com.ataulm.artcollector.ApplicationComponent
import com.ataulm.artcollector.ArtCollectorApplication
import com.ataulm.artcollector.artist.domain.ArtistId
import com.ataulm.artcollector.artist.ui.ArtistActivity
import dagger.BindsInstance
import dagger.Component

@Component(modules = [ArtistModule::class], dependencies = [ApplicationComponent::class])
internal interface ArtistComponent {

fun inject(activity: ArtistActivity)

@Component.Builder
interface Builder {

@BindsInstance
fun activity(activity: ArtistActivity): Builder

fun withParent(component: ApplicationComponent): Builder

@BindsInstance
fun with(artistId: ArtistId): Builder

fun build(): ArtistComponent
}
}

internal fun ArtistActivity.injectDependencies(artistId: ArtistId) = DaggerArtistComponent.builder()
.withParent(ArtCollectorApplication.component(this))
.activity(this)
.with(artistId)
.build()
.inject(this)
@@ -0,0 +1,25 @@
package com.ataulm.artcollector.artist

import android.arch.lifecycle.ViewModelProviders
import com.ataulm.artcollector.artist.data.AndroidArtistRepository
import com.ataulm.artcollector.artist.domain.ArtistRepository
import com.ataulm.artcollector.artist.ui.ArtistActivity
import com.ataulm.artcollector.artist.ui.ArtistViewModel
import com.ataulm.artcollector.artist.ui.ArtistViewModelFactory
import dagger.Module
import dagger.Provides

@Module
internal object ArtistModule {

@JvmStatic
@Provides
fun artistRepository(artistRepository: AndroidArtistRepository): ArtistRepository {
return artistRepository
}

@JvmStatic
@Provides
fun viewModel(activity: ArtistActivity, viewModelFactory: ArtistViewModelFactory) =
ViewModelProviders.of(activity, viewModelFactory).get(ArtistViewModel::class.java)
}
@@ -0,0 +1,33 @@
package com.ataulm.artcollector.artist.data

import com.ataulm.artcollector.ApiRecord
import com.ataulm.artcollector.HarvardArtMuseumApi
import com.ataulm.artcollector.artist.domain.Artist
import com.ataulm.artcollector.artist.domain.ArtistId
import com.ataulm.artcollector.artist.domain.ArtistRepository
import com.ataulm.artcollector.artist.domain.Gallery
import com.ataulm.artcollector.artist.domain.Painting
import javax.inject.Inject

internal class AndroidArtistRepository @Inject constructor(
private val harvardArtMuseumApi: HarvardArtMuseumApi,
private val artistId: ArtistId
) : ArtistRepository {

override suspend fun artistGallery(): Gallery {
val paintings = harvardArtMuseumApi.artistGallery(artistId.value).await().records
.map { it.toPainting() }
return Gallery(paintings)
}

private fun ApiRecord.toPainting(): Painting {
val apiPerson = people.first()
return Painting(
id.toString(),
title,
description,
primaryImageUrl,
Artist(apiPerson.personId.toString(), apiPerson.name)
Copy link
Owner Author

@ataulm ataulm Nov 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably don't need the Artist in this module - it'll be the same for each one I think

)
}
}
@@ -0,0 +1,6 @@
package com.ataulm.artcollector.artist.domain

internal interface ArtistRepository {

suspend fun artistGallery(): Gallery
}
@@ -0,0 +1,10 @@
package com.ataulm.artcollector.artist.domain

import javax.inject.Inject

internal class GetArtistGalleryUseCase @Inject constructor(
private val repository: ArtistRepository
) {

suspend operator fun invoke() = repository.artistGallery()
}
@@ -0,0 +1,15 @@
package com.ataulm.artcollector.artist.domain

internal class Gallery(collection: Collection<Painting>) : ArrayList<Painting>(collection)

internal data class Painting(
val id: String,
val title: String,
val description: String?,
val imageUrl: String?, // nullable because https://github.com/harvardartmuseums/api-docs/issues/6
val artist: Artist
)

internal data class Artist(val id: String, val name: String)

internal data class ArtistId(val value: String)