Skip to content

Commit

Permalink
[MBL-821] Upgrade profile activity to RX2 (#2078)
Browse files Browse the repository at this point in the history
* upgrade profile activity to RX2

* clear disposables

---------

Co-authored-by: Leigh Douglas <[email protected]>
  • Loading branch information
mtgriego and leighdouglas authored Jul 23, 2024
1 parent 30207ed commit d2b7028
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 101 deletions.
81 changes: 47 additions & 34 deletions app/src/main/java/com/kickstarter/ui/activities/ProfileActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,126 +2,138 @@ package com.kickstarter.ui.activities

import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.core.view.isGone
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.kickstarter.R
import com.kickstarter.databinding.ProfileLayoutBinding
import com.kickstarter.libs.BaseActivity
import com.kickstarter.libs.RecyclerViewPaginator
import com.kickstarter.libs.qualifiers.RequiresActivityViewModel
import com.kickstarter.libs.rx.transformers.Transformers.observeForUI
import com.kickstarter.libs.recyclerviewpagination.RecyclerViewPaginatorV2
import com.kickstarter.libs.rx.transformers.Transformers.observeForUIV2
import com.kickstarter.libs.utils.ApplicationUtils
import com.kickstarter.libs.utils.ViewUtils
import com.kickstarter.libs.utils.extensions.addToDisposable
import com.kickstarter.libs.utils.extensions.getEnvironment
import com.kickstarter.libs.utils.extensions.getProjectIntent
import com.kickstarter.models.Project
import com.kickstarter.ui.IntentKey
import com.kickstarter.ui.adapters.ProfileAdapter
import com.kickstarter.ui.extensions.loadCircleImage
import com.kickstarter.ui.extensions.startActivityWithTransition
import com.kickstarter.viewmodels.ProfileViewModel
import io.reactivex.disposables.CompositeDisposable

@RequiresActivityViewModel(ProfileViewModel.ViewModel::class)
class ProfileActivity : BaseActivity<ProfileViewModel.ViewModel>() {
class ProfileActivity : ComponentActivity() {
private lateinit var adapter: ProfileAdapter
private lateinit var paginator: RecyclerViewPaginator
private lateinit var paginator: RecyclerViewPaginatorV2
private lateinit var binding: ProfileLayoutBinding

private lateinit var profileViewModelFactory: ProfileViewModel.Factory
private val viewModel: ProfileViewModel.ProfileViewModel by viewModels { profileViewModelFactory }

private val disposables = CompositeDisposable()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ProfileLayoutBinding.inflate(layoutInflater)

setContentView(binding.root)

getEnvironment()?.let { env ->
profileViewModelFactory = ProfileViewModel.Factory(env)
}

this.adapter = ProfileAdapter(this.viewModel)
val spanCount = if (ViewUtils.isLandscape(this)) 3 else 2
binding.recyclerView.layoutManager = GridLayoutManager(this, spanCount)
binding.recyclerView.adapter = this.adapter

this.paginator = RecyclerViewPaginator(
this.paginator = RecyclerViewPaginatorV2(
binding.recyclerView, { this.viewModel.inputs.nextPage() },
this.viewModel.outputs.isFetchingProjects()
)

this.viewModel.outputs.avatarImageViewUrl()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe { url -> binding.avatarImageView.loadCircleImage(url) }
.addToDisposable(disposables)

this.viewModel.outputs.backedCountTextViewHidden()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe {
binding.backedCountTextView.isGone = it
}
.addToDisposable(disposables)

this.viewModel.outputs.backedCountTextViewText()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe {
binding.backedCountTextView.text = it
}
.addToDisposable(disposables)

this.viewModel.outputs.backedTextViewHidden()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe {
binding.backedTextView.isGone = it
}
.addToDisposable(disposables)

this.viewModel.outputs.createdCountTextViewHidden()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe {
binding.createdCountTextView.isGone = it
}
.addToDisposable(disposables)

this.viewModel.outputs.createdCountTextViewText()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe {
binding.createdCountTextView.text = it
}
.addToDisposable(disposables)

this.viewModel.outputs.createdTextViewHidden()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe {
binding.createdTextView.isGone = it
}
.addToDisposable(disposables)

this.viewModel.outputs.dividerViewHidden()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe {
binding.dividerView.isGone = it
}
.addToDisposable(disposables)

this.viewModel.outputs.projectList()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe {
this.loadProjects(it)
}
.addToDisposable(disposables)

this.viewModel.outputs.resumeDiscoveryActivity()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe { resumeDiscoveryActivity() }
.addToDisposable(disposables)

this.viewModel.outputs.startMessageThreadsActivity()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe { this.startMessageThreadsActivity() }
.addToDisposable(disposables)

this.viewModel.outputs.startProjectActivity()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe { this.startProjectActivity(it) }
.addToDisposable(disposables)

this.viewModel.outputs.userNameTextViewText()
.compose(bindToLifecycle())
.compose(observeForUI())
.compose(observeForUIV2())
.subscribe { binding.userNameTextView.text = it }
.addToDisposable(disposables)

binding.profileActivityToolbar.messagesButton.setOnClickListener { this.viewModel.inputs.messagesButtonClicked() }
}
Expand All @@ -130,6 +142,7 @@ class ProfileActivity : BaseActivity<ProfileViewModel.ViewModel>() {
super.onDestroy()
this.paginator.stop()
binding.recyclerView.adapter = null
disposables.clear()
}

private fun loadProjects(projects: List<Project>) {
Expand Down
12 changes: 11 additions & 1 deletion app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.util.Pair
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.AnimRes
import androidx.fragment.app.Fragment
Expand Down Expand Up @@ -50,7 +52,15 @@ fun Activity.startActivityWithTransition(
@AnimRes exitAnim: Int
) {
startActivity(intent)
overridePendingTransition(enterAnim, exitAnim)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
ComponentActivity.OVERRIDE_TRANSITION_OPEN,
enterAnim,
exitAnim
)
} else {
overridePendingTransition(enterAnim, exitAnim)
}
}

fun Activity.hideKeyboard() {
Expand Down
77 changes: 48 additions & 29 deletions app/src/main/java/com/kickstarter/viewmodels/ProfileViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
package com.kickstarter.viewmodels

import android.util.Pair
import com.kickstarter.libs.ActivityViewModel
import com.kickstarter.libs.ApiPaginator
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.kickstarter.libs.ApiPaginatorV2
import com.kickstarter.libs.Environment
import com.kickstarter.libs.rx.transformers.Transformers.neverError
import com.kickstarter.libs.rx.transformers.Transformers.neverErrorV2
import com.kickstarter.libs.utils.EventContextValues
import com.kickstarter.libs.utils.NumberUtils
import com.kickstarter.libs.utils.extensions.addToDisposable
import com.kickstarter.libs.utils.extensions.isNonZero
import com.kickstarter.libs.utils.extensions.isZero
import com.kickstarter.models.Project
import com.kickstarter.services.DiscoveryParams
import com.kickstarter.services.apiresponses.DiscoverEnvelope
import com.kickstarter.ui.activities.ProfileActivity
import com.kickstarter.ui.adapters.ProfileAdapter
import com.kickstarter.ui.viewholders.EmptyProfileViewHolder
import com.kickstarter.ui.viewholders.ProfileCardViewHolder
import rx.Observable
import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject

interface ProfileViewModel {

Expand Down Expand Up @@ -68,10 +70,10 @@ interface ProfileViewModel {
fun projectList(): Observable<List<Project>>

/** Emits when we should resume the [com.kickstarter.ui.activities.DiscoveryActivity]. */
fun resumeDiscoveryActivity(): Observable<Void>
fun resumeDiscoveryActivity(): Observable<Unit>

/** Emits when we should start the [com.kickstarter.ui.activities.MessageThreadsActivity]. */
fun startMessageThreadsActivity(): Observable<Void>
fun startMessageThreadsActivity(): Observable<Unit>

/** Emits when we should start the [com.kickstarter.ui.activities.ProjectActivity]. */
fun startProjectActivity(): Observable<Project>
Expand All @@ -80,13 +82,15 @@ interface ProfileViewModel {
fun userNameTextViewText(): Observable<String>
}

class ViewModel(environment: Environment) : ActivityViewModel<ProfileActivity>(environment), ProfileAdapter.Delegate, Inputs, Outputs {
private val client = requireNotNull(environment.apiClient())
private val currentUser = requireNotNull(environment.currentUser())
class ProfileViewModel(environment: Environment) : ViewModel(), ProfileAdapter.Delegate, Inputs, Outputs {
private val client = requireNotNull(environment.apiClientV2())
private val currentUser = requireNotNull(environment.currentUserV2())
private val analytics = requireNotNull(environment.analytics())

private val exploreProjectsButtonClicked = PublishSubject.create<Void>()
private val messagesButtonClicked = PublishSubject.create<Void>()
private val nextPage = PublishSubject.create<Void>()
private val exploreProjectsButtonClicked = PublishSubject.create<Unit>()
private val messagesButtonClicked = PublishSubject.create<Unit>()
private val nextPage = PublishSubject.create<Unit>()
private val refresh = PublishSubject.create<DiscoveryParams>()
private val projectCardClicked = PublishSubject.create<Project>()

private val avatarImageViewUrl: Observable<String>
Expand All @@ -99,36 +103,39 @@ interface ProfileViewModel {
private val dividerViewHidden: Observable<Boolean>
private val isFetchingProjects = BehaviorSubject.create<Boolean>()
private val projectList: Observable<List<Project>>
private val resumeDiscoveryActivity: Observable<Void>
private val startMessageThreadsActivity: Observable<Void>
private val resumeDiscoveryActivity: Observable<Unit>
private val startMessageThreadsActivity: Observable<Unit>
private val userNameTextViewText: Observable<String>

val inputs: Inputs = this
val outputs: Outputs = this

init {
val disposables = CompositeDisposable()

init {
val freshUser = this.client.fetchCurrentUser()
.retry(2)
.compose(neverError())
.compose(neverErrorV2())

freshUser.subscribe { this.currentUser.refresh(it) }
.addToDisposable(disposables)

val params = DiscoveryParams.builder()
.backed(1)
.perPage(18)
.sort(DiscoveryParams.Sort.ENDING_SOON)
.build()

val paginator = ApiPaginator.builder<Project, DiscoverEnvelope, DiscoveryParams>()
.nextPage(this.nextPage)
val paginator = ApiPaginatorV2.builder<Project, DiscoverEnvelope, DiscoveryParams>()
.nextPage(nextPage)
.startOverWith(Observable.just(params))
.envelopeToListOfData { it.projects() }
.envelopeToMoreUrl { env -> env.urls()?.api()?.moreProjects() }
.loadWithParams { this.client.fetchProjects(params).compose(neverError()) }
.loadWithPaginationPath { this.client.fetchProjects(it).compose(neverError()) }
.loadWithParams { this.client.fetchProjects(params).compose(neverErrorV2()) }
.loadWithPaginationPath { this.client.fetchProjects(it).compose(neverErrorV2()) }
.build()

paginator.isFetching
.compose(bindToLifecycle())
.subscribe(this.isFetchingProjects)

val loggedInUser = this.currentUser.loggedInUser()
Expand Down Expand Up @@ -166,17 +173,17 @@ interface ProfileViewModel {
this.userNameTextViewText = loggedInUser.map { it.name() }

projectCardClicked
.compose(bindToLifecycle())
.subscribe { analyticEvents.trackProjectCardClicked(it, EventContextValues.ContextPageName.PROFILE.contextName) }
.subscribe { analytics.trackProjectCardClicked(it, EventContextValues.ContextPageName.PROFILE.contextName) }
.addToDisposable(disposables)
}

override fun emptyProfileViewHolderExploreProjectsClicked(viewHolder: EmptyProfileViewHolder) = this.exploreProjectsButtonClicked()

override fun exploreProjectsButtonClicked() = this.exploreProjectsButtonClicked.onNext(null)
override fun exploreProjectsButtonClicked() = this.exploreProjectsButtonClicked.onNext(Unit)

override fun messagesButtonClicked() = this.messagesButtonClicked.onNext(null)
override fun messagesButtonClicked() = this.messagesButtonClicked.onNext(Unit)

override fun nextPage() = this.nextPage.onNext(null)
override fun nextPage() = this.nextPage.onNext(Unit)

override fun profileCardViewHolderClicked(viewHolder: ProfileCardViewHolder, project: Project) = this.projectCardClicked(project)

Expand Down Expand Up @@ -209,5 +216,17 @@ interface ProfileViewModel {
override fun startMessageThreadsActivity() = this.startMessageThreadsActivity

override fun userNameTextViewText() = this.userNameTextViewText

override fun onCleared() {
disposables.clear()
super.onCleared()
}
}

class Factory(private val environment: Environment) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ProfileViewModel(environment) as T
}
}
}
Loading

0 comments on commit d2b7028

Please sign in to comment.