onCreateViewHolder is never called - android

I am trying to use recyclerView which is inside a fragment.This is fragment is nested inside the viewPager.
public class UniversityDetail extends Fragment {
RecyclerView universityDetailView;
//need to set Adapter
public static UniversityDetail newInstance(){
return new UniversityDetail();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view = inflater.inflate(R.layout.fragment_university_lsit,container,false);
universityDetailView = (RecyclerView)view;
setupViews();
return view;
}
private void setupViews(){
//set the adapter
UniversityDetailAdapter detailAdapter = new UniversityDetailAdapter(new ArrayList<UniversityDetails>());
universityDetailView.addItemDecoration(new RecyclerListDecorater(getActivity()));
universityDetailView.setAdapter(detailAdapter);
universityDetailView.setLayoutManager(new LinearLayoutManager(getActivity()));
universityDetailView.setHasFixedSize(false);
}
public RecyclerView getRecyclerView(){
return this.universityDetailView;
}
}
This is the fragment which i want to be inside the viewPager.It returns a recyclerView from onCreateView.
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/scroll"
android:paddingBottom="8dp"
android:paddingTop="?attr/actionBarSize"
android:scrollbars="vertical">
fragment_university_lsit.xml
recyclerView uses below adapter.
public class UniversityDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<UniversityDetails> universityDetails;
private static Map<String,String> admissionRecommendation;
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((DetailHolder)holder).bind(universityDetails.get(position));
}
#Override
public int getItemCount() {
return universityDetails.size();
}
public UniversityDetailAdapter(List<UniversityDetails> details){
this.universityDetails = details;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,int viewType){
Log.d("CreateUniveristyDetail", "onCreateViewHolder: detail called");
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.detail_fragment,parent,false);
return new DetailHolder(itemView);
}
}
DetailHolder is the class extends ReyclerView.ViewHolder and its implementation is irrelevant here.
The adapter list is upated from handler and notifyDataSetChanged()
private void setup(){
mHandler = new Handler(Looper.getMainLooper()){
#Override
public void handleMessage(Message message){
if(message.what == ConnectionHandler.DETAILMSG){
Log.d("detail handle message", "handleMessage: Called");
List<UniversityDetails> details = (List<UniversityDetails>)message.obj;
UniversityDetailAdapter adapter = (UniversityDetailAdapter)universityDetail.getRecyclerView().getAdapter();
adapter.addAll(details);
adapter.notifyDataSetChanged();
Log.d("size", "handleMessage: " + adapter.getItemCount());
}
}
};
}
Fragment is created in activity..
private void setupNavigation(){
ViewPagerAdapter pagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
universityDetail = UniversityDetail.newInstance();
pagerAdapter.addFragment(universityDetail);
mViewPager.setAdapter(pagerAdapter);
//setup the fragment transaction.
headerTab.setViewPager(mViewPager,0);
//no need to add to back stack
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.add(R.id.view_pager,universityDetail);
transaction.commit();
}
Implementation of DetailHolder
public class DetailHolder extends RecyclerView.ViewHolder{
CardView admissionGeneral;
public View root;
public DetailHolder (View itemView){
super(itemView);
root = itemView;
admissionGeneral = (CardView)root.findViewById(R.id.admission_general);
}
public void bind(UniversityDetails detail){
Log.d("detail bind", "bind: binding to recyclerView");
}
}
here the log of adapter.getItemCount(); return 3; thats means something is being added and onCreateViewHolder is supposed to be called but it never is.
The weird thing is i have implemented another recyclerAdapter is the sameProject and its working perfectly fine.

