Forráskód Böngészése

Added basic UI for showing photos in the search library, changed service to provide photo items

Ana Sekuloski 3 éve
szülő
commit
aff8467b90
17 módosított fájl, 257 hozzáadás és 17 törlés
  1. 1 1
      app/src/main/AndroidManifest.xml
  2. 3 3
      app/src/main/java/com/livelike/livelikeandroidchallenge/FlickrServiceViewModel.kt
  3. 17 0
      flickersearchlibrary/build.gradle
  4. 6 0
      flickersearchlibrary/src/main/AndroidManifest.xml
  5. 7 2
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/Photo.kt
  6. 1 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/utils/Constants.kt
  7. 7 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/utils/PhotoUrlUtil.kt
  8. 5 5
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/service/FlickrService.kt
  9. 9 6
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/service/logic/FlickrServiceLogic.kt
  10. 73 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/FlickrPhotosActivity.kt
  11. 23 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/FlickrPhotosViewModel.kt
  12. 49 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/adapter/FlickrPhotosAdapter.kt
  13. 12 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/adapter/PhotoItemsDiffCallback.kt
  14. 3 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/model/PhotoItem.kt
  15. 29 0
      flickersearchlibrary/src/main/res/layout/activity_flickr_photos.xml
  16. 8 0
      flickersearchlibrary/src/main/res/layout/layout_photo_item.xml
  17. 4 0
      flickersearchlibrary/src/main/res/values/strings.xml

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -12,7 +12,7 @@
         android:supportsRtl="true"
         android:theme="@style/Theme.LiveLikeAndroidChallenge">
         <activity
-            android:name=".MainActivity"
+            android:name="com.livelike.flickersearchlibrary.ui.FlickrPhotosActivity"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />

+ 3 - 3
app/src/main/java/com/livelike/livelikeandroidchallenge/FlickrServiceViewModel.kt

