4 Коміти aff8467b90 ... a87d35ef4f

Автор SHA1 Опис Дата
HELLO WORLD   Ana Sekuloski a87d35ef4f Set FlickrPhotosActivity as launcher activity, and fixed the layout of photos grid view 3 роки тому
HELLO WORLD   Ana Sekuloski 4aff05f001 Renamed MainActivity in app to AppTestActivity, show titles of 10 recent photos in the activity 3 роки тому
HELLO WORLD   Ana Sekuloski 3122409897 Added pagination support for search photos 3 роки тому
HELLO WORLD   Ana Sekuloski dfddd51d7e Added pagination support for recent photos 3 роки тому

+ 4 - 0
app/build.gradle

@@ -29,6 +29,10 @@ android {
     kotlinOptions {
         jvmTarget = '1.8'
     }
+
+    viewBinding {
+        enabled = true
+    }
 }
 
 dependencies {

+ 29 - 0
app/src/main/java/com/livelike/livelikeandroidchallenge/AppTestActivity.kt

@@ -0,0 +1,29 @@
+package com.livelike.livelikeandroidchallenge
+
+import android.os.Bundle
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import com.livelike.livelikeandroidchallenge.databinding.ActivityMainBinding
+
+class AppTestActivity : AppCompatActivity() {
+
+    private val testViewModel: FlickrServiceTestViewModel by viewModels()
+
+    private val views: ActivityMainBinding by lazy {
+        ActivityMainBinding.inflate(layoutInflater)
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(views.root)
+
+        testViewModel.photos.observe(this) { photosTitles ->
+            views.textView.text = photosTitles
+        }
+
+        lifecycleScope.launchWhenCreated {
+            testViewModel.getRecentPhotosTitles()
+        }
+    }
+}

+ 24 - 0
app/src/main/java/com/livelike/livelikeandroidchallenge/FlickrServiceTestViewModel.kt

@@ -0,0 +1,24 @@
+package com.livelike.livelikeandroidchallenge
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.livelike.flickersearchlibrary.Flickr
+import kotlinx.coroutines.launch
+
+class FlickrServiceTestViewModel : ViewModel() {
+
+    private val _photosTitles = MutableLiveData<String>()
+    val photos: LiveData<String> = _photosTitles
+
+    fun getRecentPhotosTitles() = viewModelScope.launch {
+        val flickrPhotos = Flickr.getRecentPhotos(10)
+        var photosTitles = "Recent photos titles:\n"
+        flickrPhotos.onEach { photoItem ->
+            photosTitles = photosTitles.plus(photoItem.title).plus('\n')
+        }
+        _photosTitles.postValue(photosTitles)
+    }
+
+}

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

@@ -1,24 +0,0 @@
-package com.livelike.livelikeandroidchallenge
-
-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
-
-class FlickrServiceViewModel : ViewModel() {
-
-    private val _photos = MutableLiveData<List<PhotoItem>>()
-    val photos: LiveData<List<PhotoItem>> = _photos
-
-    fun searchFlickrPhotos(searchQuery: String?) = viewModelScope.launch {
-        _photos.postValue(Flickr.search(searchQuery))
-    }
-
-    fun getRecentPhotos() = viewModelScope.launch {
-        _photos.postValue(Flickr.getRecentPhotos())
-    }
-
-}

+ 0 - 25
app/src/main/java/com/livelike/livelikeandroidchallenge/MainActivity.kt

@@ -1,25 +0,0 @@
-package com.livelike.livelikeandroidchallenge
-
-import android.os.Bundle
-import android.util.Log
-import androidx.activity.viewModels
-import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.lifecycleScope
-
-class MainActivity : AppCompatActivity() {
-
-    private val viewModel: FlickrServiceViewModel by viewModels()
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_main)
-
-        viewModel.photos.observe(this) {
-            Log.d("LiveLikeTest: ", "Received result: $it")
-        }
-
-        lifecycleScope.launchWhenCreated {
-            viewModel.getRecentPhotos()
-        }
-    }
-}

+ 2 - 2
app/src/main/res/layout/activity_main.xml

@@ -4,12 +4,12 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".MainActivity">
+    tools:context=".AppTestActivity">
 
     <TextView
+        android:id="@+id/textView"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="Hello World!"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"

+ 18 - 4
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/Flickr.kt

