Add error handling to all fragments

This commit is contained in:
jarnedemeulemeester 2021-07-29 12:20:49 +02:00
parent 11793a423b
commit fe7775329a
No known key found for this signature in database
GPG key ID: 60884A0C1EBA43E5
14 changed files with 600 additions and 414 deletions

View file

@ -8,7 +8,9 @@ import android.view.ViewGroup
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
@ -36,6 +38,27 @@ class LibraryFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.viewModel = viewModel binding.viewModel = viewModel
val snackbar =
Snackbar.make(
binding.mainLayout,
getString(R.string.error_loading_data),
Snackbar.LENGTH_INDEFINITE
)
snackbar.setAction(getString(R.string.retry)) {
viewModel.loadItems(args.libraryId)
}
viewModel.error.observe(viewLifecycleOwner, { error ->
if (error) {
snackbar.show()
}
})
viewModel.finishedLoading.observe(viewLifecycleOwner, {
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
})
binding.itemsRecyclerView.adapter = binding.itemsRecyclerView.adapter =
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item -> ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(item) navigateToMediaInfoFragment(item)

View file

@ -7,7 +7,9 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.adapters.CollectionListAdapter import dev.jdtech.jellyfin.adapters.CollectionListAdapter
import dev.jdtech.jellyfin.databinding.FragmentMediaBinding import dev.jdtech.jellyfin.databinding.FragmentMediaBinding
import dev.jdtech.jellyfin.viewmodels.MediaViewModel import dev.jdtech.jellyfin.viewmodels.MediaViewModel
@ -26,6 +28,16 @@ class MediaFragment : Fragment() {
): View { ): View {
binding = FragmentMediaBinding.inflate(inflater, container, false) binding = FragmentMediaBinding.inflate(inflater, container, false)
val snackbar =
Snackbar.make(
binding.mainLayout,
getString(R.string.error_loading_data),
Snackbar.LENGTH_INDEFINITE
)
snackbar.setAction(getString(R.string.retry)) {
viewModel.loadData()
}
binding.lifecycleOwner = this binding.lifecycleOwner = this
binding.viewModel = viewModel binding.viewModel = viewModel
binding.viewsRecyclerView.adapter = binding.viewsRecyclerView.adapter =
@ -34,8 +46,12 @@ class MediaFragment : Fragment() {
}) })
viewModel.finishedLoading.observe(viewLifecycleOwner, { viewModel.finishedLoading.observe(viewLifecycleOwner, {
if (it) { binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
binding.loadingIndicator.visibility = View.GONE })
viewModel.error.observe(viewLifecycleOwner, { error ->
if (error) {
snackbar.show()
} }
}) })

View file

