Bladeren bron

Added swipe refresh functionality

Vadik Sirekanyan 7 jaren geleden
bovenliggende
commit
f370d97a10

+ 52 - 11
app/src/main/java/me/vadik/knigopis/BookRepository.kt

@@ -1,21 +1,46 @@
 package me.vadik.knigopis
 
 import io.reactivex.Completable
+import io.reactivex.Single
+import io.reactivex.rxkotlin.Singles
 import me.vadik.knigopis.api.Endpoint
 import me.vadik.knigopis.auth.KAuth
-import me.vadik.knigopis.model.FinishedBookToSend
-import me.vadik.knigopis.model.PlannedBookToSend
+import me.vadik.knigopis.common.ResourceProvider
+import me.vadik.knigopis.model.*
 
 interface BookRepository {
+
+    fun loadBooks(): Single<List<Book>>
+
     fun saveBook(bookId: String?, book: FinishedBookToSend, done: Boolean?): Completable
+
     fun saveBook(bookId: String?, book: PlannedBookToSend, done: Boolean?): Completable
+
 }
 
 class BookRepositoryImpl(
     private val api: Endpoint,
-    private val auth: KAuth
+    private val auth: KAuth,
+    private val resources: ResourceProvider
 ) : BookRepository {
 
+    override fun loadBooks(): Single<List<Book>> =
+        Singles.zip(
+            api.getPlannedBooks(auth.getAccessToken())
+                .map { it.sortedByDescending(PlannedBook::priority) },
+            api.getFinishedBooks(auth.getAccessToken())
+                .map { it.sortedByDescending(FinishedBook::order) }
+                .map { it.groupFinishedBooks() }
+        ).map { (planned, finished) ->
+            mutableListOf<Book>().apply {
+                if (planned.isNotEmpty()) {
+                    add(BookHeader(resources.getString(R.string.book_header_todo)))
+                }
+                addAll(planned)
+                addAll(finished)
+            }
+        }
+
     override fun saveBook(bookId: String?, book: FinishedBookToSend, done: Boolean?): Completable =
         when {
             bookId == null -> api.createFinishedBook(auth.getAccessToken(), book)
@@ -37,13 +62,29 @@ class BookRepositoryImpl(
                     .andThen(api.deleteFinishedBook(bookId, auth.getAccessToken()))
             }
         }
-}
-
-class BookRepositoryMock : BookRepository {
-
-    override fun saveBook(bookId: String?, book: FinishedBookToSend, done: Boolean?): Completable =
-        Completable.fromAction { Thread.sleep(2000) }
 
-    override fun saveBook(bookId: String?, book: PlannedBookToSend, done: Boolean?): Completable =
-        Completable.fromAction { Thread.sleep(2000) }
+    private fun List<FinishedBook>.groupFinishedBooks(): List<Book> {
+        val groupedBooks = mutableListOf<Book>()
+        var previousReadYear = Int.MAX_VALUE.toString()
+        forEachIndexed { index, book ->
+            val readYear = book.readYear
+            if (previousReadYear != readYear) {
+                groupedBooks.add(
+                    BookHeader(
+                        when {
+                            book.readYear.isEmpty() ->
+                                resources.getString(R.string.book_header_done_other)
+                            index == 0 ->
+                                resources.getString(R.string.book_header_done_first, readYear)
+                            else ->
+                                resources.getString(R.string.book_header_done, readYear)
+                        }
+                    )
+                )
+            }
+            groupedBooks.add(book)
+            previousReadYear = book.readYear
+        }
+        return groupedBooks
+    }
 }

+ 70 - 90
app/src/main/java/me/vadik/knigopis/MainActivity.kt

@@ -6,7 +6,6 @@ import android.content.Intent.ACTION_VIEW
 import android.net.Uri
 import android.os.Bundle
 import android.provider.Settings
-import android.support.design.widget.BottomNavigationView
 import android.support.v7.app.AlertDialog
 import android.support.v7.app.AppCompatActivity
 import android.support.v7.widget.LinearLayoutManager
@@ -16,7 +15,6 @@ import android.text.format.DateUtils
 import android.view.MenuItem
 import android.view.View
 import com.tbruyelle.rxpermissions2.RxPermissions