@@ -16,20 +16,34 @@ object Flickr {
 
     private val flickrService: FlickrService by inject(FlickrService::class.java)
 
+    private const val DEFAULT_NUMBER_OF_PHOTOS_PER_PAGE = 20
+
     /**
-     * Provides search results for given search term.
+     * Provides search results for given search term. [numOfPhotos] will be returned on given [page].
+     * By default 20 photos will be returned on the first page.
      * If no search query is provided, empty results will be returned.
      *
      * @param searchQuery [String] the search term for which photo results should be returned.
+     * @param numOfPhotos [Int] number of photos to return.
+     * @param page [Int] the page where the photos should be contained.
      * @return [List] of [Photo]s matching the given search term, or empty results if no term is provided.
      */
-    suspend fun search(searchQuery: String?) = flickrService.search(searchQuery)
+    suspend fun search(
+        searchQuery: String?, numOfPhotos: Int = DEFAULT_NUMBER_OF_PHOTOS_PER_PAGE,
+        page: Int = 1
+    ) = flickrService.search(searchQuery, numOfPhotos, page)
 
     /**
-     * Retrieves recently published photos on Flickr.
+     * Retrieves [numOfPhotos] recently published photos on Flickr on the given [page].
+     * By default, 20 photos are returned, and the default page is the first page.
      *
+     * @param numOfPhotos [Int] number of photos to return.
+     * @param page [Int] the page where photos should be contained.
      * @return [List] of recently published photos on Flickr.
      */
-    suspend fun getRecentPhotos() = flickrService.getRecentPhotos()
+    suspend fun getRecentPhotos(
+        numOfPhotos: Int = DEFAULT_NUMBER_OF_PHOTOS_PER_PAGE,
+        page: Int = 1
+    ) = flickrService.getRecentPhotos(numOfPhotos, page)
 
 }

+ 4 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/FlickrApi.kt

