Android RecyclerView

λ:

# 倉庫地址: https://github.com/lzyprime/android_demos/tree/recyclerview

git clone -b recyclerview https://github.com/lzyprime/android_demos

RecyclerView作 Android 列表項的展示組件鸟辅。相比ListView椿每,緩存機制做的更細致萧朝,提升流暢度略荡。以空間換時間

兩個重要參數(shù):

  1. LayoutManager: 排版
  2. RecyclerView.Adapter: 列表項獲取方式

LayoutManager

LayoutManager 可以在xml中直接配置. 也可在邏輯代碼中設(shè)置。

// xml
   <androidx.recyclerview.widget.RecyclerView
        ...
        // LayoutManager類型
        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
        // 幾欄
        app:spanCount="1"
        />

全部可配參數(shù):

1.png

1. LinearLayoutManager

public class LinearLayoutManager extends RecyclerView.LayoutManager implements
        ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider

單欄線性布局满哪。無法多欄展示婿斥。構(gòu)造函數(shù)參數(shù):

  1. orientation: 方向
  2. reverseLayout: 反轉(zhuǎn)劝篷,倒序列表項

stackFromEnd 用來兼容 android.widget.AbsListView.setStackFromBottom(boolean)。相當(dāng)于reverseLayout 的效果民宿。

同時實現(xiàn)了ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider

2.png

2. GridLayoutManager

public class GridLayoutManager extends LinearLayoutManager

網(wǎng)格布局娇妓。LinearLayoutManager 升級版,可以通過spanCount設(shè)置分幾欄

3.png

3. StaggeredGridLayoutManager

public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements
        RecyclerView.SmoothScroller.ScrollVectorProvider

流布局活鹰。 當(dāng)列表項尺寸不一致時, GridLayoutManager 根據(jù)尺寸較大項確定網(wǎng)格尺寸哈恰。導(dǎo)致較小項會有空白部分。StaggeredGridLayoutManager 則緊湊拼接每一項华望。 通過 setGapStrategy(int) 設(shè)置間隙處理策略蕊蝗。

4.png

Adapter

RecyclerView.Adapter<VH : RecyclerView.ViewHolder>

public abstract static class Adapter<VH extends ViewHolder> {
    ...
    @NonNull
    public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType);

    public abstract void onBindViewHolder(@NonNull VH holder, int position);

    public abstract int getItemCount();
}

public abstract static class ViewHolder {
    public ViewHolder(@NonNull View itemView) { ... }
}

一個Adapter至少需要override這三個函數(shù)。

getItemCount

返回列表項的個數(shù)赖舟。

onCreateViewHolder, getItemViewType

創(chuàng)建一個ViewHolder, 如果 ViewHolder 有多種類型蓬戚,可以通過viewType參數(shù)判斷。 viewType 的值來自 getItemViewType(position: Int) 函數(shù)宾抓。默認返回0子漩。 0 <= position < getItemCount()

以聊天消息為例:

sealed class Msg {
    data class Text(val content: String) : Msg()
    data class Image(val url: String) : Msg()
    data class Video(...) : Msg()
    ...
}

class MsgListAdapter : RecyclerView.Adapter<MsgListAdapter.MsgViewHolder>() {
    sealed class MsgViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        class Text(...) : MsgViewHolder(...)
        class Image(...) : MsgViewHolder(...)
        ...
    }

    private var dataList: List<Msg> = listOf()

    override fun getItemViewType(position: Int): Int =
        when (dataList[position]) {
            is Msg.Text -> 1
            is Msg.Image -> 2
            ...
        }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MsgViewHolder =
        when (viewType) {
            1 -> MsgViewHolder.Text(...)
            2 -> MsgViewHolder.Image(...)
            ...
        }
}

onBindViewHolder

View 創(chuàng)建完成,開始綁定數(shù)據(jù)石洗。包括事件監(jiān)聽注冊幢泼。