This problem would be easier to answer if you could show how the universityDetail was assigned from whatever class contains the Handler usage.
It's also not clear how message.obj is able to be cast to a List.
I believe you are using a different instance of the Fragment there than the one that you want to update.
So, you know this line is good
List<UniversityDetails> details = (List<UniversityDetails>)message.obj;
because you see 3 in the output when you do this
Log.d("size", "handleMessage: " + adapter.getItemCount());
Now, these lines could likely be the problem.
UniversityDetailAdapter adapter = (UniversityDetailAdapter)universityDetail.getRecyclerView().getAdapter();
adapter.addAll(details);
adapter.notifyDataSetChanged();
Firstly, there is no addAll method to RecyclerView.Adapter. So I'm not sure how that compiled. Anyways...
Try to keep cross-class references to a minimum. Here, we expose only the method to add the details rather than the entire RecyclerView.
Then, for RecyclerViews, you should notify only the data that was inserted rather than all of it using notifyItemRangeInserted
public class UniversityDetailFragment extends Fragment {
private RecyclerView universityDetailView;
private UniversityDetailAdapter detailAdapter;
private List<UniversityDetails> details = new ArrayList<UniversityDetails>();
...
public void setupViews() {
detailAdapter = new UniversityDetailAdapter(details);
// etc...
}
public void addDetails(List<UniversityDetails> details) {
int curSize = detailAdapter.getItemCount();
this.details.addAll(details);
detailAdapter.notifyItemRangeInserted(curSize, details.size());
}
}
And then just use the instance of your Fragment
if(message.what == ConnectionHandler.DETAILMSG){
Log.d("detail handle message", "handleMessage: Called");
List<UniversityDetails> details = (List<UniversityDetails>) message.obj;
universityDetail.addDetails(details);
Another recommendation would be to use the correct generic type for the holder class to avoid unnecessary casts.
public class UniversityDetailAdapter extends RecyclerView.Adapter<DetailHolder>

Related

Android: Recyclerview in Bottom Navigation Bar Fragment populated with SQLite will not update

I have a BottomNavigationBar with 3 fragments. In the first fragment, I try to put SQLite data into a recyclerview. It works fine except for the fact that I need to switch between the Navigation Bar items in order to see the refreshed recyclerview. When I use a handler with postDelayed however, it does show the refreshed recyclerview if I set around 1 sec of delay. 0.2 secs wont work already.
Even though this is still very generic: is there any best practice for this? It seems to me that I need to use AsyncTask which has been -however- deprecated.
Thanks!
Simon
HomeFragment
public class HomeFragment extends Fragment {
private HomeViewModel homeViewModel;
private Context context;
private CardView cardview;
private LinearLayout.LayoutParams layoutparams;
private TextView textview;
private RelativeLayout relativeLayout;
private myDbAdapter helper;
RecyclerView myView;
public View onCreateView(#NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
homeViewModel =
new ViewModelProvider(this).get(HomeViewModel.class);
View root = inflater.inflate(R.layout.fragment_home, container, false);
helper = new myDbAdapter(getContext());
myView = (RecyclerView) root.findViewById(R.id.recyclerview_home);
RecyclerViewAdapter3 adapter = new RecyclerViewAdapter3(new ArrayList<String>(Arrays.asList(helper.classes())));
myView.setHasFixedSize(true);
myView.setAdapter(adapter);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
llm.setOrientation(LinearLayoutManager.VERTICAL);
myView.setLayoutManager(llm);
homeViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(#Nullable String s) {
textView.setText(s);
}
});
return root;
}
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
public void refresh(View v){
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
myView = (RecyclerView) v.findViewById(R.id.recyclerview_home);
helper = new myDbAdapter(v.getContext());
ArrayList<String> classes = new ArrayList(Arrays.asList(helper.classes()));
ArrayList<String> subClasses = new ArrayList(Arrays.asList(helper.subClasses()));
RecyclerViewAdapter3 adapter = new RecyclerViewAdapter3(classes);
myView.setHasFixedSize(true);
myView.setAdapter(adapter);
LinearLayoutManager llm = new LinearLayoutManager(v.getContext());
llm.setOrientation(LinearLayoutManager.VERTICAL);
myView.setLayoutManager(llm);
}
}, 1000); //time in millis
}
}
RecyclerViewAdapter3
public class RecyclerViewAdapter3 extends RecyclerView.Adapter<RecyclerViewAdapter3.MyViewHolder> {
public ArrayList<String> classArrayList;
public ArrayList<String> subClassArrayList;
myDbAdapter helper;
public RecyclerViewAdapter3(ArrayList<String> classArrayList){
this.classArrayList = classArrayList;
}
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View listItem = LayoutInflater.from(parent.getContext()).inflate(R.layout.cardview, parent, false);
return new MyViewHolder(listItem);
}
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
holder.class.setText(classArrayList.get(position));
holder.delete.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
helper = new myDbAdapter(v.getContext());
helper.delete(classArrayList.get(position));
HomeFragment homeFragment = new HomeFragment();
homeFragment.refresh(v.getRootView());
}
});
holder.selectButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
}});}
#Override
public int getItemCount() {
return classArrayList.size();
}
public static class MyViewHolder extends RecyclerView.ViewHolder {
private TextView class;
private Button selectButton;
private ImageView delete;
public MyViewHolder(View itemView) {
super(itemView);
class = (TextView)itemView.findViewById(R.id.name);
selectButton = (Button) itemView.findViewById(R.id.selectButton);
delete = (ImageView) itemView.findViewById(R.id.delete);
}
}
}
Thanks for posting your code :)
There are a fair few things that can go wrong in your code as it is right now, and I can't really pinpoint what causes it to work when you use postDelay. I'm going to list a few, which you can look into:
From your onClick() inside your ViewHolder
HomeFragment homeFragment = new HomeFragment();
homeFragment.refresh(v.getRootView());
You should really not instantiate your fragments like this. You can instead pass a callback from your fragment to your adapter (eg.: View.OnClickListener)
You keep re-instantiating your adapter and your helper needlessly. You should create your adapter only once, set it as your recycler view adapter, and save it in a member variable.
Proposed solution
I see that you're already using ViewModel, so you're on a great path for a less error-prone screen, so I suggest that you move your db query-ing logic to your view model. If you're using raw SQLite (instead of Room), you can extend AndroidViewModel, so you'll have access to a context right away. And as you do with your homeViewModel.getText(), you should expose the classes array as live data, observe it, and submit the new list to your adapter.
For submitting your list to your adapter I suggest using ListAdapter, this will provide you a submitList method for submitting the list in the fragment, and inside the adapter, you will have a getItem(int position) method, which you can query inside the onBindViewHolder method.
Inside your fragment, it'll look something like this:
ClassAdapter adapter = null;
View onCreateView(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
View root = inflater.inflate(R.layout.fragment_home, container, false);
adapter = new ClassAdapter(
new ClassDeleteCallback() {
#Override
void onClassDeleted(Class class) {
// inside view model we can modify db state, than refresh live data
viewModel.deleteClass(class);
}
},
new ClassSelectedCallback() {
// follows same pattern of above
}
);
RecyclerView rv = root.findViewById(R.id.my_rv);
rv.setAdapter(adapter);
rv.setLayoutManager(new LinearLayoutManager(getContext());
homeViewModel.getClasses().observe(getViewLifecycleOwner(), new Observer<List<Class>>() {
#Override
public void onChanged(#Nullable List<Class> classes) {
adapter.submitList(classes);
}
});
homeViewModel.refreshClasses();
return root;
}
I can highly recommend for you to study this project a bit, because it covers lot of the basics which can lead to a much stabler app: Sunflower sample app
I think you should read a bit more about the architecture components, and then go through some code-labs and stuff, and have another go with this screen starting from square one, because it will be easier than fixing the current state :)
I hope this was helpful, and not too discouraging!

