Skip to content

Commit

Permalink
Optimize load images (esafirm#364)
Browse files Browse the repository at this point in the history
* optimize load images

remove mvp view

* add test
  • Loading branch information
esafirm authored and Sarthak Mishra committed Sep 1, 2023
1 parent 8a02255 commit 37b3aef
Show file tree
Hide file tree
Showing 22 changed files with 424 additions and 272 deletions.
6 changes: 2 additions & 4 deletions imagepicker/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,9 @@ artifacts {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "com.github.bumptech.glide:glide:4.11.0"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'androidx.recyclerview:recyclerview:1.2.1'

implementation "androidx.appcompat:appcompat:1.2.0"
implementation 'androidx.activity:activity-ktx:1.2.0'
implementation 'androidx.fragment:fragment-ktx:1.3.0'
implementation 'androidx.appcompat:appcompat:1.3.0'

implementation "androidx.core:core-ktx:$core_ktx_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.esafirm.imagepicker.R
import com.esafirm.imagepicker.adapter.ImagePickerAdapter.ImageViewHolder
import com.esafirm.imagepicker.features.imageloader.ImageLoader
import com.esafirm.imagepicker.features.imageloader.ImageType
import com.esafirm.imagepicker.helper.ImagePickerUtils
import com.esafirm.imagepicker.helper.diff.SimpleDiffUtilCallBack
import com.esafirm.imagepicker.listeners.OnImageClickListener
import com.esafirm.imagepicker.listeners.OnImageSelectedListener
import com.esafirm.imagepicker.model.Image
Expand All @@ -28,7 +31,10 @@ class ImagePickerAdapter(
private val itemClickListener: OnImageClickListener
) : BaseListAdapter<ImageViewHolder>(context, imageLoader) {

private val images: MutableList<Image> = mutableListOf()
private val listDiffer by lazy {
AsyncListDiffer<Image>(this, SimpleDiffUtilCallBack())
}

val selectedImages: MutableList<Image> = mutableListOf()

private var imageSelectedListener: OnImageSelectedListener? = null
Expand All @@ -50,7 +56,7 @@ class ImagePickerAdapter(
}

override fun onBindViewHolder(viewHolder: ImageViewHolder, position: Int) {
val image = images.getOrNull(position) ?: return
val image = getItem(position) ?: return

val isSelected = isSelected(image)
imageLoader.loadImage(image, viewHolder.imageView, ImageType.GALLERY)
Expand Down Expand Up @@ -99,11 +105,10 @@ class ImagePickerAdapter(
return selectedImages.any { it.path == image.path }
}

override fun getItemCount() = images.size
override fun getItemCount() = listDiffer.currentList.size

fun setData(images: List<Image>) {
this.images.clear()
this.images.addAll(images)
listDiffer.submitList(images)
}

private fun addSelected(image: Image, position: Int) {
Expand Down Expand Up @@ -136,7 +141,7 @@ class ImagePickerAdapter(
this.imageSelectedListener = imageSelectedListener
}

fun getItem(position: Int) = images.getOrNull(position)
fun getItem(position: Int) = listDiffer.currentList.getOrNull(position)

class ImageViewHolder(itemView: View) : ViewHolder(itemView) {
val imageView: ImageView = itemView.image_view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import androidx.lifecycle.LifecycleOwner

class ContentObserverTrigger(
private val contentResolver: ContentResolver,
private val loadData: () -> Unit
private val callback: () -> Unit
) : LifecycleEventObserver {

private var handler: Handler? = null
Expand All @@ -33,7 +33,7 @@ class ContentObserverTrigger(

observer = object : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) {
loadData()
callback()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.esafirm.imagepicker.features

import com.esafirm.imagepicker.helper.state.ObservableState
import com.esafirm.imagepicker.helper.state.SingleEvent
import com.esafirm.imagepicker.model.Folder
import com.esafirm.imagepicker.model.Image

data class ImagePickerState(
val images: List<Image> = emptyList(),
val folders: List<Folder> = emptyList(),
val isLoading: Boolean = false,
val error: SingleEvent<Throwable>? = null,
val finishPickImage: SingleEvent<List<Image>>? = null,
val showCapturedImage: SingleEvent<Unit>? = null
)

interface ImagePickerAction {
fun getUiState(): ObservableState<ImagePickerState>
fun loadData(config: ImagePickerConfig)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ import com.esafirm.imagepicker.helper.ImagePickerUtils
import com.esafirm.imagepicker.helper.IpCrasher
import com.esafirm.imagepicker.helper.LocaleManager
import com.esafirm.imagepicker.helper.ViewUtils
import com.esafirm.imagepicker.model.Folder
import com.esafirm.imagepicker.model.Image

class ImagePickerActivity : AppCompatActivity(), ImagePickerInteractionListener, ImagePickerView {
class ImagePickerActivity : AppCompatActivity(), ImagePickerInteractionListener {

companion object {
private const val RC_CAMERA = 1011
Expand Down Expand Up @@ -169,32 +168,4 @@ class ImagePickerActivity : AppCompatActivity(), ImagePickerInteractionListener,
setResult(RESULT_OK, result)
finish()
}

/* --------------------------------------------------- */
/* > View Methods */
/* --------------------------------------------------- */

override fun showLoading(isLoading: Boolean) {
imagePickerFragment.showLoading(isLoading)
}

override fun showFetchCompleted(images: List<Image>, folders: List<Folder>) {
imagePickerFragment.showFetchCompleted(images, folders)
}

override fun showError(throwable: Throwable?) {
imagePickerFragment.showError(throwable)
}

override fun showEmpty() {
imagePickerFragment.showEmpty()
}

override fun showCapturedImage() {
imagePickerFragment.showCapturedImage()
}

override fun finishPickImages(images: List<Image>?) {
imagePickerFragment.finishPickImages(images)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ import com.esafirm.imagepicker.helper.ConfigUtils
import com.esafirm.imagepicker.helper.ImagePickerPreferences
import com.esafirm.imagepicker.helper.ImagePickerUtils
import com.esafirm.imagepicker.helper.IpLogger
import com.esafirm.imagepicker.helper.state.fetch
import com.esafirm.imagepicker.model.Folder
import com.esafirm.imagepicker.model.Image
import java.util.ArrayList

class ImagePickerFragment : Fragment(), ImagePickerView {
class ImagePickerFragment : Fragment() {

private var binding: EfFragmentImagePickerBinding? = null
private var recyclerViewManager: RecyclerViewManager? = null
private lateinit var recyclerViewManager: RecyclerViewManager

private val preferences: ImagePickerPreferences by lazy {
ImagePickerPreferences(requireContext())
Expand All @@ -56,7 +57,7 @@ class ImagePickerFragment : Fragment(), ImagePickerView {
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setupComponents()
presenter = ImagePickerPresenter(DefaultImageFileLoader(requireContext()))

if (::interactionListener.isInitialized.not()) {
throw RuntimeException("ImagePickerFragment needs an " +
Expand Down Expand Up @@ -100,10 +101,45 @@ class ImagePickerFragment : Fragment(), ImagePickerView {
return view
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeToUiState()
}

private fun subscribeToUiState() = presenter.getUiState().observe(this) { state ->
showLoading(state.isLoading)

state.error.fetch {
showError(this)
}

val isEmpty = state.images.isEmpty()
if (isEmpty) {
showEmpty()
return@observe
}

if (config.isFolderMode) {
setFolderAdapter(state.folders)
} else {
setImageAdapter(state.images)
}

state.finishPickImage.fetch {
val images = this
interactionListener.finishPickImages(
ImagePickerUtils.createResultIntent(images)
)
}

state.showCapturedImage.fetch {
loadDataWithPermission()
}
}

override fun onDestroyView() {
super.onDestroyView()
binding = null
recyclerViewManager = null
}

private fun createRecyclerViewManager(
Expand All @@ -129,20 +165,15 @@ class ImagePickerFragment : Fragment(), ImagePickerView {
}
}

private fun setupComponents() {
presenter = ImagePickerPresenter(DefaultImageFileLoader(requireContext()))
presenter.attachView(this)
}

override fun onResume() {
super.onResume()
loadDataWithPermission()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(STATE_KEY_RECYCLER, recyclerViewManager?.recyclerState)
outState.putParcelableArrayList(STATE_KEY_SELECTED_IMAGES, recyclerViewManager?.selectedImages as ArrayList<out Parcelable?>)
outState.putParcelable(STATE_KEY_RECYCLER, recyclerViewManager.recyclerState)
outState.putParcelableArrayList(STATE_KEY_SELECTED_IMAGES, recyclerViewManager.selectedImages as ArrayList<out Parcelable?>)
}

/**
Expand All @@ -152,25 +183,25 @@ class ImagePickerFragment : Fragment(), ImagePickerView {
* 3. Update title
*/
private fun setImageAdapter(images: List<Image>) {
recyclerViewManager?.setImageAdapter(images)
recyclerViewManager.setImageAdapter(images)
updateTitle()
}

private fun setFolderAdapter(folders: List<Folder>) {
recyclerViewManager?.setFolderAdapter(folders)
recyclerViewManager.setFolderAdapter(folders)
updateTitle()
}

private fun updateTitle() {
interactionListener.setTitle(recyclerViewManager!!.title)
interactionListener.setTitle(recyclerViewManager.title)
}

/**
* On finish selected image
* Get all selected images then return image to caller activity
*/
fun onDone() {
presenter.onDoneSelectImages(recyclerViewManager!!.selectedImages, config)
presenter.onDoneSelectImages(recyclerViewManager.selectedImages, config)
}

/**
Expand All @@ -180,7 +211,7 @@ class ImagePickerFragment : Fragment(), ImagePickerView {
super.onConfigurationChanged(newConfig)

// recyclerViewManager can be null here if we use cameraOnly mode
recyclerViewManager?.changeOrientation(newConfig.orientation)
recyclerViewManager.changeOrientation(newConfig.orientation)
}

/**
Expand All @@ -195,10 +226,7 @@ class ImagePickerFragment : Fragment(), ImagePickerView {
}
}

private fun loadData() {
presenter.abortLoad()
presenter.loadImages(config)
}
private fun loadData() = presenter.loadData(config)

/**
* Request for permission
Expand Down Expand Up @@ -284,14 +312,13 @@ class ImagePickerFragment : Fragment(), ImagePickerView {
override fun onDestroy() {
super.onDestroy()
presenter.abortLoad()
presenter.detachView()
}

/**
* @return true if the [Fragment] consume the back event
*/
fun handleBack(): Boolean {
if (recyclerViewManager!!.handleBack()) {
if (recyclerViewManager.handleBack()) {
// Handled.
updateTitle()
return true
Expand All @@ -300,7 +327,7 @@ class ImagePickerFragment : Fragment(), ImagePickerView {
}

val isShowDoneButton: Boolean
get() = recyclerViewManager!!.isShowDoneButton
get() = recyclerViewManager.isShowDoneButton

override fun onAttach(context: Context) {
super.onAttach(context)
Expand All @@ -313,43 +340,23 @@ class ImagePickerFragment : Fragment(), ImagePickerView {
interactionListener = listener
}

/* --------------------------------------------------- */
/* > View Methods */
/* --------------------------------------------------- */

override fun finishPickImages(images: List<Image>?) {
interactionListener.finishPickImages(ImagePickerUtils.createResultIntent(images))
}

override fun showCapturedImage() {
loadDataWithPermission()
}

override fun showFetchCompleted(images: List<Image>, folders: List<Folder>) {
if (config.isFolderMode) {
setFolderAdapter(folders)
} else {
setImageAdapter(images)
}
}

override fun showError(throwable: Throwable?) {
private fun showError(throwable: Throwable?) {
var message = "Unknown Error"
if (throwable is NullPointerException) {
message = "Images do not exist"
}
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
}

override fun showLoading(isLoading: Boolean) {
private fun showLoading(isLoading: Boolean) {
binding?.run {
progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
recyclerView.visibility = if (isLoading) View.GONE else View.VISIBLE
tvEmptyImages.visibility = View.GONE
}
}

override fun showEmpty() {
private fun showEmpty() {
binding?.run {
progressBar.visibility = View.GONE
recyclerView.visibility = View.GONE
Expand Down

0 comments on commit 37b3aef

Please sign in to comment.