class VBViewHolder<VB : ViewBinding>(private val binding : VB) : ViewHolder(binding.root) {
    fun bind(data: T, onClick:() -> Unit) {
        binding.data = data
        ...
        binding.anyView.setOnClickListener { onClick() }
        ...
    }
}

class Adapter(private val onItemClick: () -> Unit) : RecyclerView.Adapter<VBViewHolder<XXX>>() {
    override fun onBindViewHolder(holder: VBViewHolder<XXX>, position: Int) =
        holder.bindHolder(dataList[position], onItemClick)
}

更新

由于緩存機制,更新完數(shù)據(jù)源, ViewHolder 也并不會立刻刷新讲衫。需要通過Adapter的一系列方法缕棵,顯式通知發(fā)生變化的列表項。

  • notifyDataSetChanged()
  • notifyItemChanged(position: Int), notifyItemChanged(position: Int, payload: Any?)
  • notifyItemRangeChanged(positionStart: Int, itemCount: Int), notifyItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?)
  • notifyItemMoved(fromPosition: Int, toPosition: Int)
  • notifyItemInserted(position: Int)
  • notifyItemRangeInserted(positionStart: Int, itemCount: Int)
  • notifyItemRemoved(position: Int)
  • notifyItemRangeRemoved(positionStart: Int, itemCount: Int)

payload: Any? 要配合 AdapteronBindViewHolder(holder: VH, position: Int, payloads: MutableList<Any>) 實現(xiàn) View 的局部刷新涉兽。否則招驴,執(zhí)行 onBindViewHolder(holder: VBViewHolder<VH>, position: Int)

緩存機制

主要邏輯在 RecyclerView.Recycler。 緩存主要有 Scrap, CachedView, RecycledViewPool枷畏。 ViewCacheExtension 用于額外自定義緩存别厘。

  • Scrap: 當(dāng)前正在展示的部分。
  • CachedView: 剛劃出展示區(qū)域的部分拥诡,默認最大存儲 DEFAULT_CACHE_SIZE = 2触趴。 FIFO更新
  • RecycledViewPool: CachedView 淘汰后,只保留 ViewHolder, 清空數(shù)據(jù)綁定渴肉。 復(fù)用時需要重新執(zhí)行onBindViewHolder冗懦。

RecycledViewPool 內(nèi)部是一個SparseArray<ScrapData> 下標為 holder.viewTypeScrapData 內(nèi)嵌ArrayList<ViewHolder>, 默認最大存儲 DEFAULT_MAX_SCRAP = 5ViewHolder宾娜。 所以簡化一下RecycledViewPool ~= SparseArray<ArrayList<ViewHolder>>批狐。

public final class Recycler {
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

    private final List<ViewHolder>
            mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

    private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
    int mViewCacheMax = DEFAULT_CACHE_SIZE;

    RecycledViewPool mRecyclerPool;

    private ViewCacheExtension mViewCacheExtension;

    static final int DEFAULT_CACHE_SIZE = 2;

    ...
}
public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;

    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }

    SparseArray<ScrapData> mScrap = new SparseArray<>();

    private int mAttachCount = 0;
    ...
}

取, getViewForPosition

跟一下該函數(shù)就大概知道各級緩存如何配合。

@NonNull
public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
@Nullable
RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    ...
    boolean fromScrapOrHiddenOrCache = false;
    RecyclerView.ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        ...
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ...
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            ...
        }
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not
            // know it.
            final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
            ...
        }
        if (holder == null) { // fallback to pool
            ...
            holder = getRecycledViewPool().getRecycledView(type);
            ...
        }
        if (holder == null) {
            ...
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }
    }
    ...

    return holder;
}
  • getChangedScrapViewForPosition
  • getScrapOrHiddenOrCachedHolderForPosition
  • getScrapOrCachedViewForId
  • mViewCacheExtension.getViewForPositionAndType
  • getRecycledViewPool().getRecycledView(type)
  • mAdapter.createViewHolder(RecyclerView.this, type)