How to do you implement a recyclerview onClick in a Fragment?

My question is similar to this one.
Best approach to communicate between Fragment/Activity and RecyclerView.Adapter?
I know how to implement an onClickListener, an Interface and an OnItemSelected method in the corresponding activity when the user clicks on a button in a Fragment. I have a fragment with an array list. By clicking on each item in the array list in the fragment, an ExoPlayer will open in a new activity or a fragment. As I understand it, onItemClickListener doesn't work with Recyclerview. How do I set the onClick method to the items in the list? Also, do I set onClick outside of RecyclerView? This is my RecyclerView adapter class. Should the interface take additional parameters? Thank you in advance.
public class StepsAdapter extends RecyclerView.Adapter {
private static final String TAG = StepsAdapter.class.getSimpleName();
private ArrayList<Steps> stepsList = new ArrayList<Steps>();
private StepsAdapter.StepsAdapterOnClickHandler mClickHandler;
/**
* The interface that receives onClick messages.
*/
public interface StepsAdapterOnClickHandler {
void onClick(Steps stepClick);
}
/**
* Creates a StepsAdapter.
*
* #param clickHandler The on-click handler for this adapter. This single handler is called
* when an item is clicked.
*/
public StepsAdapter(StepsAdapterOnClickHandler clickHandler,ArrayList<Steps> stepsList) {
mClickHandler = clickHandler;
this.stepsList = stepsList;
}
/**
* Cache of the children views for a steps list item.
*/
public class StepsAdapterViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
#BindView(R.id.step_short_desc)
public TextView stepShortDescription;
#BindView(R.id.step_description)
public TextView stepDescription;
public StepsAdapterViewHolder(View view) {
super(view);
ButterKnife.bind(this, view);
view.setOnClickListener(this);
}
/**
* This gets called by the child views during a click.
*
* #param v The View that was clicked
*/
#Override
public void onClick(View v) {
int adapterPosition = getAdapterPosition();
Steps stepClick = stepsList.get(adapterPosition);
mClickHandler.onClick(stepClick);
}
}
#Override
public StepsAdapter.StepsAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
Context context = viewGroup.getContext();
int layoutIdForListItem = R.layout.steps_list_item;
LayoutInflater inflater = LayoutInflater.from(context);
boolean shouldAttachToParentImmediately = false;
View view = inflater.inflate(layoutIdForListItem, viewGroup, shouldAttachToParentImmediately);
return new StepsAdapter.StepsAdapterViewHolder(view);
}
#Override
public void onBindViewHolder(StepsAdapter.StepsAdapterViewHolder holder, int position) {
//Binding data
final Steps stepsView = stepsList.get(position);
holder.stepShortDescription.setText(stepsView.getStepShortDescription());
holder.stepDescription.setText(stepsView.getStepDescription());
}
#Override
public int getItemCount() {
return stepsList.size();
}
public void setStepsList(ArrayList<Steps> mStepsList) {
this.stepsList = mStepsList;
notifyDataSetChanged();
}
}
The corresponding fragment. Is the click method implemented correctly?
public class StepsListFragment extends Fragment implements StepsAdapter.StepsAdapterOnClickListener {
// Tag for logging
private final String TAG = StepsListFragment.class.getSimpleName();
#BindView(R.id.recyclerview_steps)
RecyclerView mRecyclerView;
ArrayList<Steps> stepsArrayList;
Recipes recipes;
// Final Strings to store state information about the list of steps and list index
public static final String STEPS_LIST_INDEX = "list_index";
// Define a new interface OnStepsClickListener that triggers a callback in the host activity
OnStepClickListener mCallback;
// OnStepsClickListener interface, calls a method in the host activity named onStepSelected
public interface OnStepClickListener {
void onClick(Steps stepClick);
}
// Override onAttach to make sure that the container activity has implemented the callback
#Override
public void onAttach(Context context) {
super.onAttach(context);
// // This makes sure that the host activity has implemented the callback interface
// // If not, it throws an exception
try {
mCallback = (OnStepClickListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString()
+ " must implement OnStepSelectedListener");
}
}
/**
* Mandatory empty constructor for the fragment manager to instantiate the fragment
*/
public StepsListFragment() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// //Inflate the Steps fragment layout
View rootView = inflater.inflate(R.layout.fragment_steps, container, false);
// // Bind the views
ButterKnife.bind(this, rootView);
Bundle bundle = this.getArguments();
if (bundle != null) {
recipes = getArguments().getParcelable("Recipes");
stepsArrayList = new ArrayList<>();
stepsArrayList = recipes.getRecipeSteps();
}
if (savedInstanceState != null) {
//Restore the fragment's state here
stepsArrayList = savedInstanceState.getParcelableArrayList(STEPS_LIST_INDEX);
}
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getContext());
mRecyclerView.setLayoutManager(mLayoutManager);
Log.i("listSteps", stepsArrayList.size() + "");
StepsAdapter stepsAdapter = new StepsAdapter(this, stepsArrayList);
mRecyclerView.setAdapter(stepsAdapter);
// Return the root view
return rootView;
}
public void onClick(Steps stepClick){
mCallback.onClick(stepClick);
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveIn`enter code here`stanceState(outState);
//Save the fragment's state here
outState.putParcelableArrayList(STEPS_LIST_INDEX, stepsArrayList);
super.onSaveInstanceState(outState);
} }
The best way to implement click function on each item of recyclerview is initialise onClickListener when the view is populated inside in the recyclerview viewholder. Then in the onClick method, use a custom interface/listener to catch the click activity in your parent fragment/activity.
eg: create a custom interface like this;
public interface RecyclerviewOnClickListener{
void recyclerviewClick(int position);
}
Now implement this interface in your parent activity/fragment containing the recyclerview. Suppose your fragment name is ChatFragment. Then,
public class ChatFragment extends Fragment implements RecyclerviewOnClickListener{
.
.
}
This will implement the function onClick(int position) in your fragment. In your adapter constructor, you should create a field for the RecyclerviewOnClickListener. Suppose your adapter name is ChatAdapter, then
Adapter Constructor.
public ChatAdapter(RecyclerviewClickListener listener, .....<other params>){
this.listener = listener;
}
In your fragment, you can initialise your adapter like the following
ChatAdapter adapter = new ChatAdapter(this, <any additional params>);
You should pass the same instance of 'listener' to your viewholder also, and initialise the listener there also
Now in your recyclerview ViewHolder, you can set view.setOnClickListener(new OnClickListener{
this.listener.recyclerviewClick(getAdapterPosition())
});
The getAdapterPosition() function returns the position of the click in the recyclerview, which will get a callback in the recyclerviewClick() function in your fragment.
About the number of parameter you are passing, you can use as much as you want, but for a click function in recyclerview, the ideal way is to use only one param which the position. Now you can modify the contents in the list that you are passing from your fragment to adapter and call notifyDataSetChanged() which will update the recyclerview. Hope it's clear.
Try this inside OnBindviewHolder
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//code here
}

