Prechádzať zdrojové kódy

Added image-search endpoint, added rx to retrofit

sirekanyan 8 rokov pred
rodič
commit
774e900d15

+ 9 - 3
app/build.gradle

@@ -15,11 +15,17 @@ android {
 }
 
 dependencies {
+    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
     compile 'com.android.support:appcompat-v7:26.0.0-alpha1'
     compile 'com.android.support:design:26.0.0-alpha1'
     compile 'com.android.support:support-vector-drawable:26.0.0-alpha1'
-    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
-    compile 'com.squareup.retrofit2:retrofit:2.3.0'
-    compile 'com.squareup.retrofit2:converter-gson:2.3.0'
+    compile "io.reactivex.rxjava2:rxjava:2.1.4"
+    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
+    compile "com.squareup.retrofit2:retrofit:$retrofit_version"
+    compile "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
+    compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
+    compile('com.github.bumptech.glide:glide:4.1.1') {
+        exclude group: 'com.android.support' // todo remove
+    }
     compile(name: 'ulogin-sdk-v1.1', ext: 'aar')
 }

+ 23 - 2
app/src/main/java/me/vadik/knigopis/App.kt

@@ -1,15 +1,36 @@
 package me.vadik.knigopis
 
 import android.app.Application
+import com.google.gson.GsonBuilder
+import me.vadik.knigopis.gson.ImageThumbnailDeserializer
+import me.vadik.knigopis.model.ImageThumbnail
 import retrofit2.Retrofit
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
 import retrofit2.converter.gson.GsonConverterFactory
 