@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.adapters.PersonListAdapter import dev.jdtech.jellyfin.adapters.PersonListAdapter
@ -42,8 +43,24 @@ class MediaInfoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val snackbar =
Snackbar.make(
binding.mainLayout,
getString(R.string.error_loading_data),
Snackbar.LENGTH_INDEFINITE
)
snackbar.setAction(getString(R.string.retry)) {
viewModel.loadData(args.itemId, args.itemType)
}
binding.viewModel = viewModel binding.viewModel = viewModel
viewModel.error.observe(viewLifecycleOwner, { error ->
if (error) {
snackbar.show()
}
})
viewModel.item.observe(viewLifecycleOwner, { item -> viewModel.item.observe(viewLifecycleOwner, { item ->
if (item.originalTitle != item.name) { if (item.originalTitle != item.name) {
binding.originalTitle.visibility = View.VISIBLE binding.originalTitle.visibility = View.VISIBLE

View file

@ -8,7 +8,9 @@ import android.view.ViewGroup
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.adapters.EpisodeListAdapter import dev.jdtech.jellyfin.adapters.EpisodeListAdapter
import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding
import dev.jdtech.jellyfin.viewmodels.SeasonViewModel import dev.jdtech.jellyfin.viewmodels.SeasonViewModel
@ -34,6 +36,27 @@ class SeasonFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.viewModel = viewModel binding.viewModel = viewModel
val snackbar =
Snackbar.make(
binding.mainLayout,
getString(R.string.error_loading_data),
Snackbar.LENGTH_INDEFINITE
)
snackbar.setAction(getString(R.string.retry)) {
viewModel.loadEpisodes(args.seriesId, args.seasonId)
}
viewModel.error.observe(viewLifecycleOwner, { error ->
if (error) {
snackbar.show()
}
})
viewModel.finishedLoading.observe(viewLifecycleOwner, {
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
})
binding.episodesRecyclerView.adapter = binding.episodesRecyclerView.adapter =
EpisodeListAdapter(EpisodeListAdapter.OnClickListener { episode -> EpisodeListAdapter(EpisodeListAdapter.OnClickListener { episode ->
navigateToEpisodeBottomSheetFragment(episode) navigateToEpisodeBottomSheetFragment(episode)

View file

@ -10,6 +10,7 @@ import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.MediaSourceInfo
import timber.log.Timber
import java.text.DateFormat import java.text.DateFormat
import java.time.ZoneOffset import java.time.ZoneOffset
import java.util.* import java.util.*
@ -42,12 +43,17 @@ constructor(
fun loadEpisode(episodeId: UUID) { fun loadEpisode(episodeId: UUID) {
viewModelScope.launch { viewModelScope.launch {
_item.value = jellyfinRepository.getItem(episodeId) try {
_runTime.value = "${_item.value?.runTimeTicks?.div(600000000)} min" val item = jellyfinRepository.getItem(episodeId)
_dateString.value = getDateString(_item.value!!) _item.value = item
_runTime.value = "${item.runTimeTicks?.div(600000000)} min"
_dateString.value = getDateString(item)
_mediaSources.value = jellyfinRepository.getMediaSources(episodeId) _mediaSources.value = jellyfinRepository.getMediaSources(episodeId)
_played.value = _item.value?.userData?.played _played.value = item.userData?.played
_favorite.value = _item.value?.userData?.isFavorite _favorite.value = item.userData?.isFavorite
} catch (e: Exception) {
Timber.e(e)
}
} }
} }

View file

@ -85,12 +85,11 @@ constructor(
_views.value = items + views.map { HomeItem.ViewItem(it) } _views.value = items + views.map { HomeItem.ViewItem(it) }
_finishedLoading.value = true
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
_finishedLoading.value = true
_error.value = true _error.value = true
} }
_finishedLoading.value = true
} }
} }
} }

View file

@ -5,6 +5,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -19,9 +20,19 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
private val _finishedLoading = MutableLiveData<Boolean>() private val _finishedLoading = MutableLiveData<Boolean>()
val finishedLoading: LiveData<Boolean> = _finishedLoading val finishedLoading: LiveData<Boolean> = _finishedLoading
private val _error = MutableLiveData<Boolean>()
val error: LiveData<Boolean> = _error
fun loadItems(parentId: UUID) { fun loadItems(parentId: UUID) {
_error.value = false
_finishedLoading.value = false
viewModelScope.launch { viewModelScope.launch {
try {
_items.value = jellyfinRepository.getItems(parentId) _items.value = jellyfinRepository.getItems(parentId)
} catch (e: Exception) {
Timber.e(e)
_error.value = true
}
_finishedLoading.value = true _finishedLoading.value = true
} }
} }

View file

@ -13,6 +13,7 @@ import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.MediaSourceInfo
import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -62,8 +63,13 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
private val _favorite = MutableLiveData<Boolean>() private val _favorite = MutableLiveData<Boolean>()
val favorite: LiveData<Boolean> = _favorite val favorite: LiveData<Boolean> = _favorite
private val _error = MutableLiveData<Boolean>()
val error: LiveData<Boolean> = _error
fun loadData(itemId: UUID, itemType: String) { fun loadData(itemId: UUID, itemType: String) {
_error.value = false
viewModelScope.launch { viewModelScope.launch {
try {
_item.value = jellyfinRepository.getItem(itemId) _item.value = jellyfinRepository.getItem(itemId)
_actors.value = getActors(_item.value!!) _actors.value = getActors(_item.value!!)
_director.value = getDirector(_item.value!!) _director.value = getDirector(_item.value!!)
@ -82,6 +88,10 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
if (itemType == "Movie") { if (itemType == "Movie") {
_mediaSources.value = jellyfinRepository.getMediaSources(itemId) _mediaSources.value = jellyfinRepository.getMediaSources(itemId)
} }
} catch (e: Exception) {
Timber.e(e)
_error.value = true
}
} }
} }

View file

@ -5,6 +5,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -20,8 +21,18 @@ constructor(
private val _finishedLoading = MutableLiveData<Boolean>() private val _finishedLoading = MutableLiveData<Boolean>()
val finishedLoading: LiveData<Boolean> = _finishedLoading val finishedLoading: LiveData<Boolean> = _finishedLoading
private val _error = MutableLiveData<Boolean>()
val error: LiveData<Boolean> = _error
init { init {
loadData()
}
fun loadData() {
_finishedLoading.value = false
_error.value = false
viewModelScope.launch { viewModelScope.launch {
try {
val items = jellyfinRepository.getItems() val items = jellyfinRepository.getItems()
_collections.value = _collections.value =
items.filter { items.filter {
@ -30,6 +41,10 @@ constructor(
it.collectionType != "playlists" && it.collectionType != "playlists" &&
it.collectionType != "boxsets" it.collectionType != "boxsets"
} }
} catch (e: Exception) {
Timber.e(e)
_error.value = true
}
_finishedLoading.value = true _finishedLoading.value = true
} }
} }

View file

@ -9,6 +9,7 @@ import dev.jdtech.jellyfin.adapters.EpisodeItem
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFields
import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -20,9 +21,23 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
private val _episodes = MutableLiveData<List<EpisodeItem>>() private val _episodes = MutableLiveData<List<EpisodeItem>>()
val episodes: LiveData<List<EpisodeItem>> = _episodes val episodes: LiveData<List<EpisodeItem>> = _episodes
private val _finishedLoading = MutableLiveData<Boolean>()
val finishedLoading: LiveData<Boolean> = _finishedLoading
private val _error = MutableLiveData<Boolean>()
val error: LiveData<Boolean> = _error
fun loadEpisodes(seriesId: UUID, seasonId: UUID) { fun loadEpisodes(seriesId: UUID, seasonId: UUID) {
_error.value = false
_finishedLoading.value = false
viewModelScope.launch { viewModelScope.launch {
try {
_episodes.value = getEpisodes(seriesId, seasonId) _episodes.value = getEpisodes(seriesId, seasonId)
} catch (e: Exception) {
Timber.e(e)
_error.value = true
}
_finishedLoading.value = true
} }
} }

View file

@ -2,17 +2,35 @@
<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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<variable <variable
name="viewModel" name="viewModel"
type="dev.jdtech.jellyfin.viewmodels.LibraryViewModel" /> type="dev.jdtech.jellyfin.viewmodels.LibraryViewModel" />
</data> </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".fragments.LibraryFragment"> tools:context=".fragments.LibraryFragment">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:trackCornerRadius="10dp" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/items_recycler_view" android:id="@+id/items_recycler_view"
android:layout_width="0dp" android:layout_width="0dp"
@ -31,5 +49,7 @@
tools:listitem="@layout/base_item" /> tools:listitem="@layout/base_item" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>

View file

@ -10,6 +10,11 @@
type="dev.jdtech.jellyfin.viewmodels.MediaViewModel" /> type="dev.jdtech.jellyfin.viewmodels.MediaViewModel" />
</data> </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -45,4 +50,5 @@
tools:listitem="@layout/collection_item" /> tools:listitem="@layout/collection_item" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>

View file

@ -12,6 +12,11 @@
type="dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel" /> type="dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel" />
</data> </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -374,4 +379,6 @@
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>

View file

@ -12,10 +12,27 @@
</data> </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:trackCornerRadius="10dp" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/episodes_recycler_view" android:id="@+id/episodes_recycler_view"
android:layout_width="0dp" android:layout_width="0dp"
@ -30,5 +47,6 @@
tools:itemCount="4" tools:itemCount="4"
tools:listitem="@layout/episode_item" /> tools:listitem="@layout/episode_item" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>