Android - RecyclerView multiplies itself every time device is rotated

I have a landscape configuration for one of my app's activities. This activity contains a fragment and this fragment contains one textview and one recyclerview. Everytime when i switch in between portrait and landscape modes, the recyclerview leaves the view of itself like however it was before i turned the device. It might be a little difficult to understand what i try to ask here, so i recorded a gif for that.
https://giphy.com/gifs/3Wv7NAtT8ezP1SQhDu
This is my activity
public class RecipeStepsActivity extends AppCompatActivity {
static Recipe recipe;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recipe_steps);
if (StepDetailActivity.SDA_TAG.equals(StepDetailActivity.NEGATIVE))
recipe = getIntent().getParcelableExtra("recipe");
Bundle b = new Bundle();
b.putParcelable("recipe", recipe);
ActionBar ab = getSupportActionBar();
if (ab != null)
ab.setTitle(recipe.getName());
RecipeStepsFragment recipeStepsFragment = new RecipeStepsFragment();
recipeStepsFragment.setArguments(b);
FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().add(R.id.frame_layout_steps, recipeStepsFragment).commit();
}
}
This is my fragment
public class RecipeStepsFragment extends Fragment {
#BindView(R.id.recipe_steps_rv)
RecyclerView recyclerView;
#BindView(R.id.ingredients_tv)
TextView tv_ingredients;
List<Step> steps;
public RecipeStepsFragment(){}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_recipe_steps, container, false);
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
List<Ingredients> ingredients;
Recipe recipe = getArguments().getParcelable("recipe");
steps = recipe.getSteps();
initRecyclerView();
ingredients = recipe.getIngredients();
String ingredientsAppended = "INGREDIENTS" + "\n\n";
if (ingredients == null){
ingredientsAppended = "Not Available";
} else {
for (int a = 0 ; a < ingredients.size() ; a++) {
Ingredients i = ingredients.get(a);
ingredientsAppended += String.valueOf(i.getQuantity()) + " " +
i.getMeasure() + " " +
i.getIngredient();
if (a+1 != ingredients.size()){
ingredientsAppended += '\n';
}
}
}
tv_ingredients.setText(ingredientsAppended);
if(savedInstanceState != null){
recyclerView.scrollToPosition(savedInstanceState.getInt("position"));
}
}
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
outState.putInt("position", recyclerView.getVerticalScrollbarPosition());
super.onSaveInstanceState(outState);
}
private void initRecyclerView(){
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL));
RecipeStepsRecyclerAdapter recipeStepsRecyclerAdapter =
new RecipeStepsRecyclerAdapter(steps, new RecipeStepsRecyclerAdapter.ClickListener() {
#Override
public void onItemClick(int clickedItemPosition) {
Intent intentToStepDetail = new Intent(getActivity(), StepDetailActivity.class);
Step step = steps.get(clickedItemPosition);
intentToStepDetail.putExtra("step", step);
startActivity(intentToStepDetail);
}
}, getContext());
recyclerView.setAdapter(recipeStepsRecyclerAdapter);
recipeStepsRecyclerAdapter.notifyDataSetChanged();
}
}
This is my adapter
public class RecipeStepsRecyclerAdapter extends RecyclerView.Adapter<RecipeStepsRecyclerAdapter.ViewHolder> {
private List<Step> stepList;
private LayoutInflater mInflater;
final private ClickListener clickListener;
public RecipeStepsRecyclerAdapter(List<Step> stepList, ClickListener clickListener, Context context){
this.stepList = stepList;
this.clickListener = clickListener;
mInflater = LayoutInflater.from(context);
}
#NonNull
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = mInflater.inflate(R.layout.recipe_steps_recyclerview_adapter, parent, false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
Step step = stepList.get(position);
String stepContent = step.getShortDescription();
holder.listingNumber.setText(String.valueOf(position+1));
holder.stepContent.setText(stepContent);
}
#Override
public int getItemCount() {
return stepList.size();
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
TextView listingNumber;
TextView stepContent;
private ViewHolder(View itemView) {
super(itemView);
listingNumber = itemView.findViewById(R.id.list_number_tv);
stepContent = itemView.findViewById(R.id.step_content_tv);
itemView.setOnClickListener(this);
}
#Override
public void onClick(View view) {
int clickedPosition = getAdapterPosition();
clickListener.onItemClick(clickedPosition);
}
}
public interface ClickListener{
void onItemClick(int clickedItemPosition);
}
}
Layout files are as I already explained above. I guess nothing special to post here.
Thanks in advance.
The problem is that you add a new fragment to the Activity every time it is created. Here's the end of your onCreate(...):
RecipeStepsFragment recipeStepsFragment = new RecipeStepsFragment();
recipeStepsFragment.setArguments(b);
FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().add(R.id.frame_layout_steps, recipeStepsFragment).commit();
The FragmentManager keeps a reference to the fragment you add to it even if the host Activity is destroyed. Thus, you keep adding new instances of the RecipeStepsFragment eventually overlaying each other and producing the seen behavior.
Don't worry, the fix is pretty simple: use replace(...) instead of add(...):
fm.beginTransaction().replace(R.id.frame_layout_steps, recipeStepsFragment).commit();
P.S.: Note, however, that replacing the current fragment with a new one every time the host activity is destroyed is not a good idea. You should check either if the savedInstanceState is null (which indicates that it's a non-recreated Activity) or if the given fragment is already added (define a tag when adding the fragment and try finding it in onCreate(...) before replacing the old one).
Every time you change the rotation of the screen your activity is destroyed and recreated. That is the reason why your list is populating with new items each time when you switch between your orientation.
So it's important you only instantiate your objects once, and not keep recreating them each time your app is recreated. Which then adds them to your list.
You can use onSaveInstanceState and onRestoreInstanceState.
If you want your app to allow orientation change, apply logic so your list don't get new items each time when activity is created.
Put configChanges attribute in your activity in menifest file
<activity android:name=".VideoActivity" android:configChanges="keyboardHidden|orientation|screenSize"/>

Can't save fragment state in Android while configuration is changed

This type of question is asked many times in SO, I have tried all of them, but couldn't achieve what I actually want. In my app I have Fragment, inside that fragment I have a recycler view. recyclerview would be manipulated by the data which I have got from API.
What I want that when rotation is changed, the app doesn't call the API again. To achieve this I know I have to put the data in onSaveInstanceState, If I want to save a complex object then the class must be implements Parcelable. I have done everything that is recommended, but couldn't achieve what I want.
I am sharing my code here, I have created a simplified example.
Code for SimpleFragment.java
public class SimpleFragment extends Fragment {
RecyclerView recyclerView;
ArrayList<TestModel> testModelList = new ArrayList<TestModel>() ;
SimpleAdapter simpleAdapter;
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.simple_fragment, container, false);
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(mLayoutManager);
simpleAdapter = new SimpleAdapter(getActivity());
recyclerView.setAdapter(simpleAdapter);
if(savedInstanceState != null){
//if this fragment starts after a rotation or configuration change, load the existing model from a parcelable
testModelList = savedInstanceState.getParcelableArrayList("key");
// I have done necessary debug , after rotation , it comes here ,
// but suddenly savedInstanceState becomes null and getTestModelList() get called
}else {
//if this fragment starts for the first time, load the list of model
getTestModelList();
}
simpleAdapter.setModel(testModelList);
return rootView;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("key",testModelList);
}
public void getTestModelList(){
for (int i=0;i<10;i++){
testModelList.add(new TestModel(i,"test"+i));
}
Toast.makeText(getActivity(),"Called",Toast.LENGTH_SHORT).show();
}
}
layout file for SimpleFragment: simple_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler"
android:background="#android:color/black"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
Here is code for my data model TestModel.java
public class TestModel implements Parcelable {
String text;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public TestModel(int number, String text) {
this.text = text;
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.text);
}
protected TestModel(Parcel in) {
this.text = in.readString();
}
public static final Parcelable.Creator<TestModel> CREATOR
= new Parcelable.Creator<TestModel>() {
public TestModel createFromParcel(Parcel in) {
return new TestModel(in);
}
public TestModel[] newArray(int size) {
return new TestModel[size];
}
};
}
My adapter class: SimpleAdapter.java
public class SimpleAdapter extends RecyclerView.Adapter<SimpleAdapter.ViewHolder> {
Context context;
ArrayList<TestModel> testModels;
public SimpleAdapter(Context context) {
this.context = context;
}
public void setModel(ArrayList<TestModel> model){
this.testModels = model;
notifyDataSetChanged();
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.single_row, viewGroup, false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.list_item.setText(testModels.get(position).getText());
}
#Override
public int getItemCount() {
return testModels.size();
}
public class ViewHolder extends RecyclerView.ViewHolder{
TextView list_item;
public ViewHolder(View itemView) {
super(itemView);
list_item = (TextView) itemView.findViewById(R.id.list_item);
}
}
}
Code for single row layout for adapter single_row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:id="#+id/list_item"
android:textColor="#android:color/white"/>
</LinearLayout>
These are what I have done, and if change the orientation getTestModelList() method in my SimpleFragment is called again, which I want to avoid; in real app scenario this method will do some API call. I want to save the state of ArrayList<TestModel> testModelList.
you need to save state of activity and check it before configuration
follow this code
public class MainActivity extends AppCompatActivity{
private HomeFragment homeFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = getSupportFragmentManager();
if (savedInstanceState == null) {
homeFragment = new HomeFragment();
((fm.beginTransaction()).replace(R.id.mainframe, homeFragment)).commit();
}
}}}
You can save Fragment instance like this in Fragment onCreate method for orientation change to save instance :
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set Instance true
this.setRetainInstance(true);
}
Android recreates your activity whenever the orientation changes. The reason behind this is you may need to change your layout but you can still achieve that with onConfigurationChanged() of the Activity or the Fragment I suggest you to add
android:configChanges="orientation|keyboardHidden|screenSize"
in your manifest file (refer to this and this). This will stop recreation of Activity on Confifuration changes.