放前塔,recycleView

跟一下該函數(shù)嚣艇,了解放入緩存過程和策略

public void recycleView(@NonNull View view) {
    ViewHolder holder = getChildViewHolderInt(view);
    ... // 清空flag

    recycleViewHolderInternal(holder);
    ...
}

void recycleViewHolderInternal(ViewHolder holder) {
    ... 
    final boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();
    @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter != null && transientStatePreventsRecycling && mAdapter.onFailedToRecycleView(holder);
    boolean cached = false;
    boolean recycled = false;
    
    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(...)) {
            // Retire oldest cached view
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // when adding the view, skip past most recently prefetched views
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        ... // Log
    }
    // even if the holder is not removed, we still call this method so that it is removed
    // from view holder lists.
    mViewInfoStore.removeViewHolder(holder);
    if (!cached && !recycled && transientStatePreventsRecycling) {
        holder.mBindingAdapter = null;
        holder.mOwnerRecyclerView = null;
    }
}
  • mCachedViews.add(targetCacheIndex, holder)
  • addViewHolderToRecycledViewPool

簡化 & 封裝 & 工具

一個 Adapter 的實現(xiàn)皆撩,大多數(shù)時候只關(guān)注 onBindViewHolder 的過程溃列,以及數(shù)據(jù)更新時 notify 更新邏輯。剩下的操作蝗肪,基本是重復(fù)的寂屏。

ListAdapter

默認實現(xiàn)了fun getItemCount() = dataList.size()贰谣。

需要一個 DiffUtil.ItemCallback<T>,內(nèi)部構(gòu)造mDiffer: AsyncListDiffer<T>, 用于比較列表項的變化迁霎,然后自動刷新吱抚。

通過 submitList(List<T>?) 提交數(shù)據(jù)。

通過 getItem(position: Int): T = dataList[position] 獲取當(dāng)前位置對應(yīng)數(shù)據(jù)考廉。

省去了數(shù)據(jù)更新和notify的過程秘豹, 只需要關(guān)注onCreateViewHolder, onBindViewHolder

PS: 注意 submitList()和傳引用問題昌粤。 做數(shù)據(jù)比較時 previousList, currentList 以及 Item 的比較既绕,全是靠引用拿到,diff(previousList[index], currentList[index])涮坐。所以如果 submitList() 如果提交的同一份List凄贩, diff比較就會失效。

如果使用 Paging3 分頁庫, 在View層會有 PagingDataAdapter, 與 ListAdapter 類似袱讹。 將數(shù)據(jù)源 PagingData 等設(shè)置好后疲扎,列表便可以自動刷新,加載更多等捷雕。

public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
    final AsyncListDiffer<T> mDiffer;
    private final AsyncListDiffer.ListListener<T> mListener = ...;

    protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) { ... }
    protected ListAdapter(@NonNull AsyncDifferConfig<T> config) { ... }

    public void submitList(@Nullable List<T> list) { mDiffer.submitList(list); }
    public void submitList(@Nullable List<T> list, @Nullable final Runnable commitCallback) { mDiffer.submitList(list, commitCallback); }

    protected T getItem(int position) { return mDiffer.getCurrentList().get(position); }

    @Override public int getItemCount() { return mDiffer.getCurrentList().size(); }

    @NonNull public List<T> getCurrentList() { return mDiffer.getCurrentList(); }

    public void onCurrentListChanged(@NonNull List<T> previousList, @NonNull List<T> currentList) {}
}

DSL + ViewBinding

