0

Been trying to get this to work for the last couple days and would really appreciate some help.

What I want is simple: I have a queue of elements, and I want to display the next element in that queue to the user.

For now, let's pretend there's just one type of content: text

And therefore, there's only two states the queue can be in

  • it can be empty
  • it can have a text element

Since the idea is to later support more content types, I want my "QueueFragment" to have a ViewPager with which I can exchange one view for the other.

That's ALL the queue fragment is

  • a view pager holding a content view fragment
  • a floating delete button used to dequeue

So there's only three user stories:

  • user opens app
    • fragment gets displayed
    • fragment displays current item or the no content view if there is no current item
  • app is already running but user is on a different fragment
    • user switches to fragment
    • fragment displays current item or the no content view if there is no current item
  • fragment is already being displayed
    • user hits delete button
    • fragment dequeues
    • fragment displays new current item or the no content view if there is no current item

My question is simple:

HOW do I get hold of a fragment in order to update it?

Here's my current attempt:

Since we're only supporting text, I can get away with a single content view layout:

<?xml version="1.0" encoding="utf-8"?>
<android.widget.LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:textAlignment="center"
        android:gravity="center"
        tools:text="PLACEHOLDER" />
</android.widget.LinearLayout>

And we create two instances for that...

One with a hard-coded string showing up when the queue is empty

class NoContentView : AbstractContentView() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val fragmentView = inflater.inflate(R.layout.fragment_process_component_text_view, container, false)
        fragmentView.content.text = Html.fromHtml(getString(R.string.process_queueIsEmpty))
        return fragmentView
    }

    override fun displayThought(thought: IThoughtWithContent) {
        throw IllegalStateException("this view cannot display any thoughts")
    }
}

and one with a settable content

class TextContentView:AbstractContentView(){
    @Volatile private var displayedData:String? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_process_component_text_view, container, false)
        displayedData?.let { view.content.text = it }
        return view
    }

    /**The idea here behind setting [displayedData] is as follows:
       if the view has already been created, [content] will not be null
                                       and we can set its text directly
       otherwise, the content will be set in [onCreateView]
    */
    override fun displayThought(thought: IThoughtWithContent) {
        displayedData = String(thought.data, Charsets.UTF_8)
        content?.text = displayedData
    }
}

where

import android.support.v4.app.Fragment    
abstract class AbstractContentView:Fragment(){
    abstract fun displayThought(thought:IThoughtWithContent)
}

and

interface IThoughtWithContent: IThought {
    val type : IContentType
    val data : ByteArray

    val timestampCreated:Date
}

Now I've been told that the FragmentStatePagerAdapter must at all times return a new instance instead of caching fragments for re-use.

So the current idea is to make the adapter aware of the data it's supposed to display and update the fragments as it returns them:

class ProcessFragmentStatePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
    companion object {
        private const val NOF_VIEWS_AVAILABLE = 2

        const val EMPTY = 0
        const val TEXT = 1
    }

    override fun getCount(): Int { return NOF_VIEWS_AVAILABLE }

    override fun getItemPosition(`object`: Any): Int {
        return PagerAdapter.POSITION_NONE
    }

    @Volatile var nextThought: IThoughtWithContent? = null

    override fun getItem(position: Int): Fragment {
        return when(position){
            EMPTY -> {
                nextThought = null
                NoContentView()
            }
            TEXT -> {
                val fragment = TextContentView()
                nextThought?.let { fragment.displayThought(it) }
                fragment
            }
            else -> throw IllegalArgumentException("unexpected fragment position: $position")
        }
    }
}

On to our queue fragment...

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <android.support.v4.view.ViewPager
            android:id="@+id/view_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/btn_delete"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:layout_margin="16dp"
        android:src="@drawable/ic_checkmark" />

</android.support.design.widget.CoordinatorLayout>

again, very simply layout.

And the fragment itself is very simple, too:

We set deleteButton.setOnClickListener {dequeue()}. dequeue removes the current element from the queue, and displays the next one (or at least that's what it's supposed to do):

private fun dequeue(thoughts: IThoughtSet? = null){
    val queue = thoughts ?: currentThoughts()
    queue.pop()
    displayNextThought(queue)
}

We also displayNextThought in

override fun onResume() {
    super.onResume()
    if(initialized)displayNextThought()
}

