10 Commits b138994d9c ... 3c792fe3be

Autor SHA1 Mensagem Data
HELLO WORLD   Ana Sekuloski 3c792fe3be Added ApiManager for executing API calls and refactored FlickrServiceLogic há 3 anos atrás
HELLO WORLD   Ana Sekuloski 41f0096252 Added function for returning Flickr recent photos há 3 anos atrás
HELLO WORLD   Ana Sekuloski 9bff5aba33 Added possibility to provide nullable search query in FlickrService há 3 anos atrás
HELLO WORLD   Ana Sekuloski c6e786e8c6 Refactored FlickrServiceLogic to handle exceptions há 3 anos atrás
HELLO WORLD   Ana Sekuloski d2db92ce49 Return list of photos instead of PhotosPage data, implemented local cache of results in FlickrServiceLogic há 3 anos atrás
HELLO WORLD   Ana Sekuloski 28d1946d2d Added and use Flickr search library in the app, and fixed found problems in the library há 3 anos atrás
HELLO WORLD   Ana Sekuloski a4fbc423a4 Added Flickr object for service features, and refactored dependencies há 3 anos atrás
HELLO WORLD   Ana Sekuloski 2e952e0f87 Added FlickrService interface, implementation and dependencies for it há 3 anos atrás
HELLO WORLD   Ana Sekuloski ed8295a2d3 Added FlickrApi interfaces and models for search response há 3 anos atrás
HELLO WORLD   Ana Sekuloski 3458ad2399 Updated dependencies and Gradle to latest versions há 3 anos atrás
19 ficheiros alterados com 355 adições e 15 exclusões
  1. 12 5
      app/build.gradle
  2. 2 0
      app/src/main/AndroidManifest.xml
  3. 24 0
      app/src/main/java/com/livelike/livelikeandroidchallenge/FlickrServiceViewModel.kt
  4. 15 1
      app/src/main/java/com/livelike/livelikeandroidchallenge/MainActivity.kt
  5. 2 2
      build.gradle
  6. 10 6
      flickersearchlibrary/build.gradle
  7. 2 0
      flickersearchlibrary/src/main/AndroidManifest.xml
  8. 35 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/Flickr.kt
  9. 30 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/FlickrApi.kt
  10. 24 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/Photo.kt
  11. 16 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/PhotosPage.kt
  12. 11 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/response/ApiResponse.kt
  13. 9 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/response/FlickrResponse.kt
  14. 33 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/utils/ApiManager.kt
  15. 22 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/utils/Constants.kt
  16. 44 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/di/Dependencies.kt
  17. 25 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/service/FlickrService.kt
  18. 38 0
      flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/service/logic/FlickrServiceLogic.kt
  19. 1 1
      gradle/wrapper/gradle-wrapper.properties

+ 12 - 5
app/build.gradle

