Improve EmojiChooserFragment: DI

This commit is contained in:
Benoit Marty 2019-12-09 23:41:35 +01:00
parent 63e0b15f3d
commit f00f34b244
10 changed files with 105 additions and 69 deletions

View File

@ -43,7 +43,6 @@ import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.di.VectorComponent
import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.rx.setupRxPlugin import im.vector.riotx.core.rx.setupRxPlugin
import im.vector.riotx.core.utils.initKnownEmojiHashSet
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
@ -137,7 +136,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
}) })
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
// This should be done as early as possible // This should be done as early as possible
initKnownEmojiHashSet(appContext) // initKnownEmojiHashSet(appContext)
} }
override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION) override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)

View File

@ -38,6 +38,7 @@ import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.login.* import im.vector.riotx.features.login.*
import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.reactions.EmojiChooserFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
@ -255,4 +256,9 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(BreadcrumbsFragment::class) @FragmentKey(BreadcrumbsFragment::class)
fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(EmojiChooserFragment::class)
fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment
} }

View File

@ -49,6 +49,7 @@ private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" +
"|\uD83C\uDCCF\uFE0F?" + "|\uD83C\uDCCF\uFE0F?" +
"|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))") "|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))")
/*
// A hashset from all supported emoji // A hashset from all supported emoji
private var knownEmojiSet: HashSet<String>? = null private var knownEmojiSet: HashSet<String>? = null
@ -77,6 +78,7 @@ fun isSingleEmoji(string: String): Boolean {
} }
return knownEmojiSet?.contains(string) ?: false return knownEmojiSet?.contains(string) ?: false
} }
*/
/** /**
* Test if a string contains emojis. * Test if a string contains emojis.

View File

@ -17,13 +17,18 @@ package im.vector.riotx.features.reactions
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.lifecycle.observe
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import javax.inject.Inject import javax.inject.Inject
class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() { class EmojiChooserFragment @Inject constructor(
private val emojiRecyclerAdapter: EmojiRecyclerAdapter
) : VectorBaseFragment(),
EmojiRecyclerAdapter.InteractionListener,
ReactionClickListener {
override fun getLayoutResId() = R.layout.emoji_chooser_fragment override fun getLayoutResId() = R.layout.emoji_chooser_fragment
@ -32,15 +37,32 @@ class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java)
viewModel.initWithContext(context!!)
emojiRecyclerAdapter.reactionClickListener = this
emojiRecyclerAdapter.interactionListener = this
(view as? RecyclerView)?.let { (view as? RecyclerView)?.let {
it.adapter = viewModel.adapter it.adapter = emojiRecyclerAdapter
it.adapter?.notifyDataSetChanged() it.adapter?.notifyDataSetChanged()
} }
viewModel.moveToSection.observe(viewLifecycleOwner) { section ->
emojiRecyclerAdapter.scrollToSection(section)
}
}
override fun firstVisibleSectionChange(section: Int) {
viewModel.setCurrentSection(section)
}
override fun onReactionSelected(reaction: String) {
viewModel.onReactionSelected(reaction)
} }
override fun onDestroyView() { override fun onDestroyView() {
(view as? RecyclerView)?.cleanup() (view as? RecyclerView)?.cleanup()
emojiRecyclerAdapter.reactionClickListener = null
emojiRecyclerAdapter.interactionListener = null
super.onDestroyView() super.onDestroyView()
} }
} }

View File

@ -15,46 +15,33 @@
*/ */
package im.vector.riotx.features.reactions package im.vector.riotx.features.reactions
import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.reactions.data.EmojiDataSource
import javax.inject.Inject import javax.inject.Inject
class EmojiChooserViewModel @Inject constructor() : ViewModel() { class EmojiChooserViewModel @Inject constructor() : ViewModel() {
// TODO Move the adapter out of the ViewModel
var adapter: EmojiRecyclerAdapter? = null
val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()
val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData() val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var selectedReaction: String? = null var selectedReaction: String? = null
var eventId: String? = null var eventId: String? = null
val currentSection: MutableLiveData<Int> = MutableLiveData() val currentSection: MutableLiveData<Int> = MutableLiveData()
val moveToSection: MutableLiveData<Int> = MutableLiveData()
var reactionClickListener = object : ReactionClickListener { fun onReactionSelected(reaction: String) {
override fun onReactionSelected(reaction: String) {
selectedReaction = reaction selectedReaction = reaction
navigateEvent.value = LiveEvent(NAVIGATE_FINISH) navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
} }
}
fun initWithContext(context: Context) { // Called by the Fragment, when the List is scrolled
// TODO load async fun setCurrentSection(section: Int) {
val emojiDataSource = EmojiDataSource(context)
emojiSourceLiveData.value = emojiDataSource
adapter = EmojiRecyclerAdapter(emojiDataSource, reactionClickListener)
adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener {
override fun firstVisibleSectionChange(section: Int) {
currentSection.value = section currentSection.value = section
} }
}
}
fun scrollToSection(sectionIndex: Int) { // Called by the Activity, when a tab item is clicked
adapter?.scrollToSection(sectionIndex) fun scrollToSection(section: Int) {
moveToSection.value = section
} }
companion object { companion object {

View File

@ -35,6 +35,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.reactions.data.EmojiDataSource
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.* import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.*
import timber.log.Timber import timber.log.Timber
@ -44,7 +45,6 @@ import javax.inject.Inject
/** /**
* *
* TODO: Loading indicator while getting emoji data source? * TODO: Loading indicator while getting emoji data source?
* TODO: migrate to MvRx
* TODO: Finish Refactor to vector base activity * TODO: Finish Refactor to vector base activity
*/ */
class EmojiReactionPickerActivity : VectorBaseActivity(), class EmojiReactionPickerActivity : VectorBaseActivity(),
@ -60,7 +60,9 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
override fun getTitleRes() = R.string.title_activity_emoji_reaction_picker override fun getTitleRes() = R.string.title_activity_emoji_reaction_picker
@Inject lateinit var emojiSearchResultViewModelFactory: EmojiSearchResultViewModel.Factory
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@Inject lateinit var emojiDataSource: EmojiDataSource
private val searchResultViewModel: EmojiSearchResultViewModel by viewModel() private val searchResultViewModel: EmojiSearchResultViewModel by viewModel()
@ -93,13 +95,12 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID) viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID)
viewModel.emojiSourceLiveData.observe(this, Observer { emojiDataSource.rawData?.categories?.let { categories ->
it.rawData?.categories?.let { categories ->
for (category in categories) { for (category in categories) {
val s = category.emojis[0] val s = category.emojis[0]
tabLayout.newTab() tabLayout.newTab()
.also { tab -> .also { tab ->
tab.text = it.rawData!!.emojis[s]!!.emojiString() tab.text = emojiDataSource.rawData!!.emojis[s]!!.emoji
tab.contentDescription = category.name tab.contentDescription = category.name
} }
.also { tab -> .also { tab ->
@ -108,7 +109,6 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
} }
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener) tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
} }
})
viewModel.currentSection.observe(this, Observer { section -> viewModel.currentSection.observe(this, Observer { section ->
section?.let { section?.let {
@ -136,7 +136,6 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
override fun compatibilityFontUpdate(typeface: Typeface?) { override fun compatibilityFontUpdate(typeface: Typeface?) {
EmojiDrawView.configureTextPaint(this, typeface) EmojiDrawView.configureTextPaint(this, typeface)
searchResultViewModel.dataSource
} }
override fun onDestroy() { override fun onDestroy() {

View File

@ -34,6 +34,7 @@ import im.vector.riotx.features.reactions.data.EmojiDataSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
/** /**
@ -43,10 +44,12 @@ import kotlin.math.abs
* TODO: Performances * TODO: Performances
* TODO: Scroll to section - Find a way to snap section to the top * TODO: Scroll to section - Find a way to snap section to the top
*/ */
class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null, class EmojiRecyclerAdapter @Inject constructor(
private var reactionClickListener: ReactionClickListener?) : private val dataSource: EmojiDataSource?
) :
RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() { RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {
var reactionClickListener: ReactionClickListener? = null
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
private var mRecyclerView: RecyclerView? = null private var mRecyclerView: RecyclerView? = null
@ -73,7 +76,7 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
val sectionMojis = categories[sectionNumber].emojis val sectionMojis = categories[sectionNumber].emojis
val sectionOffset = getSectionOffset(sectionNumber) val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[itemPosition - sectionOffset] val emoji = sectionMojis[itemPosition - sectionOffset]
val item = dataSource.rawData!!.emojis.getValue(emoji).emojiString() val item = dataSource.rawData!!.emojis.getValue(emoji).emoji
reactionClickListener?.onReactionSelected(item) reactionClickListener?.onReactionSelected(item)
} }
} }
@ -197,7 +200,7 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
val sectionMojis = categories[sectionNumber].emojis val sectionMojis = categories[sectionNumber].emojis
val sectionOffset = getSectionOffset(sectionNumber) val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[position - sectionOffset] val emoji = sectionMojis[position - sectionOffset]
val item = dataSource.rawData!!.emojis[emoji]!!.emojiString() val item = dataSource.rawData!!.emojis[emoji]!!.emoji
(holder as EmojiViewHolder).data = item (holder as EmojiViewHolder).data = item
if (scrollState != ScrollState.SETTLING || !isFastScroll) { if (scrollState != ScrollState.SETTLING || !isFastScroll) {
// Log.i("PERF","Bind with draw at position:$position") // Log.i("PERF","Bind with draw at position:$position")

View File

@ -43,12 +43,12 @@ abstract class EmojiSearchResultItem : EpoxyModelWithHolder<EmojiSearchResultIte
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
// TODO use query string to highlight the matched query in name and keywords? // TODO use query string to highlight the matched query in name and keywords?
holder.emojiText.text = emojiItem.emojiString() holder.emojiText.text = emojiItem.emoji
holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT
holder.emojiNameText.text = emojiItem.name holder.emojiNameText.text = emojiItem.name
holder.emojiKeywordText.setTextOrHide(emojiItem.keywords?.joinToString()) holder.emojiKeywordText.setTextOrHide(emojiItem.keywords?.joinToString())
holder.view.setOnClickListener { holder.view.setOnClickListener {
onClickListener?.onReactionSelected(emojiItem.emojiString()) onClickListener?.onReactionSelected(emojiItem.emoji)
} }
} }

View File

@ -15,9 +15,9 @@
*/ */
package im.vector.riotx.features.reactions package im.vector.riotx.features.reactions
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.*
import com.airbnb.mvrx.MvRxViewModelFactory import com.squareup.inject.assisted.Assisted
import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.reactions.data.EmojiDataSource import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.reactions.data.EmojiItem import im.vector.riotx.features.reactions.data.EmojiItem
@ -27,9 +27,24 @@ data class EmojiSearchResultViewState(
val results: List<EmojiItem> = emptyList() val results: List<EmojiItem> = emptyList()
) : MvRxState ) : MvRxState
class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState: EmojiSearchResultViewState) class EmojiSearchResultViewModel @AssistedInject constructor(
@Assisted initialState: EmojiSearchResultViewState,
private val dataSource: EmojiDataSource)
: VectorViewModel<EmojiSearchResultViewState, EmojiSearchAction>(initialState) { : VectorViewModel<EmojiSearchResultViewState, EmojiSearchAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: EmojiSearchResultViewState): EmojiSearchResultViewModel
}
companion object : MvRxViewModelFactory<EmojiSearchResultViewModel, EmojiSearchResultViewState> {
override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? {
val activity: EmojiReactionPickerActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.emojiSearchResultViewModelFactory.create(state)
}
}
override fun handle(action: EmojiSearchAction) { override fun handle(action: EmojiSearchAction) {
when (action) { when (action) {
is EmojiSearchAction.UpdateQuery -> updateQuery(action) is EmojiSearchAction.UpdateQuery -> updateQuery(action)
@ -55,12 +70,4 @@ class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState:
) )
} }
} }
companion object : MvRxViewModelFactory<EmojiSearchResultViewModel, EmojiSearchResultViewState> {
override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? {
// TODO get the data source from activity? share it with other fragment
return EmojiSearchResultViewModel(EmojiDataSource(viewModelContext.activity), state)
}
}
} }

View File

@ -18,17 +18,28 @@ package im.vector.riotx.features.reactions.data
import android.content.Context import android.content.Context
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenScope
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.measureTimeMillis
class EmojiDataSource(val context: Context) { @ScreenScope
class EmojiDataSource @Inject constructor(
context: Context
) {
var rawData: EmojiData? = null var rawData: EmojiData? = null
init { init {
measureTimeMillis {
context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input -> context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input ->
val moshi = Moshi.Builder().build() val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(EmojiData::class.java) val jsonAdapter = moshi.adapter(EmojiData::class.java)
val inputAsString = input.bufferedReader().use { it.readText() } val inputAsString = input.bufferedReader().use { it.readText() }
this.rawData = jsonAdapter.fromJson(inputAsString) this.rawData = jsonAdapter.fromJson(inputAsString)
} }
}.also {
Timber.e("Emoji: $it millis")
}
} }
} }