-import io.reactivex.rxkotlin.Singles
 import kotlinx.android.synthetic.main.about.view.*
 import kotlinx.android.synthetic.main.activity_main.*
 import me.vadik.knigopis.adapters.BooksAdapter
@@ -25,8 +23,11 @@ import me.vadik.knigopis.adapters.users.UsersAdapter
 import me.vadik.knigopis.api.BookCoverSearch
 import me.vadik.knigopis.api.Endpoint
 import me.vadik.knigopis.auth.KAuth
-import me.vadik.knigopis.model.*
+import me.vadik.knigopis.model.Book
+import me.vadik.knigopis.model.CurrentTab
 import me.vadik.knigopis.model.CurrentTab.*
+import me.vadik.knigopis.model.FinishedBook
+import me.vadik.knigopis.model.PlannedBook
 import me.vadik.knigopis.model.note.Identity
 import me.vadik.knigopis.model.note.Note
 import me.vadik.knigopis.model.subscription.Subscription
@@ -46,6 +47,7 @@ class MainActivity : AppCompatActivity(), Router {
     private val bookCoverSearch by inject<BookCoverSearch>()
     private val config by inject<Configuration>()
     private val auth by inject<KAuth>()
+    private val bookRepository by inject<BookRepository>()
     private val allBooks = mutableListOf<Book>()
     private val allUsers = mutableListOf<Subscription>()
     private val allNotes = mutableListOf<Note>()
@@ -53,11 +55,6 @@ class MainActivity : AppCompatActivity(), Router {
     private val allBooksAdapter by lazy { booksAdapter.build(allBooks) }
     private val usersAdapter by lazy { UsersAdapter(allUsers, this) }
     private val notesAdapter by lazy { NotesAdapter(allNotes, this) }
-    private val navigation by lazy {
-        findView<BottomNavigationView>(R.id.navigation).apply {
-            visibility = if (config.isDevMode()) View.VISIBLE else View.GONE
-        }
-    }
     private var userLoggedIn = false
     private var booksChanged = false
     private lateinit var loginOption: MenuItem
@@ -88,6 +85,9 @@ class MainActivity : AppCompatActivity(), Router {
                 }
             }
         })
