Better modularization + move image loading to view model

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2019-10-17 12:54:15 +02:00
parent 06ed99f3ad
commit ead2ca24c9
12 changed files with 174 additions and 115 deletions

View File

@ -28,7 +28,10 @@ import com.nextcloud.talk.utils.ApiUtils
class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository {
override suspend fun getConversationsForUser(user: UserEntity): List<Conversation> {
return apiService.getConversations(ApiUtils.getCredentials(user.username, user.token),
ApiUtils.getUrlForGetRooms(user.baseUrl)).ocs.data
return apiService.getConversations(
ApiUtils.getCredentials(user.username, user.token),
ApiUtils.getUrlForGetRooms(user.baseUrl)
)
.ocs.data
}
}

View File

@ -64,7 +64,9 @@ class ApiErrorHandler {
else -> null
}
return errorModel ?: ErrorModel(
NextcloudTalkApplication.sharedApplication?.resources!!.getString(R.string.nc_not_defined_error), 0, ErrorModel.ErrorStatus.BAD_RESPONSE
NextcloudTalkApplication.sharedApplication?.resources!!.getString(
R.string.nc_not_defined_error
), 0, ErrorModel.ErrorStatus.BAD_RESPONSE
)
}

View File

@ -27,6 +27,6 @@ val CommunicationModule = module {
single { createEventBus() }
}
fun createEventBus() : EventBus {
fun createEventBus(): EventBus {
return EventBus.getDefault()
}

View File

@ -174,8 +174,11 @@ fun createTrustManager(): MagicTrustManager {
return MagicTrustManager()
}
fun createSslSocketFactory(magicKeyManager: MagicKeyManager, magicTrustManager:
MagicTrustManager) : SSLSocketFactoryCompat {
fun createSslSocketFactory(
magicKeyManager: MagicKeyManager,
magicTrustManager:
MagicTrustManager
): SSLSocketFactoryCompat {
return SSLSocketFactoryCompat(magicKeyManager, magicTrustManager)
}
@ -227,14 +230,14 @@ fun createProxy(appPreferences: AppPreferences): Proxy {
}
fun createDispatcher() : Dispatcher {
fun createDispatcher(): Dispatcher {
val dispatcher = Dispatcher()
dispatcher.maxRequestsPerHost = 100
dispatcher.maxRequests = 100
return dispatcher
}
fun createCache(androidApplication: NextcloudTalkApplication) : Cache {
fun createCache(androidApplication: NextcloudTalkApplication): Cache {
val cacheSize = 128 * 1024 * 1024 // 128 MB
return Cache(androidApplication.cacheDir, cacheSize.toLong())
}

View File

@ -57,6 +57,6 @@ fun createDataStore(sqlCipherDatabaseSource: SqlCipherDatabaseSource): ReactiveE
return ReactiveSupport.toReactiveStore(EntityDataStore(configuration))
}
fun createUserUtils(dataStore : ReactiveEntityStore<Persistable>) : UserUtils {
fun createUserUtils(dataStore: ReactiveEntityStore<Persistable>): UserUtils {
return UserUtils(dataStore)
}

View File

@ -20,17 +20,19 @@
package com.nextcloud.talk.newarch.features.conversationsList
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase
import com.nextcloud.talk.utils.database.user.UserUtils
class ConversationListViewModelFactory constructor(
private val application: Application,
private val conversationsUseCase: GetConversationsUseCase,
private val userUtils: UserUtils
): ViewModelProvider.Factory {
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ConversationsListViewModel(conversationsUseCase, userUtils) as T
return ConversationsListViewModel(application, conversationsUseCase, userUtils) as T
}
}

View File

@ -22,8 +22,6 @@ package com.nextcloud.talk.newarch.features.conversationsList
import android.app.SearchManager
import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.text.InputType
@ -36,7 +34,6 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.view.MenuItemCompat
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.RecyclerView.ViewHolder
@ -45,12 +42,6 @@ import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
import com.facebook.common.executors.UiThreadImmediateExecutorService
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
import com.facebook.imagepipeline.image.CloseableImage
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.items.ConversationItem
import com.nextcloud.talk.controllers.ContactsController
@ -64,7 +55,6 @@ import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED_EMPTY
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADING
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ConductorRemapping
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.animations.SharedElementTransition
@ -77,6 +67,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
import kotlinx.android.synthetic.main.controller_conversations_rv.view.dataStateView
import kotlinx.android.synthetic.main.controller_conversations_rv.view.floatingActionButton
import kotlinx.android.synthetic.main.controller_conversations_rv.view.recyclerView
import kotlinx.android.synthetic.main.controller_conversations_rv.view.swipeRefreshLayoutView
import kotlinx.android.synthetic.main.fast_scroller.view.fast_scroller
import kotlinx.android.synthetic.main.view_states.view.errorStateImageView
import kotlinx.android.synthetic.main.view_states.view.errorStateTextView
@ -95,6 +86,7 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
private val recyclerViewAdapter = FlexibleAdapter(mutableListOf())
private var searchItem: MenuItem? = null
private var settingsItem: MenuItem? = null
private var searchView: SearchView? = null
override fun onCreateOptionsMenu(
@ -115,7 +107,17 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
recyclerViewAdapter.filterItems()
}
loadUserAvatar(menu.findItem(R.id.action_settings))
settingsItem = menu.findItem(R.id.action_settings)
val iconSize = settingsItem?.icon?.intrinsicHeight?.toFloat()
?.let {
DisplayUtils.convertDpToPixel(
it,
activity
)
.toInt()
}
iconSize?.let { viewModel.loadAvatar(it) }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -182,6 +184,7 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
viewState.observe(this@ConversationsListView, Observer { value ->
when (value) {
LOADING -> {
view?.swipeRefreshLayoutView?.isEnabled = false
view?.loadingStateView?.visibility = View.VISIBLE
view?.stateWithMessageView?.visibility = View.GONE
view?.dataStateView?.visibility = View.GONE
@ -189,6 +192,10 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
searchItem?.isVisible = false
}
LOADED -> {
view?.swipeRefreshLayoutView?.isEnabled = true
view?.swipeRefreshLayoutView?.post {
view?.swipeRefreshLayoutView?.isRefreshing = false
}
view?.loadingStateView?.visibility = View.GONE
view?.stateWithMessageView?.visibility = View.GONE
view?.dataStateView?.visibility = View.VISIBLE
@ -196,6 +203,10 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
searchItem?.isVisible = true
}
LOADED_EMPTY, FAILED -> {
view?.swipeRefreshLayoutView?.post {
view?.swipeRefreshLayoutView?.isRefreshing = false
}
view?.swipeRefreshLayoutView?.isEnabled = true
view?.loadingStateView?.visibility = View.GONE
view?.dataStateView?.visibility = View.GONE
view?.floatingActionButton?.visibility = View.GONE
@ -218,61 +229,31 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
// We should not be here
}
}
})
searchQuery.observe(this@ConversationsListView, Observer {
recyclerViewAdapter.setFilter(it)
recyclerViewAdapter.filterItems(500)
})
conversationsListData.observe(this@ConversationsListView, Observer {
val newConversations = mutableListOf<ConversationItem>()
for (conversation in it) {
newConversations.add(ConversationItem(conversation, viewModel.currentUser, activity))
}
conversationsListData.observe(this@ConversationsListView, Observer {
val newConversations = mutableListOf<ConversationItem>()
for (conversation in it) {
newConversations.add(ConversationItem(conversation, viewModel.currentUser, activity))
}
recyclerViewAdapter.updateDataSet(newConversations as List<IFlexible<ViewHolder>>?)
})
recyclerViewAdapter.updateDataSet(newConversations as List<IFlexible<ViewHolder>>?)
})
searchQuery.observe(this@ConversationsListView, Observer {
recyclerViewAdapter.setFilter(it)
recyclerViewAdapter.filterItems(500)
})
currentUserAvatar.observe(this@ConversationsListView, Observer {
settingsItem?.icon = it
})
}
return super.onCreateView(inflater, container)
}
private fun loadUserAvatar(menuItem: MenuItem) {
if (activity != null) {
val avatarSize =
DisplayUtils.convertDpToPixel(menuItem.icon.intrinsicHeight.toFloat(), activity!!)
.toInt()
val imageRequest = DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatarWithNameAndPixels(
viewModel.currentUser.baseUrl,
viewModel.currentUser.userId, avatarSize
), null
)
val imagePipeline = Fresco.getImagePipeline()
val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
dataSource.subscribe(object : BaseBitmapDataSubscriber() {
override fun onNewResultImpl(bitmap: Bitmap?) {
if (bitmap != null && resources != null) {
val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(
resources as Resources,
bitmap
)
roundedBitmapDrawable.isCircular = true
roundedBitmapDrawable.setAntiAlias(true)
menuItem.icon = roundedBitmapDrawable
}
}
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
menuItem.setIcon(R.drawable.ic_settings_white_24dp)
}
}, UiThreadImmediateExecutorService.getInstance())
}
}
override fun getLayoutId(): Int {
return R.layout.controller_conversations_rv
}
@ -309,6 +290,9 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
SmoothScrollLinearLayoutManager(view.context), recyclerViewAdapter
)
view.swipeRefreshLayoutView.setOnRefreshListener { viewModel.loadConversations() }
view.swipeRefreshLayoutView.setColorSchemeResources(R.color.colorPrimary)
recyclerViewAdapter.fastScroller = view.fast_scroller
recyclerViewAdapter.mItemClickListener = this
recyclerViewAdapter.mItemLongClickListener = this

