From 44b8b10edfbd311751a47be80ff854e98a10083d Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Mon, 27 Jan 2020 01:12:37 +0900 Subject: [PATCH 01/42] Implement posting thumbs-up count to firestore --- .../confsched2020/data/firestore/Firestore.kt | 1 + .../data/firestore/internal/FirestoreImpl.kt | 75 +++++++++++++++++++ .../droidkaigi/confsched2020/model/Shard.kt | 3 + .../confsched2020/model/ThumbsUpCounter.kt | 3 + 4 files changed, 82 insertions(+) create mode 100644 model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/Shard.kt create mode 100644 model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCounter.kt diff --git a/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt b/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt index e6ceccf79..2c1d10ce8 100644 --- a/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt +++ b/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt @@ -6,4 +6,5 @@ import kotlinx.coroutines.flow.Flow interface Firestore { fun getFavoriteSessionIds(): Flow> suspend fun toggleFavorite(sessionId: SessionId) + fun thumbsUp(sessionId: SessionId): Flow } diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index 0e97bdc9d..77c2c2c01 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -1,15 +1,20 @@ package io.github.droidkaigi.confsched2020.data.firestore.internal +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.CollectionReference import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.Query import com.google.firebase.firestore.QuerySnapshot import com.google.firebase.firestore.Source import io.github.droidkaigi.confsched2020.data.firestore.Firestore import io.github.droidkaigi.confsched2020.model.SessionId +import io.github.droidkaigi.confsched2020.model.Shard +import io.github.droidkaigi.confsched2020.model.ThumbsUpCounter import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -20,8 +25,13 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.tasks.await import timber.log.Timber import timber.log.debug +import kotlin.math.floor +import kotlin.math.sin internal class FirestoreImpl @Inject constructor() : Firestore { + companion object { + const val NUM_SHARDS = 10 + } override fun getFavoriteSessionIds(): Flow> { val setupFavorites = flow { @@ -63,6 +73,15 @@ internal class FirestoreImpl @Inject constructor() : Firestore { Timber.debug { "toggleFavorite: end" } } + override fun thumbsUp(sessionId: SessionId): Flow { + return flow { + signInIfNeeded() + val counterRef = createThumbsUpCounterIfNeeded(sessionId = sessionId, numShards = NUM_SHARDS) + incrementThumbsUpCount(counterRef) + emit(getThumbsUpCount(counterRef)) + } + } + private fun getFavoritesRef(): CollectionReference { val firebaseAuth = FirebaseAuth.getInstance() val firebaseUserId = firebaseAuth.currentUser?.uid ?: throw RuntimeException( @@ -83,6 +102,62 @@ internal class FirestoreImpl @Inject constructor() : Firestore { firebaseAuth.signInAnonymously().await() Timber.debug { "signInIfNeeded end" } } + + private suspend fun createThumbsUpCounterIfNeeded(sessionId: SessionId, numShards: Int): DocumentReference { + val firebaseAuth = FirebaseAuth.getInstance() + val firebaseUserId = firebaseAuth.currentUser?.uid ?: throw RuntimeException( + "RuntimeException" + ) + val counterDocumentRef = FirebaseFirestore + .getInstance() + .collection("confsched/2020/sessions/$sessionId/thumbsup_counters") + .document(firebaseUserId) + + if (counterDocumentRef.fastGet().exists()) { + Timber.debug { "createThumbsUpCounterIfNeeded counter already exists" } + return counterDocumentRef + } + + counterDocumentRef.set(ThumbsUpCounter(numShards)) + .continueWithTask { task -> + if (!task.isSuccessful) { + throw task.exception!! + } + + val tasks = arrayListOf>() + + // Initialize each shard with count=0 + for (i in 0 until numShards) { + val makeShard = counterDocumentRef.collection("shards") + .document(i.toString()) + .set(Shard(count = 0)) + + tasks.add(makeShard) + } + + Tasks.whenAll(tasks) + }.await() + + Timber.debug { "createThumbsUpCounterIfNeeded creating counter completed" } + return counterDocumentRef + } + + private fun incrementThumbsUpCount(counterRef: DocumentReference) { + val shardId = floor(Math.random() * NUM_SHARDS).toInt() + counterRef.collection("shards") + .document(shardId.toString()) + .update("count", FieldValue.increment(1)) + } + + private suspend fun getThumbsUpCount(counterRef: DocumentReference): Int { + val shards = counterRef.collection("shards").fastGet() + var count = 0 + shards.forEach { snap -> + val shard = snap.toObject(Shard::class.java) + count += shard.count + } + return count + } } private suspend fun DocumentReference.fastGet(): DocumentSnapshot { diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/Shard.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/Shard.kt new file mode 100644 index 000000000..e8e832881 --- /dev/null +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/Shard.kt @@ -0,0 +1,3 @@ +package io.github.droidkaigi.confsched2020.model + +data class Shard(val count: Int) diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCounter.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCounter.kt new file mode 100644 index 000000000..5a0e99aab --- /dev/null +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCounter.kt @@ -0,0 +1,3 @@ +package io.github.droidkaigi.confsched2020.model + +data class ThumbsUpCounter(val numShards: Int) From b49b8b010ebf3df7362024cff5eca839cf8f09e5 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Thu, 30 Jan 2020 03:56:41 +0900 Subject: [PATCH 02/42] Fix multiple counter to single counter, and adopt realtime updates --- .../confsched2020/data/firestore/Firestore.kt | 3 +- .../data/firestore/internal/FirestoreImpl.kt | 104 ++++++++---------- .../confsched2020/model/ThumbsUpCounter.kt | 3 - 3 files changed, 49 insertions(+), 61 deletions(-) delete mode 100644 model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCounter.kt diff --git a/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt b/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt index 2c1d10ce8..433d773f0 100644 --- a/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt +++ b/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt @@ -6,5 +6,6 @@ import kotlinx.coroutines.flow.Flow interface Firestore { fun getFavoriteSessionIds(): Flow> suspend fun toggleFavorite(sessionId: SessionId) - fun thumbsUp(sessionId: SessionId): Flow + fun getThumbsUpCount(sessionId: SessionId): Flow + suspend fun incrementThumbsUpCount(sessionId: SessionId) } diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index 77c2c2c01..b4d83234d 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -14,19 +14,18 @@ import com.google.firebase.firestore.Source import io.github.droidkaigi.confsched2020.data.firestore.Firestore import io.github.droidkaigi.confsched2020.model.SessionId import io.github.droidkaigi.confsched2020.model.Shard -import io.github.droidkaigi.confsched2020.model.ThumbsUpCounter -import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.tasks.await import timber.log.Timber import timber.log.debug +import javax.inject.Inject import kotlin.math.floor -import kotlin.math.sin internal class FirestoreImpl @Inject constructor() : Firestore { companion object { @@ -73,15 +72,38 @@ internal class FirestoreImpl @Inject constructor() : Firestore { Timber.debug { "toggleFavorite: end" } } - override fun thumbsUp(sessionId: SessionId): Flow { - return flow { + override fun getThumbsUpCount(sessionId: SessionId): Flow { + val setupThumbsUp = flow { signInIfNeeded() - val counterRef = createThumbsUpCounterIfNeeded(sessionId = sessionId, numShards = NUM_SHARDS) - incrementThumbsUpCount(counterRef) - emit(getThumbsUpCount(counterRef)) + val counterRef = getThumbsUpCounterRef(sessionId) + createShardsIfNeeded(counterRef) + emit(counterRef) + } + + val thumbsUpSnapshot = setupThumbsUp.flatMapLatest { + it.toFlow() + } + + return thumbsUpSnapshot.map { shards -> + var count = 0 + shards.forEach { snap -> + val shard = snap.toObject(Shard::class.java) + count += shard.count + } + count } } + override suspend fun incrementThumbsUpCount(sessionId: SessionId) { + signInIfNeeded() + val counterRef = getThumbsUpCounterRef(sessionId) + val shardId = floor(Math.random() * NUM_SHARDS).toInt() + counterRef + .document(shardId.toString()) + .update("count", FieldValue.increment(1)) + .await() + } + private fun getFavoritesRef(): CollectionReference { val firebaseAuth = FirebaseAuth.getInstance() val firebaseUserId = firebaseAuth.currentUser?.uid ?: throw RuntimeException( @@ -103,60 +125,28 @@ internal class FirestoreImpl @Inject constructor() : Firestore { Timber.debug { "signInIfNeeded end" } } - private suspend fun createThumbsUpCounterIfNeeded(sessionId: SessionId, numShards: Int): DocumentReference { - val firebaseAuth = FirebaseAuth.getInstance() - val firebaseUserId = firebaseAuth.currentUser?.uid ?: throw RuntimeException( - "RuntimeException" - ) - val counterDocumentRef = FirebaseFirestore + private fun getThumbsUpCounterRef(sessionId: SessionId): CollectionReference { + return FirebaseFirestore .getInstance() - .collection("confsched/2020/sessions/$sessionId/thumbsup_counters") - .document(firebaseUserId) - - if (counterDocumentRef.fastGet().exists()) { - Timber.debug { "createThumbsUpCounterIfNeeded counter already exists" } - return counterDocumentRef - } - - counterDocumentRef.set(ThumbsUpCounter(numShards)) - .continueWithTask { task -> - if (!task.isSuccessful) { - throw task.exception!! - } - - val tasks = arrayListOf>() - - // Initialize each shard with count=0 - for (i in 0 until numShards) { - val makeShard = counterDocumentRef.collection("shards") - .document(i.toString()) - .set(Shard(count = 0)) - - tasks.add(makeShard) - } - - Tasks.whenAll(tasks) - }.await() - - Timber.debug { "createThumbsUpCounterIfNeeded creating counter completed" } - return counterDocumentRef + .collection("confsched/2020/sessions/$sessionId/thumbsup_counter") } - private fun incrementThumbsUpCount(counterRef: DocumentReference) { - val shardId = floor(Math.random() * NUM_SHARDS).toInt() - counterRef.collection("shards") - .document(shardId.toString()) - .update("count", FieldValue.increment(1)) - } + private suspend fun createShardsIfNeeded(counterRef: CollectionReference) { + if (counterRef.fastGet().isEmpty) { + Timber.debug { "createShardsIfNeeded shards already exist" } + return + } - private suspend fun getThumbsUpCount(counterRef: DocumentReference): Int { - val shards = counterRef.collection("shards").fastGet() - var count = 0 - shards.forEach { snap -> - val shard = snap.toObject(Shard::class.java) - count += shard.count + val tasks = arrayListOf>() + (0 until NUM_SHARDS).forEach { + val makeShard = counterRef + .document(it.toString()) + .set(Shard(count = 0)) + tasks.add(makeShard) } - return count + + Tasks.whenAll(tasks).await() + Timber.debug { "createShardsIfNeeded creating shards completed" } } } diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCounter.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCounter.kt deleted file mode 100644 index 5a0e99aab..000000000 --- a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCounter.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.droidkaigi.confsched2020.model - -data class ThumbsUpCounter(val numShards: Int) From 4a2de2517c3f9534ca0f1d34283caeecab2fd9c1 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sun, 2 Feb 2020 17:38:59 +0900 Subject: [PATCH 03/42] Apply some fixes for thumbs up --- .../data/firestore/internal/FirestoreImpl.kt | 26 ++++++++++--------- .../droidkaigi/confsched2020/model/Shard.kt | 3 --- 2 files changed, 14 insertions(+), 15 deletions(-) delete mode 100644 model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/Shard.kt diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index b4d83234d..c88b47a05 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -13,7 +13,6 @@ import com.google.firebase.firestore.QuerySnapshot import com.google.firebase.firestore.Source import io.github.droidkaigi.confsched2020.data.firestore.Firestore import io.github.droidkaigi.confsched2020.model.SessionId -import io.github.droidkaigi.confsched2020.model.Shard import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -28,9 +27,6 @@ import javax.inject.Inject import kotlin.math.floor internal class FirestoreImpl @Inject constructor() : Firestore { - companion object { - const val NUM_SHARDS = 10 - } override fun getFavoriteSessionIds(): Flow> { val setupFavorites = flow { @@ -44,7 +40,7 @@ internal class FirestoreImpl @Inject constructor() : Firestore { emit(favoritesRef) } val favoritesSnapshotFlow = setupFavorites.flatMapLatest { - it.whereEqualTo("favorite", true).toFlow() + it.whereEqualTo(FAVORITE_VALUE_KEY, true).toFlow() } return favoritesSnapshotFlow.mapLatest { favorites -> Timber.debug { "favoritesSnapshotFlow onNext" } @@ -66,7 +62,7 @@ internal class FirestoreImpl @Inject constructor() : Firestore { } else { Timber.debug { "toggleFavorite: $sessionId document not exits" } document.reference - .set(mapOf("favorite" to newFavorite)) + .set(mapOf(FAVORITE_VALUE_KEY to newFavorite)) .await() } Timber.debug { "toggleFavorite: end" } @@ -87,8 +83,7 @@ internal class FirestoreImpl @Inject constructor() : Firestore { return thumbsUpSnapshot.map { shards -> var count = 0 shards.forEach { snap -> - val shard = snap.toObject(Shard::class.java) - count += shard.count + count += snap.get(SHARDS_COUNT_KEY, Int::class.java) ?: 0 } count } @@ -97,10 +92,11 @@ internal class FirestoreImpl @Inject constructor() : Firestore { override suspend fun incrementThumbsUpCount(sessionId: SessionId) { signInIfNeeded() val counterRef = getThumbsUpCounterRef(sessionId) + createShardsIfNeeded(counterRef) val shardId = floor(Math.random() * NUM_SHARDS).toInt() counterRef .document(shardId.toString()) - .update("count", FieldValue.increment(1)) + .update(SHARDS_COUNT_KEY, FieldValue.increment(1)) .await() } @@ -128,11 +124,11 @@ internal class FirestoreImpl @Inject constructor() : Firestore { private fun getThumbsUpCounterRef(sessionId: SessionId): CollectionReference { return FirebaseFirestore .getInstance() - .collection("confsched/2020/sessions/$sessionId/thumbsup_counter") + .collection("confsched/2020/sessions/${sessionId.id}/thumbsup_counters") } private suspend fun createShardsIfNeeded(counterRef: CollectionReference) { - if (counterRef.fastGet().isEmpty) { + if (!counterRef.fastGet().isEmpty) { Timber.debug { "createShardsIfNeeded shards already exist" } return } @@ -141,13 +137,19 @@ internal class FirestoreImpl @Inject constructor() : Firestore { (0 until NUM_SHARDS).forEach { val makeShard = counterRef .document(it.toString()) - .set(Shard(count = 0)) + .set(mapOf(SHARDS_COUNT_KEY to 0)) tasks.add(makeShard) } Tasks.whenAll(tasks).await() Timber.debug { "createShardsIfNeeded creating shards completed" } } + + companion object { + const val NUM_SHARDS = 10 + const val SHARDS_COUNT_KEY = "shards" + const val FAVORITE_VALUE_KEY = "favorite" + } } private suspend fun DocumentReference.fastGet(): DocumentSnapshot { diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/Shard.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/Shard.kt deleted file mode 100644 index e8e832881..000000000 --- a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/Shard.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.droidkaigi.confsched2020.model - -data class Shard(val count: Int) From 286d6bd6215a47973480359260e07fe35414ff93 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 2 Feb 2020 02:52:08 +0900 Subject: [PATCH 04/42] Debugging thumbs-up function of firestore --- .../data/firestore/internal/FirestoreImpl.kt | 6 ++- .../internal/DataSessionRepository.kt | 10 ++++- .../session/ui/SessionDetailFragment.kt | 8 +++- .../session/ui/item/SessionDetailTitleItem.kt | 10 ++++- .../ui/viewmodel/SessionDetailViewModel.kt | 41 ++++++++++++++++--- .../res/layout/item_session_detail_title.xml | 18 ++++++-- .../model/repository/SessionRepository.kt | 2 + 7 files changed, 81 insertions(+), 14 deletions(-) diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index c88b47a05..6cbb4aff3 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -141,7 +141,11 @@ internal class FirestoreImpl @Inject constructor() : Firestore { tasks.add(makeShard) } - Tasks.whenAll(tasks).await() + try { + Tasks.whenAll(tasks).await() + } catch(e: Exception) { + println("⭐" + e.message) + } Timber.debug { "createShardsIfNeeded creating shards completed" } } diff --git a/data/repository/src/main/java/io/github/droidkaigi/confsched2020/data/repository/internal/DataSessionRepository.kt b/data/repository/src/main/java/io/github/droidkaigi/confsched2020/data/repository/internal/DataSessionRepository.kt index 10e5bc022..774ef8b9b 100644 --- a/data/repository/src/main/java/io/github/droidkaigi/confsched2020/data/repository/internal/DataSessionRepository.kt +++ b/data/repository/src/main/java/io/github/droidkaigi/confsched2020/data/repository/internal/DataSessionRepository.kt @@ -18,7 +18,6 @@ import io.github.droidkaigi.confsched2020.model.SessionId import io.github.droidkaigi.confsched2020.model.SessionList import io.github.droidkaigi.confsched2020.model.SpeechSession import io.github.droidkaigi.confsched2020.model.repository.SessionRepository -import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter @@ -26,6 +25,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import timber.log.Timber import timber.log.debug +import javax.inject.Inject internal class DataSessionRepository @Inject constructor( private val droidKaigiApi: DroidKaigiApi, @@ -129,4 +129,12 @@ internal class DataSessionRepository @Inject constructor( val response = droidKaigiApi.getSessions() sessionDatabase.save(response) } + + override fun thumbsUpCounts(sessionId: SessionId): Flow { + return firestore.getThumbsUpCount(sessionId) + } + + override suspend fun incrementThumbsUpCount(sessionId: SessionId) { + firestore.incrementThumbsUpCount(sessionId) + } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt index 74ded3edc..1b5a5ab76 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt @@ -130,6 +130,7 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject uiModel.searchQuery ) } + println("⭐ ${uiModel.thumbsUpCount}") } binding.bottomAppBar.setOnMenuItemClickListener { menuItem -> @@ -184,7 +185,12 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject "${session.id}-${navArgs.transitionNameSuffix}" val items = mutableListOf() - items += sessionDetailTitleItemFactory.create(session, searchQuery) + items += sessionDetailTitleItemFactory.create( + session, + searchQuery + ) { + sessionDetailViewModel.thumbsUp(session) + } items += sessionDetailDescriptionItemFactory.create( session, showEllipsis, diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index c74a43ae3..dd00911e0 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -21,7 +21,8 @@ import java.util.regex.Pattern class SessionDetailTitleItem @AssistedInject constructor( @Assisted private val session: Session, - @Assisted private val searchQuery: String? + @Assisted private val searchQuery: String?, + @Assisted private val thumbsUpListener: () -> Unit ) : BindableItem() { override fun getLayout() = R.layout.item_session_detail_title @@ -59,6 +60,10 @@ class SessionDetailTitleItem @AssistedInject constructor( // Test Code // binding.sessionMessage.text = "セッション部屋がRoom1からRoom3に変更になりました(サンプル)" // binding.sessionMessage.isVisible = true + + binding.thumbsUp.setOnClickListener { + thumbsUpListener.invoke() + } } } @@ -87,7 +92,8 @@ class SessionDetailTitleItem @AssistedInject constructor( interface Factory { fun create( session: Session, - searchQuery: String? = null + searchQuery: String? = null, + thumbsUpListener: () -> Unit ): SessionDetailTitleItem } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index 523ceca5e..e8f4e5ca2 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -11,11 +11,11 @@ import io.github.droidkaigi.confsched2020.ext.combine import io.github.droidkaigi.confsched2020.ext.toAppError import io.github.droidkaigi.confsched2020.ext.toLoadingState import io.github.droidkaigi.confsched2020.model.AppError -import io.github.droidkaigi.confsched2020.model.TextExpandState import io.github.droidkaigi.confsched2020.model.LoadState import io.github.droidkaigi.confsched2020.model.LoadingState import io.github.droidkaigi.confsched2020.model.Session import io.github.droidkaigi.confsched2020.model.SessionId +import io.github.droidkaigi.confsched2020.model.TextExpandState import io.github.droidkaigi.confsched2020.model.repository.SessionRepository import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map @@ -32,10 +32,18 @@ class SessionDetailViewModel @AssistedInject constructor( val error: AppError?, val session: Session?, val showEllipsis: Boolean, - val searchQuery: String? + val searchQuery: String?, + val thumbsUpCount: Int ) { companion object { - val EMPTY = UiModel(false, null, null, true, null) + val EMPTY = UiModel( + isLoading = false, + error = null , + session = null, + showEllipsis = true, + searchQuery = null, + thumbsUpCount = 0 + ) } } @@ -55,16 +63,25 @@ class SessionDetailViewModel @AssistedInject constructor( private val descriptionTextExpandStateLiveData: MutableLiveData = MutableLiveData(TextExpandState.COLLAPSED) + private val thumbsUpCountLiveData: LiveData = liveData { + sessionRepository.thumbsUpCounts(sessionId) + .collect { thumbsUpCount -> + emit(thumbsUpCount) + } + } + // Produce UiModel val uiModel: LiveData = combine( initialValue = UiModel.EMPTY, liveData1 = sessionLoadStateLiveData, liveData2 = favoriteLoadingStateLiveData, - liveData3 = descriptionTextExpandStateLiveData + liveData3 = descriptionTextExpandStateLiveData, + liveData4 = thumbsUpCountLiveData ) { current: UiModel, sessionLoadState: LoadState, favoriteState: LoadingState, - descriptionTextExpandState: TextExpandState -> + descriptionTextExpandState: TextExpandState, + thumbsUpCount: Int -> val isLoading = sessionLoadState.isLoading || favoriteState.isLoading val sessions = when (sessionLoadState) { @@ -87,7 +104,8 @@ class SessionDetailViewModel @AssistedInject constructor( .toAppError(), session = sessions, showEllipsis = showEllipsis, - searchQuery = searchQuery + searchQuery = searchQuery, + thumbsUpCount = thumbsUpCount ) } @@ -107,6 +125,17 @@ class SessionDetailViewModel @AssistedInject constructor( descriptionTextExpandStateLiveData.value = TextExpandState.EXPANDED } + fun thumbsUp(session: Session) { + viewModelScope.launch { + try { + sessionRepository.incrementThumbsUpCount(session.id) + } catch (e: Exception) { + // TODO: implement + println("⭐ " + e.message) + } + } + } + @AssistedInject.Factory interface Factory { fun create( diff --git a/feature/session/src/main/res/layout/item_session_detail_title.xml b/feature/session/src/main/res/layout/item_session_detail_title.xml index 61d9531d4..2c91978d0 100644 --- a/feature/session/src/main/res/layout/item_session_detail_title.xml +++ b/feature/session/src/main/res/layout/item_session_detail_title.xml @@ -99,7 +99,7 @@ + + diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/repository/SessionRepository.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/repository/SessionRepository.kt index aa84395ec..ff0c9b4d8 100644 --- a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/repository/SessionRepository.kt +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/repository/SessionRepository.kt @@ -17,4 +17,6 @@ interface SessionRepository { session: SpeechSession, sessionFeedback: SessionFeedback ) + fun thumbsUpCounts(sessionId: SessionId): Flow + suspend fun incrementThumbsUpCount(sessionId: SessionId) } From bf57b49126f02b44c6886618d50606b9a6554e52 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Mon, 3 Feb 2020 02:02:33 +0900 Subject: [PATCH 05/42] Success to show thumbs-up counts if shards are already exists --- .../data/firestore/internal/FirestoreImpl.kt | 1 + .../confsched2020/session/ui/SessionDetailFragment.kt | 10 ++++++---- .../session/ui/item/SessionDetailTitleItem.kt | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index 6cbb4aff3..f53a01f0f 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -144,6 +144,7 @@ internal class FirestoreImpl @Inject constructor() : Firestore { try { Tasks.whenAll(tasks).await() } catch(e: Exception) { + // FIXME: debug code println("⭐" + e.message) } Timber.debug { "createShardsIfNeeded creating shards completed" } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt index 1b5a5ab76..b1d769650 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt @@ -127,10 +127,10 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject adapter, session, uiModel.showEllipsis, - uiModel.searchQuery + uiModel.searchQuery, + uiModel.thumbsUpCount ) } - println("⭐ ${uiModel.thumbsUpCount}") } binding.bottomAppBar.setOnMenuItemClickListener { menuItem -> @@ -179,7 +179,8 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject adapter: GroupAdapter>, session: Session, showEllipsis: Boolean, - searchQuery: String? + searchQuery: String?, + thumbsUpCount: Int ) { binding.sessionDetailRecycler.transitionName = "${session.id}-${navArgs.transitionNameSuffix}" @@ -187,7 +188,8 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject val items = mutableListOf() items += sessionDetailTitleItemFactory.create( session, - searchQuery + searchQuery, + thumbsUpCount ) { sessionDetailViewModel.thumbsUp(session) } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index dd00911e0..373d3ba23 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -22,6 +22,7 @@ import java.util.regex.Pattern class SessionDetailTitleItem @AssistedInject constructor( @Assisted private val session: Session, @Assisted private val searchQuery: String?, + @Assisted private val thumbsUpCount: Int, @Assisted private val thumbsUpListener: () -> Unit ) : BindableItem() { @@ -61,6 +62,7 @@ class SessionDetailTitleItem @AssistedInject constructor( // binding.sessionMessage.text = "セッション部屋がRoom1からRoom3に変更になりました(サンプル)" // binding.sessionMessage.isVisible = true + binding.thumbsUp.text = thumbsUpCount.toString() binding.thumbsUp.setOnClickListener { thumbsUpListener.invoke() } @@ -93,6 +95,7 @@ class SessionDetailTitleItem @AssistedInject constructor( fun create( session: Session, searchQuery: String? = null, + thumbsUpCount: Int, thumbsUpListener: () -> Unit ): SessionDetailTitleItem } From da8e92fe375cf6fcf458661ee4a531260a6038db Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Mon, 3 Feb 2020 02:11:28 +0900 Subject: [PATCH 06/42] Remove a white space --- .../session/ui/viewmodel/SessionDetailViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index e8f4e5ca2..dc9373c8d 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -38,7 +38,7 @@ class SessionDetailViewModel @AssistedInject constructor( companion object { val EMPTY = UiModel( isLoading = false, - error = null , + error = null, session = null, showEllipsis = true, searchQuery = null, From 075e3b28a5573d46d243adac4d3f00f50abef72b Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Mon, 3 Feb 2020 21:23:06 +0900 Subject: [PATCH 07/42] Change processing whether shard exists into checking a shard that shard id is NUM_SHARDS --- .../data/firestore/internal/FirestoreImpl.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index f53a01f0f..6c7351cf1 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -128,7 +128,13 @@ internal class FirestoreImpl @Inject constructor() : Firestore { } private suspend fun createShardsIfNeeded(counterRef: CollectionReference) { - if (!counterRef.fastGet().isEmpty) { + val lastShardId = NUM_SHARDS - 1 + val lastShard = counterRef + .document(lastShardId.toString()) + .get(Source.SERVER) + .await() + + if (lastShard.exists()) { Timber.debug { "createShardsIfNeeded shards already exist" } return } From a4c3287d9a6dce51a55900ef718d3ced27832a44 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Tue, 4 Feb 2020 01:13:21 +0900 Subject: [PATCH 08/42] Bind error to SystemViewModel --- .../data/firestore/internal/FirestoreImpl.kt | 7 +------ .../ui/viewmodel/SessionDetailViewModel.kt | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index 6c7351cf1..b57984db7 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -147,12 +147,7 @@ internal class FirestoreImpl @Inject constructor() : Firestore { tasks.add(makeShard) } - try { - Tasks.whenAll(tasks).await() - } catch(e: Exception) { - // FIXME: debug code - println("⭐" + e.message) - } + Tasks.whenAll(tasks).await() Timber.debug { "createShardsIfNeeded creating shards completed" } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index dc9373c8d..4047c244c 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -63,8 +63,9 @@ class SessionDetailViewModel @AssistedInject constructor( private val descriptionTextExpandStateLiveData: MutableLiveData = MutableLiveData(TextExpandState.COLLAPSED) - private val thumbsUpCountLiveData: LiveData = liveData { + private val thumbsUpCountLoadStateLiveData: LiveData> = liveData { sessionRepository.thumbsUpCounts(sessionId) + .toLoadingState() .collect { thumbsUpCount -> emit(thumbsUpCount) } @@ -76,12 +77,12 @@ class SessionDetailViewModel @AssistedInject constructor( liveData1 = sessionLoadStateLiveData, liveData2 = favoriteLoadingStateLiveData, liveData3 = descriptionTextExpandStateLiveData, - liveData4 = thumbsUpCountLiveData + liveData4 = thumbsUpCountLoadStateLiveData ) { current: UiModel, sessionLoadState: LoadState, favoriteState: LoadingState, descriptionTextExpandState: TextExpandState, - thumbsUpCount: Int -> + thumbsUpCountLoadState: LoadState -> val isLoading = sessionLoadState.isLoading || favoriteState.isLoading val sessions = when (sessionLoadState) { @@ -93,6 +94,14 @@ class SessionDetailViewModel @AssistedInject constructor( } } val showEllipsis = descriptionTextExpandState == TextExpandState.COLLAPSED + val thumbsUpCount = when (thumbsUpCountLoadState) { + is LoadState.Loaded -> { + thumbsUpCountLoadState.value + } + else -> { + current.thumbsUpCount + } + } UiModel( isLoading = isLoading, @@ -100,6 +109,9 @@ class SessionDetailViewModel @AssistedInject constructor( .getErrorIfExists() .toAppError() ?: favoriteState + .getErrorIfExists() + .toAppError() + ?: thumbsUpCountLoadState .getErrorIfExists() .toAppError(), session = sessions, From 6f6b1c0c927078fa312f749abb04a9b3a1c1e22a Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Tue, 4 Feb 2020 02:39:13 +0900 Subject: [PATCH 09/42] Apply some thumbs-up button designs --- .../androidcomponent/src/main/res/values/themes.xml | 1 + .../androidcomponent/src/main/res/values/type.xml | 7 +++++++ .../src/main/res/layout/item_session_detail_title.xml | 7 ++++++- feature/session/src/main/res/values/styles.xml | 9 +++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/corecomponent/androidcomponent/src/main/res/values/themes.xml b/corecomponent/androidcomponent/src/main/res/values/themes.xml index f6c7b336a..39c9dac8f 100644 --- a/corecomponent/androidcomponent/src/main/res/values/themes.xml +++ b/corecomponent/androidcomponent/src/main/res/values/themes.xml @@ -10,6 +10,7 @@ @style/TextAppearance.DroidKaigi.Body1 @style/TextAppearance.DroidKaigi.Body2 @style/TextAppearance.DroidKaigi.Caption + @style/TextAppearance.DroidKaigi.Button @style/Widget.DroidKaigi.FilterChip diff --git a/corecomponent/androidcomponent/src/main/res/values/type.xml b/corecomponent/androidcomponent/src/main/res/values/type.xml index 240f952e5..3e602731d 100644 --- a/corecomponent/androidcomponent/src/main/res/values/type.xml +++ b/corecomponent/androidcomponent/src/main/res/values/type.xml @@ -50,6 +50,13 @@ ?colorOnBackground + + + + + + + From 3cf94b85b42968f7ebce91a91d22ebc1b1f2c3ce Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Tue, 4 Feb 2020 02:46:33 +0900 Subject: [PATCH 10/42] Add a thumbs-up icon --- .../src/main/res/drawable/ic_thumbs_up_black_24dp.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 feature/session/src/main/res/drawable/ic_thumbs_up_black_24dp.xml diff --git a/feature/session/src/main/res/drawable/ic_thumbs_up_black_24dp.xml b/feature/session/src/main/res/drawable/ic_thumbs_up_black_24dp.xml new file mode 100644 index 000000000..a924030a4 --- /dev/null +++ b/feature/session/src/main/res/drawable/ic_thumbs_up_black_24dp.xml @@ -0,0 +1,10 @@ + + + From 0b0483b5c1cf3b2d5bf8ed2f538bc053db01c65d Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Tue, 4 Feb 2020 02:55:11 +0900 Subject: [PATCH 11/42] Fix indent and variable name --- .../session/ui/viewmodel/SessionDetailViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index 4047c244c..cc3d19998 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -66,8 +66,8 @@ class SessionDetailViewModel @AssistedInject constructor( private val thumbsUpCountLoadStateLiveData: LiveData> = liveData { sessionRepository.thumbsUpCounts(sessionId) .toLoadingState() - .collect { thumbsUpCount -> - emit(thumbsUpCount) + .collect { loadState: LoadState -> + emit(loadState) } } From 0cd848466973b5a40fcf9d1473f3b507e2c3f33d Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Tue, 4 Feb 2020 17:13:11 +0900 Subject: [PATCH 12/42] Adjust thumbs-up button styles --- .../src/main/res/layout/item_session_detail_title.xml | 5 ++++- feature/session/src/main/res/values/styles.xml | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/feature/session/src/main/res/layout/item_session_detail_title.xml b/feature/session/src/main/res/layout/item_session_detail_title.xml index fea80c055..65fd2f61c 100644 --- a/feature/session/src/main/res/layout/item_session_detail_title.xml +++ b/feature/session/src/main/res/layout/item_session_detail_title.xml @@ -123,13 +123,16 @@ android:id="@+id/thumbs_up" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:minWidth="56dp" android:layout_marginTop="24dp" android:backgroundTint="?colorPrimary" android:textColor="?colorSurface" android:textAppearance="?textAppearanceButton" - style="@style/Widget.DroidKaigi.Button.OutlinedButton" + style="@style/Widget.DroidKaigi.Button.OvalButton" app:icon="@drawable/ic_thumbs_up_black_24dp" app:iconTint="?colorSurface" + app:iconPadding="4dp" + app:iconSize="16dp" app:layout_constraintStart_toStartOf="@id/guideline_start" app:layout_constraintTop_toBottomOf="@id/tags" app:layout_constraintBottom_toTopOf="@id/divider_survey_and_text" diff --git a/feature/session/src/main/res/values/styles.xml b/feature/session/src/main/res/values/styles.xml index 488b15fcb..dd18cc1c1 100644 --- a/feature/session/src/main/res/values/styles.xml +++ b/feature/session/src/main/res/values/styles.xml @@ -8,12 +8,12 @@ ?colorPrimary - - From 707fb7b8e3fc3d5de9c750cf74907b4414a99397 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Wed, 5 Feb 2020 01:34:01 +0900 Subject: [PATCH 13/42] Show outlined button if thumbs-up count is zero --- .../session/ui/item/SessionDetailTitleItem.kt | 2 +- .../main/res/layout/item_session_detail_title.xml | 14 +++++++++++--- feature/session/src/main/res/values/strings.xml | 3 ++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index 373d3ba23..a828e49d4 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -62,7 +62,7 @@ class SessionDetailTitleItem @AssistedInject constructor( // binding.sessionMessage.text = "セッション部屋がRoom1からRoom3に変更になりました(サンプル)" // binding.sessionMessage.isVisible = true - binding.thumbsUp.text = thumbsUpCount.toString() + binding.thumbsUpCount = thumbsUpCount binding.thumbsUp.setOnClickListener { thumbsUpListener.invoke() } diff --git a/feature/session/src/main/res/layout/item_session_detail_title.xml b/feature/session/src/main/res/layout/item_session_detail_title.xml index 65fd2f61c..90832ce17 100644 --- a/feature/session/src/main/res/layout/item_session_detail_title.xml +++ b/feature/session/src/main/res/layout/item_session_detail_title.xml @@ -18,6 +18,11 @@ name="lang" type="io.github.droidkaigi.confsched2020.model.Lang" /> + + ON GOING Filter - Filtering + Filtering Intended audience Share Save to Calendar @@ -21,4 +21,5 @@ MOVIE SLIDE You choose your favorite sessions, then you can make a plan for you. + + From f0db2dddd179ba8438c03dd40ba68e95f316e511 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Wed, 5 Feb 2020 23:22:09 +0900 Subject: [PATCH 14/42] Fix grradlew.bat --- gradlew.bat | 200 ++++++++++++++++++++++++++-------------------------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/gradlew.bat b/gradlew.bat index 24467a141..9618d8d96 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,100 +1,100 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 6ebcbfafd0cde6745cf2d3973313f1bdbcc8aa63 Mon Sep 17 00:00:00 2001 From: Ippei Mukaida Date: Thu, 6 Feb 2020 01:15:51 +0900 Subject: [PATCH 15/42] Change number of shards; 10 -> 5 Co-Authored-By: Takahiro Menju --- .../confsched2020/data/firestore/internal/FirestoreImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index b57984db7..b84e08519 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -152,7 +152,7 @@ internal class FirestoreImpl @Inject constructor() : Firestore { } companion object { - const val NUM_SHARDS = 10 + const val NUM_SHARDS = 5 const val SHARDS_COUNT_KEY = "shards" const val FAVORITE_VALUE_KEY = "favorite" } From e478ca9de05061c94e212738a3a753b5e526dbe5 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Wed, 5 Feb 2020 23:56:23 +0900 Subject: [PATCH 16/42] Remove unnecessary changes --- .../androidcomponent/src/main/res/values/type.xml | 7 ------- .../src/main/res/layout/item_session_detail_title.xml | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/corecomponent/androidcomponent/src/main/res/values/type.xml b/corecomponent/androidcomponent/src/main/res/values/type.xml index 3e602731d..f91693b70 100644 --- a/corecomponent/androidcomponent/src/main/res/values/type.xml +++ b/corecomponent/androidcomponent/src/main/res/values/type.xml @@ -56,11 +56,4 @@ @font/notosans_medium ?colorOnBackground - - - diff --git a/feature/session/src/main/res/layout/item_session_detail_title.xml b/feature/session/src/main/res/layout/item_session_detail_title.xml index 71795df8c..1beed88d7 100644 --- a/feature/session/src/main/res/layout/item_session_detail_title.xml +++ b/feature/session/src/main/res/layout/item_session_detail_title.xml @@ -105,7 +105,7 @@ Date: Sun, 9 Feb 2020 01:14:12 +0900 Subject: [PATCH 17/42] Fix SessionDetailViewModelTest --- .../viewmodel/SessionDetailViewModelTest.kt | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt index 82a4961df..85dfe73f9 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt @@ -35,8 +35,10 @@ class SessionDetailViewModelTest { .test() val valueHistory = testObserver.valueHistory() - valueHistory[0] shouldBe SessionDetailViewModel.UiModel.EMPTY.copy(isLoading = true) - valueHistory[1].apply { + // Observer does not receive UiModel.EMPTY, + // because sessionRepository.sessionContents does not always emit + // before UiModel is observed + valueHistory[0].apply { isLoading shouldBe false session shouldBe Dummies.speachSession1 error shouldBe null @@ -59,8 +61,7 @@ class SessionDetailViewModelTest { .test() val valueHistory = testObserver.valueHistory() - valueHistory[0] shouldBe SessionDetailViewModel.UiModel.EMPTY.copy(isLoading = true) - valueHistory[1].apply { + valueHistory[0].apply { isLoading shouldBe false session shouldBe null error shouldNotBe null @@ -86,22 +87,21 @@ class SessionDetailViewModelTest { verify { sessionDetailViewModel.favorite(Dummies.speachSession1) } val valueHistory = testObserver.valueHistory() - valueHistory[0] shouldBe SessionDetailViewModel.UiModel.EMPTY.copy(isLoading = true) - valueHistory[1].apply { + valueHistory[0].apply { isLoading shouldBe false session shouldBe Dummies.speachSession1 error shouldBe null showEllipsis shouldBe true searchQuery shouldBe null } - valueHistory[2].apply { + valueHistory[1].apply { isLoading shouldBe true session shouldBe Dummies.speachSession1 error shouldBe null showEllipsis shouldBe true searchQuery shouldBe null } - valueHistory[3].apply { + valueHistory[2].apply { isLoading shouldBe false session shouldBe Dummies.speachSession1 error shouldBe null @@ -147,12 +147,7 @@ class SessionDetailViewModelTest { .test() val valueHistory = testObserver.valueHistory() - valueHistory[0] shouldBe - SessionDetailViewModel.UiModel.EMPTY.copy( - isLoading = true, - searchQuery = "query" - ) - valueHistory[1].apply { + valueHistory[0].apply { isLoading shouldBe false session shouldBe Dummies.speachSession1 error shouldBe null From 3bac614b48188c3c2947479d8b27164d4af2dca9 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 9 Feb 2020 01:24:08 +0900 Subject: [PATCH 18/42] Add test to get thumbs-up count --- .../session/ui/viewmodel/Dummies.kt | 2 ++ .../viewmodel/SessionDetailViewModelTest.kt | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/Dummies.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/Dummies.kt index 478ea302b..6b12b5ce2 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/Dummies.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/Dummies.kt @@ -80,4 +80,6 @@ object Dummies { category = listOf(category), levels = listOf(Level.BEGINNER, Level.INTERMEDIATE, Level.ADVANCED) ) + + val thumbsUpCount = 10 } diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt index 85dfe73f9..c81027d7e 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt @@ -155,4 +155,30 @@ class SessionDetailViewModelTest { searchQuery shouldBe "query" } } + + @Test + fun thumbsUpCount() { + coEvery { + sessionRepository.thumbsUpCounts(Dummies.speachSession1.id) + } returns flowOf(Dummies.thumbsUpCount) + val sessionDetailViewModel = SessionDetailViewModel( + sessionId = Dummies.speachSession1.id, + sessionRepository = sessionRepository, + searchQuery = null + ) + val testObserver = sessionDetailViewModel + .uiModel + .test() + + val valueHistory = testObserver.valueHistory() + valueHistory[0] shouldBe SessionDetailViewModel.UiModel.EMPTY.copy(isLoading = true) + valueHistory[1].apply { + isLoading shouldBe true + session shouldBe null + error shouldBe null + showEllipsis shouldBe true + searchQuery shouldBe null + thumbsUpCount shouldBe Dummies.thumbsUpCount + } + } } From 8e26ef03466afcefbb67667be85e0113ad42003d Mon Sep 17 00:00:00 2001 From: takahirom Date: Fri, 7 Feb 2020 23:12:24 +0900 Subject: [PATCH 19/42] Count up at intervals for FireStore charges --- data/firestore/build.gradle | 8 +++ .../data/firestore/internal/FirestoreImpl.kt | 70 +++++++++++++++++-- .../firestore/internal/FirestoreImplTest.kt | 61 ++++++++++++++++ 3 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 data/firestore/src/test/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImplTest.kt diff --git a/data/firestore/build.gradle b/data/firestore/build.gradle index 6f20567fa..79fbdce3e 100644 --- a/data/firestore/build.gradle +++ b/data/firestore/build.gradle @@ -52,6 +52,14 @@ kotlin { implementation Dep.Test.KotlinMultiPlatform.commonModuleTestAnnotations implementation Dep.MockK.common } + test { + dependsOn commonMain + dependencies { + implementation Dep.Test.KotlinMultiPlatform.jvmModuleTest + implementation Dep.Test.KotlinMultiPlatform.jvmModuleTestJunit + implementation Dep.MockK.jvm + } + } androidTest { dependsOn commonMain dependencies { diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index b84e08519..33384dc17 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -13,13 +13,22 @@ import com.google.firebase.firestore.QuerySnapshot import com.google.firebase.firestore.Source import io.github.droidkaigi.confsched2020.data.firestore.Firestore import io.github.droidkaigi.confsched2020.model.SessionId +import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.tasks.await import timber.log.Timber import timber.log.debug @@ -27,6 +36,7 @@ import javax.inject.Inject import kotlin.math.floor internal class FirestoreImpl @Inject constructor() : Firestore { + private val thumbsUpEventChannel = BroadcastChannel(10000) override fun getFavoriteSessionIds(): Flow> { val setupFavorites = flow { @@ -68,6 +78,7 @@ internal class FirestoreImpl @Inject constructor() : Firestore { Timber.debug { "toggleFavorite: end" } } + @Suppress("EXPERIMENTAL_API_USAGE") override fun getThumbsUpCount(sessionId: SessionId): Flow { val setupThumbsUp = flow { signInIfNeeded() @@ -76,9 +87,41 @@ internal class FirestoreImpl @Inject constructor() : Firestore { emit(counterRef) } - val thumbsUpSnapshot = setupThumbsUp.flatMapLatest { - it.toFlow() - } + var appliedCount = -1 + val unappliedCountChannel = BroadcastChannel(10000) + val incrementFlow = thumbsUpEventChannel.asFlow() + .filter { it == sessionId } + .withIndex() + .onEach { + unappliedCountChannel.send( + minOf( + it.index - appliedCount, + MAX_APPLY_COUNT + ) + ) + } + // For firebase pricing + .debounce(INCREMENT_DEBOUNCE_MILLIS) + .map { + val result = minOf(it.index - appliedCount, MAX_APPLY_COUNT) + appliedCount = it.index + unappliedCountChannel.send(0) + it.value to result + } + .map { (sessionId, count) -> + Timber.debug { "thumb up increment:$count" } + incrementThumbsUpCountImpl(sessionId, count) + count + } + .onStart { + emit(0) + unappliedCountChannel.send(0) + } + val thumbsUpSnapshot = setupThumbsUp + .flatMapLatest { + it.toFlow() + } + .combine(incrementFlow) { thumbsCount, _ -> thumbsCount } return thumbsUpSnapshot.map { shards -> var count = 0 @@ -87,16 +130,33 @@ internal class FirestoreImpl @Inject constructor() : Firestore { } count } + .combine(unappliedCountChannel.asFlow()) { firestoreCount, unappliedCount -> + firestoreCount + unappliedCount + }.scan(0) { prev, value -> + // Prevent counts from dropping due to calculations + if (prev < value) { + value + } else { + prev + } + } } override suspend fun incrementThumbsUpCount(sessionId: SessionId) { + thumbsUpEventChannel.send(sessionId) + } + + private suspend fun incrementThumbsUpCountImpl( + sessionId: SessionId, + count: Int + ) { signInIfNeeded() val counterRef = getThumbsUpCounterRef(sessionId) createShardsIfNeeded(counterRef) val shardId = floor(Math.random() * NUM_SHARDS).toInt() counterRef .document(shardId.toString()) - .update(SHARDS_COUNT_KEY, FieldValue.increment(1)) + .update(SHARDS_COUNT_KEY, FieldValue.increment(count.toLong())) .await() } @@ -153,6 +213,8 @@ internal class FirestoreImpl @Inject constructor() : Firestore { companion object { const val NUM_SHARDS = 5 + const val MAX_APPLY_COUNT = 50 + const val INCREMENT_DEBOUNCE_MILLIS = 500L const val SHARDS_COUNT_KEY = "shards" const val FAVORITE_VALUE_KEY = "favorite" } diff --git a/data/firestore/src/test/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImplTest.kt b/data/firestore/src/test/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImplTest.kt new file mode 100644 index 000000000..b2eb49746 --- /dev/null +++ b/data/firestore/src/test/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImplTest.kt @@ -0,0 +1,61 @@ +package io.github.droidkaigi.confsched2020.data.firestore.internal + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.withIndex +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Ignore +import org.junit.Test + +class FirestoreImplTest { + @Ignore + @Test + fun thumbsUpIncrement() { + val channel = BroadcastChannel(10000) + GlobalScope.launch { + delay(100) + println("send") + channel.send(Unit) + println("send") + channel.send(Unit) + println("send") + channel.send(Unit) + delay(600) + println("send") + channel.send(Unit) + delay(200) + println("send") + channel.send(Unit) + delay(200) + println("send") + channel.send(Unit) + delay(200) + println("send") + channel.send(Unit) + delay(600) + channel.cancel() + } + runBlocking { + var lastIndex = -1 + channel.asFlow() + .withIndex() + .debounce(300) + .map { + val result = minOf(it.index - lastIndex, 50) + lastIndex = it.index + result + } + .collect { + println(it) + } + } + } +} + + From 59aad5a0290410cf1505064348dd67dec7dcd2f6 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 9 Feb 2020 03:52:14 +0900 Subject: [PATCH 20/42] Apply ktlintFormat --- .../confsched2020/data/firestore/internal/FirestoreImplTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/data/firestore/src/test/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImplTest.kt b/data/firestore/src/test/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImplTest.kt index b2eb49746..df165bb9a 100644 --- a/data/firestore/src/test/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImplTest.kt +++ b/data/firestore/src/test/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImplTest.kt @@ -57,5 +57,3 @@ class FirestoreImplTest { } } } - - From 67e699a958c3c0c4b7e2f02484540fe64a6ddc6a Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 9 Feb 2020 21:17:43 +0900 Subject: [PATCH 21/42] Separate incremented count flow from total count flow --- .../droidkaigi/confsched2020/ext/LiveDatas.kt | 37 +++++++++ .../confsched2020/data/firestore/Firestore.kt | 2 +- .../data/firestore/internal/FirestoreImpl.kt | 67 +-------------- .../internal/DataSessionRepository.kt | 11 ++- .../session/ui/SessionDetailFragment.kt | 2 +- .../ui/viewmodel/SessionDetailViewModel.kt | 82 +++++++++++++++---- .../viewmodel/SessionDetailViewModelTest.kt | 2 +- .../model/repository/SessionRepository.kt | 2 +- 8 files changed, 119 insertions(+), 86 deletions(-) diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt index be14795fd..759f2f43a 100644 --- a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt @@ -108,6 +108,43 @@ inline fun combine }.distinctUntilChanged() } +inline fun combine( + initialValue: T, + liveData1: LiveData, + liveData2: LiveData, + liveData3: LiveData, + liveData4: LiveData, + liveData5: LiveData, + crossinline block: (T, LIVE1, LIVE2, LIVE3, LIVE4, LIVE5) -> T +): LiveData { + return MediatorLiveData().apply { + value = initialValue + listOf(liveData1, liveData2, liveData3, liveData4, liveData5).forEach { liveData -> + addSource(liveData) { + val currentValue = value + val liveData1Value = liveData1.value + val liveData2Value = liveData2.value + val liveData3Value = liveData3.value + val liveData4Value = liveData4.value + val liveData5Value = liveData5.value + if (currentValue != null && liveData1Value != null && + liveData2Value != null && liveData3Value != null && + liveData4Value != null && liveData5Value != null + ) { + value = block( + currentValue, + liveData1Value, + liveData2Value, + liveData3Value, + liveData4Value, + liveData5Value + ) + } + } + } + }.distinctUntilChanged() +} + fun LiveData.setOnEach(mutableLiveData: MutableLiveData): LiveData { return map { mutableLiveData.value = it diff --git a/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt b/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt index 433d773f0..7cbfe5596 100644 --- a/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt +++ b/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/data/firestore/Firestore.kt @@ -7,5 +7,5 @@ interface Firestore { fun getFavoriteSessionIds(): Flow> suspend fun toggleFavorite(sessionId: SessionId) fun getThumbsUpCount(sessionId: SessionId): Flow - suspend fun incrementThumbsUpCount(sessionId: SessionId) + suspend fun incrementThumbsUpCount(sessionId: SessionId, count: Int) } diff --git a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt index 33384dc17..7500b093b 100644 --- a/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt +++ b/data/firestore/src/main/java/io/github/droidkaigi/confsched2020/data/firestore/internal/FirestoreImpl.kt @@ -13,22 +13,13 @@ import com.google.firebase.firestore.QuerySnapshot import com.google.firebase.firestore.Source import io.github.droidkaigi.confsched2020.data.firestore.Firestore import io.github.droidkaigi.confsched2020.model.SessionId -import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.scan -import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.tasks.await import timber.log.Timber import timber.log.debug @@ -36,7 +27,6 @@ import javax.inject.Inject import kotlin.math.floor internal class FirestoreImpl @Inject constructor() : Firestore { - private val thumbsUpEventChannel = BroadcastChannel(10000) override fun getFavoriteSessionIds(): Flow> { val setupFavorites = flow { @@ -78,7 +68,6 @@ internal class FirestoreImpl @Inject constructor() : Firestore { Timber.debug { "toggleFavorite: end" } } - @Suppress("EXPERIMENTAL_API_USAGE") override fun getThumbsUpCount(sessionId: SessionId): Flow { val setupThumbsUp = flow { signInIfNeeded() @@ -87,41 +76,9 @@ internal class FirestoreImpl @Inject constructor() : Firestore { emit(counterRef) } - var appliedCount = -1 - val unappliedCountChannel = BroadcastChannel(10000) - val incrementFlow = thumbsUpEventChannel.asFlow() - .filter { it == sessionId } - .withIndex() - .onEach { - unappliedCountChannel.send( - minOf( - it.index - appliedCount, - MAX_APPLY_COUNT - ) - ) - } - // For firebase pricing - .debounce(INCREMENT_DEBOUNCE_MILLIS) - .map { - val result = minOf(it.index - appliedCount, MAX_APPLY_COUNT) - appliedCount = it.index - unappliedCountChannel.send(0) - it.value to result - } - .map { (sessionId, count) -> - Timber.debug { "thumb up increment:$count" } - incrementThumbsUpCountImpl(sessionId, count) - count - } - .onStart { - emit(0) - unappliedCountChannel.send(0) - } - val thumbsUpSnapshot = setupThumbsUp - .flatMapLatest { - it.toFlow() - } - .combine(incrementFlow) { thumbsCount, _ -> thumbsCount } + val thumbsUpSnapshot = setupThumbsUp.flatMapLatest { + it.toFlow() + } return thumbsUpSnapshot.map { shards -> var count = 0 @@ -130,23 +87,9 @@ internal class FirestoreImpl @Inject constructor() : Firestore { } count } - .combine(unappliedCountChannel.asFlow()) { firestoreCount, unappliedCount -> - firestoreCount + unappliedCount - }.scan(0) { prev, value -> - // Prevent counts from dropping due to calculations - if (prev < value) { - value - } else { - prev - } - } - } - - override suspend fun incrementThumbsUpCount(sessionId: SessionId) { - thumbsUpEventChannel.send(sessionId) } - private suspend fun incrementThumbsUpCountImpl( + override suspend fun incrementThumbsUpCount( sessionId: SessionId, count: Int ) { @@ -213,8 +156,6 @@ internal class FirestoreImpl @Inject constructor() : Firestore { companion object { const val NUM_SHARDS = 5 - const val MAX_APPLY_COUNT = 50 - const val INCREMENT_DEBOUNCE_MILLIS = 500L const val SHARDS_COUNT_KEY = "shards" const val FAVORITE_VALUE_KEY = "favorite" } diff --git a/data/repository/src/main/java/io/github/droidkaigi/confsched2020/data/repository/internal/DataSessionRepository.kt b/data/repository/src/main/java/io/github/droidkaigi/confsched2020/data/repository/internal/DataSessionRepository.kt index 25139f175..3f0536e48 100644 --- a/data/repository/src/main/java/io/github/droidkaigi/confsched2020/data/repository/internal/DataSessionRepository.kt +++ b/data/repository/src/main/java/io/github/droidkaigi/confsched2020/data/repository/internal/DataSessionRepository.kt @@ -34,7 +34,6 @@ internal class DataSessionRepository @Inject constructor( private val firestore: Firestore, private val favoriteToggleWork: FavoriteToggleWork ) : SessionRepository { - override fun sessionContents(): Flow { val sessionsFlow = sessions() .map { @@ -134,7 +133,13 @@ internal class DataSessionRepository @Inject constructor( return firestore.getThumbsUpCount(sessionId) } - override suspend fun incrementThumbsUpCount(sessionId: SessionId) { - firestore.incrementThumbsUpCount(sessionId) + override suspend fun incrementThumbsUpCount( + sessionId: SessionId, + count: Int + ) { + firestore.incrementThumbsUpCount( + sessionId = sessionId, + count = count + ) } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt index 9c51f6019..377c1b9aa 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt @@ -124,7 +124,7 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject session, uiModel.showEllipsis, uiModel.searchQuery, - uiModel.thumbsUpCount + uiModel.totalThumbsUpCount ) } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index cc3d19998..986a183a7 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -17,9 +17,16 @@ import io.github.droidkaigi.confsched2020.model.Session import io.github.droidkaigi.confsched2020.model.SessionId import io.github.droidkaigi.confsched2020.model.TextExpandState import io.github.droidkaigi.confsched2020.model.repository.SessionRepository +import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import timber.log.Timber +import timber.log.debug class SessionDetailViewModel @AssistedInject constructor( @Assisted private val sessionId: SessionId, @@ -33,7 +40,8 @@ class SessionDetailViewModel @AssistedInject constructor( val session: Session?, val showEllipsis: Boolean, val searchQuery: String?, - val thumbsUpCount: Int + val totalThumbsUpCount: Int, + val incrementThumbsUpCount: Int ) { companion object { val EMPTY = UiModel( @@ -42,7 +50,8 @@ class SessionDetailViewModel @AssistedInject constructor( session = null, showEllipsis = true, searchQuery = null, - thumbsUpCount = 0 + totalThumbsUpCount = 0, + incrementThumbsUpCount = 0 ) } } @@ -63,7 +72,7 @@ class SessionDetailViewModel @AssistedInject constructor( private val descriptionTextExpandStateLiveData: MutableLiveData = MutableLiveData(TextExpandState.COLLAPSED) - private val thumbsUpCountLoadStateLiveData: LiveData> = liveData { + private val totalThumbsUpCountLoadStateLiveData: LiveData> = liveData { sessionRepository.thumbsUpCounts(sessionId) .toLoadingState() .collect { loadState: LoadState -> @@ -71,18 +80,26 @@ class SessionDetailViewModel @AssistedInject constructor( } } + private val incrementThumbsUpCountLiveData: MutableLiveData = + MutableLiveData(0) + + private val incrementThumbsUpCountEvent: BroadcastChannel> = + BroadcastChannel(Channel.BUFFERED) + // Produce UiModel val uiModel: LiveData = combine( initialValue = UiModel.EMPTY, liveData1 = sessionLoadStateLiveData, liveData2 = favoriteLoadingStateLiveData, liveData3 = descriptionTextExpandStateLiveData, - liveData4 = thumbsUpCountLoadStateLiveData + liveData4 = totalThumbsUpCountLoadStateLiveData, + liveData5 = incrementThumbsUpCountLiveData ) { current: UiModel, sessionLoadState: LoadState, favoriteState: LoadingState, descriptionTextExpandState: TextExpandState, - thumbsUpCountLoadState: LoadState -> + totalThumbsUpCountLoadState: LoadState, + incrementThumbsUpCount: Int -> val isLoading = sessionLoadState.isLoading || favoriteState.isLoading val sessions = when (sessionLoadState) { @@ -94,12 +111,12 @@ class SessionDetailViewModel @AssistedInject constructor( } } val showEllipsis = descriptionTextExpandState == TextExpandState.COLLAPSED - val thumbsUpCount = when (thumbsUpCountLoadState) { + val totalThumbsUpCount = when (totalThumbsUpCountLoadState) { is LoadState.Loaded -> { - thumbsUpCountLoadState.value + totalThumbsUpCountLoadState.value } else -> { - current.thumbsUpCount + current.totalThumbsUpCount } } @@ -111,16 +128,27 @@ class SessionDetailViewModel @AssistedInject constructor( ?: favoriteState .getErrorIfExists() .toAppError() - ?: thumbsUpCountLoadState + ?: totalThumbsUpCountLoadState .getErrorIfExists() .toAppError(), session = sessions, showEllipsis = showEllipsis, searchQuery = searchQuery, - thumbsUpCount = thumbsUpCount + totalThumbsUpCount = totalThumbsUpCount, + incrementThumbsUpCount = incrementThumbsUpCount ) } + init { + viewModelScope.launch { + setupIncrementThumbsUpEvent() + } + // For debug + incrementThumbsUpCountLiveData.observeForever { + Timber.debug { "⭐ increment livedata $it" } + } + } + fun favorite(session: Session) { viewModelScope.launch { favoriteLoadingStateLiveData.value = LoadingState.Loading @@ -138,16 +166,33 @@ class SessionDetailViewModel @AssistedInject constructor( } fun thumbsUp(session: Session) { + val now = incrementThumbsUpCountLiveData.value ?: 0 + val incremented = minOf(now + 1, MAX_APPLY_COUNT) + incrementThumbsUpCountLiveData.value = incremented + viewModelScope.launch { - try { - sessionRepository.incrementThumbsUpCount(session.id) - } catch (e: Exception) { - // TODO: implement - println("⭐ " + e.message) - } + incrementThumbsUpCountEvent.send(session.id to incremented) } } + @Suppress("EXPERIMENTAL_API_USAGE") + private suspend fun setupIncrementThumbsUpEvent() { + return incrementThumbsUpCountEvent.asFlow() + .debounce(INCREMENT_DEBOUNCE_MILLIS) + .catch { e -> + // TODO: Implement + Timber.debug { "⭐ error $e" } + } + .collect { (sessionId, count) -> + sessionRepository.incrementThumbsUpCount( + sessionId = sessionId, + count = count + ) + Timber.debug { "⭐ increment $count posted" } + incrementThumbsUpCountLiveData.value = 0 + } + } + @AssistedInject.Factory interface Factory { fun create( @@ -155,4 +200,9 @@ class SessionDetailViewModel @AssistedInject constructor( searchQuery: String? = null ): SessionDetailViewModel } + + companion object { + private const val INCREMENT_DEBOUNCE_MILLIS = 500L + private const val MAX_APPLY_COUNT = 50 + } } diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt index c81027d7e..ee579f22c 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt @@ -178,7 +178,7 @@ class SessionDetailViewModelTest { error shouldBe null showEllipsis shouldBe true searchQuery shouldBe null - thumbsUpCount shouldBe Dummies.thumbsUpCount + totalThumbsUpCount shouldBe Dummies.thumbsUpCount } } } diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/repository/SessionRepository.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/repository/SessionRepository.kt index ff0c9b4d8..dd104f0b8 100644 --- a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/repository/SessionRepository.kt +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/repository/SessionRepository.kt @@ -18,5 +18,5 @@ interface SessionRepository { sessionFeedback: SessionFeedback ) fun thumbsUpCounts(sessionId: SessionId): Flow - suspend fun incrementThumbsUpCount(sessionId: SessionId) + suspend fun incrementThumbsUpCount(sessionId: SessionId, count: Int) } From 6f074947c007cc13a227421f6e166d425de373de Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Tue, 11 Feb 2020 15:29:48 +0900 Subject: [PATCH 22/42] Display incremented thumbs-up count by own --- .../session/ui/SessionDetailFragment.kt | 9 ++++++--- .../session/ui/item/SessionDetailTitleItem.kt | 19 +++++++++++++++---- ...incremented_thumbs_up_count_background.xml | 12 ++++++++++++ .../res/layout/item_session_detail_title.xml | 17 +++++++++++++++++ 4 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 feature/session/src/main/res/drawable/shape_incremented_thumbs_up_count_background.xml diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt index 377c1b9aa..f94846667 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt @@ -124,7 +124,8 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject session, uiModel.showEllipsis, uiModel.searchQuery, - uiModel.totalThumbsUpCount + uiModel.totalThumbsUpCount, + uiModel.incrementThumbsUpCount ) } } @@ -185,7 +186,8 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject session: Session, showEllipsis: Boolean, searchQuery: String?, - thumbsUpCount: Int + totalThumbsUpCount: Int, + incrementThumbsUpCount: Int ) { binding.sessionDetailRecycler.transitionName = "${session.id}-${navArgs.transitionNameSuffix}" @@ -194,7 +196,8 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject items += sessionDetailTitleItemFactory.create( session, searchQuery, - thumbsUpCount + totalThumbsUpCount, + incrementThumbsUpCount ) { sessionDetailViewModel.thumbsUp(session) } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index 9cbf9b4f5..8bbc4654d 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -1,10 +1,11 @@ package io.github.droidkaigi.confsched2020.session.ui.item -import androidx.appcompat.content.res.AppCompatResources import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.BackgroundColorSpan +import android.view.View import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.doOnPreDraw import androidx.core.view.isVisible import com.google.android.material.chip.Chip @@ -22,7 +23,8 @@ import java.util.regex.Pattern class SessionDetailTitleItem @AssistedInject constructor( @Assisted private val session: Session, @Assisted private val searchQuery: String?, - @Assisted private val thumbsUpCount: Int, + @Assisted private val totalThumbsUpCount: Int, + @Assisted private val incrementThumbsUpCount: Int, @Assisted private val thumbsUpListener: () -> Unit ) : BindableItem(session.id.hashCode().toLong()) { override fun getLayout() = R.layout.item_session_detail_title @@ -75,10 +77,18 @@ class SessionDetailTitleItem @AssistedInject constructor( // binding.sessionMessage.text = "セッション部屋がRoom1からRoom3に変更になりました(サンプル)" // binding.sessionMessage.isVisible = true - binding.thumbsUpCount = thumbsUpCount + binding.thumbsUpCount = totalThumbsUpCount binding.thumbsUp.setOnClickListener { thumbsUpListener.invoke() } + binding.incrementedThumbsUpCount.apply { + text = "+${incrementThumbsUpCount}" + visibility = if (incrementThumbsUpCount > 0) { + View.VISIBLE + } else { + View.INVISIBLE + } + } } } @@ -106,7 +116,8 @@ class SessionDetailTitleItem @AssistedInject constructor( fun create( session: Session, searchQuery: String? = null, - thumbsUpCount: Int, + totalThumbsUpCount: Int, + incrementThumbsUpCount: Int, thumbsUpListener: () -> Unit ): SessionDetailTitleItem } diff --git a/feature/session/src/main/res/drawable/shape_incremented_thumbs_up_count_background.xml b/feature/session/src/main/res/drawable/shape_incremented_thumbs_up_count_background.xml new file mode 100644 index 000000000..c98a35b98 --- /dev/null +++ b/feature/session/src/main/res/drawable/shape_incremented_thumbs_up_count_background.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/feature/session/src/main/res/layout/item_session_detail_title.xml b/feature/session/src/main/res/layout/item_session_detail_title.xml index 1beed88d7..fc3e4e553 100644 --- a/feature/session/src/main/res/layout/item_session_detail_title.xml +++ b/feature/session/src/main/res/layout/item_session_detail_title.xml @@ -126,6 +126,23 @@ app:layout_constraintTop_toBottomOf="@id/session_message" /> + + Date: Thu, 13 Feb 2020 03:20:08 +0900 Subject: [PATCH 23/42] Pop-up and drop-out animation --- .../droidkaigi/confsched2020/ext/Animators.kt | 41 +++++++ .../droidkaigi/confsched2020/ext/Views.kt | 40 +++++++ .../session/ui/SessionDetailFragment.kt | 12 +- .../session/ui/item/SessionDetailTitleItem.kt | 106 ++++++++++++++++-- .../ui/viewmodel/SessionDetailViewModel.kt | 18 +-- ...incremented_thumbs_up_count_background.xml | 2 +- .../res/layout/item_session_detail_title.xml | 16 +-- .../confsched2020/model/ThumbsUpCount.kt | 15 +++ 8 files changed, 216 insertions(+), 34 deletions(-) create mode 100644 corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/Animators.kt create mode 100644 corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/Views.kt create mode 100644 model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCount.kt diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/Animators.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/Animators.kt new file mode 100644 index 000000000..615d71d1b --- /dev/null +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/Animators.kt @@ -0,0 +1,41 @@ +package io.github.droidkaigi.confsched2020.ext + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +// This function is copied from https://medium.com/androiddevelopers/suspending-over-views-19de9ebd7020 + +suspend fun Animator.awaitEnd() = suspendCancellableCoroutine { cont -> + // Add an invokeOnCancellation listener. If the coroutine is + // cancelled, cancel the animation too that will notify + // listener's onAnimationCancel() function + cont.invokeOnCancellation { cancel() } + + addListener(object : AnimatorListenerAdapter() { + private var endedSuccessfully = true + + override fun onAnimationCancel(animation: Animator) { + // Animator has been cancelled, so flip the success flag + endedSuccessfully = false + } + + override fun onAnimationEnd(animation: Animator) { + // Make sure we remove the listener so we don't keep + // leak the coroutine continuation + animation.removeListener(this) + + if (cont.isActive) { + // If the coroutine is still active... + if (endedSuccessfully) { + // ...and the Animator ended successfully, resume the coroutine + cont.resume(Unit) + } else { + // ...and the Animator was cancelled, cancel the coroutine too + cont.cancel() + } + } + } + }) +} diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/Views.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/Views.kt new file mode 100644 index 000000000..9418adea6 --- /dev/null +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/Views.kt @@ -0,0 +1,40 @@ +package io.github.droidkaigi.confsched2020.ext + +import android.view.View +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +// This function is copied from https://medium.com/androiddevelopers/suspending-over-views-19de9ebd7020 + +suspend fun View.awaitNextLayout() = suspendCancellableCoroutine { cont -> + // This lambda is invoked immediately, allowing us to create + // a callback/listener + + val listener = object : View.OnLayoutChangeListener { + override fun onLayoutChange( + v: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + // The next layout has happened! + // First remove the listener to not leak the coroutine + v?.removeOnLayoutChangeListener(this) + // Finally resume the continuation, and + // wake the coroutine up + cont.resume(Unit) + } + } + // If the coroutine is cancelled, remove the listener + cont.invokeOnCancellation { removeOnLayoutChangeListener(listener) } + // And finally add the listener to view + addOnLayoutChangeListener(listener) + + // The coroutine will now be suspended. It will only be resumed + // when calling cont.resume() in the listener above +} diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt index f94846667..0b1aef9e2 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt @@ -6,6 +6,7 @@ import androidx.annotation.IdRes import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.observe import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController @@ -25,6 +26,7 @@ import io.github.droidkaigi.confsched2020.ext.isShow import io.github.droidkaigi.confsched2020.model.Session import io.github.droidkaigi.confsched2020.model.Speaker import io.github.droidkaigi.confsched2020.model.SpeechSession +import io.github.droidkaigi.confsched2020.model.ThumbsUpCount import io.github.droidkaigi.confsched2020.model.defaultLang import io.github.droidkaigi.confsched2020.session.R import io.github.droidkaigi.confsched2020.session.databinding.FragmentSessionDetailBinding @@ -124,8 +126,7 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject session, uiModel.showEllipsis, uiModel.searchQuery, - uiModel.totalThumbsUpCount, - uiModel.incrementThumbsUpCount + uiModel.thumbsUpCount ) } } @@ -186,8 +187,7 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject session: Session, showEllipsis: Boolean, searchQuery: String?, - totalThumbsUpCount: Int, - incrementThumbsUpCount: Int + thumbsUpCount: ThumbsUpCount ) { binding.sessionDetailRecycler.transitionName = "${session.id}-${navArgs.transitionNameSuffix}" @@ -196,8 +196,8 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject items += sessionDetailTitleItemFactory.create( session, searchQuery, - totalThumbsUpCount, - incrementThumbsUpCount + viewLifecycleOwner.lifecycleScope, + thumbsUpCount ) { sessionDetailViewModel.thumbsUp(session) } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index 8bbc4654d..bef3e0eb1 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -1,5 +1,6 @@ package io.github.droidkaigi.confsched2020.session.ui.item +import android.animation.ObjectAnimator import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.BackgroundColorSpan @@ -8,23 +9,29 @@ import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.doOnPreDraw import androidx.core.view.isVisible +import androidx.lifecycle.LifecycleCoroutineScope import com.google.android.material.chip.Chip import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.xwray.groupie.databinding.BindableItem +import io.github.droidkaigi.confsched2020.ext.awaitEnd +import io.github.droidkaigi.confsched2020.ext.awaitNextLayout import io.github.droidkaigi.confsched2020.ext.getThemeColor import io.github.droidkaigi.confsched2020.model.Session import io.github.droidkaigi.confsched2020.model.SpeechSession +import io.github.droidkaigi.confsched2020.model.ThumbsUpCount import io.github.droidkaigi.confsched2020.model.defaultLang import io.github.droidkaigi.confsched2020.session.R import io.github.droidkaigi.confsched2020.session.databinding.ItemSessionDetailTitleBinding +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import java.util.regex.Pattern class SessionDetailTitleItem @AssistedInject constructor( @Assisted private val session: Session, @Assisted private val searchQuery: String?, - @Assisted private val totalThumbsUpCount: Int, - @Assisted private val incrementThumbsUpCount: Int, + @Assisted private val lifecycleCoroutineScope: LifecycleCoroutineScope, + @Assisted private val thumbsUpCount: ThumbsUpCount, @Assisted private val thumbsUpListener: () -> Unit ) : BindableItem(session.id.hashCode().toLong()) { override fun getLayout() = R.layout.item_session_detail_title @@ -77,17 +84,19 @@ class SessionDetailTitleItem @AssistedInject constructor( // binding.sessionMessage.text = "セッション部屋がRoom1からRoom3に変更になりました(サンプル)" // binding.sessionMessage.isVisible = true - binding.thumbsUpCount = totalThumbsUpCount binding.thumbsUp.setOnClickListener { thumbsUpListener.invoke() } - binding.incrementedThumbsUpCount.apply { - text = "+${incrementThumbsUpCount}" - visibility = if (incrementThumbsUpCount > 0) { - View.VISIBLE - } else { - View.INVISIBLE - } + + + binding.thumbsUpCount = thumbsUpCount + if (!thumbsUpCount.incrementedUpdated) { + return + } else if (thumbsUpCount.incremented > 0) { + binding.incrementedThumbsUpCount.text = "+${thumbsUpCount.incremented}" + binding.incrementedThumbsUpCount.showWithPopUpAnimation() + } else { + binding.incrementedThumbsUpCount.hideWithDropOutAnimation() } } } @@ -111,13 +120,86 @@ class SessionDetailTitleItem @AssistedInject constructor( } } + private fun View.showWithPopUpAnimation() { + val target = this + + lifecycleCoroutineScope.launch { + target.isVisible = true + target.awaitNextLayout() + val popupHeight = (target.height / 2).toFloat() + target.translationY = popupHeight + + val fadeIn = async { + ObjectAnimator.ofFloat( + target, + View.ALPHA, + 1f + ).run { + start() + awaitEnd() + } + } + + val up = async { + ObjectAnimator.ofFloat( + target, + View.TRANSLATION_Y, + -popupHeight + ).run { + duration = 100 + start() + awaitEnd() + } + } + + fadeIn.await() + up.await() + } + } + + private fun View.hideWithDropOutAnimation() { + val target = this + + lifecycleCoroutineScope.launch { + val popupHeight = (target.height / 2).toFloat() + + val fadeOut = async { + ObjectAnimator.ofFloat( + target, + View.ALPHA, + 0f + ).run { + duration = 100 + start() + awaitEnd() + } + } + + val down = async { + ObjectAnimator.ofFloat( + target, + View.TRANSLATION_Y, + popupHeight + ).run { + duration = 100 + start() + awaitEnd() + } + } + + fadeOut.await() + down.await() + target.isVisible = false + } + } + @AssistedInject.Factory interface Factory { fun create( session: Session, searchQuery: String? = null, - totalThumbsUpCount: Int, - incrementThumbsUpCount: Int, + lifecycleCoroutineScope: LifecycleCoroutineScope, + thumbsUpCount: ThumbsUpCount, thumbsUpListener: () -> Unit ): SessionDetailTitleItem } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index 986a183a7..d988d0156 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -16,6 +16,7 @@ import io.github.droidkaigi.confsched2020.model.LoadingState import io.github.droidkaigi.confsched2020.model.Session import io.github.droidkaigi.confsched2020.model.SessionId import io.github.droidkaigi.confsched2020.model.TextExpandState +import io.github.droidkaigi.confsched2020.model.ThumbsUpCount import io.github.droidkaigi.confsched2020.model.repository.SessionRepository import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.Channel @@ -40,8 +41,7 @@ class SessionDetailViewModel @AssistedInject constructor( val session: Session?, val showEllipsis: Boolean, val searchQuery: String?, - val totalThumbsUpCount: Int, - val incrementThumbsUpCount: Int + val thumbsUpCount: ThumbsUpCount ) { companion object { val EMPTY = UiModel( @@ -50,8 +50,7 @@ class SessionDetailViewModel @AssistedInject constructor( session = null, showEllipsis = true, searchQuery = null, - totalThumbsUpCount = 0, - incrementThumbsUpCount = 0 + thumbsUpCount = ThumbsUpCount.ZERO ) } } @@ -116,10 +115,16 @@ class SessionDetailViewModel @AssistedInject constructor( totalThumbsUpCountLoadState.value } else -> { - current.totalThumbsUpCount + current.thumbsUpCount.total } } + val thumbsUpCount = ThumbsUpCount( + total = totalThumbsUpCount, + incremented = incrementThumbsUpCount, + incrementedUpdated = current.thumbsUpCount.incremented != incrementThumbsUpCount + ) + UiModel( isLoading = isLoading, error = sessionLoadState @@ -134,8 +139,7 @@ class SessionDetailViewModel @AssistedInject constructor( session = sessions, showEllipsis = showEllipsis, searchQuery = searchQuery, - totalThumbsUpCount = totalThumbsUpCount, - incrementThumbsUpCount = incrementThumbsUpCount + thumbsUpCount = thumbsUpCount ) } diff --git a/feature/session/src/main/res/drawable/shape_incremented_thumbs_up_count_background.xml b/feature/session/src/main/res/drawable/shape_incremented_thumbs_up_count_background.xml index c98a35b98..e1b9de341 100644 --- a/feature/session/src/main/res/drawable/shape_incremented_thumbs_up_count_background.xml +++ b/feature/session/src/main/res/drawable/shape_incremented_thumbs_up_count_background.xml @@ -2,7 +2,7 @@ - + @@ -130,9 +130,9 @@ android:id="@+id/incremented_thumbs_up_count" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="8dp" + android:layout_margin="4dp" android:textColor="@color/white" - android:textAppearance="?textAppearanceBody2" + android:textAppearance="?textAppearanceBody1" android:gravity="center" android:visibility="invisible" android:background="@drawable/shape_incremented_thumbs_up_count_background" @@ -149,16 +149,16 @@ android:layout_height="wrap_content" android:minWidth="56dp" android:layout_marginTop="24dp" - android:backgroundTint="@{ thumbsUpCount > 0 ? @color/indigo_900 : @color/white }" - android:text="@{ thumbsUpCount > 0 ? thumbsUpCount.toString() : @string/thumbs_up_default_label }" - android:textColor="@{ thumbsUpCount > 0 ? @color/white: @color/indigo_900 }" + android:backgroundTint="@{ thumbsUpCount.total > 0 ? @color/indigo_900 : @color/white }" + android:text="@{ thumbsUpCount.total > 0 ? String.valueOf(thumbsUpCount.total) : @string/thumbs_up_default_label }" + android:textColor="@{ thumbsUpCount.total > 0 ? @color/white: @color/indigo_900 }" android:textAppearance="?textAppearanceButton" style="@style/Widget.DroidKaigi.Button.OvalButton" app:icon="@drawable/ic_thumbs_up_black_24dp" - app:iconTint="@{ thumbsUpCount > 0 ? @color/white: @color/indigo_900 }" + app:iconTint="@{ thumbsUpCount.total > 0 ? @color/white: @color/indigo_900 }" app:iconPadding="4dp" app:iconSize="16dp" - app:strokeColor="@{ thumbsUpCount > 0 ? @color/white : @color/indigo_900 }" + app:strokeColor="@{ thumbsUpCount.total > 0 ? @color/white : @color/indigo_900 }" app:strokeWidth="1dp" app:layout_constraintStart_toStartOf="@id/guideline_start" app:layout_constraintTop_toBottomOf="@id/tags" diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCount.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCount.kt new file mode 100644 index 000000000..ca94ea8c9 --- /dev/null +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/ThumbsUpCount.kt @@ -0,0 +1,15 @@ +package io.github.droidkaigi.confsched2020.model + +data class ThumbsUpCount( + val total: Int, + val incremented: Int, + val incrementedUpdated: Boolean +) { + companion object { + val ZERO = ThumbsUpCount( + total = 0, + incremented = 0, + incrementedUpdated = false + ) + } +} From ea1e3d49905577de76a4425b916c7e4ec2608286 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Fri, 14 Feb 2020 00:06:45 +0900 Subject: [PATCH 24/42] Use resource string placeholder --- .../confsched2020/session/ui/item/SessionDetailTitleItem.kt | 6 +++++- feature/session/src/main/res/values/strings.xml | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index bef3e0eb1..14a02459f 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -93,7 +93,11 @@ class SessionDetailTitleItem @AssistedInject constructor( if (!thumbsUpCount.incrementedUpdated) { return } else if (thumbsUpCount.incremented > 0) { - binding.incrementedThumbsUpCount.text = "+${thumbsUpCount.incremented}" + val context = binding.incrementedThumbsUpCount.context + binding.incrementedThumbsUpCount.text = context.getString( + R.string.thumbs_up_increment_label, + thumbsUpCount.incremented + ) binding.incrementedThumbsUpCount.showWithPopUpAnimation() } else { binding.incrementedThumbsUpCount.hideWithDropOutAnimation() diff --git a/feature/session/src/main/res/values/strings.xml b/feature/session/src/main/res/values/strings.xml index 402bc46cd..a42e4c329 100644 --- a/feature/session/src/main/res/values/strings.xml +++ b/feature/session/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ SLIDE You choose your favorite sessions, then you can make a plan for you. + + +%1$d Time Icon Favorite Icon https://droidkaigi.jp/2020/en/timetable/%d From 8c644279c5a11d6dd0c83f05f682cfe4e02d610c Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Fri, 14 Feb 2020 13:26:56 +0900 Subject: [PATCH 25/42] Apply ktlintFormat --- .../confsched2020/session/ui/item/SessionDetailTitleItem.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index 14a02459f..c4493b58c 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -88,7 +88,6 @@ class SessionDetailTitleItem @AssistedInject constructor( thumbsUpListener.invoke() } - binding.thumbsUpCount = thumbsUpCount if (!thumbsUpCount.incrementedUpdated) { return From 54940d751bfb1d4a951a554fdad283d888f5bc7d Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Fri, 14 Feb 2020 14:09:06 +0900 Subject: [PATCH 26/42] Pass lintDebug task --- .../confsched2020/session/ui/item/SessionDetailTitleItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index c4493b58c..d91f43604 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -95,7 +95,7 @@ class SessionDetailTitleItem @AssistedInject constructor( val context = binding.incrementedThumbsUpCount.context binding.incrementedThumbsUpCount.text = context.getString( R.string.thumbs_up_increment_label, - thumbsUpCount.incremented + thumbsUpCount.incremented as Int // Need to specify a type for lintDebug task ) binding.incrementedThumbsUpCount.showWithPopUpAnimation() } else { From 035602cfac82075e764a58bed523957972fa81ea Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Fri, 14 Feb 2020 14:17:45 +0900 Subject: [PATCH 27/42] Fix thumbsUpCount test --- .../confsched2020/session/ui/viewmodel/Dummies.kt | 7 ++++++- .../session/ui/viewmodel/SessionDetailViewModelTest.kt | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/Dummies.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/Dummies.kt index 6b12b5ce2..330b2dc58 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/Dummies.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/Dummies.kt @@ -16,6 +16,7 @@ import io.github.droidkaigi.confsched2020.model.SessionType import io.github.droidkaigi.confsched2020.model.Speaker import io.github.droidkaigi.confsched2020.model.SpeakerId import io.github.droidkaigi.confsched2020.model.SpeechSession +import io.github.droidkaigi.confsched2020.model.ThumbsUpCount object Dummies { val hall = Room(1, LocaledString("JA App bar", "EN App bar"), 1) @@ -81,5 +82,9 @@ object Dummies { levels = listOf(Level.BEGINNER, Level.INTERMEDIATE, Level.ADVANCED) ) - val thumbsUpCount = 10 + val thumbsUpCount = ThumbsUpCount( + total = 10, + incremented = 0, + incrementedUpdated = false + ) } diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt index ee579f22c..3831225dd 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt @@ -160,7 +160,7 @@ class SessionDetailViewModelTest { fun thumbsUpCount() { coEvery { sessionRepository.thumbsUpCounts(Dummies.speachSession1.id) - } returns flowOf(Dummies.thumbsUpCount) + } returns flowOf(Dummies.thumbsUpCount.total) val sessionDetailViewModel = SessionDetailViewModel( sessionId = Dummies.speachSession1.id, sessionRepository = sessionRepository, @@ -178,7 +178,7 @@ class SessionDetailViewModelTest { error shouldBe null showEllipsis shouldBe true searchQuery shouldBe null - totalThumbsUpCount shouldBe Dummies.thumbsUpCount + thumbsUpCount.total shouldBe Dummies.thumbsUpCount.total } } } From 5f1c52463ed2a9b1b8908d2225c93e8e31566250 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Fri, 14 Feb 2020 20:14:33 +0900 Subject: [PATCH 28/42] Bind error of incrementation thumbs-up --- .../droidkaigi/confsched2020/ext/LiveDatas.kt | 18 ++++++++ .../session/ui/SessionDetailFragment.kt | 6 ++- .../ui/viewmodel/SessionDetailViewModel.kt | 44 +++++++++---------- .../confsched2020/model/LoadState.kt | 12 +++-- 4 files changed, 53 insertions(+), 27 deletions(-) diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt index 759f2f43a..3f7160426 100644 --- a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import com.hadilq.liveevent.LiveEvent +import io.github.droidkaigi.confsched2020.model.AppError +import io.github.droidkaigi.confsched2020.model.errorGettable fun LiveData.requireValue() = requireNotNull(value) @@ -161,3 +163,19 @@ fun LiveData.toNonNullSingleEvent(): LiveData { } return result } + +fun merge(vararg liveDatas: LiveData): LiveData { + return MediatorLiveData().apply { + liveDatas.forEach { liveData -> + addSource(liveData) {value -> + this.value = value + } + } + } +} + +fun LiveData.toAppError(): LiveData { + return map { + it.getErrorIfExists().toAppError() + } +} diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt index 0b1aef9e2..86be47ba1 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt @@ -23,6 +23,7 @@ import io.github.droidkaigi.confsched2020.di.PageScope import io.github.droidkaigi.confsched2020.ext.assistedActivityViewModels import io.github.droidkaigi.confsched2020.ext.assistedViewModels import io.github.droidkaigi.confsched2020.ext.isShow +import io.github.droidkaigi.confsched2020.model.AppError import io.github.droidkaigi.confsched2020.model.Session import io.github.droidkaigi.confsched2020.model.Speaker import io.github.droidkaigi.confsched2020.model.SpeechSession @@ -116,7 +117,6 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject sessionDetailViewModel.uiModel .observe(viewLifecycleOwner) { uiModel: SessionDetailViewModel.UiModel -> - uiModel.error?.let { systemViewModel.onError(it) } binding.progressBar.isShow = uiModel.isLoading uiModel.session ?.let { session -> @@ -131,6 +131,10 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject } } + sessionDetailViewModel.appError.observe(viewLifecycleOwner) { appError: AppError? -> + appError?.let { systemViewModel.onError(it) } + } + binding.bottomAppBar.setOnMenuItemClickListener { menuItem -> val session = binding.session ?: return@setOnMenuItemClickListener true when (menuItem.itemId) { diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index d988d0156..cfd59c103 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import io.github.droidkaigi.confsched2020.ext.combine +import io.github.droidkaigi.confsched2020.ext.merge import io.github.droidkaigi.confsched2020.ext.toAppError import io.github.droidkaigi.confsched2020.ext.toLoadingState import io.github.droidkaigi.confsched2020.model.AppError @@ -21,7 +22,6 @@ import io.github.droidkaigi.confsched2020.model.repository.SessionRepository import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.map @@ -37,7 +37,6 @@ class SessionDetailViewModel @AssistedInject constructor( // UiModel definition data class UiModel( val isLoading: Boolean, - val error: AppError?, val session: Session?, val showEllipsis: Boolean, val searchQuery: String?, @@ -46,7 +45,6 @@ class SessionDetailViewModel @AssistedInject constructor( companion object { val EMPTY = UiModel( isLoading = false, - error = null, session = null, showEllipsis = true, searchQuery = null, @@ -82,6 +80,9 @@ class SessionDetailViewModel @AssistedInject constructor( private val incrementThumbsUpCountLiveData: MutableLiveData = MutableLiveData(0) + private val incrementThumbsUpCountErrorLiveData: MutableLiveData = + MutableLiveData(null) + private val incrementThumbsUpCountEvent: BroadcastChannel> = BroadcastChannel(Channel.BUFFERED) @@ -127,15 +128,6 @@ class SessionDetailViewModel @AssistedInject constructor( UiModel( isLoading = isLoading, - error = sessionLoadState - .getErrorIfExists() - .toAppError() - ?: favoriteState - .getErrorIfExists() - .toAppError() - ?: totalThumbsUpCountLoadState - .getErrorIfExists() - .toAppError(), session = sessions, showEllipsis = showEllipsis, searchQuery = searchQuery, @@ -143,6 +135,13 @@ class SessionDetailViewModel @AssistedInject constructor( ) } + val appError: LiveData = merge( + sessionLoadStateLiveData.toAppError(), + favoriteLoadingStateLiveData.toAppError(), + totalThumbsUpCountLoadStateLiveData.toAppError(), + incrementThumbsUpCountErrorLiveData + ) + init { viewModelScope.launch { setupIncrementThumbsUpEvent() @@ -181,18 +180,19 @@ class SessionDetailViewModel @AssistedInject constructor( @Suppress("EXPERIMENTAL_API_USAGE") private suspend fun setupIncrementThumbsUpEvent() { - return incrementThumbsUpCountEvent.asFlow() + incrementThumbsUpCountEvent.asFlow() .debounce(INCREMENT_DEBOUNCE_MILLIS) - .catch { e -> - // TODO: Implement - Timber.debug { "⭐ error $e" } - } .collect { (sessionId, count) -> - sessionRepository.incrementThumbsUpCount( - sessionId = sessionId, - count = count - ) - Timber.debug { "⭐ increment $count posted" } + try { + sessionRepository.incrementThumbsUpCount( + sessionId = sessionId, + count = count + ) + Timber.debug { "⭐ increment $count posted" } + } catch (e: Exception) { + Timber.debug { "⭐ error $e" } + incrementThumbsUpCountErrorLiveData.value = e.toAppError() + } incrementThumbsUpCountLiveData.value = 0 } } diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/LoadState.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/LoadState.kt index 6b4625414..2822bbe51 100644 --- a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/LoadState.kt +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/LoadState.kt @@ -1,20 +1,24 @@ package io.github.droidkaigi.confsched2020.model -sealed class LoadState { +interface errorGettable { + fun getErrorIfExists(): Throwable? +} + +sealed class LoadState : errorGettable { object Loading : LoadState() class Loaded(val value: T) : LoadState() class Error(val e: Throwable) : LoadState() val isLoading get() = this is Loading - fun getErrorIfExists() = if (this is Error) e else null + override fun getErrorIfExists() = if (this is Error) e else null fun getValueOrNull(): T? = if (this is Loaded) value else null } -sealed class LoadingState { +sealed class LoadingState : errorGettable { object Loading : LoadingState() object Loaded : LoadingState() class Error(val e: Throwable) : LoadingState() val isLoading get() = this is Loading - fun getErrorIfExists() = if (this is Error) e else null + override fun getErrorIfExists() = if (this is Error) e else null } From c88056a456f48792866702550f118177aa5bae3e Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Fri, 14 Feb 2020 20:56:08 +0900 Subject: [PATCH 29/42] Merge the 2 increment liveDatas --- .../droidkaigi/confsched2020/ext/LiveDatas.kt | 6 ++++ .../ui/viewmodel/SessionDetailViewModel.kt | 33 +++++++++++-------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt index 3f7160426..c33314a90 100644 --- a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt @@ -179,3 +179,9 @@ fun LiveData.toAppError(): LiveData { it.getErrorIfExists().toAppError() } } + +fun > LiveData.fromResultToAppError(): LiveData { + return map { + it.exceptionOrNull()?.toAppError() + } +} diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index cfd59c103..6859aee09 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import io.github.droidkaigi.confsched2020.ext.combine +import io.github.droidkaigi.confsched2020.ext.fromResultToAppError import io.github.droidkaigi.confsched2020.ext.merge import io.github.droidkaigi.confsched2020.ext.toAppError import io.github.droidkaigi.confsched2020.ext.toLoadingState @@ -77,11 +78,8 @@ class SessionDetailViewModel @AssistedInject constructor( } } - private val incrementThumbsUpCountLiveData: MutableLiveData = - MutableLiveData(0) - - private val incrementThumbsUpCountErrorLiveData: MutableLiveData = - MutableLiveData(null) + private val incrementThumbsUpCountResultLiveData: MutableLiveData> = + MutableLiveData(Result.success(0)) private val incrementThumbsUpCountEvent: BroadcastChannel> = BroadcastChannel(Channel.BUFFERED) @@ -93,13 +91,13 @@ class SessionDetailViewModel @AssistedInject constructor( liveData2 = favoriteLoadingStateLiveData, liveData3 = descriptionTextExpandStateLiveData, liveData4 = totalThumbsUpCountLoadStateLiveData, - liveData5 = incrementThumbsUpCountLiveData + liveData5 = incrementThumbsUpCountResultLiveData ) { current: UiModel, sessionLoadState: LoadState, favoriteState: LoadingState, descriptionTextExpandState: TextExpandState, totalThumbsUpCountLoadState: LoadState, - incrementThumbsUpCount: Int -> + incrementThumbsUpCountResult: Result -> val isLoading = sessionLoadState.isLoading || favoriteState.isLoading val sessions = when (sessionLoadState) { @@ -120,6 +118,8 @@ class SessionDetailViewModel @AssistedInject constructor( } } + val incrementThumbsUpCount = incrementThumbsUpCountResult.getOrDefault(0) + val thumbsUpCount = ThumbsUpCount( total = totalThumbsUpCount, incremented = incrementThumbsUpCount, @@ -139,7 +139,7 @@ class SessionDetailViewModel @AssistedInject constructor( sessionLoadStateLiveData.toAppError(), favoriteLoadingStateLiveData.toAppError(), totalThumbsUpCountLoadStateLiveData.toAppError(), - incrementThumbsUpCountErrorLiveData + incrementThumbsUpCountResultLiveData.fromResultToAppError() ) init { @@ -147,7 +147,7 @@ class SessionDetailViewModel @AssistedInject constructor( setupIncrementThumbsUpEvent() } // For debug - incrementThumbsUpCountLiveData.observeForever { + incrementThumbsUpCountResultLiveData.observeForever { Timber.debug { "⭐ increment livedata $it" } } } @@ -169,9 +169,13 @@ class SessionDetailViewModel @AssistedInject constructor( } fun thumbsUp(session: Session) { - val now = incrementThumbsUpCountLiveData.value ?: 0 - val incremented = minOf(now + 1, MAX_APPLY_COUNT) - incrementThumbsUpCountLiveData.value = incremented + val liveDataValue = incrementThumbsUpCountResultLiveData.value + // Result type cannot be use as la left operand of '?.' + val currentIncremented = if (liveDataValue != null) { + liveDataValue.getOrDefault(0) + } else { 0 } + val incremented = minOf(currentIncremented + 1, MAX_APPLY_COUNT) + incrementThumbsUpCountResultLiveData.value = Result.success(incremented) viewModelScope.launch { incrementThumbsUpCountEvent.send(session.id to incremented) @@ -189,11 +193,12 @@ class SessionDetailViewModel @AssistedInject constructor( count = count ) Timber.debug { "⭐ increment $count posted" } + // Return initial value + incrementThumbsUpCountResultLiveData.value = Result.success(0) } catch (e: Exception) { Timber.debug { "⭐ error $e" } - incrementThumbsUpCountErrorLiveData.value = e.toAppError() + incrementThumbsUpCountResultLiveData.value = Result.failure(e) } - incrementThumbsUpCountLiveData.value = 0 } } From f9d5066d81cb71986a8f31809bb43aa454e6c712 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sat, 15 Feb 2020 12:22:16 +0900 Subject: [PATCH 30/42] Appley ktlintFormat --- .../io/github/droidkaigi/confsched2020/ext/LiveDatas.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt index c33314a90..7f0d89e6b 100644 --- a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt @@ -167,20 +167,20 @@ fun LiveData.toNonNullSingleEvent(): LiveData { fun merge(vararg liveDatas: LiveData): LiveData { return MediatorLiveData().apply { liveDatas.forEach { liveData -> - addSource(liveData) {value -> + addSource(liveData) { value -> this.value = value } } } } -fun LiveData.toAppError(): LiveData { +fun LiveData.toAppError(): LiveData { return map { it.getErrorIfExists().toAppError() } } -fun > LiveData.fromResultToAppError(): LiveData { +fun > LiveData.fromResultToAppError(): LiveData { return map { it.exceptionOrNull()?.toAppError() } From 7e54b27527daadb102461834fab5a2427fab7790 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sat, 15 Feb 2020 13:40:35 +0900 Subject: [PATCH 31/42] Fix SessionDetailViewModelTest; separate error --- .../viewmodel/SessionDetailViewModelTest.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt index 3831225dd..6005f09a1 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt @@ -41,7 +41,6 @@ class SessionDetailViewModelTest { valueHistory[0].apply { isLoading shouldBe false session shouldBe Dummies.speachSession1 - error shouldBe null showEllipsis shouldBe true searchQuery shouldBe null } @@ -56,18 +55,24 @@ class SessionDetailViewModelTest { searchQuery = null ) - val testObserver = sessionDetailViewModel + val uiModelTestObserver = sessionDetailViewModel .uiModel .test() - val valueHistory = testObserver.valueHistory() - valueHistory[0].apply { + val appErrorTestObserver = sessionDetailViewModel + .appError + .test() + + val uiModelValueHistory = uiModelTestObserver.valueHistory() + uiModelValueHistory[0].apply { isLoading shouldBe false session shouldBe null - error shouldNotBe null showEllipsis shouldBe true searchQuery shouldBe null } + + val appErrorValueHistory = appErrorTestObserver.valueHistory() + appErrorValueHistory[0] shouldNotBe null } @Test @@ -90,21 +95,18 @@ class SessionDetailViewModelTest { valueHistory[0].apply { isLoading shouldBe false session shouldBe Dummies.speachSession1 - error shouldBe null showEllipsis shouldBe true searchQuery shouldBe null } valueHistory[1].apply { isLoading shouldBe true session shouldBe Dummies.speachSession1 - error shouldBe null showEllipsis shouldBe true searchQuery shouldBe null } valueHistory[2].apply { isLoading shouldBe false session shouldBe Dummies.speachSession1 - error shouldBe null showEllipsis shouldBe true searchQuery shouldBe null } @@ -127,7 +129,6 @@ class SessionDetailViewModelTest { valueHistory[1].apply { isLoading shouldBe true session shouldBe null - error shouldBe null showEllipsis shouldBe false searchQuery shouldBe null } @@ -150,7 +151,6 @@ class SessionDetailViewModelTest { valueHistory[0].apply { isLoading shouldBe false session shouldBe Dummies.speachSession1 - error shouldBe null showEllipsis shouldBe true searchQuery shouldBe "query" } @@ -175,7 +175,6 @@ class SessionDetailViewModelTest { valueHistory[1].apply { isLoading shouldBe true session shouldBe null - error shouldBe null showEllipsis shouldBe true searchQuery shouldBe null thumbsUpCount.total shouldBe Dummies.thumbsUpCount.total From bc6dad48879ea1f8b6ded4a1e23bc8158cdc26e4 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sat, 15 Feb 2020 15:12:34 +0900 Subject: [PATCH 32/42] Extract animation functions from SessionDetailTileItem --- .../session/ui/animation/Increment.kt | 86 +++++++++++++++++++ .../session/ui/item/SessionDetailTitleItem.kt | 85 +----------------- 2 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/animation/Increment.kt diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/animation/Increment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/animation/Increment.kt new file mode 100644 index 000000000..faba19bd8 --- /dev/null +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/animation/Increment.kt @@ -0,0 +1,86 @@ +package io.github.droidkaigi.confsched2020.session.ui.animation + +import android.animation.ObjectAnimator +import android.view.View +import androidx.core.view.isVisible +import androidx.lifecycle.LifecycleCoroutineScope +import io.github.droidkaigi.confsched2020.ext.awaitEnd +import io.github.droidkaigi.confsched2020.ext.awaitNextLayout +import kotlinx.coroutines.async +import kotlinx.coroutines.launch + +private const val POP_UP_DURATION: Long = 100 + +internal fun View.popUp(lifecycleCoroutineScope: LifecycleCoroutineScope) { + val target = this + + lifecycleCoroutineScope.launch { + target.isVisible = true + target.awaitNextLayout() + val popupHeight = (target.height / 2).toFloat() + target.translationY = popupHeight + + val fadeIn = async { + ObjectAnimator.ofFloat( + target, + View.ALPHA, + 1f + ).run { + start() + awaitEnd() + } + } + + val up = async { + ObjectAnimator.ofFloat( + target, + View.TRANSLATION_Y, + -popupHeight + ).run { + duration = POP_UP_DURATION + start() + awaitEnd() + } + } + + fadeIn.await() + up.await() + } +} + +internal fun View.dropOut(lifecycleCoroutineScope: LifecycleCoroutineScope) { + val target = this + + lifecycleCoroutineScope.launch { + val dropOutHeight = (target.height / 2).toFloat() + + val fadeOut = async { + ObjectAnimator.ofFloat( + target, + View.ALPHA, + 0f + ).run { + duration = 100 + start() + awaitEnd() + } + } + + val down = async { + ObjectAnimator.ofFloat( + target, + View.TRANSLATION_Y, + dropOutHeight + ).run { + duration = POP_UP_DURATION + start() + awaitEnd() + } + } + + fadeOut.await() + down.await() + target.isVisible = false + } +} + diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index d91f43604..cf0393484 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -1,10 +1,8 @@ package io.github.droidkaigi.confsched2020.session.ui.item -import android.animation.ObjectAnimator import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.BackgroundColorSpan -import android.view.View import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.doOnPreDraw @@ -14,8 +12,6 @@ import com.google.android.material.chip.Chip import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.xwray.groupie.databinding.BindableItem -import io.github.droidkaigi.confsched2020.ext.awaitEnd -import io.github.droidkaigi.confsched2020.ext.awaitNextLayout import io.github.droidkaigi.confsched2020.ext.getThemeColor import io.github.droidkaigi.confsched2020.model.Session import io.github.droidkaigi.confsched2020.model.SpeechSession @@ -23,8 +19,8 @@ import io.github.droidkaigi.confsched2020.model.ThumbsUpCount import io.github.droidkaigi.confsched2020.model.defaultLang import io.github.droidkaigi.confsched2020.session.R import io.github.droidkaigi.confsched2020.session.databinding.ItemSessionDetailTitleBinding -import kotlinx.coroutines.async -import kotlinx.coroutines.launch +import io.github.droidkaigi.confsched2020.session.ui.animation.dropOut +import io.github.droidkaigi.confsched2020.session.ui.animation.popUp import java.util.regex.Pattern class SessionDetailTitleItem @AssistedInject constructor( @@ -97,9 +93,9 @@ class SessionDetailTitleItem @AssistedInject constructor( R.string.thumbs_up_increment_label, thumbsUpCount.incremented as Int // Need to specify a type for lintDebug task ) - binding.incrementedThumbsUpCount.showWithPopUpAnimation() + binding.incrementedThumbsUpCount.popUp(lifecycleCoroutineScope) } else { - binding.incrementedThumbsUpCount.hideWithDropOutAnimation() + binding.incrementedThumbsUpCount.dropOut(lifecycleCoroutineScope) } } } @@ -123,79 +119,6 @@ class SessionDetailTitleItem @AssistedInject constructor( } } - private fun View.showWithPopUpAnimation() { - val target = this - - lifecycleCoroutineScope.launch { - target.isVisible = true - target.awaitNextLayout() - val popupHeight = (target.height / 2).toFloat() - target.translationY = popupHeight - - val fadeIn = async { - ObjectAnimator.ofFloat( - target, - View.ALPHA, - 1f - ).run { - start() - awaitEnd() - } - } - - val up = async { - ObjectAnimator.ofFloat( - target, - View.TRANSLATION_Y, - -popupHeight - ).run { - duration = 100 - start() - awaitEnd() - } - } - - fadeIn.await() - up.await() - } - } - - private fun View.hideWithDropOutAnimation() { - val target = this - - lifecycleCoroutineScope.launch { - val popupHeight = (target.height / 2).toFloat() - - val fadeOut = async { - ObjectAnimator.ofFloat( - target, - View.ALPHA, - 0f - ).run { - duration = 100 - start() - awaitEnd() - } - } - - val down = async { - ObjectAnimator.ofFloat( - target, - View.TRANSLATION_Y, - popupHeight - ).run { - duration = 100 - start() - awaitEnd() - } - } - - fadeOut.await() - down.await() - target.isVisible = false - } - } - @AssistedInject.Factory interface Factory { fun create( From 6d1c674c24cd0a1ba236474f932ee439e4b7d5ad Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sat, 15 Feb 2020 15:13:17 +0900 Subject: [PATCH 33/42] Suppress useless cast for lintDebug task --- .../confsched2020/session/ui/item/SessionDetailTitleItem.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index cf0393484..45f632238 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -89,6 +89,7 @@ class SessionDetailTitleItem @AssistedInject constructor( return } else if (thumbsUpCount.incremented > 0) { val context = binding.incrementedThumbsUpCount.context + @Suppress("USELESS_CAST") binding.incrementedThumbsUpCount.text = context.getString( R.string.thumbs_up_increment_label, thumbsUpCount.incremented as Int // Need to specify a type for lintDebug task From e1cba55dea09d9749b65ff23da6acf6e1e0e9782 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sat, 15 Feb 2020 16:33:57 +0900 Subject: [PATCH 34/42] Tweak thumb up animation --- .../session/ui/item/SessionDetailTitleItem.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index d91f43604..26967ddca 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -5,6 +5,8 @@ import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.BackgroundColorSpan import android.view.View +import android.view.animation.AccelerateInterpolator +import android.view.animation.DecelerateInterpolator import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.doOnPreDraw @@ -129,15 +131,17 @@ class SessionDetailTitleItem @AssistedInject constructor( lifecycleCoroutineScope.launch { target.isVisible = true target.awaitNextLayout() - val popupHeight = (target.height / 2).toFloat() + val popupHeight = (target.height / 3).toFloat() target.translationY = popupHeight val fadeIn = async { + target.alpha = 0f ObjectAnimator.ofFloat( target, View.ALPHA, 1f ).run { + interpolator = DecelerateInterpolator() start() awaitEnd() } @@ -149,6 +153,7 @@ class SessionDetailTitleItem @AssistedInject constructor( View.TRANSLATION_Y, -popupHeight ).run { + interpolator = DecelerateInterpolator() duration = 100 start() awaitEnd() @@ -164,7 +169,7 @@ class SessionDetailTitleItem @AssistedInject constructor( val target = this lifecycleCoroutineScope.launch { - val popupHeight = (target.height / 2).toFloat() + val popupHeight = (target.height / 3).toFloat() val fadeOut = async { ObjectAnimator.ofFloat( @@ -172,6 +177,7 @@ class SessionDetailTitleItem @AssistedInject constructor( View.ALPHA, 0f ).run { + interpolator = AccelerateInterpolator() duration = 100 start() awaitEnd() @@ -184,6 +190,7 @@ class SessionDetailTitleItem @AssistedInject constructor( View.TRANSLATION_Y, popupHeight ).run { + interpolator = AccelerateInterpolator() duration = 100 start() awaitEnd() From e556cacb388fb68a68d3d24b9807901b5ca413b6 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sat, 15 Feb 2020 18:13:15 +0900 Subject: [PATCH 35/42] Use UiModel.error --- .../droidkaigi/confsched2020/ext/AppErrors.kt | 1 + .../droidkaigi/confsched2020/ext/LiveDatas.kt | 6 +-- .../session/ui/SessionDetailFragment.kt | 5 +-- .../ui/viewmodel/SessionDetailViewModel.kt | 45 +++++++++++-------- .../viewmodel/SessionDetailViewModelTest.kt | 15 +++---- .../confsched2020/model/AppError.kt | 1 + .../confsched2020/model/LoadState.kt | 21 +++++++-- 7 files changed, 57 insertions(+), 37 deletions(-) diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/AppErrors.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/AppErrors.kt index 5b398cad4..112575ed0 100644 --- a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/AppErrors.kt +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/AppErrors.kt @@ -31,6 +31,7 @@ fun Throwable?.toAppError(): AppError? { fun AppError.stringRes() = when (this) { is AppError.ApiException.NetworkException -> R.string.error_network is AppError.ApiException.ServerException -> R.string.error_server + is AppError.ApiException.SessionNotFoundException -> R.string.error_unknown is AppError.ApiException.UnknownException -> R.string.error_unknown is AppError.ExternalIntegrationError.NoCalendarIntegrationFoundException -> R.string.error_no_calendar_integration diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt index 7f0d89e6b..2397ab55b 100644 --- a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import com.hadilq.liveevent.LiveEvent import io.github.droidkaigi.confsched2020.model.AppError -import io.github.droidkaigi.confsched2020.model.errorGettable +import io.github.droidkaigi.confsched2020.model.ErrorGettable fun LiveData.requireValue() = requireNotNull(value) @@ -110,7 +110,7 @@ inline fun combine }.distinctUntilChanged() } -inline fun combine( +inline fun combine( initialValue: T, liveData1: LiveData, liveData2: LiveData, @@ -174,7 +174,7 @@ fun merge(vararg liveDatas: LiveData): LiveData { } } -fun LiveData.toAppError(): LiveData { +fun LiveData.toAppError(): LiveData { return map { it.getErrorIfExists().toAppError() } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt index a5986f7ed..cdc71bdc7 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt @@ -130,10 +130,7 @@ class SessionDetailFragment : Fragment(R.layout.fragment_session_detail), Inject uiModel.thumbsUpCount ) } - } - - sessionDetailViewModel.appError.observe(viewLifecycleOwner) { appError: AppError? -> - appError?.let { systemViewModel.onError(it) } + uiModel.error?.let { systemViewModel.onError(it) } } binding.bottomAppBar.run { diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index 6859aee09..7476dc383 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -8,17 +8,17 @@ import androidx.lifecycle.viewModelScope import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import io.github.droidkaigi.confsched2020.ext.combine -import io.github.droidkaigi.confsched2020.ext.fromResultToAppError -import io.github.droidkaigi.confsched2020.ext.merge import io.github.droidkaigi.confsched2020.ext.toAppError import io.github.droidkaigi.confsched2020.ext.toLoadingState import io.github.droidkaigi.confsched2020.model.AppError import io.github.droidkaigi.confsched2020.model.LoadState import io.github.droidkaigi.confsched2020.model.LoadingState +import io.github.droidkaigi.confsched2020.model.ResultState import io.github.droidkaigi.confsched2020.model.Session import io.github.droidkaigi.confsched2020.model.SessionId import io.github.droidkaigi.confsched2020.model.TextExpandState import io.github.droidkaigi.confsched2020.model.ThumbsUpCount +import io.github.droidkaigi.confsched2020.model.firstErrorOrNull import io.github.droidkaigi.confsched2020.model.repository.SessionRepository import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.Channel @@ -38,6 +38,7 @@ class SessionDetailViewModel @AssistedInject constructor( // UiModel definition data class UiModel( val isLoading: Boolean, + val error: AppError?, val session: Session?, val showEllipsis: Boolean, val searchQuery: String?, @@ -46,6 +47,7 @@ class SessionDetailViewModel @AssistedInject constructor( companion object { val EMPTY = UiModel( isLoading = false, + error = null, session = null, showEllipsis = true, searchQuery = null, @@ -57,7 +59,10 @@ class SessionDetailViewModel @AssistedInject constructor( // LiveDatas private val sessionLoadStateLiveData: LiveData> = liveData { sessionRepository.sessionContents() - .map { it.sessions.first { session -> sessionId == session.id } } + .map { + it.sessions.firstOrNull { session -> sessionId == session.id } + ?: throw AppError.ApiException.SessionNotFoundException(null) + } .toLoadingState() .collect { loadState: LoadState -> emit(loadState) @@ -78,8 +83,8 @@ class SessionDetailViewModel @AssistedInject constructor( } } - private val incrementThumbsUpCountResultLiveData: MutableLiveData> = - MutableLiveData(Result.success(0)) + private val incrementThumbsUpCountResultLiveData: MutableLiveData> = + MutableLiveData(ResultState.Success(0)) private val incrementThumbsUpCountEvent: BroadcastChannel> = BroadcastChannel(Channel.BUFFERED) @@ -97,7 +102,7 @@ class SessionDetailViewModel @AssistedInject constructor( favoriteState: LoadingState, descriptionTextExpandState: TextExpandState, totalThumbsUpCountLoadState: LoadState, - incrementThumbsUpCountResult: Result -> + incrementThumbsUpCountResult: ResultState -> val isLoading = sessionLoadState.isLoading || favoriteState.isLoading val sessions = when (sessionLoadState) { @@ -126,8 +131,17 @@ class SessionDetailViewModel @AssistedInject constructor( incrementedUpdated = current.thumbsUpCount.incremented != incrementThumbsUpCount ) + val appError = listOf( + sessionLoadState, + favoriteState, + totalThumbsUpCountLoadState, + incrementThumbsUpCountResult + ) + .firstErrorOrNull() + .toAppError() UiModel( isLoading = isLoading, + error = appError, session = sessions, showEllipsis = showEllipsis, searchQuery = searchQuery, @@ -135,13 +149,6 @@ class SessionDetailViewModel @AssistedInject constructor( ) } - val appError: LiveData = merge( - sessionLoadStateLiveData.toAppError(), - favoriteLoadingStateLiveData.toAppError(), - totalThumbsUpCountLoadStateLiveData.toAppError(), - incrementThumbsUpCountResultLiveData.fromResultToAppError() - ) - init { viewModelScope.launch { setupIncrementThumbsUpEvent() @@ -170,12 +177,14 @@ class SessionDetailViewModel @AssistedInject constructor( fun thumbsUp(session: Session) { val liveDataValue = incrementThumbsUpCountResultLiveData.value - // Result type cannot be use as la left operand of '?.' + // ResultState type cannot be use as la left operand of '?.' val currentIncremented = if (liveDataValue != null) { liveDataValue.getOrDefault(0) - } else { 0 } + } else { + 0 + } val incremented = minOf(currentIncremented + 1, MAX_APPLY_COUNT) - incrementThumbsUpCountResultLiveData.value = Result.success(incremented) + incrementThumbsUpCountResultLiveData.value = ResultState.Success(incremented) viewModelScope.launch { incrementThumbsUpCountEvent.send(session.id to incremented) @@ -194,10 +203,10 @@ class SessionDetailViewModel @AssistedInject constructor( ) Timber.debug { "⭐ increment $count posted" } // Return initial value - incrementThumbsUpCountResultLiveData.value = Result.success(0) + incrementThumbsUpCountResultLiveData.value = ResultState.Success(0) } catch (e: Exception) { Timber.debug { "⭐ error $e" } - incrementThumbsUpCountResultLiveData.value = Result.failure(e) + incrementThumbsUpCountResultLiveData.value = ResultState.Error(e) } } } diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt index 6005f09a1..5a1e6382c 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt @@ -1,13 +1,16 @@ package io.github.droidkaigi.confsched2020.session.ui.viewmodel import com.jraska.livedata.test +import io.github.droidkaigi.confsched2020.model.AppError import io.github.droidkaigi.confsched2020.model.SessionContents import io.github.droidkaigi.confsched2020.model.SessionId import io.github.droidkaigi.confsched2020.model.repository.SessionRepository import io.github.droidkaigi.confsched2020.widget.component.MockkRule import io.github.droidkaigi.confsched2020.widget.component.ViewModelTestRule +import io.kotlintest.matchers.beOfType +import io.kotlintest.should import io.kotlintest.shouldBe -import io.kotlintest.shouldNotBe +import io.kotlintest.shouldHave import io.mockk.coEvery import io.mockk.impl.annotations.MockK import io.mockk.verify @@ -47,7 +50,7 @@ class SessionDetailViewModelTest { } @Test - fun load_NotFoundSpeaker() { + fun load_NotFoundSession() { coEvery { sessionRepository.sessionContents() } returns flowOf(SessionContents.EMPTY) val sessionDetailViewModel = SessionDetailViewModel( sessionId = SessionId("1"), @@ -59,20 +62,14 @@ class SessionDetailViewModelTest { .uiModel .test() - val appErrorTestObserver = sessionDetailViewModel - .appError - .test() - val uiModelValueHistory = uiModelTestObserver.valueHistory() uiModelValueHistory[0].apply { isLoading shouldBe false + error should beOfType() session shouldBe null showEllipsis shouldBe true searchQuery shouldBe null } - - val appErrorValueHistory = appErrorTestObserver.valueHistory() - appErrorValueHistory[0] shouldNotBe null } @Test diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/AppError.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/AppError.kt index 40a5e5d6c..cf18fd1a4 100644 --- a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/AppError.kt +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/AppError.kt @@ -10,6 +10,7 @@ sealed class AppError : RuntimeException { sealed class ApiException(cause: Throwable?) : AppError(cause) { class NetworkException(cause: Throwable?) : ApiException(cause) class ServerException(cause: Throwable?) : ApiException(cause) + class SessionNotFoundException(cause: Throwable?) : AppError(cause) class UnknownException(cause: Throwable?) : AppError(cause) } diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/LoadState.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/LoadState.kt index 2822bbe51..e478764ec 100644 --- a/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/LoadState.kt +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2020/model/LoadState.kt @@ -1,10 +1,10 @@ package io.github.droidkaigi.confsched2020.model -interface errorGettable { +interface ErrorGettable { fun getErrorIfExists(): Throwable? } -sealed class LoadState : errorGettable { +sealed class LoadState : ErrorGettable { object Loading : LoadState() class Loaded(val value: T) : LoadState() class Error(val e: Throwable) : LoadState() @@ -14,7 +14,7 @@ sealed class LoadState : errorGettable { fun getValueOrNull(): T? = if (this is Loaded) value else null } -sealed class LoadingState : errorGettable { +sealed class LoadingState : ErrorGettable { object Loading : LoadingState() object Loaded : LoadingState() class Error(val e: Throwable) : LoadingState() @@ -22,3 +22,18 @@ sealed class LoadingState : errorGettable { val isLoading get() = this is Loading override fun getErrorIfExists() = if (this is Error) e else null } + +sealed class ResultState : ErrorGettable { + class Success(val value: T) : ResultState() + class Error(val e: Throwable) : ResultState() + + override fun getErrorIfExists() = if (this is Error) e else null + fun getOrDefault(default: T): T { + if (this is Success) return value + return default + } +} + +fun List.firstErrorOrNull(): Throwable? { + return mapNotNull { it.getErrorIfExists() }.firstOrNull() +} From 489fb2147564a6775006ae2981a90b9a76225bf3 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 16 Feb 2020 12:44:16 +0900 Subject: [PATCH 36/42] Scale animation when users continually press thumbs-up --- .../session/ui/animation/Increment.kt | 65 +++++++++++++++++-- .../session/ui/item/SessionDetailTitleItem.kt | 35 +++++++--- 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/animation/Increment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/animation/Increment.kt index 954610049..dd5ea1e13 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/animation/Increment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/animation/Increment.kt @@ -11,8 +11,7 @@ import io.github.droidkaigi.confsched2020.ext.awaitNextLayout import kotlinx.coroutines.async import kotlinx.coroutines.launch -private const val POP_UP_DURATION: Long = 100 - +// The view is shown with fade-in and move-up animation. internal fun View.popUp(lifecycleCoroutineScope: LifecycleCoroutineScope) { val target = this @@ -42,7 +41,7 @@ internal fun View.popUp(lifecycleCoroutineScope: LifecycleCoroutineScope) { -popupHeight ).run { interpolator = DecelerateInterpolator() - duration = POP_UP_DURATION + duration = UP_DOWN_DURATION start() awaitEnd() } @@ -53,6 +52,7 @@ internal fun View.popUp(lifecycleCoroutineScope: LifecycleCoroutineScope) { } } +// The view is hidden with fade-out and move-down animation. internal fun View.dropOut(lifecycleCoroutineScope: LifecycleCoroutineScope) { val target = this @@ -79,7 +79,7 @@ internal fun View.dropOut(lifecycleCoroutineScope: LifecycleCoroutineScope) { dropOutHeight ).run { interpolator = AccelerateInterpolator() - duration = POP_UP_DURATION + duration = UP_DOWN_DURATION start() awaitEnd() } @@ -91,3 +91,60 @@ internal fun View.dropOut(lifecycleCoroutineScope: LifecycleCoroutineScope) { } } +// The view is scale up and down at current position. +internal fun View.pop(lifecycleCoroutineScope: LifecycleCoroutineScope) { + val target = this + target.scaleX + val scaleValue = 0.2f + val scaleXAnimator = { x: Float -> + ObjectAnimator + .ofFloat(target, View.SCALE_X, x) + .also { + it.interpolator = DecelerateInterpolator() + it.duration = SCALE_DURATION + } + } + val scaleYAnimator = { y: Float -> + ObjectAnimator + .ofFloat(target, View.SCALE_Y, y) + .also { + it.interpolator = DecelerateInterpolator() + it.duration = SCALE_DURATION + } + } + + lifecycleCoroutineScope.launch { + val scaleXUp = async { + scaleXAnimator(1f + scaleValue).run { + start() + awaitEnd() + } + } + val scaleYUp = async { + scaleYAnimator(1f + scaleValue).run { + start() + awaitEnd() + } + } + scaleXUp.await() + scaleYUp.await() + + val scaleXDown = async { + scaleXAnimator(1f - scaleValue).run { + start() + awaitEnd() + } + } + val scaleYDown = async { + scaleYAnimator(1f - scaleValue).run { + start() + awaitEnd() + } + } + scaleXDown.await() + scaleYDown.await() + } +} + +private const val UP_DOWN_DURATION: Long = 100 +private const val SCALE_DURATION: Long = 100 diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index 45f632238..8911eba8f 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -20,6 +20,7 @@ import io.github.droidkaigi.confsched2020.model.defaultLang import io.github.droidkaigi.confsched2020.session.R import io.github.droidkaigi.confsched2020.session.databinding.ItemSessionDetailTitleBinding import io.github.droidkaigi.confsched2020.session.ui.animation.dropOut +import io.github.droidkaigi.confsched2020.session.ui.animation.pop import io.github.droidkaigi.confsched2020.session.ui.animation.popUp import java.util.regex.Pattern @@ -87,16 +88,22 @@ class SessionDetailTitleItem @AssistedInject constructor( binding.thumbsUpCount = thumbsUpCount if (!thumbsUpCount.incrementedUpdated) { return - } else if (thumbsUpCount.incremented > 0) { - val context = binding.incrementedThumbsUpCount.context - @Suppress("USELESS_CAST") - binding.incrementedThumbsUpCount.text = context.getString( - R.string.thumbs_up_increment_label, - thumbsUpCount.incremented as Int // Need to specify a type for lintDebug task - ) - binding.incrementedThumbsUpCount.popUp(lifecycleCoroutineScope) - } else { - binding.incrementedThumbsUpCount.dropOut(lifecycleCoroutineScope) + } + + when(thumbsUpCount.incremented) { + 0 -> binding.incrementedThumbsUpCount.dropOut(lifecycleCoroutineScope) + 1 -> { + binding.incrementedThumbsUpCount.setIncrementedText( + count = thumbsUpCount.incremented + ) + binding.incrementedThumbsUpCount.popUp(lifecycleCoroutineScope) + } + else -> { + binding.incrementedThumbsUpCount.setIncrementedText( + count = thumbsUpCount.incremented + ) + binding.incrementedThumbsUpCount.pop(lifecycleCoroutineScope) + } } } } @@ -120,6 +127,14 @@ class SessionDetailTitleItem @AssistedInject constructor( } } + private fun TextView.setIncrementedText(count: Int) { + @Suppress("USELESS_CAST") + text = context.getString( + R.string.thumbs_up_increment_label, + count as Int // Need to specify a type for lintDebug task + ) + } + @AssistedInject.Factory interface Factory { fun create( From 69c94e955311630b0d8a1a1a70169cbf6ccbe05d Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 16 Feb 2020 12:45:34 +0900 Subject: [PATCH 37/42] Apply ktlintFormat --- .../confsched2020/session/ui/SessionDetailFragment.kt | 1 - .../confsched2020/session/ui/item/SessionDetailTitleItem.kt | 2 +- .../session/ui/viewmodel/SessionDetailViewModelTest.kt | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt index cdc71bdc7..1b423a7dd 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/SessionDetailFragment.kt @@ -24,7 +24,6 @@ import io.github.droidkaigi.confsched2020.di.PageScope import io.github.droidkaigi.confsched2020.ext.assistedActivityViewModels import io.github.droidkaigi.confsched2020.ext.assistedViewModels import io.github.droidkaigi.confsched2020.ext.isShow -import io.github.droidkaigi.confsched2020.model.AppError import io.github.droidkaigi.confsched2020.model.Session import io.github.droidkaigi.confsched2020.model.Speaker import io.github.droidkaigi.confsched2020.model.SpeechSession diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index 8911eba8f..858b1d496 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -90,7 +90,7 @@ class SessionDetailTitleItem @AssistedInject constructor( return } - when(thumbsUpCount.incremented) { + when (thumbsUpCount.incremented) { 0 -> binding.incrementedThumbsUpCount.dropOut(lifecycleCoroutineScope) 1 -> { binding.incrementedThumbsUpCount.setIncrementedText( diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt index 5a1e6382c..484e9fbf2 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt @@ -10,7 +10,6 @@ import io.github.droidkaigi.confsched2020.widget.component.ViewModelTestRule import io.kotlintest.matchers.beOfType import io.kotlintest.should import io.kotlintest.shouldBe -import io.kotlintest.shouldHave import io.mockk.coEvery import io.mockk.impl.annotations.MockK import io.mockk.verify From 521ba2d297c358488b621107f5cdcceae2fd9488 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 16 Feb 2020 12:52:49 +0900 Subject: [PATCH 38/42] Restore error checking --- .../session/ui/viewmodel/SessionDetailViewModelTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt index 484e9fbf2..e8f9a955f 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModelTest.kt @@ -90,18 +90,21 @@ class SessionDetailViewModelTest { val valueHistory = testObserver.valueHistory() valueHistory[0].apply { isLoading shouldBe false + error shouldBe null session shouldBe Dummies.speachSession1 showEllipsis shouldBe true searchQuery shouldBe null } valueHistory[1].apply { isLoading shouldBe true + error shouldBe null session shouldBe Dummies.speachSession1 showEllipsis shouldBe true searchQuery shouldBe null } valueHistory[2].apply { isLoading shouldBe false + error shouldBe null session shouldBe Dummies.speachSession1 showEllipsis shouldBe true searchQuery shouldBe null @@ -124,6 +127,7 @@ class SessionDetailViewModelTest { valueHistory[0] shouldBe SessionDetailViewModel.UiModel.EMPTY.copy(isLoading = true) valueHistory[1].apply { isLoading shouldBe true + error shouldBe null session shouldBe null showEllipsis shouldBe false searchQuery shouldBe null @@ -146,6 +150,7 @@ class SessionDetailViewModelTest { val valueHistory = testObserver.valueHistory() valueHistory[0].apply { isLoading shouldBe false + error shouldBe null session shouldBe Dummies.speachSession1 showEllipsis shouldBe true searchQuery shouldBe "query" @@ -170,6 +175,7 @@ class SessionDetailViewModelTest { valueHistory[0] shouldBe SessionDetailViewModel.UiModel.EMPTY.copy(isLoading = true) valueHistory[1].apply { isLoading shouldBe true + error shouldBe null session shouldBe null showEllipsis shouldBe true searchQuery shouldBe null From eb5daa5635677a1081bd12bdbde43716f220ed6f Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 16 Feb 2020 13:13:34 +0900 Subject: [PATCH 39/42] Minor adjustment ui design --- .../session/ui/viewmodel/SessionDetailViewModel.kt | 2 +- .../session/src/main/res/layout/item_session_detail_title.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index 7476dc383..3deaf499c 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -220,7 +220,7 @@ class SessionDetailViewModel @AssistedInject constructor( } companion object { - private const val INCREMENT_DEBOUNCE_MILLIS = 500L + private const val INCREMENT_DEBOUNCE_MILLIS = 1000L private const val MAX_APPLY_COUNT = 50 } } diff --git a/feature/session/src/main/res/layout/item_session_detail_title.xml b/feature/session/src/main/res/layout/item_session_detail_title.xml index eced174cf..164a7ee5b 100644 --- a/feature/session/src/main/res/layout/item_session_detail_title.xml +++ b/feature/session/src/main/res/layout/item_session_detail_title.xml @@ -163,6 +163,7 @@ app:layout_constraintStart_toStartOf="@id/guideline_start" app:layout_constraintTop_toBottomOf="@id/tags" app:layout_constraintBottom_toTopOf="@id/divider_survey_and_text" + app:layout_constraintWidth_max="200dp" tools:text="thumbs-up" /> From 48c71ba191dd02efb869dae324bb9b4ebee9f22c Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 16 Feb 2020 13:16:36 +0900 Subject: [PATCH 40/42] =?UTF-8?q?Remove=20=E2=AD=90=20and=20rewrite=20debu?= =?UTF-8?q?g=20logging=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/ui/viewmodel/SessionDetailViewModel.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt index 3deaf499c..4a58a713b 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/viewmodel/SessionDetailViewModel.kt @@ -153,10 +153,6 @@ class SessionDetailViewModel @AssistedInject constructor( viewModelScope.launch { setupIncrementThumbsUpEvent() } - // For debug - incrementThumbsUpCountResultLiveData.observeForever { - Timber.debug { "⭐ increment livedata $it" } - } } fun favorite(session: Session) { @@ -201,11 +197,11 @@ class SessionDetailViewModel @AssistedInject constructor( sessionId = sessionId, count = count ) - Timber.debug { "⭐ increment $count posted" } + Timber.debug { "increment thumbs-up: $count posted" } // Return initial value incrementThumbsUpCountResultLiveData.value = ResultState.Success(0) } catch (e: Exception) { - Timber.debug { "⭐ error $e" } + Timber.debug { "increment thumbs-up error: $e" } incrementThumbsUpCountResultLiveData.value = ResultState.Error(e) } } From 3d4c67005845c603c9b8ebd29b098f00dc50fb81 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 16 Feb 2020 13:32:40 +0900 Subject: [PATCH 41/42] LintDebug task can success without type cast --- .../confsched2020/session/ui/item/SessionDetailTitleItem.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt index 858b1d496..1441ed827 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2020/session/ui/item/SessionDetailTitleItem.kt @@ -128,10 +128,9 @@ class SessionDetailTitleItem @AssistedInject constructor( } private fun TextView.setIncrementedText(count: Int) { - @Suppress("USELESS_CAST") text = context.getString( R.string.thumbs_up_increment_label, - count as Int // Need to specify a type for lintDebug task + count ) } From 808f2b29c14a18574a166e4e3a05143e8c6f4038 Mon Sep 17 00:00:00 2001 From: IppeiMukaida Date: Sun, 16 Feb 2020 13:45:32 +0900 Subject: [PATCH 42/42] Fix stroke color of blue thumbs-up button --- .../session/src/main/res/layout/item_session_detail_title.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/session/src/main/res/layout/item_session_detail_title.xml b/feature/session/src/main/res/layout/item_session_detail_title.xml index 164a7ee5b..8db33449e 100644 --- a/feature/session/src/main/res/layout/item_session_detail_title.xml +++ b/feature/session/src/main/res/layout/item_session_detail_title.xml @@ -158,7 +158,7 @@ app:iconTint="@{ thumbsUpCount.total > 0 ? @color/white: @color/indigo_900 }" app:iconPadding="4dp" app:iconSize="16dp" - app:strokeColor="@{ thumbsUpCount.total > 0 ? @color/white : @color/indigo_900 }" + app:strokeColor="@color/indigo_900" app:strokeWidth="1dp" app:layout_constraintStart_toStartOf="@id/guideline_start" app:layout_constraintTop_toBottomOf="@id/tags"