Skip to content

Instantly share code, notes, and snippets.

@vkay94
Last active January 21, 2021 15:31
Show Gist options
  • Save vkay94/78f6fb2126cf2b39edda483f316aa3c1 to your computer and use it in GitHub Desktop.
Save vkay94/78f6fb2126cf2b39edda483f316aa3c1 to your computer and use it in GitHub Desktop.
NewPipe Local lists with Groupie
/**
* This fragment is design to be used with persistent data such as
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained
* in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle.
* <p>
* This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is
* called and is memory efficient when in backstack.
* </p>
*
* @param <I> List of {@link org.schabi.newpipe.database.LocalItem}s
* @param <N> {@link Void}
*/
public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, SharedPreferences.OnSharedPreferenceChangeListener {
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
protected LocalListGroupAdapter<I> itemListAdapter;
protected RecyclerView itemsList;
private int updateFlags = 0;
protected DateTimeFormatter dateTimeFormatter;
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle - Creation
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
PreferenceManager.getDefaultSharedPreferences(activity)
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onDestroy() {
super.onDestroy();
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onResume() {
super.onResume();
if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
itemListAdapter.useGrid(requireContext(), isGridLayout(), getSpanSize(), itemsList);
itemListAdapter.updateItemVersion(
isGridLayout() ? ItemVersion.GRID : ItemVersion.NORMAL
);
}
updateFlags = 0;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle - View
//////////////////////////////////////////////////////////////////////////*/
// Loading
protected Item<GroupieViewHolder> getLoadingListFooter() {
return new LoadingItem();
}
protected int getSpanSize() {
final Resources resources = activity.getResources();
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
width += (24 * resources.getDisplayMetrics().density);
return (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double) width);
}
public abstract List<Item<GroupieViewHolder>> fromLocalItemsToGroupie(I items);
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Localization.getPreferredLocale(requireContext()));
itemListAdapter = new LocalListGroupAdapter<>(this::fromLocalItemsToGroupie);
itemsList = rootView.findViewById(R.id.items_list);
itemListAdapter.useGrid(requireContext(), isGridLayout(), getSpanSize(), itemsList);
itemsList.setAdapter(itemListAdapter);
// Set loading indicator as default footer
itemListAdapter.setFooter(getLoadingListFooter());
}
@Override
protected void initListeners() {
super.initListeners();
}
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle - Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
final ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar == null) {
return;
}
supportActionBar.setDisplayShowTitleEnabled(true);
}
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle - Destruction
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onDestroyView() {
super.onDestroyView();
itemsList = null;
itemListAdapter = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
}
@Override
public void showLoading() {
super.showLoading();
// if (itemsList != null) {
// animateView(itemsList, false, 200);
// }
// Not required since the header is within the itemListAdapter
// if (headerRootView != null) {
// animateView(headerRootView, false, 200);
// }
}
@Override
public void hideLoading() {
super.hideLoading();
showListFooter(false);
// if (itemsList != null) {
// animateView(itemsList, true, 200);
// }
// Not required since the footer is within the itemListAdapter
// if (headerRootView != null) {
// animateView(headerRootView, true, 200);
// }
}
@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
// if (itemsList != null) {
// animateView(itemsList, false, 200);
// }
// Not required since the header is within the itemListAdapter
// if (headerRootView != null) {
// animateView(headerRootView, false, 200);
// }
}
@Override
public void showEmptyState() {
super.showEmptyState();
showListFooter(false);
}
@Override
public void showListFooter(final boolean show) {
if (itemsList == null) {
return;
}
itemsList.post(() -> {
if (itemListAdapter != null) {
itemListAdapter.showFooter(show);
}
});
}
@Override
public void handleNextItems(final N result) {
isLoading.set(false);
}
/*//////////////////////////////////////////////////////////////////////////
// Error handling
//////////////////////////////////////////////////////////////////////////*/
protected void resetFragment() {
// TODO: Do not clear items always, better: update items if needed in those places
if (itemListAdapter != null) {
itemListAdapter.clearItems();
}
}
@Override
protected boolean onError(final Throwable exception) {
// resetFragment();
itemListAdapter.clearItems();
return super.onError(exception);
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) {
if (key.equals(getString(R.string.list_view_mode_key))) {
updateFlags |= LIST_MODE_UPDATE_FLAG;
}
}
protected boolean isGridLayout() {
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity)
.getString(getString(R.string.list_view_mode_key),
getString(R.string.list_view_mode_value));
if ("auto".equals(listMode)) {
final Configuration configuration = getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
} else {
return "grid".equals(listMode);
}
}
}
abstract class DynamicGridItem : Item<GroupieViewHolder>() {
var itemVersion: ItemVersion = ItemVersion.NORMAL
@get:LayoutRes
abstract val normalLayout: Int
@get:LayoutRes
abstract val miniLayout: Int
@get:LayoutRes
abstract val gridLayout: Int
override fun getLayout(): Int = when (itemVersion) {
ItemVersion.NORMAL -> normalLayout
ItemVersion.MINI -> miniLayout
ItemVersion.GRID -> gridLayout
}
override fun getSpanSize(spanCount: Int, position: Int): Int {
return if (itemVersion == ItemVersion.GRID) 1 else spanCount
}
}
/**
* Custom GroupAdapter which provides a header, item and footer section
*/
class LocalListGroupAdapter<T>(
private val convertItems: (result: T) -> List<Item<GroupieViewHolder>>
) : GroupAdapter<GroupieViewHolder>() {
private val DEBUG = MainActivity.DEBUG
private val TAG = javaClass.simpleName
private val itemsSection = Section()
private val headerSection = Section()
private val footerSection = Section()
// Required for showFooter(show)
private var header: Group? = null
private var footer: Group? = null
private var showFooter = false
private var itemVersion = ItemVersion.NORMAL
fun itemsSection() = itemsSection
fun isGrid() = itemVersion == ItemVersion.GRID
init {
add(headerSection)
add(itemsSection)
add(footerSection)
}
fun setHeader(header: Item<GroupieViewHolder>?) {
if (header == null) {
if (DEBUG) Log.e(TAG, "setHeader(header): Header is null")
return
}
clearHeader()
this.header = header
headerSection.add(header)
}
fun clearHeader() {
headerSection.clear()
header = null
}
fun setFooter(footer: Item<GroupieViewHolder>?) {
if (footer == null) {
if (DEBUG) Log.e(TAG, "setFooter(footer): Footer is null")
return
}
clearFooter()
this.footer = footer
// footerSection.add(footer)
}
fun clearFooter() {
footerSection.clear()
footer = null
showFooter = false
}
fun showFooter(show: Boolean) {
if (DEBUG) Log.d(TAG, "showFooter(show) called with show = [$show]")
if (show && footer != null && footerSection.groups.indexOf(footer) == -1) {
footer?.let {
footerSection.add(it)
showFooter = true
}
} else {
clearFooter()
}
}
fun isItemListEmpty(): Boolean = itemsSection.itemCount == 0
fun updateItems(items: T) {
updateItems(items, false)
}
fun updateItems(items: T, refresh: Boolean) {
when {
isItemListEmpty() -> {
itemsSection.addAll(convertItems.invoke(items))
}
refresh -> {
// itemsSection.clear()
// itemsSection.addAll(convertItems.invoke(items))
itemsSection.refresh(convertItems.invoke(items))
}
else -> {
itemsSection.update(convertItems.invoke(items))
}
}
}
fun addMoreItems(items: T) {
itemsSection.addAll(convertItems.invoke(items))
}
fun clearItems() {
itemsSection.clear()
}
fun swapItems(viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val item = this.getItem(viewHolder.adapterPosition)
val targetItem = this.getItem(target.adapterPosition)
val currentItems = itemsSection.groups
var targetIndex = currentItems.indexOf(targetItem)
currentItems.remove(item)
if (targetIndex == -1) {
targetIndex = if (target.adapterPosition < viewHolder.adapterPosition)
0
else
currentItems.size - 1
}
currentItems.add(targetIndex, item)
itemsSection.update(currentItems)
return true
}
fun itemIndexOf(item: Item<GroupieViewHolder>): Int {
// TODO: With adapterPosition?
return itemsSection.getPosition(item)
}
fun useGrid(context: Context, shouldUseGrid: Boolean, spanSize: Int, recyclerView: RecyclerView) {
spanCount = if (shouldUseGrid) spanSize else 1
val newLayoutManager = GridLayoutManager(context, spanCount)
newLayoutManager.spanSizeLookup = spanSizeLookup
recyclerView.layoutManager = newLayoutManager
}
fun updateItemVersion(itemVersion: ItemVersion) {
this.itemVersion = itemVersion
val updatedList: List<Group> = itemsSection.groups.map {
if (it is DynamicGridItem) it.itemVersion = itemVersion
it
}
itemsSection.clear()
itemsSection.addAll(updatedList)
// itemsSection.update(updatedList)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment