diff --git a/adapster/.gitignore b/adapster/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/adapster/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/adapster/README.md b/adapster/README.md new file mode 100644 index 0000000..231c69a --- /dev/null +++ b/adapster/README.md @@ -0,0 +1,7 @@ +# Adapster +A local fork of the [Adapster](https://github.com/arthur3486/adapster) library. + +The `Adapster` comes as public API of this library. However, the `Adapster` is available only on the +`jcenter` repository, which has been deprecated quite a while ago. To avoid users of this library +having to declare `jcenter` as a repository source, the `Adapster` lib has been copied over (its public +API is pretty small) to fix this exact problem. \ No newline at end of file diff --git a/adapster/build.gradle.kts b/adapster/build.gradle.kts new file mode 100644 index 0000000..71ddb01 --- /dev/null +++ b/adapster/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + androidLibrary() +} + +android { + namespace = "com.arthurivanets.adapster" + compileSdk = appConfig.compileSdkVersion + + defaultConfig { + minSdk = appConfig.minSdkVersion + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = appConfig.javaCompatibilityVersion + targetCompatibility = appConfig.javaCompatibilityVersion + } +} + +dependencies { + implementation(deps.appCompat) + implementation(deps.recyclerView) + + testImplementation(deps.jUnit) + androidTestImplementation(deps.testRunner) +} diff --git a/adapster/consumer-rules.pro b/adapster/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/adapster/proguard-rules.pro b/adapster/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/adapster/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/adapster/src/main/java/com/arthurivanets/adapster/Adapter.java b/adapster/src/main/java/com/arthurivanets/adapster/Adapter.java new file mode 100644 index 0000000..61028a6 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/Adapter.java @@ -0,0 +1,266 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster; + +import com.arthurivanets.adapster.listeners.OnDatasetChangeListener; +import com.arthurivanets.adapster.model.Item; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A contract-interface representing the Adapter and its required functionality. + * + * @param the dataset item type + * @author arthur3486 + */ +public interface Adapter { + + /** + * Adds the specified item to the end of the underlying dataset. + * + * @param item to be added + */ + void addItem(@NonNull IT item); + + /** + * Adds the specified item to the end of the underlying dataset. + * + * @param item to be added + * @param notifyAboutTheChange whether to notify the subscribers about the dataset change via DataSetChangeListener + */ + void addItem(@NonNull IT item, boolean notifyAboutTheChange); + + /** + * Adds the specified item at a specified position to the underlying dataset. + * + * @param position to add the item at + * @param item to be added + */ + void addItem(int position, @NonNull IT item); + + /** + * Adds the specified item at a specified position to the underlying dataset. + * + * @param position to add the item at + * @param item to be added + * @param notifyAboutTheChange whether to notify the subscribers about the dataset change via DataSetChangeListener + */ + void addItem(int position, @NonNull IT item, boolean notifyAboutTheChange); + + /** + * Adds the specified item or updates (replaces) the existing item + * with the specified item within the underlying dataset. + * + * @param item to be added or replaced + */ + void addOrUpdateItem(@NonNull IT item); + + /** + * Adds the specified item or updates (replaces) the existing item + * with the specified item within the underlying dataset. + * + * @param item to be added or replaced + * @param notifyAboutTheChange whether to notify the subscribers about the dataset change via DataSetChangeListener + */ + void addOrUpdateItem(@NonNull IT item, boolean notifyAboutTheChange); + + /** + * Adds the specified item at a specified position or updates (replaces) the existing item + * with the specified item within the underlying dataset. + * + * @param position to add the item at + * @param item to be added or replaced + */ + void addOrUpdateItem(int position, @NonNull IT item); + + /** + * Adds the specified item at a specified position or updates (replaces) the existing item + * with the specified item within the underlying dataset. + * + * @param position to add the item at + * @param item to be added or replaced + * @param notifyAboutTheChange whether to notify the subscribers about the dataset change via DataSetChangeListener + */ + void addOrUpdateItem(int position, @NonNull IT item, boolean notifyAboutTheChange); + + /** + * Updates item (causes item to be rebound by RecyclerView), if it's present within the dataset. + * + * @param item to be updated + */ + void updateItem(@NonNull IT item); + + /** + * Updates item (causes item to be rebound by RecyclerView) at a specified position. + * + * @param position item position. + */ + void updateItem(int position); + + /** + * Updates (Replaces) the corresponding item within the underlying dataset with the specified item. + * + * @param item replacement item + */ + void updateItemWith(@NonNull IT item); + + /** + * Updates (Replaces) the corresponding item within the underlying dataset with the specified item. + * + * @param item replacement item + * @param notifyAboutTheChange whether to notify the subscribers about the dataset change via DataSetChangeListener + */ + void updateItemWith(@NonNull IT item, boolean notifyAboutTheChange); + + /** + * Updates (Replaces) the item at a specified position + * within the underlying dataset with the specified item. + * + * @param position existing item position + * @param item replacement item + */ + void updateItemWith(int position, @NonNull IT item); + + /** + * Updates (Replaces) the item at a specified position + * within the underlying dataset with the specified item. + * + * @param position existing item position + * @param item replacement item + * @param notifyAboutTheChange whether to notify the subscribers about the dataset change via DataSetChangeListener + */ + void updateItemWith(int position, @NonNull IT item, boolean notifyAboutTheChange); + + /** + * Removes the specified item from the Adapter (from the underlying dataset), + * if it is present. + * + * @param item to be removed + */ + void deleteItem(@NonNull IT item); + + /** + * Removes the item from the Adapter (from the underlying dataset), + * at a specified position. + * + * @param position item position + */ + void deleteItem(int position); + + /** + * Checks to see if the underlying dataset contains the specified item. + * + * @param item whose presence will be checked + * @return true if the item is present, false otherwise. + */ + boolean contains(@NonNull IT item); + + /** + * Looks up the index (within the dataset) of the specified item, + * provided that the item is present in the dataset. + * + * @param item to find the index of. + * @return the found index, or -1 if no matching item was found. + */ + int indexOf(@NonNull IT item); + + /** + * Looks up the last index within the current adapter's dataset. + * + * @return the last index in this adapter's dataset, or -1 if the dataset is empty + */ + int lastIndex(); + + /** + * Removes all the items from the Adapter. + */ + void clear(); + + /** + * Sets the underlying dataset. (Notifies the adapter about the changes) + * + * @param items to be used as an underlying dataset + */ + void setItems(@NonNull List items); + + /** + * Sets the underlying dataset. + * + * @param items to be used as an underlying dataset + * @param notifyAboutTheChange whether to notify the subscribers about the dataset change via DataSetChangeListener + */ + void setItems(@NonNull List items, boolean notifyAboutTheChange); + + /** + * Retrieves the underlying dataset, in a form of a List of corresponding items. + * + * @return the underlying dataset. + */ + @NonNull + List getItems(); + + /** + * Retrieves the item at a specified position. + * + * @param position item position + * @return the corresponding item, or null. + */ + @Nullable + IT getItem(int position); + + /** + * Retrieves the item at the very first position within this adapter's dataset. + * + * @return the retrieved item, or null if the dataset is empty. + */ + @Nullable + IT getFirstItem(); + + /** + * Retrieves the item at the very last position within this adapter's dataset. + * + * @return the retrieved item, or null if the dataset is empty. + */ + @Nullable + IT getLastItem(); + + /** + * Retrieves the item count of the underlying dataset. + * + * @return the exact item count of the underlying dataset. + */ + int getItemCount(); + + /** + * Associates the specified OnDatasetChangeListener with the current adapter. + */ + void addOnDatasetChangeListener(@NonNull OnDatasetChangeListener, IT> onDatasetChangeListener); + + /** + * Dissociates the specified instance of the OnDatasetChangeListener from the current adapter. + */ + void removeOnDatasetChangeListener(@NonNull OnDatasetChangeListener, IT> onDatasetChangeListener); + + /** + * Removes all the OnDatasetChangeListener(-s) associated with the current adapter. + */ + void removeAllOnDatasetChangeListeners(); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listeners/DatasetChangeListenerAdapter.java b/adapster/src/main/java/com/arthurivanets/adapster/listeners/DatasetChangeListenerAdapter.java new file mode 100644 index 0000000..115d58a --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listeners/DatasetChangeListenerAdapter.java @@ -0,0 +1,92 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listeners; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A convenience abstract class which implements the methods of the {@link OnDatasetChangeListener}. + * + * @param dataset type + * @param item type + * @author arthur3486 + */ +public abstract class DatasetChangeListenerAdapter, IT> implements OnDatasetChangeListener { + + + + + @Override + public void onItemAdded(@NonNull DS dataset, @Nullable IT item) { + + } + + + + + @Override + public void onItemUpdated(@NonNull DS dataset, @Nullable IT item) { + + } + + + + + @Override + public void onItemReplaced(@NonNull DS dataset, @Nullable IT oldItem, @Nullable IT newItem) { + + } + + + + + @Override + public void onItemDeleted(@NonNull DS dataset, @Nullable IT item) { + + } + + + + + @Override + public void onDatasetSizeChanged(int oldSize, int newSize) { + + } + + + + + @Override + public void onDatasetReplaced(@NonNull DS newDataset) { + + } + + + + + @Override + public void onDatasetCleared(@NonNull DS dataset) { + + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listeners/ItemClickListener.java b/adapster/src/main/java/com/arthurivanets/adapster/listeners/ItemClickListener.java new file mode 100755 index 0000000..4a14c06 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listeners/ItemClickListener.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listeners; + +import android.view.View; + +/** + * A convenience class used for the encapsulation and handling of the {@link OnItemClickListener}. + * + * @param the item type + * @author arthur3486 + */ +public class ItemClickListener implements View.OnClickListener { + + + private int mPosition; + private T mItem; + + private OnItemClickListener mOnItemClickListener; + + + + + public ItemClickListener(T item, + int position, + OnItemClickListener onItemClickListener) { + mItem = item; + mPosition = position; + mOnItemClickListener = onItemClickListener; + } + + + + + @Override + public void onClick(View v) { + if(mOnItemClickListener != null) { + mOnItemClickListener.onItemClicked(v, mItem, mPosition); + } + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listeners/ItemLongClickListener.java b/adapster/src/main/java/com/arthurivanets/adapster/listeners/ItemLongClickListener.java new file mode 100755 index 0000000..6e0f1cc --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listeners/ItemLongClickListener.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listeners; + +import android.view.View; + +/** + * A convenience class used for the encapsulation and handling of the {@link OnItemLongClickListener}. + * + * @param the item type + * @author arthur3486 + */ +public class ItemLongClickListener implements View.OnLongClickListener { + + + private int mPosition; + private T mItem; + + private OnItemLongClickListener mOnItemLongClickListener; + + + + + public ItemLongClickListener(T item, + int position, + OnItemLongClickListener onItemLongClickListener) { + mItem = item; + mPosition = position; + mOnItemLongClickListener = onItemLongClickListener; + } + + + + + @Override + public boolean onLongClick(View v) { + return ((mOnItemLongClickListener != null) && mOnItemLongClickListener.onItemLongClicked(v, mItem, mPosition)); + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listeners/ItemTouchListener.java b/adapster/src/main/java/com/arthurivanets/adapster/listeners/ItemTouchListener.java new file mode 100755 index 0000000..4df7528 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listeners/ItemTouchListener.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listeners; + +import android.view.MotionEvent; +import android.view.View; + +/** + * A convenience class used for the encapsulation and handling of the {@link OnItemTouchListener} + * + * @param the item type + * @author arthur3486 + */ +public class ItemTouchListener implements View.OnTouchListener { + + + private int mPosition; + private T mItem; + + private OnItemTouchListener mOnItemTouchListener; + + + + + public ItemTouchListener(T item, + int position, + OnItemTouchListener onItemTouchListener) { + mItem = item; + mPosition = position; + mOnItemTouchListener = onItemTouchListener; + } + + + + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + return ((mOnItemTouchListener != null) && mOnItemTouchListener.onItemTouch(view, motionEvent, mItem, mPosition)); + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnDatasetChangeListener.java b/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnDatasetChangeListener.java new file mode 100755 index 0000000..097ec19 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnDatasetChangeListener.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listeners; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A contract-interface for the observation of the dataset-related events. + * + * @param dataset type + * @param item type + * @author arthur3486 + */ +public interface OnDatasetChangeListener, IT> { + + /** + * Called when the new item is added to the dataset. + * + * @param dataset the dataset + * @param item the added item + */ + void onItemAdded(@NonNull DS dataset, @Nullable IT item); + + /** + * Called when the item, contained by the dataset, is updated. + * + * @param dataset the dataset + * @param item the updated item + */ + void onItemUpdated(@NonNull DS dataset, @Nullable IT item); + + /** + * Called when the item, contained by the dataset, is replaced with a new one. + * + * @param dataset the dataset + * @param oldItem the old item (replaced) + * @param newItem the new item (replacement) + */ + void onItemReplaced(@NonNull DS dataset, @Nullable IT oldItem, @Nullable IT newItem); + + /** + * Called when the item is deleted from the dataset. + * + * @param dataset the dataset + * @param item the deleted item + */ + void onItemDeleted(@NonNull DS dataset, @Nullable IT item); + + /** + * Called when the interaction with the Dataset resulted in the changes of its size. + * + * @param oldSize the size of the dataset before the modification + * @param newSize the size of the dataset after the modification + */ + void onDatasetSizeChanged(int oldSize, int newSize); + + /** + * Called when the dataset is replaced with a new one. + * + * @param newDataset the new dataset (replacement) + */ + void onDatasetReplaced(@NonNull DS newDataset); + + /** + * Called when the dataset is cleared. + * + * @param dataset the cleared dataset + */ + void onDatasetCleared(@NonNull DS dataset); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnItemClickListener.java b/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnItemClickListener.java new file mode 100755 index 0000000..65fae41 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnItemClickListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listeners; + +import android.view.View; + +/** + * A contract-interface for the observation of the dataset item click events. + * + * @param the item type + * @author arthur3486 + */ +public interface OnItemClickListener { + + /** + * Called when the dataset item is clicked + * + * @param view the origin of the click event + * @param item the associated item + * @param position the associated position info + */ + void onItemClicked(View view, T item, int position); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnItemLongClickListener.java b/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnItemLongClickListener.java new file mode 100755 index 0000000..eee3dc2 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnItemLongClickListener.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listeners; + +import android.view.View; + +/** + * A contract-interface for the observation of the dataset item long click events. + * + * @param the item type + * @author arthur3486 + */ +public interface OnItemLongClickListener { + + /** + * Called when the dataset item is long clicked. + * + * @param view the origin of the long click event + * @param item the associated item + * @param position the associated position info + * @return true if the long click has been confirmed, false otherwise. + */ + boolean onItemLongClicked(View view, T item, int position); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnItemTouchListener.java b/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnItemTouchListener.java new file mode 100755 index 0000000..06896ab --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listeners/OnItemTouchListener.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listeners; + +import android.view.MotionEvent; +import android.view.View; + +/** + * A contract-interface for the observation of the dataset item touch events. + * + * @param the item type + * @author arthur3486 + */ +public interface OnItemTouchListener { + + /** + * Called when the dataset item is touched. + * + * @param view the origin of the click event + * @param motionEvent the generated motion event + * @param item the associated item + * @param position the associated position info + * @return true to continue the consumption of the touch events, false otherwise. + */ + boolean onItemTouch(View view, MotionEvent motionEvent, T item, int position); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listview/BaseListViewAdapter.java b/adapster/src/main/java/com/arthurivanets/adapster/listview/BaseListViewAdapter.java new file mode 100755 index 0000000..c79b263 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listview/BaseListViewAdapter.java @@ -0,0 +1,593 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listview; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import com.arthurivanets.adapster.Adapter; +import com.arthurivanets.adapster.listeners.OnDatasetChangeListener; +import com.arthurivanets.adapster.listeners.OnItemClickListener; +import com.arthurivanets.adapster.markers.SupportsFooter; +import com.arthurivanets.adapster.markers.SupportsHeader; +import com.arthurivanets.adapster.model.BaseItem; +import com.arthurivanets.adapster.model.markers.Footer; +import com.arthurivanets.adapster.model.markers.Header; +import com.arthurivanets.adapster.markers.ItemResources; +import com.arthurivanets.adapster.util.Preconditions; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A base class for the implementation of any ListView adapter. + * + * @param the dataset item type + * @param the item view holder type + * @author arthur3486 + */ +public abstract class BaseListViewAdapter> extends ArrayAdapter implements Adapter, + SupportsHeader, SupportsFooter { + + + private List mItems; + + private final LayoutInflater mLayoutInflater; + private final Set, IT>> mOnDatasetChangeListeners; + + private OnItemClickListener> mOnHeaderClickListener; + private OnItemClickListener> mOnFooterClickListener; + + + + + public BaseListViewAdapter(@NonNull Context context, @NonNull List items) { + super(context, 0, items); + + Preconditions.nonNull(context); + Preconditions.nonNull(items); + + mItems = items; + mLayoutInflater = LayoutInflater.from(context); + mOnDatasetChangeListeners = new HashSet<>(); + } + + + + + @Override + public final void addItem(@NonNull IT item) { + addItem(item, true); + } + + + + + @Override + public final void addItem(@NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.nonNull(item); + + addItem(mItems.size(), item, notifyAboutTheChange); + } + + + + + @Override + public final void addItem(int position, @NonNull IT item) { + addItem(position, item, true); + } + + + + + @Override + public final void addOrUpdateItem(@NonNull IT item) { + addOrUpdateItem(item, true); + } + + + + + @Override + public final void addOrUpdateItem(@NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.nonNull(item); + + addOrUpdateItem(mItems.size(), item, notifyAboutTheChange); + } + + + + + @Override + public final void addOrUpdateItem(int position, @NonNull IT item) { + addOrUpdateItem(position, item, true); + } + + + + + @Override + public final void updateItem(@NonNull IT item) { + notifyDataSetChanged(); + } + + + + + @Override + public void updateItem(int position) { + notifyDataSetChanged(); + } + + + + + @Override + public final void updateItemWith(@NonNull IT item) { + updateItemWith(item, true); + } + + + + + @Override + public final void updateItemWith(@NonNull IT item, boolean notifyAboutTheChange) { + final int itemIndex = indexOf(item); + + if(itemIndex != -1) { + updateItemWith(itemIndex, item, notifyAboutTheChange); + } + } + + + + + @Override + public final void updateItemWith(int position, @NonNull IT item) { + updateItemWith(position, item, true); + } + + + + + @Override + public final void deleteItem(@NonNull IT item) { + final int itemIndex = indexOf(item); + + if(itemIndex != -1) { + deleteItem(itemIndex); + } + } + + + + + /** + * + *
+ * Implements the default functionality. For more complex presence checks + * (like with a dedicated HashMap for Item tracking), please make sure + * to override this method and add the necessary functionality. + *
+ * + */ + @Override + public boolean contains(@NonNull IT item) { + Preconditions.nonNull(item); + return mItems.contains(item); + } + + + + + /** + * + *
+ * Implements the default functionality. For more complex index determining + * (like with a dedicated HashMap for Item tracking), please make sure + * to override this method and add the necessary functionality. + *
+ * + */ + @Override + public int indexOf(@NonNull IT item) { + Preconditions.nonNull(item); + return mItems.indexOf(item); + } + + + + + @Override + public final int lastIndex() { + return (mItems.size() - 1); + } + + + + + /** + * Notifies the Dataset Change Observers about the addition of a new item. + * + * @param item the added item + */ + protected final void notifyItemAdded(@NonNull IT item) { + Preconditions.nonNull(item); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onItemAdded(mItems, item); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the item update. + * + * @param item the updated item + */ + protected final void notifyItemUpdated(@NonNull IT item) { + Preconditions.nonNull(item); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onItemUpdated(mItems, item); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the replacement of the item. + * + * @param oldItem the old item that got replaced + * @param newItem the new replacement item + */ + protected final void notifyItemReplaced(@NonNull IT oldItem, @NonNull IT newItem) { + Preconditions.nonNull(oldItem); + Preconditions.nonNull(newItem); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onItemReplaced(mItems, oldItem, newItem); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the deletion of the item. + * + * @param item the deleted item + */ + protected final void notifyItemDeleted(@NonNull IT item) { + Preconditions.nonNull(item); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onItemDeleted(mItems, item); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the Dataset size changes. + * + * @param oldSize + * @param newSize + */ + protected final void notifyDatasetSizeChanged(int oldSize, int newSize) { + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onDatasetSizeChanged(oldSize, newSize); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the replacement of the dataset. + * + * @param newDataset the new replacement dataset + */ + protected final void notifyDatasetReplaced(@NonNull List newDataset) { + Preconditions.nonNull(newDataset); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onDatasetReplaced(newDataset); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the clearing of the dataset. + * + * @param dataset the cleared dataset + */ + protected final void notifyDatasetCleared(@NonNull List dataset) { + Preconditions.nonNull(dataset); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onDatasetCleared(dataset); + } + } + + + + + /** + * Performs the Item View(and ViewHolder) initialization. + */ + @SuppressWarnings("unchecked") + protected VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType, @NonNull IT item) { + return (VH) item.init(this, parent, mLayoutInflater, getResources()); + } + + + + + @SuppressWarnings("unchecked") + @CallSuper + public void onBindViewHolder(@NonNull VH holder, int position) { + final IT item = getItem(position); + + //performing the data binding + item.bind(this, holder, getResources()); + + //allowing the extenders to assign listeners(if necessary) + assignListeners(holder, position, item); + } + + + + + /** + * Gets called when it's the right time to assign the Listeners to the + * corresponding item. Override it only if you need to provide the Listener settings functionality. + */ + @SuppressWarnings("unchecked") + @CallSuper + protected void assignListeners(@NonNull VH holder, int position, @NonNull IT item) { + //assigning the default listeners + if(item instanceof Header) { + ((Header) item).setOnItemClickListener(holder, mOnHeaderClickListener); + } else if(item instanceof Footer) { + ((Footer) item).setOnItemClickListener(holder, mOnFooterClickListener); + } + } + + + + + @Override + public final void addOnDatasetChangeListener(@NonNull OnDatasetChangeListener, IT> onDatasetChangeListener) { + Preconditions.nonNull(onDatasetChangeListener); + + mOnDatasetChangeListeners.add(onDatasetChangeListener); + } + + + + + @Override + public final void removeOnDatasetChangeListener(@NonNull OnDatasetChangeListener, IT> onDatasetChangeListener) { + Preconditions.nonNull(onDatasetChangeListener); + + mOnDatasetChangeListeners.remove(onDatasetChangeListener); + } + + + + + @Override + public final void removeAllOnDatasetChangeListeners() { + mOnDatasetChangeListeners.clear(); + } + + + + + @Override + public final void setOnHeaderClickListener(OnItemClickListener> onHeaderClickListener) { + mOnHeaderClickListener = onHeaderClickListener; + } + + + + + @Override + public final void setOnFooterClickListener(OnItemClickListener> onFooterClickListener) { + mOnFooterClickListener = onFooterClickListener; + } + + + + + @SuppressWarnings("unchecked") + @NonNull + @Override + public final View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + final IT item = getItem(position); + final VH viewHolder; + + // creating/restoring the ViewHolder + if(convertView == null) { + viewHolder = onCreateViewHolder( + parent, + getItemViewType(position), + item + ); + + convertView = viewHolder.itemView; + convertView.setTag(viewHolder); + } else { + viewHolder = (VH) convertView.getTag(); + } + + // performing the data binding + onBindViewHolder(viewHolder, position); + + return convertView; + } + + + + + @NonNull + @Override + public final View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + return getView(position, convertView, parent); + } + + + + + @CallSuper + @Override + public void setItems(@NonNull List items) { + setItems(items, true); + } + + + + + @CallSuper + @Override + public void setItems(@NonNull List items, boolean notifyAboutTheChange) { + Preconditions.nonNull(items); + + final int itemCount = getItemCount(); + mItems = items; + + if(notifyAboutTheChange) { + notifyDataSetChanged(); + } + + notifyDatasetReplaced(mItems); + notifyDatasetSizeChanged(itemCount, getItemCount()); + } + + + + + @NonNull + @Override + public final List getItems() { + return mItems; + } + + + + + @Nullable + @Override + public IT getItem(int position) { + return (((position >= 0) && (position < getCount())) ? mItems.get(position) : null); + } + + + + + @Override + public final IT getFirstItem() { + return getItem(0); + } + + + + + @Override + public final IT getLastItem() { + return getItem(lastIndex()); + } + + + + + @Override + public long getItemId(int position) { + final long itemId = getItem(position).getId(); + return ((itemId != BaseItem.NO_ID) ? itemId : position); + } + + + + + @Override + public final int getItemViewType(int position) { + return getItemViewType(position, getItem(position)); + } + + + + + public abstract int getItemViewType(int position, IT item); + + + + + @Override + public final int getCount() { + return mItems.size(); + } + + + + + @Override + public final int getItemCount() { + return getCount(); + } + + + + + @NonNull + protected final LayoutInflater getLayoutInflater() { + return mLayoutInflater; + } + + + + + /** + * Retrieves the reusable {@link ItemResources} associated with the current adapter. + * + * @return the reusable {@link ItemResources} associated with the current adapter, or null if no resources have been associated. + */ + @Nullable + public ItemResources getResources() { + return null; + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/listview/TrackableListViewAdapter.java b/adapster/src/main/java/com/arthurivanets/adapster/listview/TrackableListViewAdapter.java new file mode 100755 index 0000000..87f91e3 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/listview/TrackableListViewAdapter.java @@ -0,0 +1,416 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.listview; + +import android.content.Context; + +import com.arthurivanets.adapster.model.BaseItem; +import com.arthurivanets.adapster.model.Item; +import com.arthurivanets.adapster.model.markers.Footer; +import com.arthurivanets.adapster.model.markers.Header; +import com.arthurivanets.adapster.model.markers.Trackable; +import com.arthurivanets.adapster.util.Preconditions; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * An abstract implementation of the ListView Adapter which allows for the easy item tracking + * and prevention of the duplicates. (To prevent the item duplication an item must implement the {@link Trackable} interface + * and provide a unique track key) + * + * @param the item key type + * @param the item type + * @param the item view holder type + * @author arthur3486 + */ +public abstract class TrackableListViewAdapter> extends BaseListViewAdapter { + + + private final Map> mKeyTrackableMap; + + + + + public TrackableListViewAdapter(@NonNull Context context, @NonNull List items) { + super(context, items); + mKeyTrackableMap = new HashMap<>(); + setItems(items); + } + + + + + @Override + public void addItem(int position, @NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.withinBoundsInclusive(position, getItems()); + Preconditions.nonNull(item); + + // in case the item is already present, returning + if(contains(item)) { + return; + } + + // processing the item + final int itemCount = getCount(); + final boolean isFirstItemHeaderView = ((itemCount > 0) && (getItem(0) instanceof Header)); + final boolean isLastItemFooterView = ((itemCount > 0) && (getItem(itemCount - 1) instanceof Footer)); + + if(item instanceof Header) { + if(isFirstItemHeaderView) { + throw new IllegalStateException("Only a single Header can be present in a dataset at a time. Please, remove the old Header first, and then proceed with adding a new one."); + } + + getItems().add(0, item); + } else if(item instanceof Footer) { + if(isLastItemFooterView) { + throw new IllegalStateException("Only a single Footer can be present in a dataset at a time. Please, remove the old Footer first, and then proceed with adding a new one."); + } + + getItems().add(itemCount, item); + } else { + if((position == 0) && isFirstItemHeaderView) { + getItems().add((position + 1), item); + } else if((position == itemCount) && isLastItemFooterView) { + getItems().add((position - 1), item); + } else { + getItems().add(position, item); + } + } + + // adding the item to the map of trackables (if necessary) + trackIfNecessary(item); + + // notifying the Adapter about the change (if necessary) + if(notifyAboutTheChange) { + notifyDataSetChanged(); + } + + // notifying about the dataset change + notifyItemAdded(item); + notifyDatasetSizeChanged(itemCount, getItemCount()); + } + + + + + @Override + public void addOrUpdateItem(int position, @NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.withinBoundsInclusive(position, getItems()); + Preconditions.nonNull(item); + + if(contains(item)) { + updateItemWith(item, notifyAboutTheChange); + } else { + addItem(position, item, notifyAboutTheChange); + } + } + + + + + @Override + public void updateItemWith(int position, @NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.withinBoundsExclusive(position, getItems()); + Preconditions.nonNull(item); + + final IT oldItem = getItem(position); + + // untracking the old item, as it no longer relates to this dataset + untrackIfNecessary(oldItem); + + // replacing the old item(if there was any) and retracking the item + getItems().set(position, item); + trackIfNecessary(item); + + // notifying the Adapter about the change (if necessary) + if(notifyAboutTheChange) { + notifyDataSetChanged(); + } + + // notifying about the dataset change + notifyItemReplaced(oldItem, item); + } + + + + + @Override + public void deleteItem(int position) { + Preconditions.withinBoundsExclusive(position, getItems()); + + final int itemCount = getItemCount(); + + // removing the actual item, as well as untracking it (if necessary) + final IT removedItem = getItems().remove(position); + untrackIfNecessary(removedItem); + + // notifying about the change + notifyDataSetChanged(); + notifyItemDeleted(removedItem); + notifyDatasetSizeChanged(itemCount, getItemCount()); + } + + + + + @SuppressWarnings("unchecked") + @Override + public final void addHeader(@NonNull Header header) { + Preconditions.nonNull(header); + Preconditions.isTrue("The Header Item must be based on BaseItem", (header instanceof BaseItem)); + + addItem((IT) header); + } + + + + + @Override + public final void removeHeader() { + if((getCount() > 0) && (getItem(0) instanceof Header)) { + deleteItem(0); + } + } + + + + + @SuppressWarnings("unchecked") + @Override + public final void addFooter(@NonNull Footer footer) { + Preconditions.nonNull(footer); + Preconditions.isTrue("The Footer Item must be based on BaseItem", (footer instanceof BaseItem)); + + addItem((IT) footer); + } + + + + + @Override + public final void removeFooter() { + final int itemCount = getCount(); + + if((itemCount > 0) && (getItem(itemCount - 1) instanceof Footer)) { + deleteItem(itemCount - 1); + } + } + + + + + @SuppressWarnings("unchecked") + @Override + public boolean contains(@NonNull IT item) { + Preconditions.nonNull(item); + + if(item instanceof Trackable) { + return containsTrackable((Trackable) item); + } else { + return super.contains(item); + } + } + + + + + @SuppressWarnings("unchecked") + @Override + public int indexOf(@NonNull IT item) { + Preconditions.nonNull(item); + + if(item instanceof Trackable) { + final Trackable trackable = (Trackable) item; + + if(contains(item)) { + return getItems().indexOf(getTrackable(trackable.getTrackKey())); + } else { + return -1; + } + } else { + return super.indexOf(item); + } + } + + + + + /** + * Adds the specified items to the Map of {@link Trackable} items. + * The Items must implement the {@link Trackable} interface. + * + * @param items the item to be tracked + */ + protected final void trackIfNecessary(@NonNull List items) { + Preconditions.nonNull(items); + + for(IT item : items) { + trackIfNecessary(item); + } + } + + + + + /** + * Adds the specified item to the Map of {@link Trackable} items. + * The Item must implement the {@link Trackable} interface. + * + * @param item the item to be tracked + */ + @SuppressWarnings("unchecked") + protected final void trackIfNecessary(@NonNull IT item) { + Preconditions.nonNull(item); + + if(item instanceof Trackable) { + addTrackable((Trackable) item); + } + } + + + + + /** + * Removes the specified item from the map of {@link Trackable} items (if there's any). + * The Item must implement the {@link Trackable} interface. + * + * @param item the item to be untracked + */ + @SuppressWarnings("unchecked") + protected final void untrackIfNecessary(@NonNull IT item) { + Preconditions.nonNull(item); + + if(item instanceof Trackable) { + removeTrackable((Trackable) item); + } + } + + + + + /** + * Adds the specified {@link Trackable} to the Map of {@link Trackable} items. + * + * @param trackable trackable to be added + */ + protected final void addTrackable(@NonNull Trackable trackable) { + Preconditions.nonNull(trackable); + + mKeyTrackableMap.put(trackable.getTrackKey(), trackable); + } + + + + + /** + * Retrieves the {@link Trackable} from the Map of the {@link Trackable} items. + * + * @param key the {@link Trackable}'s key + * @return the corresponding {@link Trackable}, or null if no {@link Trackable} was found. + */ + @Nullable + public final Trackable getTrackable(@NonNull KT key) { + Preconditions.nonNull(key); + return mKeyTrackableMap.get(key); + } + + + + + /** + * Removes the {@link Trackable} from the Map of the {@link Trackable} items. + * + * @param trackable the trackable to be removed + */ + protected final void removeTrackable(@NonNull Trackable trackable) { + Preconditions.nonNull(trackable); + + mKeyTrackableMap.remove(trackable.getTrackKey()); + } + + + + + /** + * Checks whether the Map of {@link Trackable} items contains the specified {@link Trackable}. + * + * @param trackable + * @return true if contains, false otherwise + */ + protected final boolean containsTrackable(@NonNull Trackable trackable) { + Preconditions.nonNull(trackable); + return (mKeyTrackableMap.get(trackable.getTrackKey()) != null); + } + + + + + @Override + public final void setItems(@NonNull List items, boolean notifyAboutTheChange) { + Preconditions.nonNull(items); + + mKeyTrackableMap.clear(); + trackIfNecessary(items); + + super.setItems(items, notifyAboutTheChange); + } + + + + + @Override + public void clear() { + final int itemCount = getItemCount(); + + getItems().clear(); + mKeyTrackableMap.clear(); + + // notifying about the performed event + notifyDataSetChanged(); + notifyDatasetSizeChanged(itemCount, getItemCount()); + notifyDatasetCleared(getItems()); + } + + + + + @Override + public int getItemViewType(int position, IT item) { + if(item == null) { + return Item.VIEW_TYPE_INVALID; + } + + return 0; + } + + + + + @Override + public int getViewTypeCount() { + return 1; + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/markers/ItemResources.java b/adapster/src/main/java/com/arthurivanets/adapster/markers/ItemResources.java new file mode 100755 index 0000000..a42ed8b --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/markers/ItemResources.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.markers; + +/** + * A marker class that's to be implemented by the concrete reusable resources-providing classes. + * (Those reusable resources are used by the adapters) + * + * @author arthur3486 + */ +public interface ItemResources { + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/markers/SupportsFooter.java b/adapster/src/main/java/com/arthurivanets/adapster/markers/SupportsFooter.java new file mode 100644 index 0000000..3225218 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/markers/SupportsFooter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.markers; + +import com.arthurivanets.adapster.listeners.OnItemClickListener; +import com.arthurivanets.adapster.model.markers.Footer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * A marker interface which is to be implemented by the + * adapter to provide the Footer Item support. + * + * @param View Holder + * + * @author arthur3486 + */ +public interface SupportsFooter { + + /** + * Adds the Footer Item to the dataset. + * + * @param footer the Footer Item to be added to the dataset + */ + void addFooter(@NonNull Footer footer); + + /** + * Removes the Footer Item from the dataset (if there's any). + */ + void removeFooter(); + + /** + * Sets the Footer Item click listener. + * + * @param onFooterClickListener the listener to be set + */ + void setOnFooterClickListener(@Nullable OnItemClickListener> onFooterClickListener); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/markers/SupportsHeader.java b/adapster/src/main/java/com/arthurivanets/adapster/markers/SupportsHeader.java new file mode 100644 index 0000000..d4b7733 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/markers/SupportsHeader.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.markers; + +import com.arthurivanets.adapster.listeners.OnItemClickListener; +import com.arthurivanets.adapster.model.markers.Header; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * A marker interface which is to be implemented by the + * adapter to provide the Header Item support. + * + * @param View Holder + * + * @author arthur3486 + */ +public interface SupportsHeader { + + /** + * Adds the Header Item to the dataset. + * + * @param header the Header Item to be added to the dataset + */ + void addHeader(@NonNull Header header); + + /** + * Removes the Header Item from the dataset (if there's any). + */ + void removeHeader(); + + /** + * Sets the Header Item click listener. + * + * @param onHeaderClickListener the listener to be set + */ + void setOnHeaderClickListener(@Nullable OnItemClickListener> onHeaderClickListener); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/model/BaseItem.java b/adapster/src/main/java/com/arthurivanets/adapster/model/BaseItem.java new file mode 100755 index 0000000..474205c --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/model/BaseItem.java @@ -0,0 +1,167 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.model; + +import android.view.View; + +import com.arthurivanets.adapster.Adapter; +import com.arthurivanets.adapster.markers.ItemResources; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * An abstract class for the Dataset Item implementation. + * + * @param item model type + * @param view holder type + * @param reusable resources type + * @author arthur3486 + */ +public abstract class BaseItem, IR extends ItemResources> implements Item { + + + public static final long NO_ID = -1L; + + + private IM mItemModel; + private Object mTag; + + + + + public BaseItem(IM itemModel) { + mItemModel = itemModel; + } + + + + + @CallSuper + @Override + public void bind(@Nullable Adapter adapter, + @NonNull VH viewHolder, + @Nullable IR resources) { + viewHolder.bindData(getItemModel()); + } + + + + + public long getId() { + return NO_ID; + } + + + + + public final void setItemModel(IM itemModel) { + mItemModel = itemModel; + } + + + + + public final IM getItemModel() { + return mItemModel; + } + + + + + public final BaseItem setTag(Object tag) { + mTag = tag; + return this; + } + + + + + public final Object getTag() { + return mTag; + } + + + + + public final boolean hasTag() { + return (mTag != null); + } + + + + + @Override + public int hashCode() { + return ((getItemModel() != null) ? getItemModel().hashCode() : super.hashCode()); + } + + + + + @Override + public boolean equals(Object obj) { + return ((obj instanceof BaseItem) && (obj.hashCode() == hashCode())); + } + + + + + /** + * An abstract Item ViewHolder class that's to be implemented by every concrete Item View Holder implementation. + * + * @param the type of the data represented by the host Item. + */ + public abstract static class ViewHolder extends RecyclerView.ViewHolder { + + private Data mBoundData; + + + public ViewHolder(View itemView) { + super(itemView); + } + + + /** + * Binds the data to the UI. + * + * @param data the data item to base the binding on + */ + @CallSuper + public void bindData(Data data) { + mBoundData = data; + } + + + /** + * Retrieves the data item associated with this View Holder. + * + * @return the associated data item + */ + public Data getBoundData() { + return mBoundData; + } + + + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/model/Item.java b/adapster/src/main/java/com/arthurivanets/adapster/model/Item.java new file mode 100755 index 0000000..b93d17c --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/model/Item.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.model; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.arthurivanets.adapster.Adapter; +import com.arthurivanets.adapster.markers.ItemResources; + +import java.io.Serializable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * A base contract-interface to be implemented by the dataset item. + * + * @param item view holder type + * @param reusable resources type + * @author arthur3486 + */ +public interface Item extends Serializable { + + int VIEW_TYPE_INVALID = -1; + + + /** + * Initializes the Item View Holder. + * + * @param adapter the adapter + * @param parent parent view + * @param inflater layout inflater + * @param resources reusable resources + * @return the created Item View Holder + */ + @NonNull + VH init(@Nullable Adapter adapter, + @NonNull ViewGroup parent, + @NonNull LayoutInflater inflater, + @Nullable IR resources); + + + /** + * Binds the data. + * + * @param adapter the adapter + * @param viewHolder item view holder + * @param resources reusable resources + */ + void bind(@Nullable Adapter adapter, + @NonNull VH viewHolder, + @Nullable IR resources); + + + /** + * @return the Layout Id belonging to this particular Item. + * (A Unique ID used to identify the View Type of this Item) + */ + int getLayout(); + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Captionable.java b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Captionable.java new file mode 100755 index 0000000..2b7bfc7 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Captionable.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.model.markers; + +import androidx.annotation.NonNull; + +/** + * A marker interface that's to implemented by the Items that are considered captionable. + * + * @author arthur3486 + */ +public interface Captionable { + + /** + * Retrieves the captions from the corresponding item. + * + * @return the retrieved caption + */ + @NonNull + String getCaption(); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Footer.java b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Footer.java new file mode 100755 index 0000000..ca025d3 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Footer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.model.markers; + +import com.arthurivanets.adapster.listeners.OnItemClickListener; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * To be implemented by the Items that need to be treated as Footer. + * + * @param the view holder type + * @author arthur3486 + */ +public interface Footer { + + /** + * Sets the click listener responsible for handling the Footer Item click events. + * + * @param viewHolder + * @param onItemClickListener + */ + void setOnItemClickListener(@NonNull VH viewHolder, @Nullable OnItemClickListener> onItemClickListener); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Header.java b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Header.java new file mode 100755 index 0000000..7471286 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Header.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.model.markers; + +import com.arthurivanets.adapster.listeners.OnItemClickListener; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * To be implemented by the Items that need to be treated as Header. + * + * @param the view holder type + * @author arthur3486 + */ +public interface Header { + + /** + * Sets the click listener responsible for handling the Header Item click events. + * + * @param viewHolder + * @param onItemClickListener + */ + void setOnItemClickListener(@NonNull VH viewHolder, @Nullable OnItemClickListener> onItemClickListener); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Themable.java b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Themable.java new file mode 100755 index 0000000..010a2a9 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Themable.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.model.markers; + +/** + * A marker interface that's to be implemented by the Item's View Holder, to enable its theming. + * + * @param Theme item type + * @author arthur3486 + */ +public interface Themable { + + /** + * Applies the theme to the Item View Holder. + * + * @param theme the theme to be applied + */ + void applyTheme(T theme); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Trackable.java b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Trackable.java new file mode 100755 index 0000000..66a6571 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/model/markers/Trackable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.model.markers; + +import androidx.annotation.NonNull; + +/** + * A marker class that's to be implemented by the Items that need to be made "unique" within the Trackable Adapters. + * + * @param key type (e.g. {@link Integer}, {@link String}, etc.) + * @author arthur3486 + */ +public interface Trackable { + + /** + * Retrieves the track key associated with this {@link Trackable}. + * + * @return the track key + */ + @NonNull + KT getTrackKey(); + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/recyclerview/BaseRecyclerViewAdapter.java b/adapster/src/main/java/com/arthurivanets/adapster/recyclerview/BaseRecyclerViewAdapter.java new file mode 100755 index 0000000..30dd885 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/recyclerview/BaseRecyclerViewAdapter.java @@ -0,0 +1,689 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.recyclerview; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.arthurivanets.adapster.Adapter; +import com.arthurivanets.adapster.listeners.OnDatasetChangeListener; +import com.arthurivanets.adapster.listeners.OnItemClickListener; +import com.arthurivanets.adapster.markers.SupportsFooter; +import com.arthurivanets.adapster.markers.SupportsHeader; +import com.arthurivanets.adapster.model.BaseItem; +import com.arthurivanets.adapster.model.markers.Footer; +import com.arthurivanets.adapster.model.markers.Header; +import com.arthurivanets.adapster.markers.ItemResources; +import com.arthurivanets.adapster.util.Preconditions; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +/** + * A base class for the implementation of any RecyclerView adapter. + * + * @param the dataset item type + * @param the item view holder type + * @author arthur3486 + */ +public abstract class BaseRecyclerViewAdapter> extends RecyclerView.Adapter implements Adapter, + SupportsHeader, SupportsFooter { + + + private Context mContext; + private RecyclerView mRecyclerView; + + private IT mItem; + private List mItems; + + private final LayoutInflater mLayoutInflater; + private final Set, IT>> mOnDatasetChangeListeners; + + private OnItemClickListener> mOnHeaderClickListener; + private OnItemClickListener> mOnFooterClickListener; + + + + + public BaseRecyclerViewAdapter(@NonNull Context context, @NonNull List items) { + Preconditions.nonNull(context); + Preconditions.nonNull(items); + + mContext = context; + mItems = items; + mLayoutInflater = LayoutInflater.from(context); + mOnDatasetChangeListeners = new HashSet<>(); + } + + + + + @Override + public final void addItem(@NonNull IT item) { + addItem(item, true); + } + + + + + @Override + public final void addItem(@NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.nonNull(item); + + addItem(mItems.size(), item, notifyAboutTheChange); + } + + + + + @Override + public final void addItem(int position, @NonNull IT item) { + addItem(position, item, true); + } + + + + + @Override + public final void addOrUpdateItem(@NonNull IT item) { + addOrUpdateItem(item, true); + } + + + + + @Override + public final void addOrUpdateItem(@NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.nonNull(item); + + addOrUpdateItem(mItems.size(), item, notifyAboutTheChange); + } + + + + + @Override + public final void addOrUpdateItem(int position, @NonNull IT item) { + addOrUpdateItem(position, item, true); + } + + + + + @Override + public final void updateItem(@NonNull IT item) { + final int itemIndex = indexOf(item); + + if(itemIndex != -1) { + updateItem(itemIndex); + } + } + + + + + @Override + public final void updateItem(int position) { + Preconditions.withinBoundsExclusive(position, mItems); + + notifyItemChanged(position); + notifyItemUpdated(getItem(position)); + } + + + + + /** + * Updates the specified range of items. + * (Notifies the adapter about the need to perform the UI update on the specified items) + * + * @param startPosition + * @param itemCount + */ + public final void updateItems(int startPosition, int itemCount) { + Preconditions.withinBoundsExclusive(startPosition, mItems); + + notifyItemRangeChanged(startPosition, itemCount); + } + + + + + @Override + public final void updateItemWith(@NonNull IT item) { + updateItemWith(item, true); + } + + + + + @Override + public final void updateItemWith(@NonNull IT item, boolean notifyAboutTheChange) { + final int itemIndex = indexOf(item); + + if(itemIndex != -1) { + updateItemWith(itemIndex, item, notifyAboutTheChange); + } + } + + + + + @Override + public final void updateItemWith(int position, @NonNull IT item) { + updateItemWith(position, item, true); + } + + + + + @Override + public abstract void updateItemWith(int position, @NonNull IT item, boolean notifyAboutTheChange); + + + + + @Override + public final void deleteItem(@NonNull IT item) { + final int itemIndex = indexOf(item); + + if(itemIndex != -1) { + deleteItem(itemIndex); + } + } + + + + + /** + * + *
+ * Implements the default functionality. For more complex presence checks + * (like with a dedicated HashMap for Item tracking), please make sure + * to override this method and add the necessary functionality. + *
+ * + */ + @Override + public boolean contains(@NonNull IT item) { + Preconditions.nonNull(item); + return mItems.contains(item); + } + + + + + /** + * + *
+ * Implements the default functionality. For more complex index determining + * (like with a dedicated HashMap for Item tracking), please make sure + * to override this method and add the necessary functionality. + *
+ * + */ + @Override + public int indexOf(@NonNull IT item) { + Preconditions.nonNull(item); + return mItems.indexOf(item); + } + + + + + @Override + public final int lastIndex() { + return (mItems.size() - 1); + } + + + + + /** + * Notifies the Dataset Change Observers about the addition of a new item. + * + * @param item the added item + */ + protected final void notifyItemAdded(@NonNull IT item) { + Preconditions.nonNull(item); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onItemAdded(mItems, item); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the item update. + * + * @param item the updated item + */ + protected final void notifyItemUpdated(@NonNull IT item) { + Preconditions.nonNull(item); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onItemUpdated(mItems, item); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the replacement of the item. + * + * @param oldItem the old item that got replaced + * @param newItem the new replacement item + */ + protected final void notifyItemReplaced(@NonNull IT oldItem, @NonNull IT newItem) { + Preconditions.nonNull(oldItem); + Preconditions.nonNull(newItem); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onItemReplaced(mItems, oldItem, newItem); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the deletion of the item. + * + * @param item the deleted item + */ + protected final void notifyItemDeleted(@NonNull IT item) { + Preconditions.nonNull(item); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onItemDeleted(mItems, item); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the Dataset size changes. + * + * @param oldSize + * @param newSize + */ + protected final void notifyDatasetSizeChanged(int oldSize, int newSize) { + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onDatasetSizeChanged(oldSize, newSize); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the replacement of the dataset. + * + * @param newDataset the new replacement dataset + */ + protected final void notifyDatasetReplaced(@NonNull List newDataset) { + Preconditions.nonNull(newDataset); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onDatasetReplaced(newDataset); + } + } + + + + + /** + * Notifies the Dataset Change Observers about the clearing of the dataset. + * + * @param dataset the cleared dataset + */ + protected final void notifyDatasetCleared(@NonNull List dataset) { + Preconditions.nonNull(dataset); + + for(OnDatasetChangeListener, IT> listener : mOnDatasetChangeListeners) { + listener.onDatasetCleared(dataset); + } + } + + + + + /** + * You'll need to use this method instead of the .notifyDataSetChanged() + * in most of the cases, as this method updates only "dynamic" items, + * while the Header and Footer(if either of them is present) + * remain untouched.(Acts as an updateItem() method applied to the whole dataset) + */ + public final void notifyItemsChanged() { + final int itemCount = getItemCount(); + + // if the dataset is empty, returning(as there's no data to be processed) + if(itemCount == 0) { + return; + } + + // preparing the initial indices + int startIndex = 0; + int endIndex = (itemCount - 1); + + // adjusting the start index(if there's a Header present), + // as we don't need to touch the Header itself. + if(getItem(startIndex) instanceof Header) { + startIndex++; + } + + // adjusting the end index(if there's a Footer present), + // as we don't need to touch the Footer itself. + if(getItem(endIndex) instanceof Footer) { + endIndex--; + } + + // calculating the final(to be updated) item count + final int adjustedItemCount = (endIndex - startIndex + 1); + + // updating the necessary("dynamic") items(if necessary) + if(adjustedItemCount > 0) { + updateItems(startIndex, adjustedItemCount); + } + } + + + + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + } + + + + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + mRecyclerView = null; + } + + + + + @Override + public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return onCreateViewHolder(parent, viewType, mItem); + } + + + + + /** + * Performs the Item View(and ViewHolder) initialization. + */ + @SuppressWarnings("unchecked") + protected VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType, @NonNull IT item) { + return (VH) item.init(this, parent, mLayoutInflater, getResources()); + } + + + + + @SuppressWarnings("unchecked") + @CallSuper + @Override + public void onBindViewHolder(@NonNull VH holder, int position) { + final IT item = getItem(position); + + // performing the data binding + item.bind(this, holder, getResources()); + + // allowing the extenders to assign listeners(if necessary) + assignListeners(holder, position, item); + } + + + + + /** + * Gets called when it's the right time to assign the Listeners to the + * corresponding item. Override it only if you need to provide the Listener settings functionality. + */ + @SuppressWarnings("unchecked") + @CallSuper + protected void assignListeners(@NonNull VH holder, int position, @NonNull IT item) { + //assigning the default listeners + if(item instanceof Header) { + ((Header) item).setOnItemClickListener(holder, mOnHeaderClickListener); + } else if(item instanceof Footer) { + ((Footer) item).setOnItemClickListener(holder, mOnFooterClickListener); + } + } + + + + + @Override + public final void addOnDatasetChangeListener(@NonNull OnDatasetChangeListener, IT> onDatasetChangeListener) { + Preconditions.nonNull(onDatasetChangeListener); + + mOnDatasetChangeListeners.add(onDatasetChangeListener); + } + + + + + @Override + public final void removeOnDatasetChangeListener(@NonNull OnDatasetChangeListener, IT> onDatasetChangeListener) { + Preconditions.nonNull(onDatasetChangeListener); + + mOnDatasetChangeListeners.remove(onDatasetChangeListener); + } + + + + + @Override + public final void removeAllOnDatasetChangeListeners() { + mOnDatasetChangeListeners.clear(); + } + + + + + @Override + public final void setOnHeaderClickListener(OnItemClickListener> onHeaderClickListener) { + mOnHeaderClickListener = onHeaderClickListener; + } + + + + + @Override + public final void setOnFooterClickListener(OnItemClickListener> onFooterClickListener) { + mOnFooterClickListener = onFooterClickListener; + } + + + + + @CallSuper + @Override + public void setItems(@NonNull List items) { + setItems(items, true); + } + + + + + @CallSuper + @Override + public void setItems(@NonNull List items, boolean notifyAboutTheChange) { + Preconditions.nonNull(items); + + final int itemCount = getItemCount(); + mItems = items; + + if(notifyAboutTheChange) { + notifyDataSetChanged(); + } + + notifyDatasetReplaced(mItems); + notifyDatasetSizeChanged(itemCount, getItemCount()); + } + + + + + /** + * Sets the underlying dataset. + * (Uses the specified DiffUtils.Callback to calculate the item changes and dispatch the updates to the adapter) + * + * @param items + * @param callback + */ + public abstract void setItems(@NonNull List items, @NonNull DiffUtil.Callback callback); + + + + + @NonNull + public final Context getContext() { + return mContext; + } + + + + + @Nullable + public final RecyclerView getRecyclerView() { + return mRecyclerView; + } + + + + + @NonNull + @Override + public final List getItems() { + return mItems; + } + + + + + @Override + public final int getItemViewType(int position) { + mItem = getItem(position); + return getItemViewType(position, mItem); + } + + + + + public abstract int getItemViewType(int position, IT item); + + + + + @Nullable + @Override + public final IT getItem(int position) { + return (((position >= 0) && (position < getItemCount())) ? mItems.get(position) : null); + } + + + + + @Override + public final IT getFirstItem() { + return getItem(0); + } + + + + + @Override + public final IT getLastItem() { + return getItem(lastIndex()); + } + + + + + @Override + public long getItemId(int position) { + final long itemId = getItem(position).getId(); + return ((itemId != BaseItem.NO_ID) ? itemId : position); + } + + + + + /** + * Implements the default Item Count retrieval with the default NPE check. + * + * @return If dataset is NOT NULL -> dataset.size(), else 0 + */ + @Override + public final int getItemCount() { + return mItems.size(); + } + + + + + /** + * Retrieves the reusable {@link ItemResources} associated with the current adapter. + * + * @return the reusable {@link ItemResources} associated with the current adapter, or null if no resources have been associated. + */ + @Nullable + public ItemResources getResources() { + return null; + } + + + + + /** + * Calculates the difference between the datasets and dispatches the calculated data to the adapter. + * + * @param callback difference calculation helper + * @param beforeDispatch a callback to be called in-between the calculation of the difference + * and the actual dispatching of the updates to the adapter + */ + protected final void applyDiffUtils(DiffUtil.Callback callback, Runnable beforeDispatch) { + final DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback); + + beforeDispatch.run(); + + result.dispatchUpdatesTo(this); + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/recyclerview/TrackableRecyclerViewAdapter.java b/adapster/src/main/java/com/arthurivanets/adapster/recyclerview/TrackableRecyclerViewAdapter.java new file mode 100755 index 0000000..e6d815d --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/recyclerview/TrackableRecyclerViewAdapter.java @@ -0,0 +1,424 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.recyclerview; + +import android.content.Context; + +import com.arthurivanets.adapster.model.BaseItem; +import com.arthurivanets.adapster.model.Item; +import com.arthurivanets.adapster.model.markers.Footer; +import com.arthurivanets.adapster.model.markers.Header; +import com.arthurivanets.adapster.model.markers.Trackable; +import com.arthurivanets.adapster.util.Preconditions; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; + +/** + * An abstract implementation of the RecyclerView Adapter which allows for the easy item tracking + * and prevention of the duplicates. (To prevent the item duplication an item must implement the {@link Trackable} interface + * and provide a unique track key) + * + * @param the item key type + * @param the item type + * @param the item view holder type + * @author arthur3486 + */ +public abstract class TrackableRecyclerViewAdapter> extends BaseRecyclerViewAdapter { + + + private final Map> mKeyTrackableMap; + + + + + public TrackableRecyclerViewAdapter(@NonNull Context context, @NonNull List items) { + super(context, items); + mKeyTrackableMap = new HashMap<>(); + setItems(items); + } + + + + + @Override + public void addItem(int position, @NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.withinBoundsInclusive(position, getItems()); + Preconditions.nonNull(item); + + // in case the item is already present, returning + if(contains(item)) { + return; + } + + // processing the item + final int itemCount = getItemCount(); + final boolean isFirstItemHeaderView = ((itemCount > 0) && (getItem(0) instanceof Header)); + final boolean isLastItemFooterView = ((itemCount > 0) && (getItem(itemCount - 1) instanceof Footer)); + + if(item instanceof Header) { + if(isFirstItemHeaderView) { + throw new IllegalStateException("Only a single Header can be present in a dataset at a time. Please, remove the old Header first, and then proceed with adding a new one."); + } + + getItems().add((position = 0), item); + } else if(item instanceof Footer) { + if(isLastItemFooterView) { + throw new IllegalStateException("Only a single Footer can be present in a dataset at a time. Please, remove the old Footer first, and then proceed with adding a new one."); + } + + getItems().add((position = itemCount), item); + } else { + if((position == 0) && isFirstItemHeaderView) { + getItems().add((position + 1), item); + } else if((position == itemCount) && isLastItemFooterView) { + getItems().add((position - 1), item); + } else { + getItems().add(position, item); + } + } + + // adding the item to the map of trackables (if necessary) + trackIfNecessary(item); + + // notifying the Adapter about the change (if necessary) + if(notifyAboutTheChange) { + notifyItemInserted(position); + } + + // notifying about the dataset change + notifyItemAdded(item); + notifyDatasetSizeChanged(itemCount, getItemCount()); + } + + + + + @Override + public void addOrUpdateItem(int position, @NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.withinBoundsInclusive(position, getItems()); + Preconditions.nonNull(item); + + if(contains(item)) { + updateItemWith(item, notifyAboutTheChange); + } else { + addItem(position, item, notifyAboutTheChange); + } + } + + + + + @Override + public void updateItemWith(int position, @NonNull IT item, boolean notifyAboutTheChange) { + Preconditions.withinBoundsExclusive(position, getItems()); + Preconditions.nonNull(item); + + final IT oldItem = getItem(position); + + // untracking the old item, as it no longer relates to this dataset + untrackIfNecessary(oldItem); + + // replacing the old item(if there was any) and retracking the item + getItems().set(position, item); + trackIfNecessary(item); + + // notifying the Adapter about the change(if necessary) + if(notifyAboutTheChange) { + notifyItemChanged(position); + } + + // notifying about the dataset change + notifyItemReplaced(oldItem, item); + } + + + + + @Override + public void deleteItem(int position) { + Preconditions.withinBoundsExclusive(position, getItems()); + + final int itemCount = getItemCount(); + + // removing the actual item, as well as untracking it (if necessary) + final IT removedItem = getItems().remove(position); + untrackIfNecessary(removedItem); + + // notifying about the change + notifyItemRemoved(position); + notifyItemDeleted(removedItem); + notifyDatasetSizeChanged(itemCount, getItemCount()); + } + + + + + @SuppressWarnings("unchecked") + @Override + public final void addHeader(@NonNull Header header) { + Preconditions.nonNull(header); + Preconditions.isTrue("The Header Item must be based on BaseItem", (header instanceof BaseItem)); + + addItem((IT) header); + } + + + + + @Override + public final void removeHeader() { + if((getItemCount() > 0) && (getItem(0) instanceof Header)) { + deleteItem(0); + } + } + + + + + @SuppressWarnings("unchecked") + @Override + public final void addFooter(@NonNull Footer footer) { + Preconditions.nonNull(footer); + Preconditions.isTrue("The Footer Item must be based on BaseItem", (footer instanceof BaseItem)); + + addItem((IT) footer); + } + + + + + @Override + public final void removeFooter() { + final int itemCount = getItemCount(); + + if((itemCount > 0) && (getItem(itemCount - 1) instanceof Footer)) { + deleteItem(itemCount - 1); + } + } + + + + + @SuppressWarnings("unchecked") + @Override + public boolean contains(@NonNull IT item) { + Preconditions.nonNull(item); + + if(item instanceof Trackable) { + return containsTrackable((Trackable) item); + } else { + return super.contains(item); + } + } + + + + + @SuppressWarnings("unchecked") + @Override + public int indexOf(@NonNull IT item) { + Preconditions.nonNull(item); + + if(item instanceof Trackable) { + final Trackable trackable = (Trackable) item; + + if(contains(item)) { + return getItems().indexOf(getTrackable(trackable.getTrackKey())); + } else { + return -1; + } + } else { + return super.indexOf(item); + } + } + + + + + /** + * Adds the specified items to the Map of {@link Trackable} items. + * The Items must implement the {@link Trackable} interface. + * + * @param items the item to be tracked + */ + protected final void trackIfNecessary(@NonNull List items) { + Preconditions.nonNull(items); + + for(IT item : items) { + trackIfNecessary(item); + } + } + + + + + /** + * Adds the specified item to the Map of {@link Trackable} items. + * The Item must implement the {@link Trackable} interface. + * + * @param item the item to be tracked + */ + @SuppressWarnings("unchecked") + protected final void trackIfNecessary(@NonNull IT item) { + Preconditions.nonNull(item); + + if(item instanceof Trackable) { + addTrackable((Trackable) item); + } + } + + + + + /** + * Removes the specified item from the map of {@link Trackable} items (if there's any). + * The Item must implement the {@link Trackable} interface. + * + * @param item the item to be untracked + */ + @SuppressWarnings("unchecked") + protected final void untrackIfNecessary(@NonNull IT item) { + Preconditions.nonNull(item); + + if(item instanceof Trackable) { + removeTrackable((Trackable) item); + } + } + + + + + /** + * Adds the specified {@link Trackable} to the Map of {@link Trackable} items. + * + * @param trackable trackable to be added + */ + protected final void addTrackable(@NonNull Trackable trackable) { + Preconditions.nonNull(trackable); + + mKeyTrackableMap.put(trackable.getTrackKey(), trackable); + } + + + + + /** + * Retrieves the {@link Trackable} from the Map of the {@link Trackable} items. + * + * @param key the {@link Trackable}'s key + * @return the corresponding {@link Trackable}, or null if no {@link Trackable} was found. + */ + @Nullable + public final Trackable getTrackable(@NonNull KT key) { + Preconditions.nonNull(key); + return mKeyTrackableMap.get(key); + } + + + + + /** + * Removes the {@link Trackable} from the Map of the {@link Trackable} items. + * + * @param trackable the trackable to be removed + */ + protected final void removeTrackable(@NonNull Trackable trackable) { + Preconditions.nonNull(trackable); + + mKeyTrackableMap.remove(trackable.getTrackKey()); + } + + + + + /** + * Checks whether the Map of {@link Trackable} items contains the specified {@link Trackable}. + * + * @param trackable + * @return true if contains, false otherwise + */ + protected final boolean containsTrackable(@NonNull Trackable trackable) { + Preconditions.nonNull(trackable); + return (mKeyTrackableMap.get(trackable.getTrackKey()) != null); + } + + + + + @Override + public final void setItems(@NonNull final List items, @NonNull DiffUtil.Callback callback) { + Preconditions.nonNull(items); + Preconditions.nonNull(callback); + + applyDiffUtils( + callback, + new Runnable() { + @Override + public void run() { + setItems(items, false); + } + } + ); + } + + + + + @Override + public final void setItems(@NonNull List items, boolean notifyAboutTheChange) { + Preconditions.nonNull(items); + + mKeyTrackableMap.clear(); + trackIfNecessary(items); + + super.setItems(items, notifyAboutTheChange); + } + + + + + @Override + public void clear() { + final int itemCount = getItemCount(); + + getItems().clear(); + mKeyTrackableMap.clear(); + + // notifying about the performed event + notifyDataSetChanged(); + notifyDatasetSizeChanged(itemCount, getItemCount()); + notifyDatasetCleared(getItems()); + } + + + + + @Override + public final int getItemViewType(int position, IT item) { + return ((item != null) ? item.getLayout() : Item.VIEW_TYPE_INVALID); + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/util/BinaryDataPackerUnpacker.java b/adapster/src/main/java/com/arthurivanets/adapster/util/BinaryDataPackerUnpacker.java new file mode 100755 index 0000000..2a34481 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/util/BinaryDataPackerUnpacker.java @@ -0,0 +1,83 @@ +package com.arthurivanets.adapster.util; + +/** + * Created by paul + */ +public final class BinaryDataPackerUnpacker { + + + private static final int OCTET_1_OFFSET = 0; + private static final int OCTET_2_OFFSET = 8; + private static final int OCTET_3_OFFSET = 16; + private static final int OCTET_4_OFFSET = 24; + + private static final int BASE_MASK = 0b1111_1111; + + + + + public static int one(int octet1) { + return two(octet1, 0); + } + + + + + public static int two(int octet1, int octet2) { + return three(octet1, octet2, 0); + } + + + + + public static int three(int octet1, int octet2, int octet3) { + return four(octet1, octet2, octet3, 0); + } + + + + + public static int four(int octet1, int octet2, int octet3, int octet4) { + final int adjustedOctet1 = (octet1 & BASE_MASK); + final int adjustedOctet2 = (octet2 & BASE_MASK); + final int adjustedOctet3 = (octet3 & BASE_MASK); + final int adjustedOctet4 = (octet4 & BASE_MASK); + + return ((adjustedOctet4 << OCTET_4_OFFSET) + | (adjustedOctet3 << OCTET_3_OFFSET) + | (adjustedOctet2 << OCTET_2_OFFSET) + | (adjustedOctet1 << OCTET_1_OFFSET)); + } + + + + + public static UnpackedData unpack(int packedData) { + return new UnpackedData(packedData); + } + + + + + public static class UnpackedData { + + public final int octet1; + public final int octet2; + public final int octet3; + public final int octet4; + + + private UnpackedData(int packedData) { + this.octet1 = ((packedData >> OCTET_1_OFFSET) & BASE_MASK); + this.octet2 = ((packedData >> OCTET_2_OFFSET) & BASE_MASK); + this.octet3 = ((packedData >> OCTET_3_OFFSET) & BASE_MASK); + this.octet4 = ((packedData >> OCTET_4_OFFSET) & BASE_MASK); + } + + + } + + + + +} diff --git a/adapster/src/main/java/com/arthurivanets/adapster/util/Preconditions.java b/adapster/src/main/java/com/arthurivanets/adapster/util/Preconditions.java new file mode 100755 index 0000000..470c7d9 --- /dev/null +++ b/adapster/src/main/java/com/arthurivanets/adapster/util/Preconditions.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017 Arthur Ivanets, arthur.ivanets.l@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arthurivanets.adapster.util; + +import android.text.TextUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import androidx.annotation.NonNull; + +/** + * @author arthur3486 + */ +public final class Preconditions { + + + + + public static void isTrue(boolean condition) { + isTrue("Condition", condition); + } + + + + + public static void isTrue(@NonNull String info, boolean condition) { + nonNull(info); + + if(!condition) { + throw new IllegalStateException(String.format( + Locale.US, + "%s - the condition is not met. The Condition must be positive.", + info + )); + } + } + + + + + public static void nonNull(Object object) { + if(object == null) { + throw new NullPointerException("The argument must be non-null!"); + } + } + + + + + public static void nonEmpty(@NonNull String string) { + if(TextUtils.isEmpty(string)) { + throw new IllegalArgumentException("You must specify a valid raw text."); + } + } + + + + + public static void nonEmpty(@NonNull Collection collection) { + nonNull(collection); + + if(collection.isEmpty()) { + throw new IllegalArgumentException("You must specify a collection that contains at least one element."); + } + } + + + + + public static void withinBoundsExclusive(int index, @NonNull List dataset) { + nonNull(dataset); + + if((index < 0) || (index >= dataset.size())) { + throw new IndexOutOfBoundsException("The Index must lie within the bounds of the specified dataset (0 <= index < dataset.size)."); + } + } + + + + + public static void withinBoundsInclusive(int index, @NonNull List dataset) { + nonNull(dataset); + + if((index < 0) || (index > dataset.size())) { + throw new IndexOutOfBoundsException("The Index must lie within the bounds of the specified dataset (0 <= index <= dataset.size)."); + } + } + + + + +} diff --git a/build.gradle.kts b/build.gradle.kts index 394a497..bda4fbd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,7 +45,6 @@ allprojects { repositories { mavenCentral() google() - maven { url = uri("https://jitpack.io") } } } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 811f8b1..276c83a 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -64,7 +64,6 @@ object versions { const val cardView = "1.0.0" const val browser = "1.8.0" const val recyclerView = "1.3.2" - const val adapster = "1.0.13" const val annotations = "1.8.1" const val coreKtx = "1.13.1" const val commonsKtx = "1.0.4" @@ -84,6 +83,8 @@ object deps { } object local { + + const val adapster = ":adapster" const val persistentSearchView = ":persistentsearchview" } @@ -91,7 +92,6 @@ object deps { const val cardView = "androidx.cardview:cardview:${versions.cardView}" const val browser = "androidx.browser:browser:${versions.browser}" const val recyclerView = "androidx.recyclerview:recyclerview:${versions.recyclerView}" - const val adapster = "com.github.arthur3486.adapster:adapster:${versions.adapster}" const val annotations = "androidx.annotation:annotation:${versions.annotations}" const val coreKtx = "androidx.core:core-ktx:${versions.coreKtx}" const val commonsKtx = "com.paulrybitskyi.commons:commons-ktx:${versions.commonsKtx}" diff --git a/persistentsearchview/build.gradle.kts b/persistentsearchview/build.gradle.kts index 9bfdb63..d186fab 100644 --- a/persistentsearchview/build.gradle.kts +++ b/persistentsearchview/build.gradle.kts @@ -19,25 +19,17 @@ plugins { } android { + namespace = "com.paulrybitskyi.persistentsearchview" compileSdk = appConfig.compileSdkVersion - publishing { - singleVariant(publishingConfig.mavenPublicationName) { - withJavadocJar() - withSourcesJar() - } - } - defaultConfig { - namespace = "com.paulrybitskyi.persistentsearchview" minSdk = appConfig.minSdkVersion - targetSdk = appConfig.targetSdkVersion testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { - getByName("release") { + release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } @@ -47,10 +39,17 @@ android { sourceCompatibility = appConfig.javaCompatibilityVersion targetCompatibility = appConfig.javaCompatibilityVersion } + + publishing { + singleVariant(publishingConfig.mavenPublicationName) { + withJavadocJar() + withSourcesJar() + } + } } dependencies { - api(deps.adapster) + api(project(deps.local.adapster)) implementation(deps.appCompat) implementation(deps.cardView) diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index bffa532..eb773da 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -20,11 +20,11 @@ plugins { } android { + namespace = appConfig.applicationId compileSdk = appConfig.compileSdkVersion defaultConfig { applicationId = appConfig.applicationId - namespace = appConfig.applicationId minSdk = appConfig.minSdkVersion targetSdk = appConfig.targetSdkVersion @@ -32,7 +32,7 @@ android { } buildTypes { - getByName("release") { + release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } @@ -59,7 +59,6 @@ dependencies { implementation(deps.cardView) implementation(deps.browser) implementation(deps.recyclerView) - implementation(deps.adapster) implementation(deps.coreKtx) implementation(deps.commonsKtx) diff --git a/settings.gradle.kts b/settings.gradle.kts index fda4029..790f5b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,5 @@ rootProject.name = "PersistentSearchView" +include(":adapster") +include(":persistentsearchview") include(":sample") -include(":persistentsearchview") \ No newline at end of file