View File

@ -20,8 +20,20 @@
package com.nextcloud.talk.newarch.features.conversationsList
import android.app.Application
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.facebook.common.executors.UiThreadImmediateExecutorService
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
import com.facebook.imagepipeline.image.CloseableImage
import com.nextcloud.talk.R
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel
@ -33,19 +45,30 @@ import com.nextcloud.talk.newarch.mvvm.ViewState.FAILED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED_EMPTY
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADING
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.database.user.UserUtils
import org.apache.commons.lang3.builder.CompareToBuilder
class ConversationsListViewModel constructor(
application: Application,
private val conversationsUseCase: GetConversationsUseCase,
private val userUtils: UserUtils
) : BaseViewModel<ConversationsListView>() {
) : BaseViewModel<ConversationsListView>(application) {
val conversationsListData = MutableLiveData<List<Conversation>>()
val viewState = MutableLiveData<ViewState>(LOADING)
var messageData: String? = null
val searchQuery = MutableLiveData<String>()
var currentUser: UserEntity = userUtils.currentUser
var currentUserAvatar: MutableLiveData<Drawable> = MutableLiveData()
get() {
if (field.value == null) {
field.value = context.resources.getDrawable(R.drawable.ic_settings_white_24dp)
}
return field
}
fun loadConversations() {
currentUser = userUtils.currentUser
@ -81,4 +104,34 @@ class ConversationsListViewModel constructor(
})
}
}
fun loadAvatar(avatarSize: Int) {
val imageRequest = DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatarWithNameAndPixels(
currentUser.baseUrl,
currentUser.userId, avatarSize
), null
)
val imagePipeline = Fresco.getImagePipeline()
val dataSource = imagePipeline.fetchDecodedImage(imageRequest, viewModelScope)
dataSource.subscribe(object : BaseBitmapDataSubscriber() {
override fun onNewResultImpl(bitmap: Bitmap?) {
if (bitmap != null) {
val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(
context.resources as Resources,
bitmap
)
roundedBitmapDrawable.isCircular = true
roundedBitmapDrawable.setAntiAlias(true)
currentUserAvatar.value = roundedBitmapDrawable
}
}
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
currentUserAvatar.value = context.getDrawable(R.drawable.ic_settings_white_24dp)
}
}, UiThreadImmediateExecutorService.getInstance())
}
}

