Skip to content

Commit 2c25e69

Browse files
author
Marco Romano
authored
Persist state of VoiceMessagePresenter in memory (#1795)
Allows [VoiceMessagePresenter] instances to keep their progress and download states while going in and out of the timeline viewport. This is implemented by caching each instance of a TimelineItem presenter inside the RoomScope. TimelineItem presenters can move some of their state outside of the `present()` function so that such state will survive scrollings of the timeline.
1 parent 0b1d41e commit 2c25e69

File tree

4 files changed

+87
-31
lines changed

4 files changed

+87
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.impl.timeline.di
18+
19+
import androidx.compose.runtime.staticCompositionLocalOf
20+
21+
/**
22+
* Provides a [TimelineItemPresenterFactories] to the composition.
23+
*/
24+
val LocalTimelineItemPresenterFactories = staticCompositionLocalOf {
25+
TimelineItemPresenterFactories(emptyMap())
26+
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ package io.element.android.features.messages.impl.timeline.di
1818

1919
import androidx.compose.runtime.Composable
2020
import androidx.compose.runtime.remember
21-
import androidx.compose.runtime.staticCompositionLocalOf
2221
import com.squareup.anvil.annotations.ContributesTo
2322
import dagger.Module
2423
import dagger.multibindings.Multibinds
2524
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
2625
import io.element.android.libraries.architecture.Presenter
2726
import io.element.android.libraries.di.RoomScope
27+
import io.element.android.libraries.di.SingleIn
2828
import javax.inject.Inject
2929

3030
/**
@@ -40,38 +40,60 @@ interface TimelineItemPresenterFactoriesModule {
4040
}
4141

4242
/**
43-
* Wrapper around the [TimelineItemPresenterFactory] map multi binding.
43+
* Room level caching layer for the [TimelineItemPresenterFactory] instances.
4444
*
45-
* Its only purpose is to provide a nicer type name than:
46-
* `@JvmSuppressWildcards Map<Class<out TimelineItemEventContent>, TimelineItemPresenterFactory<*, *>>`.
47-
*
48-
* A typealias would have been better but typealiases on Dagger types which use @JvmSuppressWildcards
49-
* currently make Dagger crash.
50-
*
51-
* Request this type from Dagger to access the [TimelineItemPresenterFactory] map multibinding.
45+
* It will cache the presenter instances in the room scope, so that they can be
46+
* reused across recompositions of the timeline items that happen whenever an item
47+
* goes out of the [LazyColumn] viewport.
5248
*/
53-
data class TimelineItemPresenterFactories @Inject constructor(
54-
val factories: @JvmSuppressWildcards Map<Class<out TimelineItemEventContent>, TimelineItemPresenterFactory<*, *>>,
55-
)
49+
@SingleIn(RoomScope::class)
50+
class TimelineItemPresenterFactories @Inject constructor(
51+
private val factories: @JvmSuppressWildcards Map<Class<out TimelineItemEventContent>, TimelineItemPresenterFactory<*, *>>,
52+
) {
53+
private val presenters: MutableMap<TimelineItemEventContent, Presenter<*>> = mutableMapOf()
5654

57-
/**
58-
* Provides a [TimelineItemPresenterFactories] to the composition.
59-
*/
60-
val LocalTimelineItemPresenterFactories = staticCompositionLocalOf {
61-
TimelineItemPresenterFactories(emptyMap())
55+
/**
56+
* Creates and caches a presenter for the given content.
57+
*
58+
* Will throw if the presenter is not found in the [TimelineItemPresenterFactory] map multi binding.
59+
*
60+
* @param C The [TimelineItemEventContent] subtype handled by this TimelineItem presenter.
61+
* @param S The state type produced by this timeline item presenter.
62+
* @param content The [TimelineItemEventContent] instance to create a presenter for.
63+
* @param contentClass The class of [content].
64+
* @return An instance of a TimelineItem presenter that will be cached in the room scope.
65+
*/
66+
@Composable
67+
fun <C : TimelineItemEventContent, S : Any> rememberPresenter(
68+
content: C,
69+
contentClass: Class<C>,
70+
): Presenter<S> = remember(content) {
71+
presenters[content]?.let {
72+
@Suppress("UNCHECKED_CAST")
73+
it as Presenter<S>
74+
} ?: factories.getValue(contentClass).let {
75+
@Suppress("UNCHECKED_CAST")
76+
(it as TimelineItemPresenterFactory<C, S>).create(content).apply {
77+
presenters[content] = this
78+
}
79+
}
80+
}
6281
}
6382

6483
/**
65-
* Creates and remembers a presenter for the given content.
84+
* Creates and caches a presenter for the given content.
6685
*
6786
* Will throw if the presenter is not found in the [TimelineItemPresenterFactory] map multi binding.
87+
*
88+
* @param C The [TimelineItemEventContent] subtype handled by this TimelineItem presenter.
89+
* @param S The state type produced by this timeline item presenter.
90+
* @param content The [TimelineItemEventContent] instance to create a presenter for.
91+
* @return An instance of a TimelineItem presenter that will be cached in the room scope.
6892
*/
6993
@Composable
70-
inline fun <reified C : TimelineItemEventContent, reified S : Any> TimelineItemPresenterFactories.rememberPresenter(
94+
inline fun <reified C : TimelineItemEventContent, S : Any> TimelineItemPresenterFactories.rememberPresenter(
7195
content: C
72-
): Presenter<S> = remember(content) {
73-
factories.getValue(C::class.java).let {
74-
@Suppress("UNCHECKED_CAST")
75-
(it as TimelineItemPresenterFactory<C, S>).create(content)
76-
}
77-
}
96+
): Presenter<S> = rememberPresenter(
97+
content = content,
98+
contentClass = C::class.java
99+
)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import androidx.compose.runtime.derivedStateOf
2222
import androidx.compose.runtime.getValue
2323
import androidx.compose.runtime.mutableStateOf
2424
import androidx.compose.runtime.remember
25-
import androidx.compose.runtime.rememberCoroutineScope
2625
import com.squareup.anvil.annotations.ContributesTo
2726
import dagger.Binds
2827
import dagger.Module
@@ -40,6 +39,7 @@ import io.element.android.libraries.architecture.runUpdatingState
4039
import io.element.android.libraries.di.RoomScope
4140
import io.element.android.libraries.ui.utils.time.formatShort
4241
import io.element.android.services.analytics.api.AnalyticsService
42+
import kotlinx.coroutines.CoroutineScope
4343
import kotlinx.coroutines.launch
4444
import kotlin.time.Duration.Companion.milliseconds
4545

@@ -55,6 +55,7 @@ interface VoiceMessagePresenterModule {
5555
class VoiceMessagePresenter @AssistedInject constructor(
5656
voiceMessagePlayerFactory: VoiceMessagePlayer.Factory,
5757
private val analyticsService: AnalyticsService,
58+
private val scope: CoroutineScope,
5859
@Assisted private val content: TimelineItemVoiceContent,
5960
) : Presenter<VoiceMessageState> {
6061

@@ -70,13 +71,13 @@ class VoiceMessagePresenter @AssistedInject constructor(
7071
body = content.body,
7172
)
7273

74+
private val play = mutableStateOf<Async<Unit>>(Async.Uninitialized)
75+
private var progressCache: Float = 0f
76+
7377
@Composable
7478
override fun present(): VoiceMessageState {
7579

76-
val scope = rememberCoroutineScope()
77-
7880
val playerState by player.state.collectAsState(VoiceMessagePlayer.State(isPlaying = false, isMyMedia = false, currentPosition = 0L))
79-
val play = remember { mutableStateOf<Async<Unit>>(Async.Uninitialized) }
8081

8182
val button by remember {
8283
derivedStateOf {
@@ -90,7 +91,12 @@ class VoiceMessagePresenter @AssistedInject constructor(
9091
}
9192
}
9293
val progress by remember {
93-
derivedStateOf { if (playerState.isMyMedia) playerState.currentPosition / content.duration.toMillis().toFloat() else 0f }
94+
derivedStateOf {
95+
if (playerState.isMyMedia) {
96+
progressCache = playerState.currentPosition / content.duration.toMillis().toFloat()
97+
}
98+
progressCache
99+
}
94100
}
95101
val time by remember {
96102
derivedStateOf {

features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMes
3131
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
3232
import io.element.android.services.analytics.api.AnalyticsService
3333
import io.element.android.services.analytics.test.FakeAnalyticsService
34+
import kotlinx.coroutines.test.TestScope
3435
import kotlinx.coroutines.test.runTest
3536
import org.junit.Test
3637

@@ -201,7 +202,7 @@ class VoiceMessagePresenterTest {
201202
}
202203
}
203204

204-
fun createVoiceMessagePresenter(
205+
fun TestScope.createVoiceMessagePresenter(
205206
voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(),
206207
analyticsService: AnalyticsService = FakeAnalyticsService(),
207208
content: TimelineItemVoiceContent = aTimelineItemVoiceContent(),
@@ -217,5 +218,6 @@ fun createVoiceMessagePresenter(
217218
)
218219
},
219220
analyticsService = analyticsService,
221+
scope = this,
220222
content = content,
221223
)

0 commit comments

Comments
 (0)