and

override fun setUserVisibleHint(isVisibleToUser: Boolean) {
    if(userVisibleHint && initialized) displayNextThought()
    super.setUserVisibleHint(isVisibleToUser)
}

displayNextThought calls either

private fun displayNoThought(){
    viewContainer.currentItem = VIEW_EMPTY
}

or

private fun displayTextThought(thought:Thought.ThoughtWithContent){
    val adapter = viewContainer.adapter as ProcessFragmentStatePagerAdapter
    adapter.nextThought = thought
    viewContainer.currentItem = VIEW_TEXT
}

depending on whether the queue is empty or not.

(Full fragment code at the bottom of this post.)

So what's happening with this setup? Let's walk though our three user stories

Case1: user starts the app

The queue is restored and the first item is displayed, and it does this correctly whether the queue is empty or not. Great.

Case2: user is in a different fragment and switches back and forth between fragments

If there are items in the queue, then at one point, the queue fragment will just display an empty text view.

If the queue is empty, it will always correctly display the NO CONTENT view.

Case3: user has the queue fragment open and is pressing the delete button

The fragment dequeues, but the view is not updated (still shows the element it has displayed when the user switched to the with the new item UNTIL the queue is empty, at which point the fragment correctly displays the NO CONTENT view.

So yeah, not great.

I'd appreciate any help I can get.

Full fragment code:

class ProcessFragment : Fragment() {
    companion object {
        private const val VIEW_EMPTY = ProcessFragmentStatePagerAdapter.EMPTY
        private const val VIEW_TEXT = ProcessFragmentStatePagerAdapter.TEXT
    }

    private lateinit var fragmentView:View
    private lateinit var viewContainer: ViewPager
    private lateinit var deleteButton: FloatingActionButton
    private var initialized = false

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        super.onCreateView(inflater, container, savedInstanceState)

        fragmentView = inflater.inflate(R.layout.fragment_process, container, false)
        viewContainer = fragmentView.view_container
        deleteButton = fragmentView.btn_delete

        setupViewPager(viewContainer)

        deleteButton.setOnClickListener {dequeue()}

        initialized = true

        return fragmentView
    }
    private fun setupViewPager(pager:ViewPager){
        pager.adapter = ProcessFragmentStatePagerAdapter(childFragmentManager)
    }

    override fun onResume() {
        super.onResume()
        if(initialized)displayNextThought()
    }


    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        if(userVisibleHint && initialized) displayNextThought()
        super.setUserVisibleHint(isVisibleToUser)
    }

    private fun currentThoughts():IThoughtSet{
        val um = DataConfig.getUserManager()
        val au = um.getActiveUser()
        return um.getThoughts(au)
    }

    private fun dequeue(thoughts: IThoughtSet? = null){
        val queue = thoughts ?: currentThoughts()
        queue.pop()
        displayNextThought(queue)
    }

    private fun displayNextThought(thoughts: IThoughtSet? = null){
        val queue = thoughts ?: currentThoughts()
        val peek = queue.peek()
        if(peek is Reply.OK && peek.result is IThoughtWithContent){
            when(peek.result.type){
                ContentType.Text -> displayTextThought(peek.result)
                else -> throw IllegalArgumentException("don't know how to handle content type ${peek.result.type}")
            }
        }
        else displayNoThought()
    }

    private fun displayNoThought(){
        viewContainer.currentItem = VIEW_EMPTY
    }

    private fun displayTextThought(thought:IThoughtWithContent){
        val adapter = viewContainer.adapter as ProcessFragmentStatePagerAdapter
        adapter.nextThought = thought
        viewContainer.currentItem = VIEW_TEXT
    }
}
User1291
  • 7,664
  • 8
  • 51
  • 108
  • 1
    This answer should help you: https://stackoverflow.com/a/36504458/4409409 – Daniel Nugent Feb 03 '19 at 17:44
  • 1
    @DanielNugent interestingly enough, it did, thank you. So it's less of a "we can't cache fragments" issue and more of a "THAT particular method cannot use cached fragments". THANK you. Are you going to make an anwer out of your comment? Otherwise I'll just delete the question tomorrow, but I still wanted you to know I appreciate your comment. – User1291 Feb 04 '19 at 20:27

0 Answers0