@@ -14,6 +14,8 @@ internal interface FlickrApi {
     @GET(".")
     suspend fun search(
         @Query(QUERY_TEXT) searchQuery: String,
+        @Query(QUERY_PER_PAGE) perPage: Int,
+        @Query(QUERY_PAGE) page: Int,
         @Query(QUERY_METHOD) method: String = SEARCH_METHOD,
         @Query(QUERY_API_KEY) apiKey: String = DEFAULT_API_KEY,
         @Query(QUERY_FORMAT) format: String = JSON_FORMAT,
@@ -22,6 +24,8 @@ internal interface FlickrApi {
 
     @GET(".")
     suspend fun getRecent(
+        @Query(QUERY_PER_PAGE) perPage: Int,
+        @Query(QUERY_PAGE) page: Int,
         @Query(QUERY_METHOD) method: String = RECENT_IMAGES_METHOD,
         @Query(QUERY_API_KEY) apiKey: String = DEFAULT_API_KEY,
         @Query(QUERY_FORMAT) format: String = JSON_FORMAT,

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

@@ -19,5 +19,7 @@ internal const val QUERY_METHOD = "method"
 internal const val SEARCH_METHOD = "flickr.photos.search"
 internal const val RECENT_IMAGES_METHOD = "flickr.photos.getRecent"
 
-// Constant Query keys for methods supported in the library
+// Constant Query keys for arguments supported in the library
 internal const val QUERY_TEXT = "text"
+internal const val QUERY_PER_PAGE = "per_page"
+internal const val QUERY_PAGE = "page"

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

@@ -5,21 +5,25 @@ import com.livelike.flickersearchlibrary.ui.model.PhotoItem
 /**
  * Interface for the service providing Flickr features.
  */
-interface FlickrService {
+internal interface FlickrService {
 
     /**
-     * Searches for photos and retrieves [List] of [PhotoItem]s.
+     * Searches for photos and retrieves [List] of [PhotoItem]s. [numOfPhotos] will be returned on given [page].
      * If no search query is provided, empty list will be returned.
      *
      * @param searchQuery [String] text query for searching photos.
+     * @param numOfPhotos [Int] number of photos to return.
+     * @param page [Int] the page where the photos should be contained.
      * @return [List] of [PhotoItem]s matching given [searchQuery] or empty list if no query is provided.
      */
-    suspend fun search(searchQuery: String?): List<PhotoItem>
+    suspend fun search(searchQuery: String?, numOfPhotos: Int, page: Int): List<PhotoItem>
 
     /**
-     * Retrieves recently published photos on Flickr.
+     * Retrieves [numOfPhotos] recently published photos on Flickr on the given [page].
      *
+     * @param numOfPhotos [Int] number of photos to return.
+     * @param page [Int] the page where the photos should be contained.
      * @return [List] of recently published photos on Flickr.
      */
-    suspend fun getRecentPhotos(): List<PhotoItem>
+    suspend fun getRecentPhotos(numOfPhotos: Int, page: Int): List<PhotoItem>
 }

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

@@ -13,10 +13,14 @@ internal class FlickrServiceLogic(private val api: FlickrApi) : FlickrService {
 
     private val cachedPhotos = mutableListOf<PhotoItem>()
 
-    override suspend fun search(searchQuery: String?): List<PhotoItem> =
-        searchQuery?.let {
+    override suspend fun search(
+        searchQuery: String?,
+        numOfPhotos: Int,
+        page: Int
+    ): List<PhotoItem> {
+        return if (!searchQuery.isNullOrBlank()) {
             val response = executeApiCall {
-                api.search(searchQuery)
+                api.search(searchQuery, numOfPhotos, page)
             }
             if (response is ApiResponse.Success) {
                 val photoItems = response.body.map {
@@ -27,11 +31,14 @@ internal class FlickrServiceLogic(private val api: FlickrApi) : FlickrService {
             }
 
             cachedPhotos
-        } ?: emptyList()
+        } else {
+            emptyList()
+        }
+    }
 
-    override suspend fun getRecentPhotos(): List<PhotoItem> {
+    override suspend fun getRecentPhotos(numOfPhotos: Int, page: Int): List<PhotoItem> {
         val response = executeApiCall {
-            api.getRecent()
+            api.getRecent(numOfPhotos, page)
         }
         return when (response) {
             is ApiResponse.Success -> response.body.map { it.toPhotoItem() }

+ 19 - 3
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/FlickrPhotosActivity.kt

@@ -6,6 +6,7 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.widget.SearchView
 import androidx.lifecycle.lifecycleScope
 import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
 import com.livelike.flickersearchlibrary.databinding.ActivityFlickrPhotosBinding
 import com.livelike.flickersearchlibrary.ui.adapter.FlickrPhotosAdapter
 
@@ -20,6 +21,7 @@ class FlickrPhotosActivity : AppCompatActivity() {
 
     private companion object {
         private const val PHOTOS_DEFAULT_COLUMN_COUNT = 3
+        private const val BOTTOM_DIRECTION = 1
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -44,12 +46,12 @@ class FlickrPhotosActivity : AppCompatActivity() {
 
     private fun setupListeners() {
         views.searchView.setOnSearchClickListener {
-            viewModel.searchPhotos(views.searchView.query.toString())
+            search(views.searchView.query.toString())
         }
 
         views.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
             override fun onQueryTextSubmit(query: String?): Boolean {
-                viewModel.searchPhotos(query)
+                search(query)
                 return true
             }
 
@@ -57,11 +59,20 @@ class FlickrPhotosActivity : AppCompatActivity() {
                 return true
             }
         })
+
+        views.gridView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+                super.onScrolled(recyclerView, dx, dy)
+                if (!recyclerView.canScrollVertically(BOTTOM_DIRECTION)) {
+                    viewModel.loadMorePhotos()
+                }
+            }
+        })
     }
 
     private fun setupObservers() {
         viewModel.photos.observe(this) {
-            photosAdapter.setItems(it)
+            photosAdapter.appendItems(it)
         }
     }
 
@@ -70,4 +81,9 @@ class FlickrPhotosActivity : AppCompatActivity() {
             viewModel.getRecentPhotos()
         }
     }
+
+    private fun search(searchQuery: String?) {
+        photosAdapter.clearItems()
+        viewModel.searchPhotos(searchQuery)
+    }
 }

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

@@ -13,11 +13,40 @@ internal class FlickrPhotosViewModel : ViewModel() {
     private val _photos = MutableLiveData<List<PhotoItem>>()
     val photos: LiveData<List<PhotoItem>> = _photos
 
+    private lateinit var visiblePhotosState: PhotosState
+
     fun getRecentPhotos() = viewModelScope.launch {
         _photos.postValue(Flickr.getRecentPhotos())
+        visiblePhotosState = PhotosState(PhotoType.RECENT, 1, null)
     }
 
     fun searchPhotos(searchQuery: String?) = viewModelScope.launch {
         _photos.postValue(Flickr.search(searchQuery))
+        visiblePhotosState = PhotosState(PhotoType.SEARCH, 1, searchQuery)
+    }
+
+    fun loadMorePhotos() = viewModelScope.launch {
+        val nextPage = visiblePhotosState.currentPage + 1
+        when (visiblePhotosState.type) {
+            PhotoType.RECENT -> _photos.postValue(Flickr.getRecentPhotos(page = nextPage))
+            PhotoType.SEARCH -> _photos.postValue(
+                Flickr.search(
+                    page = nextPage,
+                    searchQuery = visiblePhotosState.searchQuery
+                )
+            )
+        }
+        visiblePhotosState = visiblePhotosState.copy(currentPage = nextPage)
+    }
+
+    private data class PhotosState(
+        var type: PhotoType,
+        var currentPage: Int,
+        var searchQuery: String?
+    )
+
+    private enum class PhotoType {
+        RECENT,
+        SEARCH
     }
 }

+ 8 - 2
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/ui/adapter/FlickrPhotosAdapter.kt

@@ -12,8 +12,14 @@ class FlickrPhotosAdapter : RecyclerView.Adapter<FlickrPhotosAdapter.PhotoViewHo
 
     private val itemsListDiffer = AsyncListDiffer(this, PhotoItemsDiffCallback())
 
-    fun setItems(items: List<PhotoItem>) {
-        itemsListDiffer.submitList(items)
+    fun appendItems(items: List<PhotoItem>) {
+        val currentList = itemsListDiffer.currentList.toMutableList()
+        currentList.addAll(items)
+        itemsListDiffer.submitList(currentList)
+    }
+
+    fun clearItems() {
+        itemsListDiffer.submitList(emptyList())
     }
 
     override fun onCreateViewHolder(

+ 3 - 3
flickersearchlibrary/src/main/res/layout/activity_flickr_photos.xml

@@ -10,18 +10,18 @@
         android:id="@+id/searchView"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_marginVertical="16dp"
         app:iconifiedByDefault="false"
+        app:layout_constraintBottom_toTopOf="@id/gridView"
         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"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/searchView" />