繼續(xù)簡化椒丧。

  • 大部分ViewHolderViewBinding 實現(xiàn)。 那 onCreateViewHolder() 也基本是重復(fù)的操作非区。

  • ViewBinding的創(chuàng)建過程也基本一致:ViewBinding.inflate(...)瓜挽。 可以用 《android ViewBinding, DataBinding》 中的老方法,靠反射拿到征绸。所以只需要 Adapter<VB : ViewBinding>久橙,onCreateViewHolder() 也可以省了。

  • DiffUtil.ItemCallback<T> 的實現(xiàn)也基本重復(fù)管怠。 通常只需要兩個lambda表達式說明情況淆衷。

// ViewHolder
data class BindingViewHolder<VB : ViewBinding>(val binding: VB) : RecyclerView.ViewHolder(binding.root)
// DiffUtil.ItemCallback<T>
inline fun <reified T> diffItemCallback(
    crossinline areItemsTheSame: (oldItem: T, newItem: T) -> Boolean,
    crossinline areContentsTheSame: (oldItem: T, newItem: T) -> Boolean = { o, n -> o == n },
) = object : DiffUtil.ItemCallback<T>() {
    override fun areItemsTheSame(oldItem: T, newItem: T): Boolean =
        areItemsTheSame(oldItem, newItem)

    override fun areContentsTheSame(oldItem: T, newItem: T): Boolean =
        areContentsTheSame(oldItem, newItem)
}
// ListAdapter<T, VH : ViewHolder>
fun <T, VH : RecyclerView.ViewHolder> dslListAdapter(
    diffItemCallback: DiffUtil.ItemCallback<T>,
    createHolder: (parent: ViewGroup, viewType: Int) -> VH,
    bindHolder: VH.(position: Int, data: T) -> Unit,
) = object : ListAdapter<T, VH>(diffItemCallback) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
        createHolder(parent, viewType)

    override fun onBindViewHolder(holder: VH, position: Int) =
        holder.bindHolder(position, getItem(position))
}
/** 
* ListAdapter<T, BindingViewHolder<VB : ViewBinding>>
* 
* inflate 不傳時,通過反射拿到VB的inflate 
* */
inline fun <T, reified VB : ViewBinding> dslBindingListAdapter(
    diffItemCallback: DiffUtil.ItemCallback<T>,
    noinline inflate: ((parent: ViewGroup, viewType: Int) -> VB)? = null,
    crossinline bindHolder: VB.(position: Int, data: T) -> Unit,
) = dslListAdapter(
    diffItemCallback,
    { p, v ->
        BindingViewHolder(
            inflate?.invoke(p, v) ?: VB::class.java.getMethod(
                "inflate",
                LayoutInflater::class.java,
                ViewGroup::class.java,
                Boolean::class.java
            ).invoke(null, LayoutInflater.from(p.context), p, false) as VB
        )
    },
    { p, d -> binding.bindHolder(p, d) },
)

使用:

val adapter = dslBindingListAdapter<Comment, ListItemSingleLineTextBinding>(
    diffItemCallback({ o, n -> o.id == n.id }, { o, n -> o == n }),
) { _, data ->
    // this is ListItemSingleLineTextBinding, 
    // data: Comment(id: Int, content: String)
    titleText.text = data
}

此外還有各種庫也做了封裝渤弛。最好靠(ksp祝拯,kapt)注解和編譯器插件在編譯期做代碼生成,靠反射不保險還額外費資源

ItemTouchHelper

列表項滑動和拖拽。

public class ItemTouchHelper extends RecyclerView.ItemDecoration implements RecyclerView.OnChildAttachStateChangeListener
// use:
ItemTouchHelper(callback: ItemTouchHelper.Callback).attachToRecyclerView(recyclerView: RecyclerView?)

ItemTouchHelper.Callback

需要設(shè)定滑動和拖拽的方向START(LEFT), END(RIGHT), UP, DOWN佳头。

可通過onChildDraw(), onChildDrawOver() 等自定義滑動和拖拽過程中的行為鹰贵。

object: ItemTouchHelper.Callback() {
    override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
        // 返回滑動和拖拽的方向
    }

    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        viewHolder // 被拖拽holder
        target // 正在經(jīng)過holder
        // 返回是否允許滑動
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        direction // 滑動方向
    }
}

