Getting Android RecyclerView to update view inside React Native component - android

I am making a mobile application using React Native and included list components didn't have high enough performance for it so I started using Android's RecyclerView as the list component. There is a problem though with it. The RecyclerView doesn't update its contents views until I scroll or change RecyclerView's size. What could cause this problem and how I can fix it? I have tried notifyDatasetChanged, notifyItemChanged, forceLayout, invalidate, postInvalidate and many different variations with each.

Try this one this.setIsRecyclable(true);
It will referesh your views
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
private ArrayList<String> mSingleItemLists = new ArrayList<>();
private SingleListItemAdapter mSingleListItemAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view_single_item);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(linearLayoutManager);
setDummyData();
}
private void setDummyData() {
for (int i = 0; i <= 30; i++)
mSingleItemLists.add("item" + i);
}
#Override
protected void onResume() {
super.onResume();
mSingleListItemAdapter = new SingleListItemAdapter(mSingleItemLists);
mRecyclerView.setAdapter(mSingleListItemAdapter);
}
class SingleListItemAdapter extends RecyclerView.Adapter<SingleListItemAdapter.SingleListItemHolder> {
private ArrayList<String> mSingleItemLists;
private SingleListItemAdapter(ArrayList<String> singleItemLists) {
mSingleItemLists = singleItemLists;
//You can do notifydatasetchange if u r having any saved value
}
#Override
public SingleListItemAdapter.SingleListItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View inflatedView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.row_recyclerview, parent, false);
return new SingleListItemHolder(inflatedView);
}
#Override
public void onBindViewHolder(SingleListItemAdapter.SingleListItemHolder holder, int position) {
holder.mItemDate.setText(mSingleItemLists.get(position));
}
#Override
public int getItemCount() {
return mSingleItemLists.size();
}
class SingleListItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private TextView mItemDate;
SingleListItemHolder(View v) {
super(v);
mItemDate = (TextView) v.findViewById(R.id.textview_recycler_list_item);
v.setOnClickListener(this);
this.setIsRecyclable(true); // This will help u
}
#Override
public void onClick(View v) {
//do your stuff
notifyDataSetChanged();
}
}
}
}

Related

RecyclerView not visible

