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
}
}