ItemTouchHelper.SimpleCallback

ItemTouchHelper.Callback 簡版實現(xiàn)。構(gòu)造函數(shù)傳入滑動和拖拽方向康嘉。只需要關(guān)注onMove()onSwiped()過程碉输。

public abstract static class SimpleCallback extends Callback {
    public SimpleCallback(int dragDirs, int swipeDirs)
    ...
}

自定義行為

override fun onChildDraw(
    c: Canvas, // holder所占區(qū)域的Canvas
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    dX: Float, // 用戶動作引起的x移量
    dY: Float, // 用戶動作引起的y移量
    actionState: Int, // 交互類型,swipe | drag
    isCurrentlyActive: Boolean, // 用戶是否正在控制
) { ... }

onChildDraw的默認實現(xiàn):translationX = dX, translationY = dY

public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
        int actionState, boolean isCurrentlyActive) {
    ...
    view.setTranslationX(dX);
    view.setTranslationY(dY);
}

dX, dY:

關(guān)于dX, dY的計算規(guī)則亭珍,要從頭一點點看敷钾,attachToRecyclerView() 之后。

public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { ...
    setupCallbacks();
... }

private void setupCallbacks() { ...
    mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
... }

private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() { ...
    select(...)
... };

void select(@Nullable ViewHolder selected, int actionState) { ...
    swipeIfNecessary(...)
... }

private int swipeIfNecessary(ViewHolder viewHolder) {
    checkHorizontalSwipe(...)
    checkVerticalSwipe(...)
}

// flags: 方向
private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) { ...
    mCallback.getSwipeVelocityThreshold(...) // 速度臨界點
    mCallback.getSwipeEscapeVelocity(...) // 最小速度
    final float threshold = mRecyclerView.getWidth() * 
    mCallback.getSwipeThreshold(viewHolder); // 位置臨界點肄梨,默認0.5
... }

主要關(guān)注swipe過程阻荒,以及松手之后。

// 以水平滑動為例:
// 如果是默認行為: dx == holder.translationX
val oldDX // 開始滑動時的位置众羡,也就是上次停止的位置侨赡。 abs(oldDX) == 0 || abs(oldDX) == holder.width

// 正在滑動時
val diffX: Int // 手指滑動偏移量
dX = oldDX + diffX

// 松開時:
val isSwiped = 是否超過了速度臨界點或者位置臨界點
if(true) {
    // 如果超過,dx 最終值根據(jù)oldDX和滑動方向確定纱控。 
    // 最終值 = 如果之前為未滑動狀態(tài)辆毡,則劃出屏幕。如果之前未劃出屏幕甜害,則置為未滑動
    // 值變化靠動畫補全
    dx = anim(curDX -> (abs(oldDX) == 0 ? holder.width : 0) * (direction == LEFT ? -1 : 1))
} else {
    // 如果未超過舶掖,dx開始還原回初始值
    dx = anim(curDX -> oldDX)
}

松手后會根據(jù)是否超過臨界值,而選擇最終位置尔店。

demo: 添加震動, 半透明效果, 自定義繪制等

override fun onChildDraw(
    c: Canvas,
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    dX: Float,
    dY: Float,
    actionState: Int,
    isCurrentlyActive: Boolean
) {
    val midWidth = c.width / 2
    val absCurrentX = abs(viewHolder.itemView.translationX)

    // 震動
    if (absCurrentX < midWidth && abs(dX) >= midWidth) {
        val vibrator = requireContext().getSystemService(Vibrator::class.java) as Vibrator
        if (vibrator.hasVibrator()) {
            vibrator.vibrate(VibrationEffect.createOneShot(50, 255))
        }
    }

    // 半透明
    viewHolder.itemView.alpha = if (absCurrentX >= midWidth) 0.5f else 1f

    // 背景
    if (dX != 0f) {
        c.drawRect(
            0f,
            viewHolder.itemView.top.toFloat(),
            c.width.toFloat(),
            viewHolder.itemView.bottom.toFloat(),
            Paint().apply { color = Color.RED },
        )
    }

    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}