I have this code in MainActivity
public class MainActivity extends AppCompatActivity implements MainViewInterface{
RecyclerView rvMovies;
private String TAG = "MainActivity";
MoviesAdapter adapter;
MainPresenter mainPresenter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setupMVP();
mainPresenter.getMovies();
rvMovies = (RecyclerView)findViewById(R.id.rvMovies);
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
rvMovies.setLayoutManager(manager);
//rvMovies.setHasFixedSize(true);
}
private void setupMVP() {
mainPresenter = new MainPresenter(this);
}
#Override
public void showToast(String s) {
Toast.makeText(MainActivity.this,s,Toast.LENGTH_LONG).show();
}
#Override
public void displayMovies(MovieResponse moviesResponse) {
if(moviesResponse!=null) {
Log.d(TAG,moviesResponse.getResults().get(1).getTitle());
adapter = new MoviesAdapter(moviesResponse.getResults(), MainActivity.this);
rvMovies.setAdapter(adapter);
adapter.notifyDataSetChanged();
}else{
Log.d(TAG,"Movies response null");
}
}
#Override
public void displayError(String s) {
showToast(s);
}
}
But I get this
E/RecyclerView: No adapter attached; skipping layout
I know other people asked the same question. I tried their solutions but didn't work. What am I missing?
This is my adapter code.
public class MoviesAdapter extends RecyclerView.Adapter<MoviesAdapter.MoviesHolder> {
List<Result> movieList;
Context context;
public MoviesAdapter(List<Result> movieList, Context context){
this.movieList = movieList;
this.context = context;
}
#Override
public MoviesHolder onCreateViewHolder( ViewGroup parent, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.row_movies,parent,false);
MoviesHolder mh = new MoviesHolder(v);
return mh;
}
#Override
public void onBindViewHolder( MoviesHolder holder, int position) {
holder.tvTitle.setText(movieList.get(position).getTitle());
holder.tvOverview.setText(movieList.get(position).getOverview());
holder.tvReleaseDate.setText(movieList.get(position).getReleaseDate());
Glide.with(context).load("https://image.tmdb.org/t/p/w500/"+movieList.get(position).getPosterPath()).into(holder.ivMovie);
}
#Override
public int getItemCount() {
return movieList.size();
}
public class MoviesHolder extends RecyclerView.ViewHolder {
TextView tvTitle,tvOverview,tvReleaseDate;
ImageView ivMovie;
public MoviesHolder(View itemView) {
super(itemView);
tvTitle = (TextView) itemView.findViewById(R.id.tvTitle);
tvOverview = (TextView) itemView.findViewById(R.id.tvOverView);
tvReleaseDate = (TextView) itemView.findViewById(R.id.tvReleaseDate);
ivMovie = (ImageView) itemView.findViewById(R.id.ivMovie);
}
}
Thanks,
Theo.
SOLVED
In my activivity_main.xml I had both height and with equal to 0dp!!! I honestly don't know how and who put those values in!! That's why the recycler view was not visible!!!
I think you should call rvMovies.setAdapter(new MoviesAdapter(new ArrayList<>())) on your RecyclerView during onCreate - there is nothing bad about setting an empty (from items perspective) adapter on RecyclerView, it will simply show an empty list. Then once you have your data ready (downloaded/loaded from any source you are loading from) you will simply call something like adapter.setItems(newItemsList) and adapter.notifyDataSetChanged().
Also one more note: you should not store Context in class member fields as leaks might occur (I am pretty sure your Android Studio must complain about that as well in a form of a Lint Warning check). You actually don't even need that Context instance passing to your adapter as you can simply retrieve a Context instance from parent View in onCreateViewHolder - rewrite it like this:
#Override
public MoviesHolder onCreateViewHolder( ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_movies,parent,false);
MoviesHolder mh = new MoviesHolder(v);
return mh;
}
These 2 lines:
setupMVP();
mainPresenter.getMovies();
must be written after:
rvMovies = (RecyclerView)findViewById(R.id.rvMovies);
you need to set adapter after you set the views.
rvMovies = (RecyclerView)findViewById(R.id.rvMovies);
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
rvMovies.setLayoutManager(manager);
//rvMovies.setHasFixedSize(true);
//call after setting the view
mainPresenter.getMovies();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setupMVP();
rvMovies = (RecyclerView)findViewById(R.id.rvMovies);
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
rvMovies.setLayoutManager(manager);
rvMovies.setVisibility(View.VISIBLE);
mainPresenter.getMovies(); <== put this line in last
}

Get item fully visible or not in Recyclerview adapter