+        swipeRefresh.setOnRefreshListener {
+            refresh(isForce = true)
+        }
     }
 
     override fun onStart() {
@@ -172,8 +172,9 @@ class MainActivity : AppCompatActivity(), Router {
     }
 
     private fun initNavigationView(currentTab: CurrentTab?) {
-        refresh(currentTab ?: HOME_TAB)
-        navigation.setOnNavigationItemSelectedListener { item ->
+        val defaultTab = if (auth.isAuthorized()) HOME_TAB else NOTES_TAB
+        refresh(currentTab ?: defaultTab)
+        bottomNavigation.setOnNavigationItemSelectedListener { item ->
             setCurrentTab(CurrentTab.getByItemId(item.itemId))
             true
         }
@@ -310,51 +311,60 @@ class MainActivity : AppCompatActivity(), Router {
         }
     }
 
-    private fun refresh(tab: CurrentTab = currentTab) {
-        setCurrentTab(tab)
-        navigation.selectedItemId = tab.itemId
+    private fun refresh(tab: CurrentTab = currentTab, isForce: Boolean = false) {
+        setCurrentTab(tab, isForce)
+        bottomNavigation.selectedItemId = tab.itemId
     }
 
-    private fun setCurrentTab(tab: CurrentTab) {
+    private fun setCurrentTab(tab: CurrentTab, isForce: Boolean = false) {
         addBookButton.hide()
         currentTab = tab
+        toggleRecyclerView(tab)
+        val isFirst = isFirstOpenTab(tab)
+        if (isFirst) {
+            when (tab) {
+                HOME_TAB -> booksRecyclerView.adapter = allBooksAdapter
+                USERS_TAB -> usersRecyclerView.adapter = usersAdapter
+                NOTES_TAB -> notesRecyclerView.adapter = notesAdapter
+            }
+        }
+        if (isFirst || isForce) {
+            when (tab) {
+                HOME_TAB -> refreshHomeTab()
+                USERS_TAB -> refreshUsersTab()
+                NOTES_TAB -> refreshNotesTab()
+            }
+        }
+    }
+
+    private fun isFirstOpenTab(tab: CurrentTab) =
         when (tab) {
-            HOME_TAB -> refreshHomeTab()
-            USERS_TAB -> refreshUsersTab()
-            NOTES_TAB -> refreshNotesTab()
+            HOME_TAB -> booksRecyclerView.adapter == null
+            USERS_TAB -> usersRecyclerView.adapter == null
+            NOTES_TAB -> notesRecyclerView.adapter == null
         }
+
+    private fun toggleRecyclerView(tab: CurrentTab) {
+        usersRecyclerView.show(tab == USERS_TAB)
+        notesRecyclerView.show(tab == NOTES_TAB)
+        booksRecyclerView.show(tab == HOME_TAB)
     }
 
     private fun refreshHomeTab() {
-        usersRecyclerView.hideNow()
-        notesRecyclerView.hideNow()
-        booksRecyclerView.showNow()
         if (booksProgressBar.alpha > 0) {
             return
         }
-        booksRecyclerView.adapter = allBooksAdapter
-        allBooks.clear()
-        Singles.zip(
-            api.getPlannedBooks(auth.getAccessToken())
-                .map { it.sortedByDescending(PlannedBook::priority) },
-            api.getFinishedBooks(auth.getAccessToken())
-                .map { it.sortedByDescending(FinishedBook::order) }
-                .map { it.groupFinishedBooks() }
-        ).map { (planned, finished) ->
-            mutableListOf<Book>().apply {
-                if (planned.isNotEmpty()) {
-                    add(BookHeader(getString(R.string.book_header_todo)))
-                }
-                addAll(planned)
-                addAll(finished)
-            }
-        }.io2main()
+        bookRepository.loadBooks()
+            .io2main()
             .doOnSubscribe {
-                booksProgressBar.show()
+                if (!swipeRefresh.isRefreshing) {
+                    booksProgressBar.show()
+                }
                 booksPlaceholder.hide()
             }
             .doFinally {
                 booksProgressBar.hide()
+                swipeRefresh.isRefreshing = false
             }
             .subscribe({ books ->
                 if (books.isEmpty()) {
@@ -366,93 +376,63 @@ class MainActivity : AppCompatActivity(), Router {
                 allBooksAdapter.notifyDataSetChanged()
                 addBookButton.show()
             }, {
-                logError("cannot load books", it)
-                booksPlaceholder.setText(
-                    if (it is HttpException && it.code() == 401) {
-                        R.string.error_unauthorized
-                    } else {
-                        R.string.error_loading_books
-                    }
-                )
-                booksPlaceholder.show()
+                handleNetworkError("cannot load books", it)
             })
     }
 
     private fun refreshUsersTab() {
-        booksRecyclerView.hideNow()
-        notesRecyclerView.hideNow()
-        usersRecyclerView.showNow()
-        usersRecyclerView.adapter = usersAdapter
-        allUsers.clear()
         api.getSubscriptions(auth.getAccessToken())
             .io2main()
             .doOnSubscribe {
-                booksProgressBar.show()
+                if (!swipeRefresh.isRefreshing) {
+                    booksProgressBar.show()
+                }
                 booksPlaceholder.hide()
             }
             .doFinally {
                 booksProgressBar.hide()
+                swipeRefresh.isRefreshing = false
             }
             .subscribe({ subscriptions ->
+                allUsers.clear()
                 allUsers.addAll(subscriptions)
                 usersAdapter.notifyDataSetChanged()
             }, {
-                logError("cannot load users", it)
-                booksPlaceholder.setText(
-                    if (it is HttpException && it.code() == 401) {
-                        R.string.error_unauthorized
-                    } else {
-                        R.string.error_loading_books
-                    }
-                )
-                booksPlaceholder.show()
+                handleNetworkError("cannot load users", it)
             })
     }
 
     private fun refreshNotesTab() {
-        booksRecyclerView.hideNow()
-        usersRecyclerView.hideNow()
-        notesRecyclerView.showNow()
-        notesRecyclerView.adapter = notesAdapter
-        allNotes.clear()
         api.getLatestBooksWithNotes()
             .io2main()
             .doOnSubscribe {
-                booksProgressBar.show()
+                if (!swipeRefresh.isRefreshing) {
+                    booksProgressBar.show()
+                }
                 booksPlaceholder.hide()
             }
             .doFinally {
                 booksProgressBar.hide()
+                swipeRefresh.isRefreshing = false
             }
             .subscribe({ notes ->
+                allNotes.clear()
                 allNotes.addAll(notes.values)
                 notesAdapter.notifyDataSetChanged()
             }, {
-                logError("cannot load notes", it)
-                booksPlaceholder.setText(R.string.error_loading_books)
-                booksPlaceholder.show()
+                handleNetworkError("cannot load notes", it)
             })
     }
 
-    private fun List<FinishedBook>.groupFinishedBooks(): List<Book> {
-        val groupedBooks = mutableListOf<Book>()
-        var previousReadYear = Int.MAX_VALUE.toString()
-        forEachIndexed { index, book ->
-            val readYear = book.readYear
-            if (previousReadYear != readYear) {
-                groupedBooks.add(
-                    BookHeader(
-                        when {
-                            book.readYear.isEmpty() -> getString(R.string.book_header_done_other)
-                            index == 0 -> getString(R.string.book_header_done_first, readYear)
-                            else -> getString(R.string.book_header_done, readYear)
-                        }
-                    )
-                )
+    private fun handleNetworkError(message: String, throwable: Throwable) {
+        logError(message, throwable)
+        toast(
+            if (throwable is HttpException && throwable.code() == 401) {
+                R.string.error_unauthorized
+            } else {
+                R.string.error_loading_books
             }
-            groupedBooks.add(book)
-            previousReadYear = book.readYear
-        }
-        return groupedBooks
+        )
     }
+
 }

+ 11 - 0
app/src/main/java/me/vadik/knigopis/common/ResourceProvider.kt

@@ -0,0 +1,11 @@
+package me.vadik.knigopis.common
+
+import android.app.Application
+
+interface ResourceProvider {
+    fun getString(id: Int, vararg args: Any): String
+}
+
+class ResourceProviderImpl(private val app: Application) : ResourceProvider {
+    override fun getString(id: Int, vararg args: Any): String = app.getString(id, *args)
+}

+ 4 - 1
app/src/main/java/me/vadik/knigopis/dependency/modules.kt

@@ -9,6 +9,8 @@ import me.vadik.knigopis.api.ImageEndpoint
 import me.vadik.knigopis.api.gson.ImageThumbnailDeserializer
 import me.vadik.knigopis.auth.KAuth
 import me.vadik.knigopis.auth.KAuthImpl
+import me.vadik.knigopis.common.ResourceProvider
+import me.vadik.knigopis.common.ResourceProviderImpl
 import me.vadik.knigopis.model.ImageThumbnail
 import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
@@ -22,12 +24,13 @@ private const val IMAGE_API_URL = "https://api.qwant.com/api/"
 private const val DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"
 
 val appModule = applicationContext {
-    bean { BookRepositoryImpl(get(), get()) as BookRepository }
+    bean { BookRepositoryImpl(get(), get(), get()) as BookRepository }
     bean { BookCoverSearchImpl(get(), BookCoverCacheImpl(get())) as BookCoverSearch }
     bean { KAuthImpl(get(), get()) as KAuth }
     bean { createMainEndpoint() }
     bean { createImageEndpoint() }
     bean { ConfigurationImpl(get()) as Configuration }
+    bean { ResourceProviderImpl(get()) as ResourceProvider }
 }
 
 private fun createMainEndpoint() =

+ 4 - 3
app/src/main/java/me/vadik/knigopis/extensions.kt

@@ -6,7 +6,6 @@ import android.content.Context
 import android.content.Intent
 import android.net.Uri
 import android.os.Build
-import android.support.annotation.IdRes
 import android.support.annotation.LayoutRes
 import android.support.annotation.StringRes
 import android.text.Html
@@ -46,8 +45,6 @@ fun Context.toast(@StringRes messageId: Int) =
 
 fun Activity.app() = application as App
 
-fun <T : View> Activity.findView(@IdRes id: Int): T = findViewById(id)
-
 fun logWarn(message: String) = Log.w(TAG, message)
 
 fun logError(message: String, throwable: Throwable?) = Log.e(TAG, message, throwable)
@@ -98,6 +95,10 @@ fun View.hideNow() {
     visibility = View.GONE
 }
 
+fun View.show(value: Boolean) {
+    if (value) show() else hide()
+}
+
 fun View.show() {
     animate().alpha(1f).setDuration(200)
         .withStartAction { visibility = View.VISIBLE }

+ 60 - 53
app/src/main/res/layout/activity_main.xml

@@ -22,72 +22,79 @@
 
     </android.support.design.widget.AppBarLayout>
 
-    <FrameLayout
+    <android.support.v4.widget.SwipeRefreshLayout
+        android:id="@+id/swipeRefresh"
         android:layout_width="match_parent"
         android:layout_height="0dp"
         android:layout_weight="1">
 
-        <android.support.v7.widget.RecyclerView
-            android:id="@+id/booksRecyclerView"
+        <FrameLayout
             android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            tools:listitem="@layout/book" />
+            android:layout_height="match_parent">
 
-        <android.support.v7.widget.RecyclerView
-            android:id="@+id/usersRecyclerView"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:clipToPadding="false"
-            android:paddingBottom="8dp"
-            android:paddingTop="8dp"
-            tools:listitem="@layout/user"
-            tools:visibility="gone" />
+            <android.support.v7.widget.RecyclerView
+                android:id="@+id/booksRecyclerView"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                tools:listitem="@layout/book" />
 
-        <android.support.v7.widget.RecyclerView
-            android:id="@+id/notesRecyclerView"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:clipToPadding="false"
-            android:paddingBottom="8dp"
-            android:paddingTop="8dp"
-            tools:listitem="@layout/note"
-            tools:visibility="gone" />
+            <android.support.v7.widget.RecyclerView
+                android:id="@+id/usersRecyclerView"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:clipToPadding="false"
+                android:paddingBottom="8dp"
+                android:paddingTop="8dp"
+                tools:listitem="@layout/user"
+                tools:visibility="gone" />
 
-        <ProgressBar
-            android:id="@+id/booksProgressBar"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:alpha="0"
-            tools:alpha="1" />
+            <android.support.v7.widget.RecyclerView
+                android:id="@+id/notesRecyclerView"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:clipToPadding="false"
+                android:paddingBottom="8dp"
+                android:paddingTop="8dp"
+                tools:listitem="@layout/note"
+                tools:visibility="gone" />
 
-        <TextView
-            android:id="@+id/booksPlaceholder"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:alpha="0"
-            android:gravity="center"
-            android:padding="16dp"
-            android:textSize="16sp"
-            android:visibility="gone"
-            tools:alpha="1"
-            tools:text="@string/error_loading_books"
-            tools:visibility="visible" />
+            <ProgressBar
+                android:id="@+id/booksProgressBar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:alpha="0"
+                tools:alpha="1" />
 
-        <android.support.design.widget.FloatingActionButton
-            android:id="@+id/addBookButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="end|bottom"
-            android:layout_margin="16dp"
-            android:tint="@color/white"
-            app:srcCompat="@drawable/ic_add" />
+            <TextView
+                android:id="@+id/booksPlaceholder"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:alpha="0"
+                android:gravity="center"
+                android:padding="16dp"
+                android:textSize="16sp"
+                android:visibility="gone"
+                tools:alpha="1"
+                tools:text="@string/error_loading_books"
+                tools:visibility="visible" />
+
+            <android.support.design.widget.FloatingActionButton
+                android:id="@+id/addBookButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end|bottom"
+                android:layout_margin="16dp"
+                android:tint="@color/white"
+                app:srcCompat="@drawable/ic_add" />
+
+        </FrameLayout>
 
-    </FrameLayout>
+    </android.support.v4.widget.SwipeRefreshLayout>
 
     <android.support.design.widget.BottomNavigationView
-        android:id="@+id/navigation"
+        android:id="@+id/bottomNavigation"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_gravity="bottom"

+ 1 - 1
build.gradle

@@ -7,7 +7,7 @@ buildscript {
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.0'
+        classpath 'com.android.tools.build:gradle:3.1.1'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
 }