View File

@ -20,18 +20,20 @@
package com.nextcloud.talk.newarch.features.conversationsList.di.module
import android.app.Application
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.di.module.createApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase
import com.nextcloud.talk.newarch.features.conversationsList.ConversationListViewModelFactory
import com.nextcloud.talk.utils.database.user.UserUtils
import org.koin.android.ext.koin.androidApplication
import org.koin.dsl.module
val ConversationsListModule = module {
single { createGetConversationsUseCase(get(), createApiErrorHandler()) }
//viewModel { ConversationsListViewModel(get(), get()) }
factory { createConversationListViewModelFactory(get(), get()) }
factory { createConversationListViewModelFactory(androidApplication(), get(), get()) }
}
fun createGetConversationsUseCase(
@ -41,7 +43,11 @@ fun createGetConversationsUseCase(
return GetConversationsUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createConversationListViewModelFactory(conversationsUseCase: GetConversationsUseCase,
userUtils: UserUtils): ConversationListViewModelFactory {
return ConversationListViewModelFactory(conversationsUseCase, userUtils)
fun createConversationListViewModelFactory(
application: Application,
conversationsUseCase:
GetConversationsUseCase,
userUtils: UserUtils
): ConversationListViewModelFactory {
return ConversationListViewModelFactory(application, conversationsUseCase, userUtils)
}

View File

@ -22,7 +22,6 @@
package com.nextcloud.talk.newarch.features.search
import android.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope

View File

@ -26,58 +26,60 @@ import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import butterknife.ButterKnife
import com.bluelinelabs.conductor.archlifecycle.ControllerLifecycleOwner
import com.nextcloud.talk.controllers.base.BaseController
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.ViewModelProvider
import kotlinx.android.extensions.LayoutContainer
abstract class BaseView : BaseController(), LifecycleOwner, ViewModelStoreOwner {
private val viewModelStore = ViewModelStore()
private val lifecycleOwner = ControllerLifecycleOwner(this)
private val viewModelStore = ViewModelStore()
private val lifecycleOwner = ControllerLifecycleOwner(this)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
val view = inflater.inflate(getLayoutId(), container, false)
unbinder = ButterKnife.bind(this, view)
onViewBound(view)
return view
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup
): View {
val view = inflater.inflate(getLayoutId(), container, false)
unbinder = ButterKnife.bind(this, view)
onViewBound(view)
return view
}
override fun inflateView(
inflater: LayoutInflater,
container: ViewGroup
): View {
return inflateView(inflater, container)
}
override fun inflateView(
inflater: LayoutInflater,
container: ViewGroup
): View {
return inflateView(inflater, container)
}
override fun onDestroy() {
super.onDestroy()
viewModelStore.clear()
}
override fun onDestroy() {
super.onDestroy()
viewModelStore.clear()
}
override fun getLifecycle(): Lifecycle {
return lifecycleOwner.lifecycle
}
override fun getLifecycle(): Lifecycle {
return lifecycleOwner.lifecycle
}
fun viewModelProvider(): ViewModelProvider {
return viewModelProvider(ViewModelProvider.AndroidViewModelFactory(activity!!.application))
}
fun viewModelProvider(): ViewModelProvider {
return viewModelProvider(ViewModelProvider.AndroidViewModelFactory(activity!!.application))
}
fun viewModelProvider(factory: ViewModelProvider.NewInstanceFactory): ViewModelProvider {
return ViewModelProvider(viewModelStore, factory)
}
fun viewModelProvider(factory: ViewModelProvider.NewInstanceFactory): ViewModelProvider {
return ViewModelProvider(viewModelStore, factory)
}
fun viewModelProvider(factory: ViewModelProvider.Factory): ViewModelProvider {
return ViewModelProvider(viewModelStore, factory)
}
fun viewModelProvider(factory: ViewModelProvider.Factory): ViewModelProvider {
return ViewModelProvider(viewModelStore, factory)
}
override fun getViewModelStore(): ViewModelStore {
return viewModelStore
}
override fun getViewModelStore(): ViewModelStore {
return viewModelStore
}
@LayoutRes
protected abstract fun getLayoutId(): Int
@LayoutRes
protected abstract fun getLayoutId(): Int
}

View File

@ -20,16 +20,21 @@
package com.nextcloud.talk.newarch.conversationsList.mvp
import androidx.lifecycle.ViewModel
import android.app.Application
import android.content.Context
import androidx.lifecycle.AndroidViewModel
import io.reactivex.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
abstract class BaseViewModel<V : BaseView> : ViewModel() {
abstract class BaseViewModel<V : BaseView>(application: Application) : AndroidViewModel(
application
) {
protected val disposables: CompositeDisposable = CompositeDisposable()
protected val context: Context = getApplication<Application>().applicationContext
val backgroundAndUIScope = CoroutineScope(
Job() + Dispatchers.Main