二次滑動眨攘,展示側(cè)滑菜單

最難控的就是dX, dY的變化∠荩可以把getSwipeVelocityThreshold 速度臨界點禁掉鲫售,只靠位置推算是否滑動成功。同時還要判斷失去焦點時還原该肴。

雖然能寫出來情竹,但是并不穩(wěn)定。如果真有需求匀哄,不如自己實現(xiàn)ItemTouchHelper秦效,大部分代碼不用動,修改滑動判定涎嚼,和松手后anim動畫設(shè)置即可阱州。

ConcatAdapter

Adapter 拼接。

需要引入recyclerview

implementation("androidx.recyclerview:recyclerview:latest")
public ConcatAdapter(@NonNull Adapter<? extends ViewHolder>... adapters)

public ConcatAdapter(@NonNull List<? extends Adapter<? extends ViewHolder>> adapters)

@SafeVarargs
public ConcatAdapter(
        @NonNull Config config,
        @NonNull Adapter<? extends ViewHolder>... adapters)

public ConcatAdapter(
        @NonNull Config config,
        @NonNull List<? extends Adapter<? extends ViewHolder>> adapters)

~λ:

2.25 開始寫法梯,現(xiàn)在 3.4 了苔货。雖然內(nèi)容多,中間也斷斷續(xù)續(xù),但寫總結(jié)仍然很耗時夜惭。 單純看這些源碼姻灶,寫demo,也就花一下午滥嘴,但整理要花這么久木蹬。

沒有需求一陣子了至耻。公司客戶端開發(fā)需求并不多若皱。而且本來也是我的個人愛好,當(dāng)初只是人手不夠暫時支援尘颓,結(jié)果越走越遠走触,快要回不去后端了。

現(xiàn)在沒有需求疤苹,打算回后端互广,從工作以來,寫客戶端(kotlin, flutter)的時間比后端還多卧土。頂多LeetCode刷題用一下語言(kotlin, scala, c++, rust)惫皱,平時自己玩一下Linux,但真正的開發(fā)尤莺,沒怎么正經(jīng)寫過旅敷。

所以,就算換工作颤霎,也只能投遞客戶端媳谁,投后端基本沒戲。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末友酱,一起剝皮案震驚了整個濱河市晴音,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌缔杉,老刑警劉巖锤躁,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異或详,居然都是意外死亡系羞,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門鸭叙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來觉啊,“玉大人,你說我怎么就攤上這事沈贝「苋耍” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嗡善。 經(jīng)常有香客問我辑莫,道長,這世上最難降的妖魔是什么罩引? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任各吨,我火速辦了婚禮,結(jié)果婚禮上袁铐,老公的妹妹穿的比我還像新娘揭蜒。我一直安慰自己,他們只是感情好剔桨,可當(dāng)我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布屉更。 她就那樣靜靜地躺著,像睡著了一般洒缀。 火紅的嫁衣襯著肌膚如雪瑰谜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天树绩,我揣著相機與錄音萨脑,去河邊找鬼。 笑死饺饭,一個胖子當(dāng)著我的面吹牛渤早,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播砰奕,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蛛芥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了军援?” 一聲冷哼從身側(cè)響起仅淑,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎胸哥,沒想到半個月后涯竟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡空厌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年庐船,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘲更。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡筐钟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赋朦,到底是詐尸還是另有隱情篓冲,我是刑警寧澤李破,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站壹将,受9級特大地震影響嗤攻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诽俯,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一妇菱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧暴区,春花似錦闯团、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缰盏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淹遵,已是汗流浹背口猜。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留透揣,地道東北人济炎。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像辐真,于是被迫代替她去往敵國和親须尚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容