I am making an android app in which RecyclerView consist list of GIFs, Now I want to develop such functionality like when GIF item is completely visible to the user then and then it starts loading, once it slightly looses the focus of user it should stop loading.
I have displayed GIF in recyclerview now the only part remain is how to get that such GIF is fully visible or looses focus. I want such code inside adapter which indicated that this item is fully visible and this item looses focus.
MY Adapter class
public class FeedAdapter extends RecyclerView.Adapter<FeedAdapter.MyViewHolder> {
private List<FeedModel> feedList;
Context context;
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.single_item, parent, false);
return new MyViewHolder(itemView);
}
#Override
public void onBindViewHolder(final MyViewHolder holder, int position) {
holder.setData(feedList.get(position));
new GifDataDownloader() {
#Override
protected void onPostExecute(final byte[] bytes) {
holder.gifImageView.setBytes(bytes);
holder.gifImageView.startAnimation();
}
}.execute(feedList.get(position).getUrl());
}
public FeedAdapter(Context context, List<FeedModel> feedList) {
this.feedList = feedList;
this.context = context;
}
#Override
public int getItemCount() {
return feedList.size();
}
public void setGridData(ArrayList<FeedModel> feedList) {
this.feedList = feedList;
notifyDataSetChanged();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
FeedModel item;
GifImageView gifImageView;
public MyViewHolder(View itemView) {
super(itemView);
gifImageView = itemView.findViewById(R.id.gifImageView);
}
public void setData(FeedModel item) {
this.item = item;
}
}
My activity
public class MainActivity extends AppCompatActivity {
ImageView imageView;
ImageView imageView1;
private PendingIntent pendingIntent;
RecyclerView recyclerView;
FeedAdapter mAdapter;
ProgressBar progressBar;
GridLayoutManager manager;
Spinner countrySpinner;
ArrayList<FeedModel> feedList;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
feedList = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
FeedModel feedModel = new FeedModel();
feedModel.setUrl("https://media.tenor.com/images/925bfbaad2f947987bcf18b9167b3326/tenor.gif");
feedList.add(feedModel);
}
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mAdapter = new FeedAdapter(getApplicationContext(), feedList);
manager = new GridLayoutManager(getApplicationContext(), 1, GridLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(manager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setVisibility(View.VISIBLE);
recyclerView.setAdapter(mAdapter);
}
Better way to used glide to show image and gif etc. it is take all senior that you define .
Add below dependency into app level gradle file
implementation 'com.github.bumptech.glide:glide:4.7.1'
then after used in adapter like this way..
private Context context;
// define context into constructor
public RecyclerviewAdapter(List<StepsItem> stepsItems, Context context) {
this.stepsItems = stepsItems;
this.context = context;
}
Glide.with(context).load(yoururl).into(holder.mIvIcon);
Start gif after your view is created like in onstart() method
And when you want to stop the gif when user is no more interact with app , stop the gif in onpause() method.

Image in row of recyclerview disappears after rotation

In my recyclerview I set an image (in recyclerview row) visible or not visible according some condition.
However after I rotate the screen the image disappears, how do I keep the image visible on it`s position in the recyclerview after rotating the screen?
Thank you for your help and happy holidays ;)
I am using an implementation of the SimpleAdapter.
public class SimpleAdapter extends RecyclerView.Adapter<SimpleAdapter.SimpleViewHolder> implements FollowRedirectsCallback {
public static class SimpleViewHolder extends RecyclerView.ViewHolder {
...
public final View icCancel;
public SimpleViewHolder(View view) {
super(view);
...
icCancel = view.findViewById(R.id.icCancel);
}
}
public SimpleAdapter(Context context, String[] data, long userId, int dlTypeValue) {
mContext = context;
userID = userId;
DLTypeValue = dlTypeValue;
if (data != null)
mData = new ArrayList<String>(Arrays.asList(data));
else mData = new ArrayList<String>();
}
public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(mContext).inflate(R.layout.simple_item, parent, false);
return new SimpleViewHolder(view);
}
#Override
public void onBindViewHolder(final SimpleViewHolder holder, final int position) {
...
// How to keep this image visible after rotation of the screen?
if (somecondition) {
holder.icCancel.setVisibility(View.VISIBLE);
}
}
EDIT: I initialze my adapter in the the corresponsent activity:
public class DownloadsActivity extends BaseActivity {
RecyclerView recyclerView;
Parcelable state;
SimpleAdapter mAdapter;
SimpleSectionedRecyclerViewAdapter mSectionedAdapter;
SimpleSectionedRecyclerViewAdapter.Section[] dummy;
#AfterViews
public void init() {
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
//setLayout Manager
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setHasFixedSize(true);
dummy = new SimpleSectionedRecyclerViewAdapter.Section[sections.size()];
mSectionedAdapter = new
SimpleSectionedRecyclerViewAdapter(this, R.layout.section, R.id.section_text, mAdapter);
mSectionedAdapter.setSections(sections.toArray(dummy));
//Apply this adapter to the RecyclerView
recyclerView.setAdapter(mSectionedAdapter);
}
#Override
protected void onPause() {
super.onPause();
// save RecyclerView state
state = recyclerView.getLayoutManager().onSaveInstanceState();
}
#Override
protected void onResume() {
super.onResume();
if (inProgress) {
progresstxt.setVisibility(View.VISIBLE);
downloadprogress.setVisibility(View.VISIBLE);
}
// restore RecyclerView state
recyclerView.getLayoutManager().onRestoreInstanceState(state);
}
}
You might be able to save the visibility state of the images in an ArrayList, then pass it as a parameter to the adapter. In your Activity, save that ArrayList using onSaveInstanceState.
public class YourActivity extends BaseActivity {
/* some codes */
// possible values are: VISIBLE, INVISIBLE, or GONE
private ArrayList<Integer> imagesState = new ArrayList<>();
/* some codes */
#Override
protected void onCreate(Bundle savedInstanceState) {
if(savedInstanceState != null) {
imagesState = savedInstanceState.getIntegerArrayList("key");
}
// Pass the imagesState to the adapter as a parameter here
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList("key", imagesState);
}
/* some codes */
}
Then in your Adapter:
public class SimpleAdapter extends RecyclerView.Adapter<SimpleAdapter.SimpleViewHolder> implements FollowRedirectsCallback {
/* some codes */
public SimpleAdapter(Context context, String[] data, long userId, int dlTypeValue, ArrayList<Integer> imagesState) {
/* some codes */
this.imagesState = imagesState;
}
#Override
public void onBindViewHolder(SimpleViewHolder holder, int position) {
/* some codes */
if (imagesState.get(position) == View.VISIBLE)
holder.icCancel.setVisibility(View.VISIBLE);
else if(imagesState.get(position) == View.INVISIBLE)
holder.icCancel.setVisibility(View.INVISIBLE);
else
holder.icCancel.setVisibility(View.GONE);
}
}
use
int position = recyclerView.getLayoutManager().findFirstVisibleItemPosition();
to get the first position visible. then just scroll to the position
recyclerView.getLayoutManager().scrollToPosition( <position of interest> );

Impossible : No layout manager attached; Skipping layout

I'm completely lost with this bug, I understand it, but I don't know what's wrong.
For the code :
// In the OnCreate of my activity
historyRecyclerView = (RecyclerView)findViewById(R.id.recycler_suggestions);
SearchBarHistoryAdapter searchBarHistoryAdapter = new SearchBarHistoryAdapter();
searchBarHistoryAdapter.setActivity(this);
historyRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
historyRecyclerView.setAdapter(searchBarHistoryAdapter);
searchBarDisplayManager.setTypeAdapter(SearchBarDisplayManager.SEARCH_TYPE.HISTORY, searchBarHistoryAdapter);
The SearchDisplayManager just contains a list of adapters.
The adapter :
public class SearchBarHistoryAdapter extends SearchBarAdapter {
private ArrayList<String> historyList;
private HistoryTask historyTask;
private void setHistoryList(ArrayList<String> history) {
historyList = history;
notifyDataSetChanged();
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.search_list_item, parent, false);
final HistoryViewHolder historyViewHolder = new HistoryViewHolder(v);
return historyViewHolder;
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((HistoryViewHolder) holder).bind(historyList.get(position));
}
#Override
public int getItemCount() {
return historyList == null ? 0 : historyList.size();
}
#Override
public void startSearch(String searchString) {
if(TextUtils.isEmpty(searchString)) {
if (historyTask != null) {
historyTask.cancel(true);
}
historyTask = new HistoryTask();
historyTask.execute();
}else{
setHistoryList(null);
}
}
private class HistoryTask extends AsyncTask<Void, Void, ArrayList<String>> {
#Override
protected ArrayList<String> doInBackground(Void... params) {
String history = HelperUI.getSearchbarHistory(activity);
if (!TextUtils.isEmpty(history)) {
return new ArrayList<>(Arrays.asList(history.split("--")));
}
return new ArrayList<>(0);
}
#Override
protected void onPostExecute(ArrayList<String> results) {
super.onPostExecute(results);
if (!results.isEmpty()) {
setHistoryList(results);
}
historyTask = null;
}
}
private class HistoryViewHolder extends RecyclerView.ViewHolder {
TextView hint, name;
ImageView img;
public HistoryViewHolder(View v) {
super(v);
name = (TextView) v.findViewById(R.id.app_label);
hint = ((TextView) v.findViewById(R.id.type_label));
img = ((ImageView) v.findViewById(R.id.item_icon));
}
public void bind(String suggestion) {
name.setText(Html.fromHtml(suggestion));
img.setImageDrawable(activity.getResources().getDrawable(R.drawable.ic_public_grey600_48dp));
img.setTag(suggestion);
}
}
}
Here is the crazy part, when I update the list in setHistoryList, I know the recycler view has the right adapter and it is not null ... but then it kinda loses it, no more adapter in the Recycler view when it tries to update by notifyDatasetChanged() ... it just disappears and, of cours, displays nothing.
Any idea what's wrong ?
You Have to Add a layoutmanager as you regularly do with any Adapter you create for a RecyclerView.
historyRecyclerView.setLayoutManager(new LinearLayoutManager(context));
Just add this line below of code below setAdpater where you are setting your recycler view with the adpapter.
You can add this to xml also (Thanks #Indra Kumar S)
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
For those using AndroidX, a slight modification of #Doongsil's solution:
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
My private Variable here
private RecyclerView recycleViewtask;
private RecyclerView.LayoutManager mLayoutManager;
Then I assign following things
recycleViewtask = findViewById(R.id.recycleViewtask);
recycleViewtask.setHasFixedSize(true);
// use a linear layout manager
mLayoutManager = new LinearLayoutManager(this);
recycleViewtask.setLayoutManager(mLayoutManager);

Adding a new item to a RecyclerView when button is clicked?

I just started using RecyclerViews but i cant completely understand how to add or remove items from it. Below i will attach my adapter code it is a test code and everything in the layout works fine. I feel like im also writing too much unnecessary code so any tips or criticism is appreciated.
public class PlatesAdapter extends
RecyclerView.Adapter<PlatesAdapter.ViewHolder> {
//Declaring a List<> of Plates
private List<Plates> mPlates;
int amountOfPlates;
public static class ViewHolder extends RecyclerView.ViewHolder {
//Declaring Buttons and textViews
public TextView plateWeightTextView, amountOfPlatesTextView;
public Button addButton, subButton, addLayoutButton;
public ViewHolder(View itemView) {
super(itemView);
//initializing Buttons and TextViews
plateWeightTextView = (TextView) itemView.findViewById(R.id.plate_weight_value_textView);
amountOfPlatesTextView = (TextView) itemView.findViewById(R.id.amount_of_plates_textView);
addButton = (Button) itemView.findViewById(R.id.add_button);
subButton = (Button) itemView.findViewById(R.id.subtract_button);
addLayoutButton = (Button) itemView.findViewById(R.id.button);
}
}
//Constructor
public PlatesAdapter(List<Plates> plates) {
mPlates = plates;
}
#Override
public PlatesAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View PlatesView = inflater.inflate(R.layout.plate_item_layout, parent, false);
ViewHolder viewHolder = new ViewHolder(PlatesView);
return viewHolder;
}
#Override
public void onBindViewHolder(PlatesAdapter.ViewHolder holder, int position) {
final TextView textView2 = holder.amountOfPlatesTextView;
//BUTTONS add 1 or subtract 1 from amountOfPlates;
Button button = holder.addButton;
Button button2 = holder.subButton;
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
amountOfPlates++;
textView2.setText(Integer.toString(amountOfPlates));
}
});
button2.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
amountOfPlates--;
textView2.setText(Integer.toString(amountOfPlates));
}
});
}
#Override
public int getItemCount() {
return mPlates.size();
}
Here is my Model Layer which i feel is completely wrong but im not 100% sure if it is.
public class Plates {
private int mPlateWeight;
private int mAmountOfPlates;
public Plates() {
//mPlateWeight = plateWeight;
//mAmountOfPlates = amountOfPlates;
}
public int getmPlateWeight() {
return mPlateWeight;
}
public int getmAmountOfPlates() {
return mAmountOfPlates;
}
public static List<Plates> createPlateList() {
List<Plates> plates = new ArrayList<>();
plates.add(new Plates());
return plates;
}
}
This is where im comfused. Its were do i call the addPlates or addItem method and what do i pass to it? Below is my main activity. I Just dont know where to add these addItems or addPlates methods is it to the Model Layer or the Adapter?
public class MainActivity extends AppCompatActivity {
private RecyclerView.LayoutManager mLayoutManager;
Button mButton;
private List<Plates> mData = new ArrayList<>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Button to add layout to recyclerView
mButton = (Button) findViewById(R.id.button);
//Adapter LayoutManager
mLayoutManager = new LinearLayoutManager(this);
mButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.weights_recycler_view);
PlatesAdapter adapter = new PlatesAdapter(mData);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(mLayoutManager);
}
});
}
}
What I usually do with RecyclerViews is to add a method to set the data.
In your example :
public void setPlates(List<Plates> plates) {
mPlates = plates;
notifyDataSetChanged();
}`
You can also add a getter if you want to verify if the data have changed or not.
You can add a method in your adapter to add a Plates in the arrayList and to notify the change.
Something like:
public void addPlates(Plates plate) {
if (mPlates == null) mPlates = new ArrayList();
mPlates.add(plate);
//notifyDataSetChanged();
notifyItemInserted(mPlates.size()-1)
}`
First of all the createPlateList method is not needed.
You should add a method in your adapter that looks like this:
public void addItem(Plates plate)
{
mPlates.add(plate);
}
Since your adapter works with this list, all you need to do to add or remove items, is to add/remove the items from your list. After all you need to call notifyDataSetChanged() in your adapter so it knows data was changed in your list.

Categories

Resources