+private const val MAIN_API_URL = "http://api.knigopis.com"
+private const val IMAGE_API_URL = "https://api.qwant.com/api/"
+
 class App : Application() {
 
-  val retrofit: Retrofit by lazy {
+  val baseApi: Retrofit by lazy {
     Retrofit.Builder()
-        .baseUrl("http://api.knigopis.com")
+        .baseUrl(MAIN_API_URL)
+        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
         .addConverterFactory(GsonConverterFactory.create())
         .build()
   }
+
+  val imageApi: Retrofit by lazy {
+    Retrofit.Builder()
+        .baseUrl(IMAGE_API_URL)
+        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+        .addConverterFactory(GsonConverterFactory.create(
+            GsonBuilder().registerTypeAdapter(
+                ImageThumbnail::class.java,
+                ImageThumbnailDeserializer()
+            ).create()
+        ))
+        .build()
+  }
 }

+ 7 - 7
app/src/main/java/me/vadik/knigopis/Endpoint.kt

@@ -1,27 +1,27 @@
 package me.vadik.knigopis
 
+import io.reactivex.Single
 import me.vadik.knigopis.model.Book
 import me.vadik.knigopis.model.Credentials
 import me.vadik.knigopis.model.User
 import me.vadik.knigopis.model.Wish
-import retrofit2.Call
 import retrofit2.http.GET
 import retrofit2.http.Query
 
 interface Endpoint {
 
-  @GET("/user/get-credentials")
-  fun getCredentials(@Query("token") token: String): Call<Credentials>
+  @GET("user/get-credentials")
+  fun getCredentials(@Query("token") token: String): Single<Credentials>
 
   @GET("books")
-  fun getBooks(@Query("access-token") accessToken: String): Call<List<Book>>
+  fun getBooks(@Query("access-token") accessToken: String): Single<List<Book>>
 
   @GET("wishes")
-  fun getWishes(@Query("access-token") accessToken: String): Call<List<Wish>>
+  fun getWishes(@Query("access-token") accessToken: String): Single<List<Wish>>
 
   @GET("users/latest")
-  fun getLatestUsers(): Call<Map<String, User>>
+  fun getLatestUsers(): Single<Map<String, User>>
 
   @GET("books/latest-notes")
-  fun getLatestBooksWithNotes(): Call<Map<String, Book>>
+  fun getLatestBooksWithNotes(): Single<Map<String, Book>>
 }

+ 12 - 0
app/src/main/java/me/vadik/knigopis/ImageEndpoint.kt

@@ -0,0 +1,12 @@
+package me.vadik.knigopis
+
+import io.reactivex.Single
+import me.vadik.knigopis.model.ImageThumbnail
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+interface ImageEndpoint {
+
+  @GET("search/images?count=1")
+  fun searchImage(@Query("q") query: String): Single<ImageThumbnail>
+}

+ 31 - 45
app/src/main/java/me/vadik/knigopis/MainActivity.kt

@@ -18,22 +18,20 @@ import me.vadik.knigopis.auth.KAuthImpl
 import me.vadik.knigopis.model.Book
 import me.vadik.knigopis.model.User
 import me.vadik.knigopis.model.Wish
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.Response
 
 private const val ULOGIN_REQUEST_CODE = 0
 
 class MainActivity : AppCompatActivity() {
 
-  private val api by lazy { app().retrofit.create(Endpoint::class.java) }
+  private val api by lazy { app().baseApi.create(Endpoint::class.java) }
+  private val imageApi by lazy { app().imageApi.create(ImageEndpoint::class.java) }
   private val auth by lazy { KAuthImpl(applicationContext, api) as KAuth }
   private val users = mutableListOf<User>()
   private val books = mutableListOf<Book>()
   private val wishes = mutableListOf<Wish>()
   private val usersAdapter = UsersAdapter.create(users)
-  private val booksAdapter = BooksAdapter.create(books)
-  private val wishesAdapter = WishesAdapter.create(wishes)
+  private val booksAdapter by lazy { BooksAdapter(imageApi).create(books) }
+  private val wishesAdapter by lazy { WishesAdapter(imageApi).create(wishes) }
   private lateinit var usersRecyclerView: RecyclerView
   private lateinit var booksRecyclerView: RecyclerView
   private lateinit var wishesRecyclerView: RecyclerView
@@ -126,56 +124,44 @@ class MainActivity : AppCompatActivity() {
     usersRecyclerView.visibility = View.VISIBLE
     booksRecyclerView.visibility = View.GONE
     wishesRecyclerView.visibility = View.GONE
-    api.getLatestUsers().enqueue(object : Callback<Map<String, User>> {
-      override fun onResponse(call: Call<Map<String, User>>?, response: Response<Map<String, User>>?) {
-        users.clear()
-        response?.body()?.values?.forEach { user ->
-          users.add(user)
-        }
-        usersAdapter.notifyDataSetChanged()
-      }
-
-      override fun onFailure(call: Call<Map<String, User>>?, t: Throwable?) {
-        logError("cannot load users", t)
-      }
-    })
+    api.getLatestUsers()
+        .io2main()
+        .subscribe({ latestUsers ->
+          users.clear()
+          users.addAll(latestUsers.values)
+          usersAdapter.notifyDataSetChanged()
+        }, {
+          logError("cannot load users", it)
+        })
   }
 
   private fun refreshDoneTab() {
     usersRecyclerView.visibility = View.GONE
     booksRecyclerView.visibility = View.VISIBLE
     wishesRecyclerView.visibility = View.GONE
-    api.getBooks(auth.getAccessToken()).enqueue(object : Callback<List<Book>> {
-      override fun onResponse(call: Call<List<Book>>?, response: Response<List<Book>>?) {
-        books.clear()
-        response?.body()?.forEach { book ->
-          books.add(book)
-        }
-        usersAdapter.notifyDataSetChanged()
-      }
-
-      override fun onFailure(call: Call<List<Book>>?, t: Throwable?) {
-        logError("cannot load books", t)
-      }
-    })
+    api.getBooks(auth.getAccessToken())
+        .io2main()
+        .subscribe({
+          books.clear()
+          books.addAll(it)
+          usersAdapter.notifyDataSetChanged()
+        }, {
+          logError("cannot load books", it)
+        })
   }
 
   private fun refreshTodoTab() {
     usersRecyclerView.visibility = View.GONE
     booksRecyclerView.visibility = View.GONE
     wishesRecyclerView.visibility = View.VISIBLE
-    api.getWishes(auth.getAccessToken()).enqueue(object : Callback<List<Wish>> {
-      override fun onResponse(call: Call<List<Wish>>?, response: Response<List<Wish>>?) {
-        wishes.clear()
-        response?.body()?.forEach { wish ->
-          wishes.add(wish)
-        }
-        wishesAdapter.notifyDataSetChanged()
-      }
-
-      override fun onFailure(call: Call<List<Wish>>?, t: Throwable?) {
-        logError("cannot load wishes", t)
-      }
-    })
+    api.getWishes(auth.getAccessToken())
+        .io2main()
+        .subscribe({
+          wishes.clear()
+          wishes.addAll(it)
+          wishesAdapter.notifyDataSetChanged()
+        }, {
+          logError("cannot load wishes", it)
+        })
   }
 }

+ 26 - 5
app/src/main/java/me/vadik/knigopis/adapters/BooksAdapter.kt

@@ -1,15 +1,36 @@
 package me.vadik.knigopis.adapters
 
+import android.view.View
+import android.widget.ImageView
 import android.widget.TextView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import me.vadik.knigopis.ImageEndpoint
 import me.vadik.knigopis.R
+import me.vadik.knigopis.io2main
+import me.vadik.knigopis.logError
 import me.vadik.knigopis.model.Book
+import java.util.concurrent.TimeUnit
 
-object BooksAdapter {
-  fun create(books: List<Book>) = createAdapter<Book, TextView>(
+class BooksAdapter(private val imageEndpoint: ImageEndpoint) {
+
+  fun create(books: List<Book>) = createAdapter<Book, View>(
       books,
       R.layout.book,
-      Adapter(R.id.book_title) { text = it.title },
-      Adapter(R.id.book_author) { text = it.author },
-      Adapter(R.id.book_read_date) { text = it.createdAt }
+      Adapter(R.id.book_image) { book ->
+        imageEndpoint.searchImage(book.title + " " + book.author)
+            .delay((Math.random() * 3000).toLong(), TimeUnit.MICROSECONDS)
+            .io2main()
+            .subscribe({ thumbnail ->
+              Glide.with(context)
+                  .load("https:" + thumbnail.url)
+                  .apply(RequestOptions.circleCropTransform())
+                  .into(this as ImageView)
+            }, {
+              logError("cannot load thumbnail", it)
+            })
+      },
+      Adapter(R.id.book_title) { this as TextView; text = it.title },
+      Adapter(R.id.book_author) { this as TextView; text = it.author }
   )
 }

+ 36 - 5
app/src/main/java/me/vadik/knigopis/adapters/WishesAdapter.kt

@@ -1,15 +1,46 @@
 package me.vadik.knigopis.adapters
 
+import android.view.View
+import android.widget.ImageView
 import android.widget.TextView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import me.vadik.knigopis.ImageEndpoint
 import me.vadik.knigopis.R
+import me.vadik.knigopis.io2main
+import me.vadik.knigopis.logError
 import me.vadik.knigopis.model.Wish
+import java.util.concurrent.TimeUnit
 
-object WishesAdapter {
-  fun create(wishes: List<Wish>) = createAdapter<Wish, TextView>(
+class WishesAdapter(private val imageEndpoint: ImageEndpoint) {
+
+  fun create(wishes: List<Wish>) = createAdapter<Wish, View>(
       wishes,
       R.layout.book,
-      Adapter(R.id.book_title) { text = it.title },
-      Adapter(R.id.book_author) { text = it.author },
-      Adapter(R.id.book_read_date) { text = it.createdAt }
+      Adapter(R.id.book_image) { book ->
+        imageEndpoint.searchImage(book.title)
+            .delay((Math.random() * 3000).toLong(), TimeUnit.MICROSECONDS)
+            .io2main()
+            .subscribe({ thumbnail ->
+              Glide.with(context)
+                  .load("https:" + thumbnail.url)
+                  .apply(RequestOptions.circleCropTransform())
+                  .into(this as ImageView)
+            }, {
+              logError("cannot load thumbnail", it)
+            })
+      },
+      Adapter(R.id.book_title) {
+        this as TextView
+        text = it.title
+      },
+      Adapter(R.id.book_author) {
+        this as TextView
+        text = if (it.author.isEmpty()) {
+          "(автор не указан)"
+        } else {
+          it.author
+        }
+      }
   )
 }

+ 10 - 15
app/src/main/java/me/vadik/knigopis/auth/KAuth.kt

@@ -3,13 +3,10 @@ package me.vadik.knigopis.auth
 import android.content.Context
 import android.content.Intent
 import me.vadik.knigopis.Endpoint
+import me.vadik.knigopis.io2main
 import me.vadik.knigopis.logError
-import me.vadik.knigopis.model.Credentials
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.Response
 import ru.ulogin.sdk.UloginAuthActivity
-import java.util.HashMap
+import java.util.*
 
 private const val PREFS_NAME = "knigopis"
 private const val TOKEN_KEY = "token"
@@ -48,16 +45,14 @@ class KAuthImpl(
   override fun requestAccessToken(onSuccess: () -> Unit) {
     val token = preferences.getString(TOKEN_KEY, null)
     if (token != null && !isAuthorized()) {
-      api.getCredentials(token).enqueue(object : Callback<Credentials> {
-        override fun onResponse(call: Call<Credentials>?, response: Response<Credentials>?) {
-          preferences.edit().putString(ACCESS_TOKEN_KEY, response?.body()?.accessToken).apply()
-          onSuccess()
-        }
-
-        override fun onFailure(call: Call<Credentials>?, t: Throwable?) {
-          logError("cannot get credentials", t)
-        }
-      })
+      api.getCredentials(token)
+          .io2main()
+          .subscribe({
+            preferences.edit().putString(ACCESS_TOKEN_KEY, it.accessToken).apply()
+            onSuccess()
+          }, {
+            logError("cannot get credentials", it)
+          })
     }
   }
 

+ 6 - 0
app/src/main/java/me/vadik/knigopis/extensions.kt

@@ -7,6 +7,9 @@ import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import io.reactivex.Single
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
 
 private const val TAG = "Knigopis"
 
@@ -20,3 +23,6 @@ fun logError(message: String, throwable: Throwable?) = Log.e(TAG, message, throw
 
 fun ViewGroup.inflate(@LayoutRes layout: Int): View =
     LayoutInflater.from(context).inflate(layout, this, false)
+
+fun <T> Single<T>.io2main(): Single<T> =
+    subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())

+ 20 - 0
app/src/main/java/me/vadik/knigopis/gson/ImageThumbnailDeserializer.kt

@@ -0,0 +1,20 @@
+package me.vadik.knigopis.gson
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import me.vadik.knigopis.model.ImageThumbnail
+import me.vadik.knigopis.model.emptyThumbnail
+import java.lang.reflect.Type
+
+class ImageThumbnailDeserializer : JsonDeserializer<ImageThumbnail> {
+  override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext) =
+      json.asJsonObject
+          .getAsJsonObject("data")
+          .getAsJsonObject("result")
+          .getAsJsonArray("items")
+          .firstOrNull()
+          ?.asJsonObject
+          ?.let { ImageThumbnail(it["thumbnail"].asString) }
+          ?: emptyThumbnail
+}

+ 5 - 0
app/src/main/java/me/vadik/knigopis/model/ImageThumbnail.kt

@@ -0,0 +1,5 @@
+package me.vadik.knigopis.model
+
+val emptyThumbnail = ImageThumbnail("")
+
+class ImageThumbnail(val url: String)

+ 20 - 19
app/src/main/res/layout/book.xml

@@ -1,24 +1,32 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="72dp"
-    android:orientation="horizontal"
-    android:paddingLeft="16dp"
-    android:paddingRight="16dp">
+    android:layout_height="72dp">
+
+    <ImageView
+        android:id="@+id/book_image"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="16dp"
+        tools:ignore="ContentDescription"/>
 
     <LinearLayout
-        android:layout_width="0dp"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:layout_weight="1"
-        android:orientation="vertical">
+        android:layout_gravity="center_vertical"
+        android:orientation="vertical"
+        android:paddingEnd="16dp"
+        android:paddingStart="72dp">
 
         <TextView
             android:id="@+id/book_title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLines="1"
             android:textColor="@android:color/primary_text_light"
             android:textSize="16sp"
             tools:text="Мастер и Маргарита"/>
@@ -27,19 +35,12 @@
             android:id="@+id/book_author"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLines="1"
             android:textColor="@android:color/tertiary_text_light"
             android:textSize="14sp"
             tools:text="Михаил Булгаков"/>
 
     </LinearLayout>
 
-    <TextView
-        android:id="@+id/book_read_date"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:textColor="@android:color/tertiary_text_light"
-        android:textSize="14sp"
-        tools:text="2011-08"/>
-
-</LinearLayout>
+</FrameLayout>

+ 2 - 1
build.gradle

@@ -1,6 +1,7 @@
 buildscript {
     ext {
-        kotlin_version = '1.1.4-3'
+        kotlin_version = '1.1.50'
+        retrofit_version = '2.3.0'
     }
     repositories {
         jcenter()