AppDetailsFragment: Improve data states

This commit is contained in:
Rahul Patel
2024-07-11 04:27:09 +05:30
committed by Aayush Gupta
parent 83d9895f17
commit 660c544f18
5 changed files with 167 additions and 110 deletions

View File

@@ -8,4 +8,5 @@ import com.aurora.gplayapi.helpers.contracts.TopChartsContract
typealias TopChartStash = MutableMap<TopChartsContract.Type, MutableMap<TopChartsContract.Chart, StreamCluster>> typealias TopChartStash = MutableMap<TopChartsContract.Type, MutableMap<TopChartsContract.Chart, StreamCluster>>
typealias HomeStash = MutableMap<StreamContract.Category, StreamBundle> typealias HomeStash = MutableMap<StreamContract.Category, StreamBundle>
typealias CategoryStash = MutableMap<Category.Type, List<Category>> typealias CategoryStash = MutableMap<Category.Type, List<Category>>
typealias AppStreamStash = MutableMap<String, StreamBundle>

View File

@@ -35,7 +35,7 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
@@ -56,6 +56,7 @@ import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.AppStreamStash
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.Event import com.aurora.store.data.event.Event
@@ -64,6 +65,7 @@ import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.model.State import com.aurora.store.data.model.State
import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState
import com.aurora.store.data.model.ViewState.Loading.getDataAs
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.databinding.FragmentDetailsBinding import com.aurora.store.databinding.FragmentDetailsBinding
import com.aurora.store.util.CommonUtil import com.aurora.store.util.CommonUtil
@@ -92,8 +94,8 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() { class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private val viewModel: AppDetailsViewModel by viewModels() private val viewModel: AppDetailsViewModel by activityViewModels()
private val detailsClusterViewModel: DetailsClusterViewModel by viewModels() private val detailsClusterViewModel: DetailsClusterViewModel by activityViewModels()
private val args: AppDetailsFragmentArgs by navArgs() private val args: AppDetailsFragmentArgs by navArgs()
@@ -123,6 +125,8 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private lateinit var app: App private lateinit var app: App
private var streamBundle: StreamBundle? = StreamBundle()
private var isExternal = false private var isExternal = false
private var downloadStatus = DownloadStatus.UNAVAILABLE private var downloadStatus = DownloadStatus.UNAVAILABLE
private var isUpdatable: Boolean = false private var isUpdatable: Boolean = false
@@ -196,7 +200,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
// App Details // App Details
viewModel.fetchAppDetails(view.context, app.packageName) viewModel.fetchAppDetails(app.packageName)
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.app.collect { viewModel.app.collect {
@@ -204,7 +208,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
app = it app = it
inflatePartialApp() // Re-inflate the app details, as web data may vary. inflatePartialApp() // Re-inflate the app details, as web data may vary.
inflateExtraDetails(app) inflateExtraDetails(app)
viewModel.fetchAppReviews(view.context, app.packageName) viewModel.fetchAppReviews(app.packageName)
} else { } else {
toast("Failed to fetch app details") toast("Failed to fetch app details")
} }
@@ -266,7 +270,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
// Exodus Privacy Report // Exodus Privacy Report
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.report.collect { report -> viewModel.exodusReport.collect { report ->
if (report == null) { if (report == null) {
binding.layoutDetailsPrivacy.txtStatus.text = binding.layoutDetailsPrivacy.txtStatus.text =
getString(R.string.failed_to_fetch_report) getString(R.string.failed_to_fetch_report)
@@ -801,7 +805,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
private fun inflateAppPrivacy(app: App) { private fun inflateAppPrivacy(app: App) {
viewModel.fetchAppReport(requireContext(), app.packageName) viewModel.fetchAppReport(app.packageName)
} }
private fun inflateAppDevInfo(app: App) { private fun inflateAppDevInfo(app: App) {
@@ -854,7 +858,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
btnBetaAction.text = getString(R.string.action_pending) btnBetaAction.text = getString(R.string.action_pending)
btnBetaAction.isEnabled = false btnBetaAction.isEnabled = false
viewModel.fetchTestingProgramStatus( viewModel.fetchTestingProgramStatus(
requireContext(),
app.packageName, app.packageName,
!betaProgram.isSubscribed !betaProgram.isSubscribed
) )
@@ -881,43 +884,34 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
override fun onClusterScrolled(streamCluster: StreamCluster) { override fun onClusterScrolled(streamCluster: StreamCluster) {
detailsClusterViewModel.observeCluster(streamCluster) detailsClusterViewModel.observeCluster(
it,
streamCluster
)
} }
override fun onAppClick(app: App) { override fun onAppClick(app: App) {
openDetailsFragment(app.packageName, app) openDetailsFragment(app.packageName, app)
} }
override fun onAppLongClick(app: App) { override fun onAppLongClick(app: App) {}
}
}) })
detailsClusterViewModel.liveData.observe(viewLifecycleOwner) { detailsClusterViewModel.liveData.observe(viewLifecycleOwner) { state ->
when (it) { when (state) {
is ViewState.Empty -> {
}
is ViewState.Loading -> {
}
is ViewState.Error -> {
}
is ViewState.Status -> {
}
is ViewState.Success<*> -> { is ViewState.Success<*> -> {
carouselController.setData(it.data as StreamBundle) val stash = state.getDataAs<AppStreamStash>()
streamBundle = stash[it]
carouselController.setData(streamBundle)
}
else -> {
} }
} }
} }
binding.epoxyRecyclerStream.setController(carouselController) binding.epoxyRecyclerStream.setController(carouselController)
detailsClusterViewModel.getStreamBundle(it) detailsClusterViewModel.getStreamBundle(it)
} }
} }
@@ -925,7 +919,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private fun inflateAppPermission(app: App) { private fun inflateAppPermission(app: App) {
binding.layoutDetailsPermissions.apply { binding.layoutDetailsPermissions.apply {
headerPermission.addClickListener { headerPermission.addClickListener {
if (app.permissions.size > 0) { if (app.permissions.isNotEmpty()) {
findNavController().navigate( findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToPermissionBottomSheet( AppDetailsFragmentDirections.actionAppDetailsFragmentToPermissionBottomSheet(
app app
@@ -956,6 +950,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
private fun addOrUpdateReview(app: App, review: Review, isBeta: Boolean = false) { private fun addOrUpdateReview(app: App, review: Review, isBeta: Boolean = false) {
viewModel.postAppReview(requireContext(), app.packageName, review, isBeta) viewModel.postAppReview(app.packageName, review, isBeta)
} }
} }

View File

@@ -18,6 +18,7 @@ import com.aurora.store.data.room.favourites.FavouriteDao
import com.aurora.store.util.DownloadWorkerUtil import com.aurora.store.util.DownloadWorkerUtil
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -30,6 +31,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AppDetailsViewModel @Inject constructor( class AppDetailsViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val downloadWorkerUtil: DownloadWorkerUtil, private val downloadWorkerUtil: DownloadWorkerUtil,
private val authProvider: AuthProvider, private val authProvider: AuthProvider,
private val favouriteDao: FavouriteDao private val favouriteDao: FavouriteDao
@@ -40,18 +42,27 @@ class AppDetailsViewModel @Inject constructor(
private val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/api/search/" private val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/api/search/"
private val exodusApiKey = "Token bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae" private val exodusApiKey = "Token bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae"
private val httpClient = HttpClient.getPreferredClient(context)
private val appDetailsHelper = AppDetailsHelper(authProvider.authData).using(httpClient)
private val reviewsHelper = ReviewsHelper(authProvider.authData).using(httpClient)
private val appStash: MutableMap<String, App> = mutableMapOf()
private val _app = MutableSharedFlow<App>() private val _app = MutableSharedFlow<App>()
val app = _app.asSharedFlow() val app = _app.asSharedFlow()
private val reviewsStash = mutableMapOf<String, List<Review>>()
private val _reviews = MutableSharedFlow<List<Review>>() private val _reviews = MutableSharedFlow<List<Review>>()
val reviews = _reviews.asSharedFlow() val reviews = _reviews.asSharedFlow()
private val userReviewStash = mutableMapOf<String, Review?>()
private val _userReview = MutableSharedFlow<Review>() private val _userReview = MutableSharedFlow<Review>()
val userReview = _userReview.asSharedFlow() val userReview = _userReview.asSharedFlow()
private val _report = MutableSharedFlow<Report?>() private val exodusReportStash = mutableMapOf<String, Report?>()
val report = _report.asSharedFlow() private val _exodusReport = MutableSharedFlow<Report?>()
val exodusReport = _exodusReport.asSharedFlow()
private val testProgramStatusStash = mutableMapOf<String, TestingProgramStatus?>()
private val _testingProgramStatus = MutableSharedFlow<TestingProgramStatus?>() private val _testingProgramStatus = MutableSharedFlow<TestingProgramStatus?>()
val testingProgramStatus = _testingProgramStatus.asSharedFlow() val testingProgramStatus = _testingProgramStatus.asSharedFlow()
@@ -60,16 +71,16 @@ class AppDetailsViewModel @Inject constructor(
val downloadsList get() = downloadWorkerUtil.downloadsList val downloadsList get() = downloadWorkerUtil.downloadsList
fun fetchAppDetails(context: Context, packageName: String) { fun fetchAppDetails(packageName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
isFavourite(packageName) checkFavourite(packageName)
_app.emit( val app: App = appStash.getOrPut(packageName) {
AppDetailsHelper(authProvider.authData) appDetailsHelper.getAppByPackageName(packageName)
.using(HttpClient.getPreferredClient(context)) }
.getAppByPackageName(packageName)
) _app.emit(app)
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch app details", exception) Log.e(TAG, "Failed to fetch app details", exception)
_app.emit(App("")) _app.emit(App(""))
@@ -77,14 +88,14 @@ class AppDetailsViewModel @Inject constructor(
} }
} }
fun fetchAppReviews(context: Context, packageName: String) { fun fetchAppReviews(packageName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
_reviews.emit( val reviews = reviewsStash.getOrPut(packageName) {
ReviewsHelper(authProvider.authData) reviewsHelper.getReviewSummary(packageName)
.using(HttpClient.getPreferredClient(context)) }
.getReviewSummary(packageName)
) _reviews.emit(reviews)
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch app reviews", exception) Log.e(TAG, "Failed to fetch app reviews", exception)
_reviews.emit(emptyList()) _reviews.emit(emptyList())
@@ -92,63 +103,60 @@ class AppDetailsViewModel @Inject constructor(
} }
} }
fun postAppReview(context: Context, packageName: String, review: Review, isBeta: Boolean) { fun fetchAppReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
_userReview.emit( val exodusReport = exodusReportStash.getOrPut(packageName) {
ReviewsHelper(authProvider.authData) getLatestExodusReport(packageName)
.using(HttpClient.getPreferredClient(context)) }
.addOrEditReview(
packageName,
review.title,
review.comment,
review.rating,
isBeta
)!!
)
} catch (exception: Exception) {
Log.e(TAG, "Failed to post review", exception)
_userReview.emit(Review())
}
}
}
_exodusReport.emit(exodusReport)
fun fetchAppReport(context: Context, packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val headers: MutableMap<String, String> = mutableMapOf()
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
headers["Authorization"] = exodusApiKey
val url = exodusBaseUrl + packageName
val playResponse = HttpClient.getPreferredClient(context).get(url, headers)
_report.emit(parseResponse(String(playResponse.responseBytes), packageName)[0])
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch privacy report", exception) Log.e(TAG, "Failed to fetch privacy report", exception)
_report.emit(null) exodusReportStash[packageName] = null
_exodusReport.emit(null)
} }
} }
} }
fun fetchTestingProgramStatus(context: Context, packageName: String, subscribe: Boolean) { fun fetchTestingProgramStatus(packageName: String, subscribe: Boolean) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
_testingProgramStatus.emit( val testingProgramStatus = testProgramStatusStash.getOrPut(packageName) {
AppDetailsHelper(authProvider.authData).testingProgram( appDetailsHelper.testingProgram(packageName, subscribe)
packageName, }
subscribe
) _testingProgramStatus.emit(testingProgramStatus)
)
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch testing program status", exception) Log.e(TAG, "Failed to fetch testing program status", exception)
testProgramStatusStash[packageName] = null
_testingProgramStatus.emit(null) _testingProgramStatus.emit(null)
} }
} }
} }
fun postAppReview(packageName: String, review: Review, isBeta: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
val userReview = userReviewStash.getOrPut(packageName) {
reviewsHelper.addOrEditReview(
packageName,
review.title,
review.comment,
review.rating,
isBeta
)
}
if (userReview != null) {
_userReview.emit(userReview)
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to post review", exception)
}
}
}
fun download(app: App) { fun download(app: App) {
viewModelScope.launch { downloadWorkerUtil.enqueueApp(app) } viewModelScope.launch { downloadWorkerUtil.enqueueApp(app) }
} }
@@ -157,12 +165,6 @@ class AppDetailsViewModel @Inject constructor(
viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) } viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) }
} }
private fun isFavourite(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_favourite.value = favouriteDao.isFavourite(packageName)
}
}
fun toggleFavourite(app: App) { fun toggleFavourite(app: App) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (favourite.value) { if (favourite.value) {
@@ -183,9 +185,31 @@ class AppDetailsViewModel @Inject constructor(
} }
} }
private fun parseResponse(response: String, packageName: String): List<Report> { private fun checkFavourite(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_favourite.value = favouriteDao.isFavourite(packageName)
}
}
private fun getLatestExodusReport(packageName: String): Report? {
val headers: MutableMap<String, String> = mutableMapOf()
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
headers["Authorization"] = exodusApiKey
val url = exodusBaseUrl + packageName
val playResponse = httpClient.get(url, headers)
val report = parseExodusResponse(String(playResponse.responseBytes), packageName)
.firstOrNull()
return report
}
private fun parseExodusResponse(response: String, packageName: String): List<Report> {
try { try {
val gson = GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC) val gson = GsonBuilder()
.excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC)
.create() .create()
val jsonObject = JSONObject(response) val jsonObject = JSONObject(response)
@@ -193,9 +217,10 @@ class AppDetailsViewModel @Inject constructor(
val exodusReport: ExodusReport = gson.fromJson( val exodusReport: ExodusReport = gson.fromJson(
exodusObject.toString(), ExodusReport::class.java exodusObject.toString(), ExodusReport::class.java
) )
return exodusReport.reports return exodusReport.reports
} catch (e: Exception) { } catch (e: Exception) {
throw Exception("No reports found") return emptyList()
} }
} }
} }