ViewPager fragments are all referencing the same RecyclerView and/or Adapter

I have a Fragment that contains a RecyclerView to display events for a given day. I am using a ViewPager to separate the Fragments into multiple days; A Fragment for Saturday's events and a Fragment for Sunday's events.
However, it appears that both Fragments are referencing the same RecyclerView and/or Adapter, as it is only the last tab (in this case, Sunday) whose events are shown.
In my specific case, Saturday has two events, and Sunday has no events. Both Fragments have empty RecyclerViews. To confirm my theory that it was caused by the last tab, I switched the date. This caused both RecyclerViews to have two events (the ones from Saturday).
Here is the relevant code for the individual Fragments:
public class EventListFragment extends Fragment{
private EventAdapter mEventAdapter;
private static final String DATE_ARG = "eventDate";
public static EventListFragment newInstance(LocalDate date){
EventListFragment eventListFragment = new EventListFragment();
Bundle args = new Bundle();
args.putSerializable(DATE_ARG, date);
eventListFragment.setArguments(args);
return eventListFragment;
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_event_list, container, false);
// Setup recyclerview
RecyclerView eventRecyclerView = (RecyclerView) view.findViewById(R.id.event_recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
eventRecyclerView.setLayoutManager(layoutManager);
// Get date
LocalDate eventDate = (LocalDate) getArguments().getSerializable(DATE_ARG);
// Set adapter
mEventAdapter = new EventAdapter(getActivity(), getEvents(eventDate));
eventRecyclerView.setAdapter(mEventAdapter);
return view;
}
}
getEvents() is just a private function to return events for a given date. I have used the debugger as well as unit tests to verify that it works properly. The debugger shows that it pulls the proper list for each Fragment, but as I explained they are not displayed properly.
Here is the relevant code for the parent Fragment:
public class EventFragment extends Fragment {
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_event, container, false);
// Get and set up viewpager
final ViewPager viewPager = (ViewPager) view.findViewById(R.id.event_view_pager);
EventFragmentAdapter eventFragmentAdapter = new EventFragmentAdapter(getFragmentManager(), getEventDates());
viewPager.setAdapter(eventFragmentAdapter);
// Get and set up tablayout
final TabLayout tabLayout = (TabLayout) view.findViewById(R.id.event_tabs);
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
tabLayout.post(new Runnable() {
#Override
public void run() {
tabLayout.setupWithViewPager(viewPager);
}
});
return view;
}
}
Similar to the last one, getEventDates() just pulls the dates that events are taking place. For testing purposes at the moment, I am hard coding a returned list of dates as we don't have our database set up yet. I pulled this method out because I want the app to be able to function again in 2016, which may have different dates:
private List<LocalDate> getEventDates(){
List<LocalDate> eventDates = new ArrayList<>();
eventDates.add(new LocalDate(2015, 10, 17));
eventDates.add(new LocalDate(2015, 10, 18));
return eventDates;
}
The last bit of relevant code is for the FragmentStatePagerAdapter I am using for my ViewPager:
public class EventFragmentAdapter extends FragmentStatePagerAdapter {
private List<LocalDate> mEventDates;
public EventFragmentAdapter(FragmentManager fragmentManager, List<LocalDate> eventDates){
super(fragmentManager);
this.mEventDates = eventDates;
}
#Override
public Fragment getItem(int i) {
return EventListFragment.newInstance(mEventDates.get(i));
}
#Override
public int getCount() {
return mEventDates.size();
}
#Override
public CharSequence getPageTitle(int position) {
return mEventDates.get(position).dayOfWeek().getAsText();
}
}
Any ideas why both lists are always the same, and are based on the last tab in the ViewPager? I assume that somehow they are referencing the same RecyclerView or the same RecyclerViewAdapter, but I don't have any static fields for those so I am not sure how it is happening.
A long hunt and an anti-climactic solution(as with most difficult bugs). Also a bit unfair since the bug isn't in the code posted above, I had to hunt down your git project to figure it out. The bug is in EventAdapter:
public class EventAdapter extends RecyclerView.Adapter<EventAdapter.ViewHolder> {
private static final List<Event> mEvents;
private final Context mContext;
public EventAdapter(Context context, List<Event> events){
this.mContext = context;
mEvents = events;
}
...
}
mEvents is static!... so it's shared across all instances of mEvents. This explains the bug perfectly since updates to it last will set the values for all EventAdapters.
It looks like you made mEvents static so that you could access it within your ViewHolders. Instead you can just store the individual Event within the ViewHolder and drop the dangerous static modifier. On the flip-side, hooray for Open Source Projects!
I've seen your code and I totally agree with Travor - you're using a static member, that is replaced every time you create a new Adapter (and so it gets just the last page data). I've modified your project a little bit, to make it work properly. Take a look at it, hope it can be useful.
EventFragment: replace getFragmentManager with getChildFragmentManager since you need EventListFragment to be handle by EventFragment fragment manager and not by the activity fragment manager
public class EventFragment extends Fragment {
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_event, container, false);
// Get and set up viewpager
final ViewPager viewPager = (ViewPager) view.findViewById(R.id.event_view_pager);
EventFragmentAdapter eventFragmentAdapter = new EventFragmentAdapter(getChildFragmentManager(), getEventDates());
viewPager.setAdapter(eventFragmentAdapter);
// Get and set up tablayout
final TabLayout tabLayout = (TabLayout) view.findViewById(R.id.event_tabs);
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
tabLayout.post(new Runnable() {
#Override
public void run() {
tabLayout.setupWithViewPager(viewPager);
}
});
return view;
}
/**
* Retrieves the event dates for the hackathon so that the proper events can be displayed.
* #return
*/
private List<LocalDate> getEventDates(){
List<LocalDate> eventDates = new ArrayList<>();
eventDates.add(new LocalDate(2015, 10, 17));
eventDates.add(new LocalDate(2015, 10, 18));
return eventDates;
}
}
EventListFragment - I've modified the query, since the sqlite query doesn't work with my locale (italian)
public class EventListFragment extends Fragment{
private EventAdapter mEventAdapter;
private static final String TAG = EventListFragment.class.getSimpleName();
private static final String DATE_ARG = "eventDate";
public static EventListFragment newInstance(LocalDate date){
EventListFragment eventListFragment = new EventListFragment();
Bundle args = new Bundle();
args.putSerializable(DATE_ARG, date);
eventListFragment.setArguments(args);
return eventListFragment;
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_event_list, container, false);
// Setup recyclerview
RecyclerView eventRecyclerView = (RecyclerView) view.findViewById(R.id.event_recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
eventRecyclerView.setLayoutManager(layoutManager);
// Get date
LocalDate eventDate = (LocalDate) getArguments().getSerializable(DATE_ARG);
// Set adapter
mEventAdapter = new EventAdapter(getEvents(eventDate));
eventRecyclerView.setAdapter(mEventAdapter);
Log.v(TAG, eventRecyclerView.toString());
return view;
}
/**
* Retrieves the events for the given date for the fragment.
*/
private List<Event> getEvents(LocalDate date){
List<Event> returnList = new ArrayList<>();
String dateString = Utility.getDBDateString(date);
List<String> dateList = new ArrayList<>();
Cursor cursor = getActivity().getContentResolver().query(
GHContract.EventEntry.CONTENT_URI,
new String[]{ "*", "substr(" + GHContract.EventEntry.COLUMN_TIME + ",0,11)" },
"substr(" + GHContract.EventEntry.COLUMN_TIME + ",0,11) = ? ",
new String[]{dateString},
GHContract.EventEntry.COLUMN_TIME
);
while(cursor.moveToNext()){
returnList.add(new Event(cursor));
dateList.add(cursor.getString(cursor.getColumnIndex(GHContract.EventEntry.COLUMN_TIME)));
}
cursor.close();
return returnList;
}
}
EventAdapter - removed the static keyword and the reference to the activity context (you need to get the context somewhere else)
public class EventAdapter extends RecyclerView.Adapter<EventAdapter.ViewHolder> {
private List<Event> mEvents;
public EventAdapter(List<Event> events){
mEvents = events;
}
/**
* Inflates the view for Event items.
*/
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_event, parent, false);
return new ViewHolder(view);
}
/**
* Binds the data for an event to its view.
*/
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Event event = mEvents.get(position);
holder.timeView.setText(Utility.getTimeString(event.getTime()));
holder.titleView.setText(event.getTitle());
holder.locationView.setText(event.getLocation());
// If reminder time is not null, show check mark. If it is, show plus.
if(event.getReminderTime() != null){
holder.alarmView.setImageDrawable(holder.itemView.getResources().getDrawable(R.drawable.ic_alarm_on));
} else{
holder.alarmView.setImageDrawable(holder.itemView.getResources().getDrawable(R.drawable.ic_add_alarm));
}
}
/**
* Returns the size of the adapter.
*/
#Override
public int getItemCount() {
return mEvents.size();
}
/**
* Retains a reference to the view so `findViewById` calls are only made once for the adapter.
*/
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
public final TextView timeView;
public final TextView titleView;
public final TextView locationView;
public final ImageView alarmView;
public ViewHolder(View view){
super(view);
timeView = (TextView) view.findViewById(R.id.event_time);
titleView = (TextView) view.findViewById(R.id.event_title);
locationView = (TextView) view.findViewById(R.id.event_location);
alarmView = (ImageView) view.findViewById(R.id.event_add_reminder);
alarmView.setOnClickListener(this);
}
/**
* Handles the click a user makes on the alarm image view.
*/
#Override
public void onClick(View v) {
OnEventReminderClickListener activity = (OnEventReminderClickListener) v.getContext();
activity.onEventReminderClicked(mEvents.get(getPosition()));
}
}
/**
* Interface to call back to the activity when an alarm is clicked for an event item.
*/
public interface OnEventReminderClickListener{
void onEventReminderClicked(Event event);
}
}
And finally the app/build.gradle, since you need to get the same version for all support libraries (recycler, card and so on)
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "com.adammcneilly.grizzhacks"
minSdkVersion 15
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.1'
compile 'joda-time:joda-time:2.7'
compile 'com.android.support:recyclerview-v7:22.2.1'
compile 'com.android.support:cardview-v7:22.2.1'
compile 'com.android.support:design:22.2.1'
}
Could you try this?
public class EventFragmentAdapter extends FragmentStatePagerAdapter {
private List<LocalDate> mEventDates;
private List<EventListFragment> mFragments;
public EventFragmentAdapter(FragmentManager fragmentManager, List<LocalDate> eventDates){
super(fragmentManager);
this.mEventDates = eventDates;
this.mFragments = new ArrayList<>;
for (LocalDate date : this.mEventDates) {
this.mFragments.add(EventListFragment.newInstance(date));
}
}
#Override
public Fragment getItem(int i) {
return mFragments.get(i);
}
#Override
public int getCount() {
return mEventDates.size();
}
#Override
public CharSequence getPageTitle(int position) {
return mEventDates.get(position).dayOfWeek().getAsText();
}
}
Then you check into each item of mFragents if they have the expected content.

Categories

Resources