Home > Software design >  Combine Kotlin code of 2 fragments into one class
Combine Kotlin code of 2 fragments into one class

Time:12-23

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()
    ...
}

  • Related