@@ -5,13 +5,13 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import com.livelike.flickersearchlibrary.Flickr
-import com.livelike.flickersearchlibrary.api.model.Photo
+import com.livelike.flickersearchlibrary.ui.model.PhotoItem
 import kotlinx.coroutines.launch
 
 class FlickrServiceViewModel : ViewModel() {
 
-    private val _photos = MutableLiveData<List<Photo>>()
-    val photos: LiveData<List<Photo>> = _photos
+    private val _photos = MutableLiveData<List<PhotoItem>>()
+    val photos: LiveData<List<PhotoItem>> = _photos
 
     fun searchFlickrPhotos(searchQuery: String?) = viewModelScope.launch {
         _photos.postValue(Flickr.search(searchQuery))

+ 17 - 0
flickersearchlibrary/build.gradle

@@ -24,6 +24,11 @@ android {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }
+
+    viewBinding {
+        enabled = true
+    }
+
 }
 
 dependencies {
@@ -36,6 +41,18 @@ dependencies {
     // Koin for dependencies
     implementation "io.insert-koin:koin-android:3.1.5"
 
+    // Android
+    implementation 'androidx.appcompat:appcompat:1.4.1'
+    implementation 'com.google.android.material:material:1.5.0'
+    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+
+    // View model lifecycle
+    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
+    implementation "androidx.fragment:fragment-ktx:1.4.1"
+
+    // Glide Image loading library
+    implementation "com.github.bumptech.glide:glide:4.12.0"
+
     androidTestImplementation 'androidx.test.ext:junit:1.1.3'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
 }

+ 6 - 0
flickersearchlibrary/src/main/AndroidManifest.xml

@@ -4,4 +4,10 @@
 
     <uses-permission android:name="android.permission.INTERNET" />
 
+    <application>
+        <activity
+            android:name=".ui.FlickrPhotosActivity"
+            android:exported="true" />
+    </application>
+
 </manifest>

+ 7 - 2
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/Photo.kt

@@ -1,8 +1,10 @@
 package com.livelike.flickersearchlibrary.api.model
 
+import com.livelike.flickersearchlibrary.api.utils.createPhotoUrl
+import com.livelike.flickersearchlibrary.ui.model.PhotoItem
 import com.squareup.moshi.Json
 
-data class Photo(
+internal data class Photo(
     @Json(name = "id")
     val id: String,
     @Json(name = "owner")
@@ -21,4 +23,7 @@ data class Photo(
     val isFriend: Int,
     @Json(name = "isfamily")
     val isFamily: Int
-)
+) {
+    fun toPhotoItem() = PhotoItem(createPhotoUrl(server, id, secret), title)
+}
+

+ 1 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/utils/Constants.kt

@@ -2,6 +2,7 @@ package com.livelike.flickersearchlibrary.api.utils
 
 // Base URL for the Flickr API
 internal const val BASE_URL = "https://www.flickr.com/services/rest/"
+internal const val PHOTO_BASE_URL = "https://live.staticflickr.com/"
 
 // Constant values for the FlickrApi interface
 internal const val DEFAULT_API_KEY = "3976fde3792b699fbcda31f52e1cb306"

+ 7 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/utils/PhotoUrlUtil.kt

@@ -0,0 +1,7 @@
+package com.livelike.flickersearchlibrary.api.utils
+
+/**
+ * Creates photo URL string from given parameters. The photo is with small size.
+ */
+internal fun createPhotoUrl(serverId: String, photoId: String, secret: String) =
+    "$PHOTO_BASE_URL/$serverId/${photoId}_${secret}_t.jpg"

+ 5 - 5
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/service/FlickrService.kt

@@ -1,6 +1,6 @@
 package com.livelike.flickersearchlibrary.service
 
-import com.livelike.flickersearchlibrary.api.model.Photo
+import com.livelike.flickersearchlibrary.ui.model.PhotoItem
 
 /**
  * Interface for the service providing Flickr features.
@@ -8,18 +8,18 @@ import com.livelike.flickersearchlibrary.api.model.Photo
 interface FlickrService {
 
     /**
-     * Searches for photos and retrieves [List] of [Photo]s.
+     * Searches for photos and retrieves [List] of [PhotoItem]s.
      * If no search query is provided, empty list will be returned.
      *
      * @param searchQuery [String] text query for searching photos.
-     * @return [List] of [Photo]s matching given [searchQuery] or empty list if no query is provided.
+     * @return [List] of [PhotoItem]s matching given [searchQuery] or empty list if no query is provided.
      */
-    suspend fun search(searchQuery: String?): List<Photo>
+    suspend fun search(searchQuery: String?): List<PhotoItem>
 
     /**
      * Retrieves recently published photos on Flickr.
      *
      * @return [List] of recently published photos on Flickr.
      */
-    suspend fun getRecentPhotos(): List<Photo>
+    suspend fun getRecentPhotos(): List<PhotoItem>
 }

+ 9 - 6
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/service/logic/FlickrServiceLogic.kt

@@ -1,37 +1,40 @@
 package com.livelike.flickersearchlibrary.service.logic
 
 import com.livelike.flickersearchlibrary.api.FlickrApi
-import com.livelike.flickersearchlibrary.api.model.Photo
 import com.livelike.flickersearchlibrary.api.model.response.ApiResponse
 import com.livelike.flickersearchlibrary.api.utils.executeApiCall
 import com.livelike.flickersearchlibrary.service.FlickrService
+import com.livelike.flickersearchlibrary.ui.model.PhotoItem
 
 /**
  * Implementation of [FlickrService].
  */
 internal class FlickrServiceLogic(private val api: FlickrApi) : FlickrService {
 
-    private val cachedPhotos = mutableListOf<Photo>()
+    private val cachedPhotos = mutableListOf<PhotoItem>()
 
-    override suspend fun search(searchQuery: String?) =
+    override suspend fun search(searchQuery: String?): List<PhotoItem> =
         searchQuery?.let {
             val response = executeApiCall {
                 api.search(searchQuery)
             }
             if (response is ApiResponse.Success) {
+                val photoItems = response.body.map {
+                    it.toPhotoItem()
+                }
                 cachedPhotos.clear()
-                cachedPhotos.addAll(response.body)
+                cachedPhotos.addAll(photoItems)
             }
 
             cachedPhotos
         } ?: emptyList()
 
-    override suspend fun getRecentPhotos(): List<Photo> {
+    override suspend fun getRecentPhotos(): List<PhotoItem> {
         val response = executeApiCall {
             api.getRecent()
         }
         return when (response) {
-            is ApiResponse.Success -> response.body
+            is ApiResponse.Success -> response.body.map { it.toPhotoItem() }
             is ApiResponse.Error -> emptyList()
         }
     }

+ 73 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/FlickrPhotosActivity.kt

@@ -0,0 +1,73 @@
+package com.livelike.flickersearchlibrary.ui
+
+import android.os.Bundle
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SearchView
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.GridLayoutManager
+import com.livelike.flickersearchlibrary.databinding.ActivityFlickrPhotosBinding
+import com.livelike.flickersearchlibrary.ui.adapter.FlickrPhotosAdapter
+
+class FlickrPhotosActivity : AppCompatActivity() {
+
+    private val viewModel: FlickrPhotosViewModel by viewModels()
+
+    private val views: ActivityFlickrPhotosBinding by lazy {
+        ActivityFlickrPhotosBinding.inflate(layoutInflater)
+    }
+    private val photosAdapter = FlickrPhotosAdapter()
+
+    private companion object {
+        private const val PHOTOS_DEFAULT_COLUMN_COUNT = 3
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(views.root)
+        setupViews()
+        setupListeners()
+        setupObservers()
+        initPhotosList()
+    }
+
+    private fun setupViews() {
+        views.gridView.apply {
+            adapter = photosAdapter
+            layoutManager = GridLayoutManager(
+                this@FlickrPhotosActivity,
+                PHOTOS_DEFAULT_COLUMN_COUNT
+            )
+        }
+        views.searchView.requestFocus()
+    }
+
+    private fun setupListeners() {
+        views.searchView.setOnSearchClickListener {
+            viewModel.searchPhotos(views.searchView.query.toString())
+        }
+
+        views.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+            override fun onQueryTextSubmit(query: String?): Boolean {
+                viewModel.searchPhotos(query)
+                return true
+            }
+
+            override fun onQueryTextChange(newText: String?): Boolean {
+                return true
+            }
+        })
+    }
+
+    private fun setupObservers() {
+        viewModel.photos.observe(this) {
+            photosAdapter.setItems(it)
+        }
+    }
+
+    private fun initPhotosList() {
+        lifecycleScope.launchWhenCreated {
+            viewModel.getRecentPhotos()
+        }
+    }
+}

+ 23 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/FlickrPhotosViewModel.kt

@@ -0,0 +1,23 @@
+package com.livelike.flickersearchlibrary.ui
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.livelike.flickersearchlibrary.Flickr
+import com.livelike.flickersearchlibrary.ui.model.PhotoItem
+import kotlinx.coroutines.launch
+
+internal class FlickrPhotosViewModel : ViewModel() {
+
+    private val _photos = MutableLiveData<List<PhotoItem>>()
+    val photos: LiveData<List<PhotoItem>> = _photos
+
+    fun getRecentPhotos() = viewModelScope.launch {
+        _photos.postValue(Flickr.getRecentPhotos())
+    }
+
+    fun searchPhotos(searchQuery: String?) = viewModelScope.launch {
+        _photos.postValue(Flickr.search(searchQuery))
+    }
+}

+ 49 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/adapter/FlickrPhotosAdapter.kt

@@ -0,0 +1,49 @@
+package com.livelike.flickersearchlibrary.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.AsyncListDiffer
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.livelike.flickersearchlibrary.databinding.LayoutPhotoItemBinding
+import com.livelike.flickersearchlibrary.ui.model.PhotoItem
+
+class FlickrPhotosAdapter : RecyclerView.Adapter<FlickrPhotosAdapter.PhotoViewHolder>() {
+
+    private val itemsListDiffer = AsyncListDiffer(this, PhotoItemsDiffCallback())
+
+    fun setItems(items: List<PhotoItem>) {
+        itemsListDiffer.submitList(items)
+    }
+
+    override fun onCreateViewHolder(
+        parent: ViewGroup,
+        viewType: Int
+    ): FlickrPhotosAdapter.PhotoViewHolder {
+        val view = LayoutPhotoItemBinding.inflate(
+            LayoutInflater.from(parent.context),
+            parent,
+            false
+        )
+        return PhotoViewHolder(view)
+    }
+
+    override fun onBindViewHolder(holder: FlickrPhotosAdapter.PhotoViewHolder, position: Int) {
+        val item = itemsListDiffer.currentList[position]
+        holder.bind(item)
+    }
+
+    override fun getItemCount() = itemsListDiffer.currentList.size
+
+    inner class PhotoViewHolder(private val view: LayoutPhotoItemBinding) :
+        RecyclerView.ViewHolder(view.root) {
+
+        fun bind(photoItem: PhotoItem) {
+            Glide
+                .with(view.root)
+                .load(photoItem.imageUrl)
+                .centerCrop()
+                .into(view.imageView)
+        }
+    }
+}

+ 12 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/adapter/PhotoItemsDiffCallback.kt

@@ -0,0 +1,12 @@
+package com.livelike.flickersearchlibrary.ui.adapter
+
+import androidx.recyclerview.widget.DiffUtil
+import com.livelike.flickersearchlibrary.ui.model.PhotoItem
+
+class PhotoItemsDiffCallback : DiffUtil.ItemCallback<PhotoItem>() {
+
+    override fun areItemsTheSame(oldItem: PhotoItem, newItem: PhotoItem) = oldItem == newItem
+
+    override fun areContentsTheSame(oldItem: PhotoItem, newItem: PhotoItem) =
+        oldItem.imageUrl == newItem.imageUrl && oldItem.title == newItem.title
+}

+ 3 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/model/PhotoItem.kt

@@ -0,0 +1,3 @@
+package com.livelike.flickersearchlibrary.ui.model
+
+data class PhotoItem(val imageUrl: String, val title: String)

+ 29 - 0
flickersearchlibrary/src/main/res/layout/activity_flickr_photos.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ui.FlickrPhotosActivity">
+
+    <androidx.appcompat.widget.SearchView
+        android:id="@+id/searchView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginVertical="16dp"
+        app:iconifiedByDefault="false"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/gridView"
+        app:queryHint="@string/search_photos_text" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/gridView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/searchView" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 8 - 0
flickersearchlibrary/src/main/res/layout/layout_photo_item.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/imageView"
+    android:layout_width="150dp"
+    android:layout_height="150dp"
+    android:contentDescription="@string/flickr_photo_content_description">
+
+</ImageView>

+ 4 - 0
flickersearchlibrary/src/main/res/values/strings.xml

@@ -0,0 +1,4 @@
+<resources>
+    <string name="search_photos_text">Search photos</string>
+    <string name="flickr_photo_content_description">Flickr photo</string>
+</resources>