@@ -4,7 +4,7 @@ plugins {
 }
 }
 
 
 android {
 android {
-    compileSdk 30
+    compileSdk 31
 
 
     defaultConfig {
     defaultConfig {
         applicationId "com.livelike.livelikeandroidchallenge"
         applicationId "com.livelike.livelikeandroidchallenge"
@@ -34,10 +34,17 @@ android {
 dependencies {
 dependencies {
 
 
     implementation 'androidx.core:core-ktx:1.7.0'
     implementation 'androidx.core:core-ktx:1.7.0'
-    implementation 'androidx.appcompat:appcompat:1.4.0'
-    implementation 'com.google.android.material:material:1.4.0'
-    implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
-    testImplementation 'junit:junit:4.+'
+    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"
+
+    // Flickr search library
+    implementation project(path: ':flickersearchlibrary')
+
     androidTestImplementation 'androidx.test.ext:junit:1.1.3'
     androidTestImplementation 'androidx.test.ext:junit:1.1.3'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
 }
 }

+ 2 - 0
app/src/main/AndroidManifest.xml

@@ -2,6 +2,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.livelike.livelikeandroidchallenge">
     package="com.livelike.livelikeandroidchallenge">
 
 
+    <uses-permission android:name="android.permission.INTERNET" />
+
     <application
     <application
         android:allowBackup="true"
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:icon="@mipmap/ic_launcher"

+ 24 - 0
app/src/main/java/com/livelike/livelikeandroidchallenge/FlickrServiceViewModel.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 com.livelike.flickersearchlibrary.api.model.Photo
+import kotlinx.coroutines.launch
+
+class FlickrServiceViewModel : ViewModel() {
+
+    private val _photos = MutableLiveData<List<Photo>>()
+    val photos: LiveData<List<Photo>> = _photos
+
+    fun searchFlickrPhotos(searchQuery: String?) = viewModelScope.launch {
+        _photos.postValue(Flickr.search(searchQuery))
+    }
+
+    fun getRecentPhotos() = viewModelScope.launch {
+        _photos.postValue(Flickr.getRecentPhotos())
+    }
+
+}

+ 15 - 1
app/src/main/java/com/livelike/livelikeandroidchallenge/MainActivity.kt

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

+ 2 - 2
build.gradle

@@ -5,8 +5,8 @@ buildscript {
         mavenCentral()
         mavenCentral()
     }
     }
     dependencies {
     dependencies {
-        classpath "com.android.tools.build:gradle:7.0.3"
-        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0"
+        classpath 'com.android.tools.build:gradle:7.1.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
 
 
         // NOTE: Do not place your application dependencies here; they belong
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
         // in the individual module build.gradle files

+ 10 - 6
flickersearchlibrary/build.gradle

@@ -1,15 +1,14 @@
 plugins {
 plugins {
     id 'com.android.library'
     id 'com.android.library'
+    id 'kotlin-android'
 }
 }
 
 
 android {
 android {
-    compileSdk 30
+    compileSdk 31
 
 
     defaultConfig {
     defaultConfig {
         minSdk 21
         minSdk 21
         targetSdk 30
         targetSdk 30
-        versionCode 1
-        versionName "1.0"
 
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         consumerProguardFiles "consumer-rules.pro"
         consumerProguardFiles "consumer-rules.pro"
@@ -29,9 +28,14 @@ android {
 
 
 dependencies {
 dependencies {
 
 
-    implementation 'androidx.appcompat:appcompat:1.4.0'
-    implementation 'com.google.android.material:material:1.4.0'
-    testImplementation 'junit:junit:4.+'
+    // Networking libraries (Retrofit)
+    implementation "com.squareup.retrofit2:retrofit:2.9.0"
+    implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
+    implementation "com.squareup.moshi:moshi-kotlin:1.13.0"
+
+    // Koin for dependencies
+    implementation "io.insert-koin:koin-android:3.1.5"
+
     androidTestImplementation 'androidx.test.ext:junit:1.1.3'
     androidTestImplementation 'androidx.test.ext:junit:1.1.3'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
 }
 }

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

@@ -2,4 +2,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.livelike.flickersearchlibrary">
     package="com.livelike.flickersearchlibrary">
 
 
+    <uses-permission android:name="android.permission.INTERNET" />
+
 </manifest>
 </manifest>

+ 35 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/Flickr.kt

@@ -0,0 +1,35 @@
+package com.livelike.flickersearchlibrary
+
+import com.livelike.flickersearchlibrary.api.model.Photo
+import com.livelike.flickersearchlibrary.di.Dependencies
+import com.livelike.flickersearchlibrary.service.FlickrService
+import org.koin.java.KoinJavaComponent.inject
+
+/**
+ * Defines Flickr service features.
+ */
+object Flickr {
+
+    init {
+        Dependencies.init()
+    }
+
+    private val flickrService: FlickrService by inject(FlickrService::class.java)
+
+    /**
+     * Provides search results for given search term.
+     * If no search query is provided, empty results will be returned.
+     *
+     * @param searchQuery [String] the search term for which photo results should be returned.
+     * @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)
+
+    /**
+     * Retrieves recently published photos on Flickr.
+     *
+     * @return [List] of recently published photos on Flickr.
+     */
+    suspend fun getRecentPhotos() = flickrService.getRecentPhotos()
+
+}

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

@@ -0,0 +1,30 @@
+package com.livelike.flickersearchlibrary.api
+
+import com.livelike.flickersearchlibrary.api.model.response.FlickrResponse
+import com.livelike.flickersearchlibrary.api.utils.*
+import retrofit2.Response
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+/**
+ * API interface for Flickr services.
+ */
+internal interface FlickrApi {
+
+    @GET(".")
+    suspend fun search(
+        @Query(QUERY_TEXT) searchQuery: String,
+        @Query(QUERY_METHOD) method: String = SEARCH_METHOD,
+        @Query(QUERY_API_KEY) apiKey: String = DEFAULT_API_KEY,
+        @Query(QUERY_FORMAT) format: String = JSON_FORMAT,
+        @Query(QUERY_NO_JSON_CALLBACK) noJsonCallback: Boolean = NO_JSON_CALLBACK,
+    ): Response<FlickrResponse>
+
+    @GET(".")
+    suspend fun getRecent(
+        @Query(QUERY_METHOD) method: String = RECENT_IMAGES_METHOD,
+        @Query(QUERY_API_KEY) apiKey: String = DEFAULT_API_KEY,
+        @Query(QUERY_FORMAT) format: String = JSON_FORMAT,
+        @Query(QUERY_NO_JSON_CALLBACK) noJsonCallback: Boolean = NO_JSON_CALLBACK,
+    ): Response<FlickrResponse>
+}

+ 24 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/Photo.kt

@@ -0,0 +1,24 @@
+package com.livelike.flickersearchlibrary.api.model
+
+import com.squareup.moshi.Json
+
+data class Photo(
+    @Json(name = "id")
+    val id: String,
+    @Json(name = "owner")
+    val owner: String,
+    @Json(name = "secret")
+    val secret: String,
+    @Json(name = "server")
+    val server: String,
+    @Json(name = "farm")
+    val farm: Int,
+    @Json(name = "title")
+    val title: String,
+    @Json(name = "ispublic")
+    val isPublic: Int,
+    @Json(name = "isfriend")
+    val isFriend: Int,
+    @Json(name = "isfamily")
+    val isFamily: Int
+)

+ 16 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/PhotosPage.kt

@@ -0,0 +1,16 @@
+package com.livelike.flickersearchlibrary.api.model
+
+import com.squareup.moshi.Json
+
+internal data class PhotosPage(
+    @Json(name = "page")
+    val page: Int,
+    @Json(name = "pages")
+    val pages: Int,
+    @Json(name = "perpage")
+    val perpage: Int,
+    @Json(name = "total")
+    val total: Int,
+    @Json(name = "photo")
+    val photos: List<Photo>
+)

+ 11 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/response/ApiResponse.kt

@@ -0,0 +1,11 @@
+package com.livelike.flickersearchlibrary.api.model.response
+
+import com.livelike.flickersearchlibrary.api.model.Photo
+
+/**
+ * Represents possible states of API response.
+ */
+internal sealed class ApiResponse {
+    data class Success(val body: List<Photo>) : ApiResponse()
+    data class Error(val message: String) : ApiResponse()
+}

+ 9 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/model/response/FlickrResponse.kt

@@ -0,0 +1,9 @@
+package com.livelike.flickersearchlibrary.api.model.response
+
+import com.livelike.flickersearchlibrary.api.model.PhotosPage
+import com.squareup.moshi.Json
+
+internal data class FlickrResponse(
+    @Json(name = "photos")
+    val photosPage: PhotosPage
+)

+ 33 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/api/utils/ApiManager.kt

@@ -0,0 +1,33 @@
+package com.livelike.flickersearchlibrary.api.utils
+
+import android.util.Log
+import com.livelike.flickersearchlibrary.api.model.response.ApiResponse
+import com.livelike.flickersearchlibrary.api.model.response.FlickrResponse
+import retrofit2.Response
+
+/**
+ * Executes given Flickr API call and handles errors or exceptions.
+ *
+ * @return [ApiResponse] with state and data depending on executed API call.
+ */
+internal suspend fun executeApiCall(apiCall: suspend () -> Response<FlickrResponse>): ApiResponse {
+    return runCatching {
+        val response = apiCall()
+        if (response.isSuccessful) {
+            ApiResponse.Success(response.body()?.photosPage?.photos ?: emptyList())
+        } else {
+            throw Exception(
+                "Flickr API responded with error: ${
+                    response.errorBody().toString()
+                }"
+            )
+        }
+    }.onFailure { exception: Throwable ->
+        Log.d(
+            "FlickrApiManager",
+            "Exception occurred when executing API call: ${exception.message}"
+        )
+    }.getOrElse { exception: Throwable ->
+        ApiResponse.Error(exception.message.orEmpty())
+    }
+}

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

@@ -0,0 +1,22 @@
+package com.livelike.flickersearchlibrary.api.utils
+
+// Base URL for the Flickr API
+internal const val BASE_URL = "https://www.flickr.com/services/rest/"
+
+// Constant values for the FlickrApi interface
+internal const val DEFAULT_API_KEY = "3976fde3792b699fbcda31f52e1cb306"
+internal const val JSON_FORMAT = "json"
+internal const val NO_JSON_CALLBACK = true
+
+// Constant Query keys for the Flickr API interface
+internal const val QUERY_API_KEY = "api_key"
+internal const val QUERY_FORMAT = "format"
+internal const val QUERY_NO_JSON_CALLBACK = "nojsoncallback"
+internal const val QUERY_METHOD = "method"
+
+// Constant values for methods supported in the library
+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
+internal const val QUERY_TEXT = "text"

+ 44 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/di/Dependencies.kt

@@ -0,0 +1,44 @@
+package com.livelike.flickersearchlibrary.di
+
+import com.livelike.flickersearchlibrary.api.FlickrApi
+import com.livelike.flickersearchlibrary.api.utils.BASE_URL
+import com.livelike.flickersearchlibrary.service.FlickrService
+import com.livelike.flickersearchlibrary.service.logic.FlickrServiceLogic
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import org.koin.core.context.startKoin
+import org.koin.dsl.module
+import retrofit2.Retrofit
+import retrofit2.converter.moshi.MoshiConverterFactory
+
+/**
+ * Defines dependencies required by Flickr service features.
+ */
+internal object Dependencies {
+
+    private val flickrModule = module {
+
+        single<FlickrApi> {
+            val moshi = Moshi.Builder()
+                .add(KotlinJsonAdapterFactory())
+                .build()
+            val retrofit = Retrofit.Builder()
+                .baseUrl(BASE_URL)
+                .addConverterFactory(MoshiConverterFactory.create(moshi))
+                .build()
+            retrofit.create(FlickrApi::class.java)
+        }
+
+        single<FlickrService> { FlickrServiceLogic(get()) }
+
+    }
+
+    /**
+     * Initializes all dependencies needed for Flick service features.
+     */
+    internal fun init() {
+        startKoin {
+            modules(flickrModule)
+        }
+    }
+}

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

@@ -0,0 +1,25 @@
+package com.livelike.flickersearchlibrary.service
+
+import com.livelike.flickersearchlibrary.api.model.Photo
+
+/**
+ * Interface for the service providing Flickr features.
+ */
+interface FlickrService {
+
+    /**
+     * Searches for photos and retrieves [List] of [Photo]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.
+     */
+    suspend fun search(searchQuery: String?): List<Photo>
+
+    /**
+     * Retrieves recently published photos on Flickr.
+     *
+     * @return [List] of recently published photos on Flickr.
+     */
+    suspend fun getRecentPhotos(): List<Photo>
+}

+ 38 - 0
flickersearchlibrary/src/main/java/com/livelike/flickersearchlibrary/service/logic/FlickrServiceLogic.kt

@@ -0,0 +1,38 @@
+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
+
+/**
+ * Implementation of [FlickrService].
+ */
+internal class FlickrServiceLogic(private val api: FlickrApi) : FlickrService {
+
+    private val cachedPhotos = mutableListOf<Photo>()
+
+    override suspend fun search(searchQuery: String?) =
+        searchQuery?.let {
+            val response = executeApiCall {
+                api.search(searchQuery)
+            }
+            if (response is ApiResponse.Success) {
+                cachedPhotos.clear()
+                cachedPhotos.addAll(response.body)
+            }
+
+            cachedPhotos
+        } ?: emptyList()
+
+    override suspend fun getRecentPhotos(): List<Photo> {
+        val response = executeApiCall {
+            api.getRecent()
+        }
+        return when (response) {
+            is ApiResponse.Success -> response.body
+            is ApiResponse.Error -> emptyList()
+        }
+    }
+}

+ 1 - 1
gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,6 @@
 #Tue Dec 14 13:30:57 IST 2021
 #Tue Dec 14 13:30:57 IST 2021
 distributionBase=GRADLE_USER_HOME
 distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
 distributionPath=wrapper/dists
 distributionPath=wrapper/dists
 zipStorePath=wrapper/dists
 zipStorePath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStoreBase=GRADLE_USER_HOME