1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Data View | Enable pagination (#1561)

This commit is contained in:
Evgenii Kozlov 2021-06-18 17:59:39 +03:00 committed by GitHub
parent 5ddf5b43ee
commit 79d4b261d4
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 230 additions and 80 deletions

View file

@ -18,6 +18,7 @@
* Navigate to objects from data view or relation-value screens (#1539)
* Show the Android keyboard reliably for text-based relations with empty value (#1542)
* Do not exit edit mode when deleting one of the values from relation's file list (#1543)
* Data view pagination (#1561)
## Version 0.1.11

View file

@ -131,8 +131,12 @@ open class ObjectSetFragment :
subscribe(bottomPanel.touches()) { swipeDetector.onTouchEvent(it) }
}
paginatorToolbar.onNumberClickCallback = { (num, isSelected) ->
vm.onPaginatorToolbarNumberClicked(num, isSelected)
with(paginatorToolbar) {
onNumberClickCallback = { (num, isSelected) ->
vm.onPaginatorToolbarNumberClicked(num, isSelected)
}
onNext = { vm.onPaginatorNextElsePrevious(true) }
onPrevious = { vm.onPaginatorNextElsePrevious(false) }
}
}
@ -371,6 +375,15 @@ open class ObjectSetFragment :
jobs += lifecycleScope.subscribe(vm.commands) { observeCommands(it) }
jobs += lifecycleScope.subscribe(vm.header.filterNotNull()) { observeHeader(it) }
jobs += lifecycleScope.subscribe(vm.viewerGrid) { observeGrid(it) }
jobs += lifecycleScope.subscribe(vm.pagination) { (index, count) ->
paginatorToolbar.set(count = count, index = index)
if (count > 1) {
paginatorToolbar.visible()
}
else {
paginatorToolbar.gone()
}
}
jobs += lifecycleScope.subscribe(vm.isLoading) { isLoading ->
Timber.d("isLoading: $isLoading")
if (isLoading) {

View file

@ -158,7 +158,6 @@
app:layout_constraintTop_toBottomOf="parent" />
<com.anytypeio.anytype.core_ui.widgets.toolbar.DataViewPaginatorToolbar
android:visibility="invisible"
android:id="@+id/paginatorToolbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -70,17 +70,25 @@
motion:layout_constraintTop_toTopOf="@+id/addNewButton" />
<Constraint
android:id="@id/dvProgressBar"
motion:visibilityMode="ignore"
motion:layout_constraintBottom_toBottomOf="@+id/gridContainer"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintBottom_toBottomOf="@+id/gridContainer" />
motion:visibilityMode="ignore" />
<Constraint
android:id="@id/logoProgressBar"
motion:visibilityMode="ignore"
motion:layout_constraintBottom_toBottomOf="@+id/objectSetIcon"
motion:layout_constraintEnd_toEndOf="@+id/objectSetIcon"
motion:layout_constraintStart_toStartOf="@+id/objectSetIcon"
motion:layout_constraintTop_toTopOf="@+id/objectSetIcon" />
motion:layout_constraintTop_toTopOf="@+id/objectSetIcon"
motion:visibilityMode="ignore" />
<Constraint
android:id="@id/paginatorToolbar"
android:layout_width="0dp"
android:layout_height="@dimen/default_toolbar_height"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:visibilityMode="ignore" />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
@ -146,16 +154,24 @@
motion:layout_constraintTop_toBottomOf="@+id/tvCurrentViewerName" />
<Constraint
android:id="@id/dvProgressBar"
motion:visibilityMode="ignore"
motion:layout_constraintBottom_toBottomOf="@+id/gridContainer"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintBottom_toBottomOf="@+id/gridContainer" />
motion:visibilityMode="ignore" />
<Constraint
android:id="@id/logoProgressBar"
motion:visibilityMode="ignore"
motion:layout_constraintBottom_toBottomOf="@+id/objectSetIcon"
motion:layout_constraintEnd_toEndOf="@+id/objectSetIcon"
motion:layout_constraintStart_toStartOf="@+id/objectSetIcon"
motion:layout_constraintTop_toTopOf="@+id/objectSetIcon" />
motion:layout_constraintTop_toTopOf="@+id/objectSetIcon"
motion:visibilityMode="ignore" />
<Constraint
android:id="@id/paginatorToolbar"
android:layout_width="0dp"
android:layout_height="@dimen/default_toolbar_height"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:visibilityMode="ignore" />
</ConstraintSet>
</MotionScene>

View file

@ -20,7 +20,9 @@ class DataViewPaginatorToolbar @JvmOverloads constructor(
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
var onNumberClickCallback : (Pair<Int, Boolean>) -> Unit = {}
var onNumberClickCallback: (Pair<Int, Boolean>) -> Unit = {}
var onNext: () -> Unit = {}
var onPrevious: () -> Unit = {}
private val paginatorAdapter = Adapter { onNumberClickCallback(it) }
@ -29,31 +31,50 @@ class DataViewPaginatorToolbar @JvmOverloads constructor(
setBackgroundColor(Color.WHITE)
rvPaginator.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
val spacing = resources.getDimension(R.dimen.dp_12).toInt()
addItemDecoration(
SpacingItemDecoration(
spacingStart = resources.getDimension(R.dimen.dp_12).toInt(),
spacingEnd = resources.getDimension(R.dimen.dp_12).toInt()
spacingStart = spacing,
spacingEnd = spacing,
firstItemSpacingStart = spacing * 2,
lastItemSpacingEnd = spacing * 2
)
)
adapter = paginatorAdapter
}
ivNextPage.setOnClickListener { onNext() }
ivPreviousPage.setOnClickListener { onPrevious() }
}
fun set(count: Int, index: Int) {
val update = mutableListOf<Pair<Int, Boolean>>()
repeat(count) {
val number = it.inc()
val number = it
update.add(
Pair(number, index == number)
)
}
paginatorAdapter.submitList(update)
if (index > 0) rvPaginator.smoothScrollToPosition(index)
if (index == 0) {
ivPreviousPage.isEnabled = false
ivPreviousPage.alpha = 0.2f
} else {
ivPreviousPage.isEnabled = true
ivPreviousPage.alpha = 1.0f
}
if (index == count - 1) {
ivNextPage.isEnabled = false
ivNextPage.alpha = 0.2f
} else {
ivNextPage.isEnabled = true
ivNextPage.alpha = 1f
}
}
class Adapter(
private val onNumberClicked: (Pair<Int, Boolean>) -> Unit
): ListAdapter<Pair<Int, Boolean>, Adapter.ViewHolder>(PageNumberDiffer) {
) : ListAdapter<Pair<Int, Boolean>, Adapter.ViewHolder>(PageNumberDiffer) {
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int
@ -79,7 +100,7 @@ class DataViewPaginatorToolbar @JvmOverloads constructor(
) {
fun bind(item: Pair<Int, Boolean>) {
val (num, isSelected) = item
itemView.tvNumber.text = num.toString()
itemView.tvNumber.text = num.inc().toString()
itemView.tvNumber.isSelected = isSelected
}
}
@ -90,6 +111,7 @@ class DataViewPaginatorToolbar @JvmOverloads constructor(
oldItem: Pair<Int, Boolean>,
newItem: Pair<Int, Boolean>
): Boolean = newItem.first == oldItem.first
override fun areContentsTheSame(
oldItem: Pair<Int, Boolean>,
newItem: Pair<Int, Boolean>

View file

@ -1,30 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
<ImageView
android:id="@+id/ivPreviousPage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:src="@drawable/ic_paginator_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
android:src="@drawable/ic_paginator_arrow" />
<androidx.recyclerview.widget.RecyclerView
android:overScrollMode="never"
android:scrollbars="none"
android:id="@+id/rvPaginator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="36dp"
android:layout_marginEnd="36dp"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:overScrollMode="never"
android:scrollbars="none" />
<ImageView
android:rotation="180"
android:id="@+id/ivNextPage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="12dp"
android:src="@drawable/ic_paginator_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
android:rotation="180"
android:src="@drawable/ic_paginator_arrow" />
</merge>

View file

@ -15,7 +15,6 @@ org.gradle.caching=true
org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.configureondemand=true
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=false

View file

@ -37,6 +37,8 @@ object ObjectSetConfig {
const val FILE_EXT_KEY = "fileExt"
const val FILE_MIME_KEY = "fileMimeType"
const val DEFAULT_LIMIT = 50
}
val Map<String, Any?>.type: String

View file

@ -1,10 +1,8 @@
package com.anytypeio.anytype.presentation.sets
import com.anytypeio.anytype.core_models.DV
import com.anytypeio.anytype.core_models.DVViewer
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Event.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.presentation.extension.updateFields
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
@ -13,15 +11,9 @@ import timber.log.Timber
class ObjectSetReducer {
private val eventChannel: Channel<List<Event>> = Channel()
private val effectChannel: Channel<List<SideEffect>> = Channel()
val state = MutableStateFlow(ObjectSet.init())
val effects = effectChannel
.consumeAsFlow()
.filter { it.isNotEmpty() }
.flatMapConcat { it.asFlow() }
.distinctUntilChanged()
val effects = MutableSharedFlow<List<SideEffect>>()
suspend fun run() {
eventChannel
@ -34,7 +26,7 @@ class ObjectSetReducer {
}
.collect { transformation ->
state.value = transformation.state
effectChannel.send(transformation.effects)
effects.emit(transformation.effects)
}
}
@ -67,6 +59,7 @@ class ObjectSetReducer {
)
}
is Command.DataView.SetView -> {
effects.add(SideEffect.ResetOffset(event.offset))
state.copy(
blocks = state.blocks.map { block ->
if (block.id == event.target) {
@ -179,14 +172,10 @@ class ObjectSetReducer {
}
sealed class SideEffect {
data class ViewerUpdate(
val target: Id,
val viewer: DVViewer
) : SideEffect()
data class ResetOffset(val offset: Int) : SideEffect()
}
fun clear() {
eventChannel.close()
effectChannel.close()
}
}

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.*
import com.anytypeio.anytype.core_models.ext.content
import com.anytypeio.anytype.core_models.restrictions.DataViewRestriction
import com.anytypeio.anytype.core_utils.common.EventWrapper
import com.anytypeio.anytype.domain.block.interactor.UpdateText
@ -28,6 +29,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.math.ceil
class ObjectSetViewModel(
private val reducer: ObjectSetReducer,
@ -46,6 +48,16 @@ class ObjectSetViewModel(
private val session: ObjectSetSession
) : ViewModel(), SupportNavigation<EventWrapper<AppNavigation.Command>> {
private val total = MutableStateFlow(0)
private val offset = MutableStateFlow(0)
val pagination = total.combine(offset) { t, o ->
val idx = ceil(o.toDouble() / ObjectSetConfig.DEFAULT_LIMIT).toInt()
val pages = ceil(t.toDouble() / ObjectSetConfig.DEFAULT_LIMIT).toInt()
Pair(idx, pages)
}
private val _viewerTabs = MutableStateFlow<List<ViewerTabView>>(emptyList())
val viewerTabs = _viewerTabs.asStateFlow()
@ -84,29 +96,29 @@ class ObjectSetViewModel(
reducer.state.filter { it.isInitialized }.collect { set ->
Timber.d("Set updated!")
_viewerTabs.value = set.tabs(session.currentViewerId)
val viewerIndex =
reducer.state.value.viewers.indexOfFirst { it.id == session.currentViewerId }
val viewerIndex = set.viewers.indexOfFirst { it.id == session.currentViewerId }
set.render(viewerIndex, context, urlBuilder).let { vs ->
_viewerGrid.value = vs.viewer
_header.value = vs.title
}
if (set.viewers.isNotEmpty()) {
val viewer = if (viewerIndex != -1)
set.viewers[viewerIndex]
else
set.viewers.first()
val db = set.viewerDb[viewer.id]
total.value = db?.total ?: 0
}
}
}
viewModelScope.launch {
reducer.effects.collect { effect ->
when (effect) {
is ObjectSetReducer.SideEffect.ViewerUpdate -> {
updateDataViewViewer(
UpdateDataViewViewer.Params(
context = context,
target = effect.target,
viewer = effect.viewer
)
).process(
success = defaultPayloadConsumer,
failure = { Timber.e(it, "Error while updating data view's viewer") }
)
reducer.effects.collect { effects ->
effects.forEach { effect ->
when (effect) {
is ObjectSetReducer.SideEffect.ResetOffset -> {
offset.value = effect.offset
}
}
}
}
@ -144,7 +156,10 @@ class ObjectSetViewModel(
viewModelScope.launch {
isLoading.value = true
openObjectSet(ctx).process(
success = { defaultPayloadConsumer(it).also { isLoading.value = false } },
success = { payload ->
defaultPayloadConsumer(payload).also { isLoading.value = false }
proceedWithStartupPaging()
},
failure = {
isLoading.value = false
Timber.e(it, "Error while opening object set: $ctx")
@ -191,7 +206,7 @@ class ObjectSetViewModel(
context = context,
block = reducer.state.value.dataview.id,
view = viewer,
limit = 0,
limit = ObjectSetConfig.DEFAULT_LIMIT,
offset = 0
)
).process(
@ -569,16 +584,61 @@ class ObjectSetViewModel(
return dVRestrictions != null && dVRestrictions.restrictions.any { it == restriction }
}
//region {PAGINATION LOGIC}
//region { PAGINATION LOGIC }
private suspend fun proceedWithStartupPaging() {
val set = reducer.state.value.dataview
val dv = set.content<Block.Content.DataView>()
val viewer = dv.viewers.find { it.id == session.currentViewerId } ?: dv.viewers.first()
proceedWithViewerPaging(set = set, viewer = viewer.id)
}
fun onPaginatorToolbarNumberClicked(number: Int, isSelected: Boolean) {
if (isSelected) {
Timber.d("This page is already selected")
} else {
// TODO proceed with pagination logic.
viewModelScope.launch {
offset.value = number * ObjectSetConfig.DEFAULT_LIMIT
val set = reducer.state.value.dataview
val dv = set.content<Block.Content.DataView>()
val viewer = dv.viewers.find { it.id == session.currentViewerId } ?: dv.viewers.first()
proceedWithViewerPaging(set = set, viewer = viewer.id)
}
}
}
fun onPaginatorNextElsePrevious(next: Boolean) {
viewModelScope.launch {
offset.value = if (next) {
offset.value + ObjectSetConfig.DEFAULT_LIMIT
} else {
offset.value - ObjectSetConfig.DEFAULT_LIMIT
}
val set = reducer.state.value.dataview
val dv = set.content<Block.Content.DataView>()
val viewer = dv.viewers.find { it.id == session.currentViewerId } ?: dv.viewers.first()
proceedWithViewerPaging(set = set, viewer = viewer.id)
}
}
private suspend fun proceedWithViewerPaging(
set: Block,
viewer: Id
) {
setActiveViewer(
SetActiveViewer.Params(
context = context,
block = set.id,
view = viewer,
limit = ObjectSetConfig.DEFAULT_LIMIT,
offset = offset.value
)
).process(
success = { payload -> defaultPayloadConsumer(payload) },
failure = { Timber.e(it, "Error while setting view during pagination") }
)
}
//endregion
override fun onCleared() {

View file

@ -2,8 +2,8 @@ package com.anytypeio.anytype.presentation.sets
import MockDataFactory
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Relation
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
@ -202,8 +202,10 @@ class ObjectSetReducerTest {
children = listOf()
)
val expected =
ObjectSetReducer.Transformation(ObjectSet(blocks = listOf(title, expectedDataView)))
val expected = ObjectSetReducer.Transformation(
state = ObjectSet(blocks = listOf(title, expectedDataView)),
effects = listOf(ObjectSetReducer.SideEffect.ResetOffset(event.offset))
)
assertEquals(expected, result)
}
@ -356,8 +358,10 @@ class ObjectSetReducerTest {
children = listOf()
)
val expected =
ObjectSetReducer.Transformation(ObjectSet(blocks = listOf(title, expectedDataView)))
val expected = ObjectSetReducer.Transformation(
state = ObjectSet(blocks = listOf(title, expectedDataView)),
effects = listOf(ObjectSetReducer.SideEffect.ResetOffset(event.offset))
)
assertEquals(expected, result)
}

View file

@ -3,7 +3,11 @@ package com.anytypeio.anytype.presentation.sets.main
import MockDataFactory
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.dataview.interactor.SetActiveViewer
import com.anytypeio.anytype.presentation.TypicalTwoRecordObjectSet
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
import com.anytypeio.anytype.presentation.sets.model.CellView
import com.anytypeio.anytype.presentation.sets.model.ColumnView
import com.anytypeio.anytype.presentation.sets.model.Viewer
@ -12,6 +16,8 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@ -54,17 +60,54 @@ class ObjectSetSettingActiveViewerTest : ObjectSetViewModelTestSetup() {
)
)
)
stubSetActiveViewer(
events = listOf(
Event.Command.DataView.SetRecords(
setActiveViewer.stub {
onBlocking { invoke(
SetActiveViewer.Params(
context = root,
id = doc.dv.id,
view = doc.viewer2.id,
records = updatedRecords,
total = MockDataFactory.randomInt()
block = doc.dv.id,
view = doc.viewer1.id,
limit = ObjectSetConfig.DEFAULT_LIMIT
)
) } doReturn Either.Right(
Payload(
context = root,
events = listOf(
Event.Command.DataView.SetRecords(
context = root,
view = doc.viewer1.id,
id = doc.dv.id,
total = MockDataFactory.randomInt(),
records = doc.initialRecords
)
)
)
)
)
}
setActiveViewer.stub {
onBlocking { invoke(
SetActiveViewer.Params(
context = root,
block = doc.dv.id,
view = doc.viewer2.id,
limit = ObjectSetConfig.DEFAULT_LIMIT
)
) } doReturn Either.Right(
Payload(
context = root,
events = listOf(
Event.Command.DataView.SetRecords(
context = root,
id = doc.dv.id,
view = doc.viewer2.id,
records = updatedRecords,
total = MockDataFactory.randomInt()
)
)
)
)
}
val vm = buildViewModel()

View file

@ -8,6 +8,7 @@ import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.domain.dataview.interactor.SetActiveViewer
import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer
import com.anytypeio.anytype.presentation.TypicalTwoRecordObjectSet
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
import com.anytypeio.anytype.presentation.sets.model.SortingExpression
import com.anytypeio.anytype.presentation.sets.model.Viewer
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
@ -74,7 +75,7 @@ class ObjectSetUpdateViewerSortTest : ObjectSetViewModelTestSetup() {
context = root,
block = doc.dv.id,
view = doc.viewer2.id,
limit = 0,
limit = ObjectSetConfig.DEFAULT_LIMIT,
offset = 0
)
)

View file

@ -7,6 +7,7 @@ import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.domain.dataview.interactor.SetActiveViewer
import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer
import com.anytypeio.anytype.presentation.TypicalTwoRecordObjectSet
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
import com.anytypeio.anytype.presentation.sets.model.FilterExpression
import com.anytypeio.anytype.presentation.sets.model.FilterValue
import com.anytypeio.anytype.presentation.sets.model.Viewer
@ -74,7 +75,7 @@ class ObjectSetViewerFilterTest : ObjectSetViewModelTestSetup() {
context = root,
block = doc.dv.id,
view = doc.viewer2.id,
limit = 0,
limit = ObjectSetConfig.DEFAULT_LIMIT,
offset = 0
)
)