View File

@@ -19,7 +19,6 @@
package com.aurora.store.viewmodel.details package com.aurora.store.viewmodel.details
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -29,6 +28,7 @@ import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.StreamHelper import com.aurora.gplayapi.helpers.StreamHelper
import com.aurora.gplayapi.helpers.contracts.StreamContract import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.store.AppStreamStash
import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState
import com.aurora.store.data.network.HttpClient import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
@@ -41,18 +41,18 @@ import kotlinx.coroutines.supervisorScope
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class DetailsClusterViewModel @Inject constructor( class DetailsClusterViewModel @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val authProvider: AuthProvider authProvider: AuthProvider
) : ViewModel() { ) : ViewModel() {
private var appDetailsHelper = AppDetailsHelper(authProvider.authData) private var appDetailsHelper = AppDetailsHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context)) .using(HttpClient.getPreferredClient(context))
private var streamHelper = StreamHelper(authProvider.authData) private var streamHelper = StreamHelper(authProvider.authData)
val liveData: MutableLiveData<ViewState> = MutableLiveData() val liveData: MutableLiveData<ViewState> = MutableLiveData()
var streamBundle: StreamBundle = StreamBundle() private val stash: AppStreamStash = mutableMapOf()
lateinit var type: StreamContract.Type lateinit var type: StreamContract.Type
lateinit var category: StreamContract.Category lateinit var category: StreamContract.Category
@@ -60,9 +60,22 @@ class DetailsClusterViewModel @Inject constructor(
fun getStreamBundle(streamUrl: String) { fun getStreamBundle(streamUrl: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { supervisorScope {
val bundle = targetBundle(streamUrl)
if (bundle.streamClusters.isNotEmpty()) {
liveData.postValue(ViewState.Success(stash))
}
try { try {
streamBundle = appDetailsHelper.getDetailsStream(streamUrl) if (!bundle.hasCluster() || bundle.hasNext()) {
liveData.postValue(ViewState.Success(streamBundle)) val newBundle = appDetailsHelper.getDetailsStream(streamUrl)
bundle.apply {
streamClusters.putAll(newBundle.streamClusters)
streamNextPageUrl = newBundle.streamNextPageUrl
}
liveData.postValue(ViewState.Success(stash))
}
} catch (e: Exception) { } catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message)) liveData.postValue(ViewState.Error(e.message))
} }
@@ -70,15 +83,15 @@ class DetailsClusterViewModel @Inject constructor(
} }
} }
fun observeCluster(streamCluster: StreamCluster) { fun observeCluster(url: String, streamCluster: StreamCluster) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { supervisorScope {
try { try {
if (streamCluster.hasNext()) { if (streamCluster.hasNext()) {
val newCluster = val newCluster =
streamHelper.getNextStreamCluster(streamCluster.clusterNextPageUrl) streamHelper.getNextStreamCluster(streamCluster.clusterNextPageUrl)
updateCluster(newCluster) updateCluster(url, streamCluster.id, newCluster)
liveData.postValue(ViewState.Success(streamBundle)) liveData.postValue(ViewState.Success(stash))
} else { } else {
Log.i("End of cluster") Log.i("End of cluster")
streamCluster.clusterNextPageUrl = String() streamCluster.clusterNextPageUrl = String()
@@ -90,10 +103,22 @@ class DetailsClusterViewModel @Inject constructor(
} }
} }
private fun updateCluster(newCluster: StreamCluster) { private fun updateCluster(
streamBundle.streamClusters[newCluster.id]?.apply { url: String,
clusterID: Int,
newCluster: StreamCluster
) {
targetBundle(url).streamClusters[clusterID]?.apply {
clusterAppList.addAll(newCluster.clusterAppList) clusterAppList.addAll(newCluster.clusterAppList)
clusterNextPageUrl = newCluster.clusterNextPageUrl clusterNextPageUrl = newCluster.clusterNextPageUrl
} }
} }
private fun targetBundle(url: String): StreamBundle {
val streamBundle = stash.getOrPut(url) {
StreamBundle()
}
return streamBundle
}
} }

View File

@@ -1,12 +1,15 @@
package com.aurora.store.viewmodel.details package com.aurora.store.viewmodel.details
import android.content.Context
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
@@ -15,21 +18,30 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class DetailsMoreViewModel @Inject constructor( class DetailsMoreViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val authProvider: AuthProvider private val authProvider: AuthProvider
) : ViewModel() { ) : ViewModel() {
private val TAG = DetailsMoreViewModel::class.java.simpleName private val TAG = DetailsMoreViewModel::class.java.simpleName
private val appDetailsHelper = AppDetailsHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
private val dependantAppsStash = mutableMapOf<String, List<App>>()
private val _dependentApps = MutableSharedFlow<List<App>>() private val _dependentApps = MutableSharedFlow<List<App>>()
val dependentApps = _dependentApps.asSharedFlow() val dependentApps = _dependentApps.asSharedFlow()
fun fetchDependentApps(app: App) { fun fetchDependentApps(app: App) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
_dependentApps.emit(AppDetailsHelper(authProvider.authData) val dependantApps = dependantAppsStash.getOrPut(app.packageName) {
.getAppByPackageName(app.dependencies.dependentPackages)) appDetailsHelper.getAppByPackageName(app.dependencies.dependentPackages)
}
_dependentApps.emit(dependantApps)
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch dependencies", exception) Log.e(TAG, "Failed to fetch dependencies", exception)
dependantAppsStash[app.packageName] = emptyList()
_dependentApps.emit(emptyList()) _dependentApps.emit(emptyList())
} }
} }