I have a RecyclerView which contains complex items, represented by a class containing a TextView widget, a Spinner widget & an EditText widget. (Think of a list of tasks, with task name, a spinner to select which stage of processing the task has reached, and a completion date for that stage of processing.)
The data for this list comes from a file, and changes to items in the list will be written back to the file. When the user comes back to the app later, the list should reflect the data as he saw it previously.
Currently, my app displays the list of items, except that I don't know how to set the initial value of the Spinner to the value read from the file. How can I set a different initial selection for each spinner in the list?
Currently, when the user makes a selection from the
Spinner, the list disappears and the selection does not appear as the user's choice. I saw from another post - Android: Spinner not showing the selected value - that I should callsetSelection()(inherited bySpinnerclass from AbsSpinner). But that did nothing. In the XML layout, my Spinner has:android:textColor="@color/colorPrimaryDark"so I don't think it's a problem of the text being there in a color that matches the background. See this List of Tasks. How can I get the user's selection to be retained?In
OnItemSelected(), how do I tell the activity to note the selected value so it can be saved back to file? The example code I've found online usually just uses a Toast to show that the selection was registered. I think I need to know which item in theRecyclerViewcontains this Spinner...
Here are the details...
activity_task.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBG"
tools:context="org.myorg.myapp.DetailActivity">
...
<android.support.v7.widget.RecyclerView
android:id="@+id/rvChapList"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/txtTaskLabel"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/imgBorder"
app:layout_constraintRight_toRightOf="parent"
/>
</android.support.constraint.ConstraintLayout>
subtask_detail.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="24dp"
android:background="@color/colorBG"
android:id="@+id/subtask_detail"
>
<android.support.constraint.Guideline
android:id="@+id/LGuideLine2"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintGuide_begin="40dp"
tools:layout_editor_absoluteY="0dp"
tools:layout_editor_absoluteX="40dp" />
<android.support.constraint.Guideline
android:id="@+id/RGuideLine2"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintGuide_begin="280dp"
tools:layout_editor_absoluteY="0dp"
tools:layout_editor_absoluteX="280dp" />
<TextView
android:id="@+id/txtChapNum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="6dp"
tools:text="150"
android:textAppearance="@style/TextAppearance.AppCompat"
android:textColor="@color/colorPrimaryDark"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/LGuideLine2"
/>
<Spinner
android:id="@+id/spnSteps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnerMode="dialog"
tools:text="Quality Check"
android:prompt="@string/step_prompt"
style="@android:style/Widget.Holo.Light.Spinner"
android:entries="@array/step_array"
android:textColor="@color/colorPrimaryDark"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@+id/LGuideLine2"
app:layout_constraintRight_toLeftOf="@+id/RGuideLine2"
/>
<EditText
android:id="@+id/edtDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="date"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
tools:text="12-30-2020"
android:textSize="14sp"
android:textColor="@color/colorPrimaryDark"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@+id/RGuideLine2"
app:layout_constraintRight_toRightOf="parent"
/>
</android.support.constraint.ConstraintLayout>
DetailActivity.java:
package org.myorg.myapp;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import java.util.ArrayList;
public class DetailActivity extends AppCompatActivity {
private RecyclerView _rv;
private LayoutInflater _li;
private SubtaskDetailAdapter _adapter;
private ArrayList _alEntries;
private ArrayList _alTaskEntries;
private String _sTaskName = null;
private static final String EXTRA_TASK = "EXTRA_TASK";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task);
Intent i = getIntent();
_sTaskName = i.getStringExtra(EXTRA_TASK);
TextView tvTaskName = (TextView) findViewById(R.id.txtTaskName2);
tvTaskName.setText(_sTaskName);
// create item detail array & populate it based on task item array
_alEntries = Globals.getArrayList();
_alTaskEntries = new ArrayList();
PopulateTaskDetailList();
_rv = (RecyclerView) findViewById(R.id.rvChapList);
_li = getLayoutInflater();
_rv.setLayoutManager(new LinearLayoutManager(this));
_adapter = new SubtaskDetailAdapter();
_rv.setAdapter(_adapter);
}
public void PopulateTaskDetailList() {
int iNumEntries = _alEntries.size();
String sSubtask = "";
TaskItem tiEntry = null;
DetailItem diEntry = null;
// extract subtasks for indicated task
for (int i = 0; i < iNumEntries; i++) {
tiEntry = (TaskItem) _alEntries.get(i);
// if this task entry has the indicated Task name, save it's data
if (tiEntry.get_TaskName().equals(_sTaskName)) {
diEntry = new DetailItem(tiEntry.get_Subtask(),
tiEntry.get_StepCompleted(),
tiEntry.get_DateCompleted());
_alTaskEntries.add(diEntry);
}
}
}
private class DetailItem {
private String _sSubTaskName = "";
private String _sStep = "";
private String _sDate = "";
private DetailItem(String sSubTaskName, String sStep, String sDate) {
_sSubTaskName = sSubTaskName;
_sStep = sStep;
_sDate = sDate;
}
private String get_Subtask() { return _sSubTaskName; }
public void set_Subtask(String sTaskName) { _sSubTaskName = sTaskName; }
private String get_Step() { return _sStep; }
public void set_Step(String sStep) { _sStep = sStep; }
private String get_Date() { return _sDate; }
public void set_Date(String sDate) { _sDate = sDate; }
}
private class SubtaskDetailAdapter extends RecyclerView.Adapter<SubtaskDetailAdapter.DetailViewHolder> {
/**
* Inflates (creates & fills) a new subtask_detail View, and then creates/returns a new
* DetailViewHolder object for that view.
* @param parent Unfortunately the docs currently don't explain this at all :(
* @param viewType Unfortunately the docs currently don't explain this at all :(
* @return
*/
@Override
public SubtaskDetailAdapter.DetailViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// "inflate" (create & fill) a new holder / container view based on the task_item
// layout, without "attaching" it to the parent view
View v = _li.inflate(R.layout.subtask_detail, parent, false);
// create an instance of DetailViewHolder based on this "inflated" "holder" view
return new SubtaskDetailAdapter.DetailViewHolder(v);
}
/**
* This method "Binds" or assigns Data (from _alTaskEntries) to each SubtaskDetail (ViewHolder).
* @param holder The SubtaskDetail instance at a given position in the list
* @param position The current position of the SubtaskDetail we are Binding to, based upon
* our (listOfData). So for the second ViewHolder we create, we'll bind data
* from the second Item in listOfData.
*/
@Override
public void onBindViewHolder(SubtaskDetailAdapter.DetailViewHolder holder, int position) {
// the ViewHolder data
DetailItem currentItem = (DetailItem) _alTaskEntries.get(position);
holder._tvSubtask.setText(currentItem.get_Subtask());
holder._etDate.setText(currentItem.get_Date());
holder._spSteps.setSelection(position, true);
}
/**
* This method helps our Adapter determine how many ViewHolders it needs to create,
* based on the size of the Dataset (List) it is working with.
* Returning 0 here would tell our Adapter not to make any Items.
*
* @return the size of the dataset to be represented in the RecyclerView
**/
@Override
public int getItemCount() { return _alTaskEntries.size(); }
/**
* A ViewHolder is a container for a set of Views we want to populate with Data
**/
class DetailViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
// view holders for views to bind in the layout
private TextView _tvSubtask;
private EditText _etDate;
private Spinner _spSteps;
private ViewGroup _vgContainer;
private DetailViewHolder(View itemView) {
super(itemView);
// use itemView with findViewByID, because we are looking for an ID in
// the SubtaskDetail view container we created / inflated above
_tvSubtask = (TextView) itemView.findViewById(R.id.txtChapNum);
_spSteps = (Spinner) itemView.findViewById(R.id.spnSteps);
_etDate = (EditText) itemView.findViewById(R.id.edtDate);
_vgContainer = (ViewGroup) itemView.findViewById(R.id.subtask_detail);
_spSteps.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapter, View v,
int position, long id) {
// On selecting a spinner item
String sStep = adapter.getItemAtPosition(position).toString();
_spSteps.setSelection(position);
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
// TODO Auto‐generated method stub
}
});
/* We can pass "this" as an Argument, because DetailViewHolder implements the
View.OnClickListener interface. */
_vgContainer.setOnClickListener(this);
}
@Override
public void onClick(View v) {
// currently unused
}
}
}
}
Thanks for any help you can offer as I try to learn this stuff!
NEW INFORMATION
I have found a solution for my question #3, though it feels like a kluge. There may be a better way, but since no one has enlightened me to it, I'm left with this.
My array of data to display in the RecyclerView is an array of DetailItem objects. I have added to the DetailItem class another instance variable to hold the Spinner view which will be associated with that subtask step.
Here's the updated DetailItem class definition:
private class DetailItem {
private String _sSubTaskName = "";
private String _sStep = "";
private Spinner _spSteps;
private String _sDate = "";
private DetailItem(String sSubTaskName, String sStep, String sDate) {
_sSubTaskName = sSubTaskName;
_sStep = sStep;
_sDate = sDate;
}
private String get_Subtask() { return _sSubTaskName; }
public void set_Subtask(String sTaskName) { _sSubTaskName = sTaskName; }
private String get_Step() { return _sStep; }
public void set_Step(String sStep) { _sStep = sStep; }
private Spinner get_Spin() { return _spSteps; }
public void set_Spin(Spinner spSteps) { _spSteps = spSteps; }
private String get_Date() { return _sDate; }
public void set_Date(String sDate) { _sDate = sDate; }
}
I modified the adapter to store with the task step the Spinner that displays / selects it. I also moved the code to set the Listener for the Spinner from the ViewHolder to the Adapter.
Here's the updated SubtaskDetailAdapter class definition with its expanded onBindViewHolder method:
private class SubtaskDetailAdapter extends RecyclerView.Adapter<SubtaskDetailAdapter.DetailViewHolder> {
/** no changes */
@Override
public SubtaskDetailAdapter.DetailViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// ...
}
@Override
public void onBindViewHolder(SubtaskDetailAdapter.DetailViewHolder holder, int position) {
// the ViewHolder data
DetailItem currentItem = (DetailItem) _alTaskEntries.get(position);
holder._tvSubtask.setText(currentItem.get_Subtask());
holder._etDate.setText(currentItem.get_Date());
// store the spinner in the DetailItem object
currentItem.set_Spin(holder._spSteps);
// store DetailItem object in the array
_alTaskEntries.set(position, currentItem);
// look for Spinner step matching this entry's step
String sStep = currentItem.get_Step();
int iSel = 0;
while (iSel < sSteps.length && !sSteps[iSel].equals(sStep))
iSel++;
// if matching step is found, set Spinner to show it
if (iSel < sSteps.length) holder._spSteps.setSelection(iSel, true);
// if matching step isn't found, show error message
else Toast.makeText(getApplicationContext(),
"Unrecognized Step: " + sStep,
Toast.LENGTH_SHORT).show();
// set listener for spinner selections
holder._spSteps.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapter, View v,
int position, long id) {
// get selected step
String sStep = adapter.getItemAtPosition(position).toString();
DetailItem currentItem = null;
int iNumDetails = _alTaskEntries.size();
int iDetail = 0;
// fast-forward to array entry for this adapter (spinner)
while (iDetail < iNumDetails) {
currentItem = (DetailItem) _alTaskEntries.get(iDetail);
if (currentItem.get_Spin().equals(adapter))
break;
else
iDetail++;
}
// if found, save it in the array of detail items
if ((iDetail < iNumDetails) && (currentItem != null)) {
currentItem.set_Step(sStep);
_alTaskEntries.set(iDetail, currentItem);
}
adapter.setSelection(position);
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
// TODO Auto-generated method stub
}
});
}
I removed the code in the ViewHolder class to set an OnClickListener on the container view, since I don't want to respond to taps on the RecyclerView item, but only on individual views within it. An empty OnClick is required, since the holder is declared to implement the OnClickListener interface (I don't know if this is required).
Here's the updated (simpler) ViewHolder class :
class DetailViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
// view holders for views to bind in the layout
private TextView _tvSubtask;
private EditText _etDate;
private Spinner _spSteps;
private DetailViewHolder(View itemView) {
super(itemView);
// use itemView with findViewByID, because we are looking for an ID in
// the SubtaskDetail view container we created / inflated above
_tvSubtask = (TextView) itemView.findViewById(R.id.txtChapNum);
_spSteps = (Spinner) itemView.findViewById(R.id.spnSteps);
_etDate = (EditText) itemView.findViewById(R.id.edtDate);
}
@Override
public void onClick(View v) {
// currently unused
}
}
My questions #1 & #2 are still unsolved, which makes my app fairly unusable at the moment... Anyone want credit for solving this???