Add refresh (#59)
* Add refresh to home fragment * Remove forgotten code * Remove unnecessary condition and fix HomeSection equality check * Make HomeFragment fragment view model again * Add order dependent check for home items equality * Fix loading state overwriting error state on home refresh * Revert to older swiperefreshlayout version * Fixing error and loading state Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
parent
98cb038c24
commit
d7a47b0a3e
13 changed files with 234 additions and 152 deletions
|
@ -59,6 +59,8 @@ dependencies {
|
|||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
implementation("androidx.appcompat:appcompat:1.3.1")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
|
||||
|
||||
// Material
|
||||
implementation("com.google.android.material:material:1.4.0")
|
||||
|
|
|
@ -10,7 +10,7 @@ import dev.jdtech.jellyfin.databinding.NextUpSectionBinding
|
|||
import dev.jdtech.jellyfin.databinding.ViewItemBinding
|
||||
import dev.jdtech.jellyfin.models.HomeSection
|
||||
import dev.jdtech.jellyfin.models.View
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
private const val ITEM_VIEW_TYPE_NEXT_UP = 0
|
||||
private const val ITEM_VIEW_TYPE_VIEW = 1
|
||||
|
@ -51,7 +51,8 @@ class ViewListAdapter(
|
|||
|
||||
companion object DiffCallback : DiffUtil.ItemCallback<HomeItem>() {
|
||||
override fun areItemsTheSame(oldItem: HomeItem, newItem: HomeItem): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
return oldItem.ids.size == newItem.ids.size
|
||||
&& oldItem.ids.mapIndexed { i, old -> old == newItem.ids[i] }.all { it }
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: HomeItem, newItem: HomeItem): Boolean {
|
||||
|
@ -106,12 +107,12 @@ class ViewListAdapter(
|
|||
|
||||
sealed class HomeItem {
|
||||
data class Section(val homeSection: HomeSection) : HomeItem() {
|
||||
override val id = homeSection.id
|
||||
override val ids = homeSection.items.map { it.id }
|
||||
}
|
||||
|
||||
data class ViewItem(val view: View) : HomeItem() {
|
||||
override val id = view.id
|
||||
override val ids = view.items?.map { it.id }.orEmpty()
|
||||
}
|
||||
|
||||
abstract val id: UUID
|
||||
abstract val ids: List<UUID>
|
||||
}
|
|
@ -7,8 +7,12 @@ import android.view.MenuInflater
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
|
@ -17,8 +21,15 @@ import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
|||
import dev.jdtech.jellyfin.adapters.ViewListAdapter
|
||||
import dev.jdtech.jellyfin.databinding.FragmentHomeBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.models.ContentType
|
||||
import dev.jdtech.jellyfin.models.ContentType.EPISODE
|
||||
import dev.jdtech.jellyfin.models.ContentType.MOVIE
|
||||
import dev.jdtech.jellyfin.models.ContentType.TVSHOW
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.utils.contentType
|
||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel.Loading
|
||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel.LoadingError
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -57,49 +68,79 @@ class HomeFragment : Fragment() {
|
|||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
binding.viewsRecyclerView.adapter = ViewListAdapter(ViewListAdapter.OnClickListener {
|
||||
navigateToLibraryFragment(it)
|
||||
}, ViewItemListAdapter.OnClickListener {
|
||||
|
||||
setupView()
|
||||
bindState()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.refreshData()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
binding.refreshLayout.setOnRefreshListener {
|
||||
viewModel.refreshData()
|
||||
}
|
||||
|
||||
binding.viewsRecyclerView.adapter = ViewListAdapter(
|
||||
onClickListener = ViewListAdapter.OnClickListener { navigateToLibraryFragment(it) },
|
||||
onItemClickListener = ViewItemListAdapter.OnClickListener {
|
||||
navigateToMediaInfoFragment(it)
|
||||
}, HomeEpisodeListAdapter.OnClickListener { item ->
|
||||
when (item.type) {
|
||||
"Episode" -> {
|
||||
navigateToEpisodeBottomSheetFragment(item)
|
||||
}
|
||||
"Movie" -> {
|
||||
navigateToMediaInfoFragment(item)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
||||
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
||||
})
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.viewsRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||
binding.viewsRecyclerView.visibility = View.VISIBLE
|
||||
},
|
||||
onNextUpClickListener = HomeEpisodeListAdapter.OnClickListener { item ->
|
||||
when (item.contentType()) {
|
||||
EPISODE -> navigateToEpisodeBottomSheetFragment(item)
|
||||
MOVIE -> navigateToMediaInfoFragment(item)
|
||||
else -> Toast.makeText(requireContext(), R.string.unknown_error, LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
})
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.loadData()
|
||||
}
|
||||
|
||||
private fun bindState() {
|
||||
viewModel.onStateUpdate(lifecycleScope) { state ->
|
||||
when (state) {
|
||||
is Loading -> bindLoading(state)
|
||||
is LoadingError -> bindError(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindError(state: LoadingError) {
|
||||
checkIfLoginRequired(state.message)
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
binding.viewsRecyclerView.isVisible = false
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
||||
ErrorDialogFragment(state.message).show(
|
||||
parentFragmentManager,
|
||||
"errordialog"
|
||||
)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindLoading(state: Loading) {
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
binding.viewsRecyclerView.isVisible = true
|
||||
|
||||
binding.loadingIndicator.visibility = when {
|
||||
state.inProgress && binding.refreshLayout.isRefreshing -> View.GONE
|
||||
state.inProgress -> View.VISIBLE
|
||||
else -> {
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToLibraryFragment(view: dev.jdtech.jellyfin.models.View) {
|
||||
|
@ -113,12 +154,12 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||
if (item.type == "Episode") {
|
||||
if (item.contentType() == EPISODE) {
|
||||
findNavController().navigate(
|
||||
HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment(
|
||||
item.seriesId!!,
|
||||
item.seriesName,
|
||||
"Series"
|
||||
TVSHOW.type
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
@ -126,7 +167,7 @@ class HomeFragment : Fragment() {
|
|||
HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment(
|
||||
item.id,
|
||||
item.name,
|
||||
item.type ?: "Unknown"
|
||||
item.type ?: ContentType.UNKNOWN.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package dev.jdtech.jellyfin.models
|
||||
|
||||
import dev.jdtech.jellyfin.models.CollectionType.Books
|
||||
import dev.jdtech.jellyfin.models.CollectionType.HomeVideos
|
||||
import dev.jdtech.jellyfin.models.CollectionType.LiveTv
|
||||
import dev.jdtech.jellyfin.models.CollectionType.Music
|
||||
import dev.jdtech.jellyfin.models.CollectionType.Playlists
|
||||
|
||||
enum class CollectionType (val type: String) {
|
||||
HomeVideos("homevideos"),
|
||||
Music("music"),
|
||||
Playlists("playlists"),
|
||||
Books("books"),
|
||||
LiveTv("livetv")
|
||||
}
|
||||
|
||||
fun unsupportedCollections() = listOf(
|
||||
HomeVideos, Music, Playlists, Books, LiveTv
|
||||
)
|
|
@ -3,5 +3,6 @@ package dev.jdtech.jellyfin.models
|
|||
enum class ContentType(val type: String) {
|
||||
MOVIE("Movie"),
|
||||
TVSHOW("Series"),
|
||||
EPISODE("Episode"),
|
||||
UNKNOWN("")
|
||||
}
|
|
@ -1,10 +1,8 @@
|
|||
package dev.jdtech.jellyfin.models
|
||||
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import java.util.*
|
||||
|
||||
data class HomeSection(
|
||||
val id: UUID,
|
||||
val name: String?,
|
||||
var items: List<BaseItemDto>? = null
|
||||
val name: String,
|
||||
var items: List<BaseItemDto>
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
package dev.jdtech.jellyfin.models
|
||||
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
data class View(
|
||||
val id: UUID,
|
||||
|
|
|
@ -48,7 +48,7 @@ internal class HomeFragment : BrowseSupportFragment() {
|
|||
setOnClickListener { navigateToSettingsFragment() }
|
||||
}
|
||||
|
||||
viewModel.views.observe(viewLifecycleOwner) { homeItems ->
|
||||
viewModel.views().observe(viewLifecycleOwner) { homeItems ->
|
||||
rowsAdapter.clear()
|
||||
homeItems.map { section -> rowsAdapter.add(section.toListRow()) }
|
||||
}
|
||||
|
|
|
@ -16,13 +16,11 @@ import dev.jdtech.jellyfin.models.DownloadMetadata
|
|||
import dev.jdtech.jellyfin.models.DownloadRequestItem
|
||||
import dev.jdtech.jellyfin.models.PlayerItem
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.UserItemDataDto
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: Fragment) {
|
||||
// Storage permission for downloads isn't necessary from Android 10 onwards
|
||||
|
@ -209,7 +207,7 @@ fun parseMetadataFile(metadataFile: List<String>) : DownloadMetadata {
|
|||
|
||||
suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context: Context) {
|
||||
val items = loadDownloadedEpisodes(context)
|
||||
items.forEach(){
|
||||
items.forEach{
|
||||
try {
|
||||
val localPlaybackProgress = it.metadata?.playbackPosition
|
||||
val localPlayedPercentage = it.metadata?.playedPercentage
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package dev.jdtech.jellyfin.utils
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
|
||||
fun View.toggleVisibility() {
|
||||
isVisible = !isVisible
|
||||
}
|
|
@ -22,6 +22,7 @@ fun BaseItemDto.toView(): View {
|
|||
fun BaseItemDto.contentType() = when (type) {
|
||||
"Movie" -> ContentType.MOVIE
|
||||
"Series" -> ContentType.TVSHOW
|
||||
"Episode" -> ContentType.EPISODE
|
||||
else -> ContentType.UNKNOWN
|
||||
}
|
||||
|
||||
|
@ -32,5 +33,6 @@ fun Fragment.checkIfLoginRequired(error: String) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
inline fun Context.toast(@StringRes text: Int, duration: Int = Toast.LENGTH_SHORT) =
|
||||
Toast.makeText(this, text, duration).show()
|
|
@ -1,6 +1,7 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
@ -8,108 +9,104 @@ import androidx.lifecycle.viewModelScope
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.adapters.HomeItem
|
||||
import dev.jdtech.jellyfin.adapters.HomeItem.Section
|
||||
import dev.jdtech.jellyfin.adapters.HomeItem.ViewItem
|
||||
import dev.jdtech.jellyfin.models.HomeSection
|
||||
import dev.jdtech.jellyfin.models.View
|
||||
import dev.jdtech.jellyfin.models.unsupportedCollections
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import dev.jdtech.jellyfin.utils.syncPlaybackProgress
|
||||
import dev.jdtech.jellyfin.utils.toView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
class HomeViewModel @Inject internal constructor(
|
||||
private val application: Application,
|
||||
private val jellyfinRepository: JellyfinRepository
|
||||
private val repository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val views = MutableLiveData<List<HomeItem>>()
|
||||
private val state = MutableSharedFlow<State>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
init {
|
||||
loadData(updateCapabilities = true)
|
||||
}
|
||||
|
||||
private val continueWatchingString = application.resources.getString(R.string.continue_watching)
|
||||
private val nextUpString = application.resources.getString(R.string.next_up)
|
||||
|
||||
private val _views = MutableLiveData<List<HomeItem>>()
|
||||
val views: LiveData<List<HomeItem>> = _views
|
||||
fun views(): LiveData<List<HomeItem>> = views
|
||||
|
||||
private val _items = MutableLiveData<List<BaseItemDto>>()
|
||||
val items: LiveData<List<BaseItemDto>> = _items
|
||||
|
||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
|
||||
init {
|
||||
loadData()
|
||||
fun onStateUpdate(
|
||||
scope: LifecycleCoroutineScope,
|
||||
collector: (State) -> Unit
|
||||
) {
|
||||
scope.launch { state.collect { collector(it) } }
|
||||
}
|
||||
|
||||
fun loadData() {
|
||||
_error.value = null
|
||||
_finishedLoading.value = false
|
||||
fun refreshData() = loadData(updateCapabilities = false)
|
||||
|
||||
private fun loadData(updateCapabilities: Boolean) {
|
||||
state.tryEmit(Loading(inProgress = true))
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
jellyfinRepository.postCapabilities()
|
||||
if (updateCapabilities) repository.postCapabilities()
|
||||
|
||||
val items = mutableListOf<HomeItem>()
|
||||
val updated = loadDynamicItems() + loadViews()
|
||||
views.postValue(updated)
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
|
||||
val resumeItems = jellyfinRepository.getResumeItems()
|
||||
val resumeSection =
|
||||
HomeSection(UUID.randomUUID(), continueWatchingString, resumeItems)
|
||||
|
||||
if (!resumeItems.isNullOrEmpty()) {
|
||||
items.add(HomeItem.Section(resumeSection))
|
||||
syncPlaybackProgress(repository, application)
|
||||
}
|
||||
|
||||
val nextUpItems = jellyfinRepository.getNextUp()
|
||||
val nextUpSection = HomeSection(UUID.randomUUID(), nextUpString, nextUpItems)
|
||||
|
||||
if (!nextUpItems.isNullOrEmpty()) {
|
||||
items.add(HomeItem.Section(nextUpSection))
|
||||
}
|
||||
}
|
||||
|
||||
val views: MutableList<View> = mutableListOf()
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
val userViews = jellyfinRepository.getUserViews()
|
||||
|
||||
for (view in userViews) {
|
||||
Timber.d("Collection type: ${view.collectionType}")
|
||||
if (view.collectionType == "homevideos" ||
|
||||
view.collectionType == "music" ||
|
||||
view.collectionType == "playlists" ||
|
||||
view.collectionType == "books" ||
|
||||
view.collectionType == "livetv"
|
||||
) continue
|
||||
val latestItems = jellyfinRepository.getLatestMedia(view.id)
|
||||
if (latestItems.isEmpty()) continue
|
||||
val v = view.toView()
|
||||
v.items = latestItems
|
||||
views.add(v)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
syncPlaybackProgress(jellyfinRepository, application)
|
||||
}
|
||||
|
||||
_views.value = items + views.map { HomeItem.ViewItem(it) }
|
||||
|
||||
|
||||
state.tryEmit(Loading(inProgress = false))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
state.tryEmit(LoadingError(e.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadDynamicItems() = withContext(Dispatchers.IO) {
|
||||
val resumeItems = repository.getResumeItems()
|
||||
val nextUpItems = repository.getNextUp()
|
||||
|
||||
val items = mutableListOf<HomeSection>()
|
||||
if (resumeItems.isNotEmpty()) {
|
||||
items.add(HomeSection(continueWatchingString, resumeItems))
|
||||
}
|
||||
|
||||
if (nextUpItems.isNotEmpty()) {
|
||||
items.add(HomeSection(nextUpString, nextUpItems))
|
||||
}
|
||||
|
||||
items.map { Section(it) }
|
||||
}
|
||||
|
||||
private suspend fun loadViews() = withContext(Dispatchers.IO) {
|
||||
repository
|
||||
.getUserViews()
|
||||
.filter { view -> unsupportedCollections().none { it.type == view.collectionType } }
|
||||
.map { view -> view to repository.getLatestMedia(view.id) }
|
||||
.filter { (_, latest) -> latest.isNotEmpty() }
|
||||
.map { (view, latest) -> view.toView().apply { items = latest } }
|
||||
.map { ViewItem(it) }
|
||||
}
|
||||
|
||||
sealed class State
|
||||
|
||||
data class LoadingError(val message: String) : State()
|
||||
data class Loading(val inProgress: Boolean) : State()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,20 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
>
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.HomeViewModel" />
|
||||
type="dev.jdtech.jellyfin.viewmodels.HomeViewModel"
|
||||
/>
|
||||
</data>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.HomeFragment">
|
||||
tools:context=".fragments.HomeFragment"
|
||||
>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_indicator"
|
||||
|
@ -23,11 +32,14 @@
|
|||
android:indeterminate="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<include
|
||||
android:id="@+id/error_layout"
|
||||
layout="@layout/error_panel" />
|
||||
layout="@layout/error_panel"
|
||||
/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/views_recycler_view"
|
||||
|
@ -40,9 +52,12 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:views="@{viewModel.views}"
|
||||
app:views="@{viewModel.views()}"
|
||||
tools:itemCount="4"
|
||||
tools:listitem="@layout/view_item" />
|
||||
tools:listitem="@layout/view_item"
|
||||
/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</layout>
|
||||
|
|
Loading…
Reference in a new issue