I have a Fragment that I would Like to have twice in the App, but with slight changes. Is there a way to use some kind of Abstract Class? I already tried to find a solution my self, but I cant find a way to get the Viewmodel
from the Activity using delegated properties as Android Studio is saying the can't be abstract. Another problem that I'm facing is the Arguments I'm passing to the Fragment.
(To clarify: The Property that should change is the Viewmodel; I'd like to have another kind of Viewmodel for the second Fragment. Also, the Viewmodel I use in the Code below and the other Viewmodel both inherit from the same class, so switching the Type shouldn't be that big of a problem)
Here's the Code of the Fragment:
package net.informatikag.thomapp.viewables.fragments.ThomsLine.main
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.android.volley.*
import com.android.volley.toolbox.JsonArrayRequest
import com.android.volley.toolbox.Volley
import com.google.android.material.snackbar.Snackbar
import net.informatikag.thomapp.MainActivity
import net.informatikag.thomapp.R
import net.informatikag.thomapp.databinding.ThomslineMainFragmentBinding
import net.informatikag.thomapp.utils.handlers.WordpressRecyclerAdapter
import net.informatikag.thomapp.utils.ArticleListSpacingDecoration
import net.informatikag.thomapp.utils.models.ArticleClickHandler
import net.informatikag.thomapp.utils.models.data.WordpressArticle
import net.informatikag.thomapp.utils.models.data.WordpressPage
import net.informatikag.thomapp.utils.models.view.ThomsLineViewModel
import java.util.*
import kotlin.collections.ArrayList
/**
* Pulls a list of articles from the JSON API of the Wordpress instance of the ThomsLine student newspaper.
* The articles are dynamically loaded with a RecyclerView.
*/
class ThomsLineFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener, ArticleClickHandler {
private var _binding: ThomslineMainFragmentBinding? = null // Verweis zum Layout
private val viewModel: ThomsLineViewModel by activityViewModels() // Das Viewmodel in dem die wichtigen Daten des Fragments gespeichert werden
private lateinit var recyclerAdapter: WordpressRecyclerAdapter // Hier werden die Artikel angezeigt
private lateinit var swipeRefreshLayout: SwipeRefreshLayout // wird benutz um die Artikel neu zu laden
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
/**
* Will be executed when the fragment is opened
*/
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate Layout
_binding = ThomslineMainFragmentBinding.inflate(inflater, container, false)
val root: View = binding.root
//Instantiate Variables
recyclerAdapter = WordpressRecyclerAdapter(this, viewModel)
//Add Observer to articles to update Recyclerview
viewModel.articles.observe(viewLifecycleOwner, Observer {
swipeRefreshLayout.isRefreshing = false
})
//region Init SwipeRefresh Layout
swipeRefreshLayout = root.findViewById(R.id.thomsline_swipe_container)
swipeRefreshLayout.setOnRefreshListener(this)
swipeRefreshLayout.setColorSchemeResources(
R.color.primaryColor,
R.color.secondaryColor
)
if(viewModel.isEmpty()) {
swipeRefreshLayout.post {
// Display Refresh Indicator
swipeRefreshLayout.isRefreshing = true
// Load First Article Page
loadArticles(0, true)
}
}
//endregion
//region Init Recycler View
_binding?.thomslineRecyclerView?.apply {
layoutManager = LinearLayoutManager([email protected])
addItemDecoration(ArticleListSpacingDecoration())
adapter = recyclerAdapter
}
//endregion
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/**
* Called when the SwipeRefresh Layout is triggerd
*/
override fun onRefresh() {
loadArticles(0, true)
}
/**
* Loads all Article pages until "page" and removes all cached pages after it
*/
fun loadArticles(page:Int, reloadAll: Boolean){
// Remove all cached pages after the given one
if(page == 0) {
viewModel.removeArticlePagesFromIndex(1, recyclerAdapter)
viewModel.lastPage = -1
}
// Create a new Request Queue
val requestQueue = Volley.newRequestQueue(this.context)
// Add requests to load the Pages to the requestQueue
if(reloadAll)
for (i in 0 until page 1) {
reloadPage(i, requestQueue)
}
else reloadPage(page)
}
// Reload a page without a given Request Queue
fun reloadPage(id:Int){
reloadPage(id, Volley.newRequestQueue(this.context))
}
// Reload a Page while adding the Requests to a given Request Queue
fun reloadPage(id: Int, requestQueue:RequestQueue) {
Log.d("ThomsLine", "Requesting Data for page $id")
// Start the Request
requestQueue.add(JsonArrayRequest(viewModel.BASE_URL MainActivity.WORDPRESS_BASE_URL_LITE "&&page=${id 1}",
{ response ->
Log.d("ThomsLine", "Got Data for page $id")
// A Variable to load the Articles to
val data = ArrayList<WordpressArticle>()
// Load the Articles from the JSON
for (j in 0 until response.length()) data.add(WordpressArticle(response.getJSONObject(j), true, viewModel.BASE_URL))
// Update the RecyclerView
viewModel.setArticlePage(id, WordpressPage(data.toTypedArray()), recyclerAdapter)
},
{ volleyError ->
Log.d("ThomsLine", "Request Error while loading Data for page $id")
// Check if the Error is caused because loading a non Existing Page
if (volleyError.networkResponse?.statusCode == 400){
// Update the Last Page Variable
viewModel.lastPage = if(id-1<viewModel.lastPage) viewModel.lastPage else id-1
recyclerAdapter.notifyItemChanged(recyclerAdapter.itemCount-1)
Log.d("ThomsLine", "Page does not exist (last page: ${viewModel.lastPage})")
} else {
Log.d("ThomsLine", "Request failed: ${volleyError.message.toString()}")
// Display a Snackbar, stating the Error
Snackbar.make(requireActivity().findViewById(R.id.app_bar_main), WordpressArticle.getVolleyError(volleyError, requireActivity()), Snackbar.LENGTH_LONG).show()
}
//recyclerAdapter.notifyItemChanged(id)
}
))
}
/**
* Called when a Article is clicked
*/
override fun onItemClick(wordpressArticle: WordpressArticle) {
val action = ThomsLineFragmentDirections.actionNavThomslineToNavThomslineArticleView(wordpressArticle.id)
findNavController().navigate(action)
}
}
CodePudding user response:
You don't assign the ViewModel
in your abstract class (which is what you're doing by using a delegate, by activityViewModels()
). Just make it an abstract property in your abstract class, and then your concrete Fragment subclasses will be required to assign a value to it (using by activityViewModels()
if you like):
abstract class BaseFragment : Fragment() {
...
abstract val viewModel: BaseViewModel
...
}
class RealFragment : BaseFragment() {
...
// you can specify a subtype of BaseViewModel in your implementation
override val viewModel: SomeViewModel by activityViewModels()
...
}