Claude Code Plugins

Community-maintained marketplace

Feedback

android-dev-extras

@est7/agent-tool
0
0

Android 功能开发扩展模板。包含上拉刷新(SmartRefreshLayout)和 TabLayout+ViewPager2 的完整实现模板。仅在需要刷新或 Tab 切换功能时加载。

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name android-dev-extras
description Android 功能开发扩展模板。包含上拉刷新(SmartRefreshLayout)和 TabLayout+ViewPager2 的完整实现模板。仅在需要刷新或 Tab 切换功能时加载。
metadata [object Object]

Android 功能开发扩展模板

本 Skill 包含上拉刷新和 Tab 切换的完整实现模板,仅在需要这些功能时加载。


1. 上拉刷新与加载更多

When you need to integrate "pull-to-refresh and load-more" functionality, refer to the following code. Use LoadState to track loading states, use LoadSir's showState for page state changes, and use BaseRecyclerViewAdapterHelper v3.x for Adapter implementation:

1.1 布局文件

fragment_sample_load_more.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:background="@color/white" android:layout_height="match_parent"
    tools:ignore="ContentDescription">

    <com.scwang.smart.refresh.layout.SmartRefreshLayout android:id="@+id/refreshLayout"
        android:layout_width="match_parent" android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent">

        <com.androidtool.common.widget.MyRefreshLayout android:layout_width="match_parent"
            android:layout_height="54dp" />

        <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView"
            android:layout_width="match_parent" android:layout_height="match_parent"
            android:animateLayoutChanges="false" android:clipChildren="false"
            android:clipToPadding="false" android:overScrollMode="never"
            android:paddingHorizontal="12dp" android:paddingVertical="3dp"
            android:scrollbars="none" />

    </com.scwang.smart.refresh.layout.SmartRefreshLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

1.2 Fragment 实现

SampleLoadMoreFragment:

import android.os.Bundle
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.androidtool.common.base.BaseBindingFragment
import com.androidtool.common.base.page.EmptyPage
import com.androidtool.common.base.page.ErrorPage
import com.androidtool.common.base.page.LoadingPage
import com.androidtool.common.extension.onClick
import com.androidtool.common.utils.collectWhenStarted
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.module.LoadMoreModule
import com.chad.library.adapter.base.viewholder.BaseViewHolder

/**
 * Pagination loading example Fragment
 * @author: est8
 * @date: 2025/2/28
 */
class SampleLoadMoreFragment : BaseBindingFragment<FragmentSampleLoadMoreBinding>() {
    private val viewModel: SampleLoadMoreViewModel by viewModels()
    private lateinit var id: String

    // refresh placeholder usage
    override fun registerState() = binding.recyclerView

    private val adapter by lazy {
        SampleLoadMoreAdapter()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        id = arguments?.getString("id") ?: "1000"
        viewModel.initDataById(id = id)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupRefreshLayout()
        setupRecyclerView()
        observeUiState()
    }

    /**
     * Setup pull-to-refresh
     */
    private fun setupRefreshLayout() {
        binding.refreshLayout.setOnRefreshListener {
            viewModel.refresh()
        }
    }

    /**
     * Setup RecyclerView and load more
     */
    private fun setupRecyclerView() {
        binding.recyclerView.layoutManager = LinearLayoutManager(context)
        binding.recyclerView.adapter = adapter

        adapter.loadMoreModule.apply {
            isAutoLoadMore = true
            isEnableLoadMoreIfNotFullPage = false
            setOnLoadMoreListener {
                viewModel.loadMore()
            }
        }
    }

    /**
     * Observe UI state changes
     */
    private fun observeUiState() {
        viewModel.sampleLoadMoreUiState.collectWhenStarted { state ->
            handleUiState(state)
        }
    }

    /**
     * Handle UI state
     */
    private fun handleUiState(state: SampleLoadMoreUiState<SampleUserInfo>) {
        when (state) {
            is SampleLoadMoreUiState.Loading -> {
                loadSir.show<LoadingPage>()
            }

            is SampleLoadMoreUiState.Empty -> {
                binding.refreshLayout.finishRefresh()
                loadSir.show<EmptyPage>()
            }

            is SampleLoadMoreUiState.Success -> {
                loadSir.showSuccess()
                handleSuccessState(state)
            }

            is SampleLoadMoreUiState.Error -> {
                binding.refreshLayout.finishRefresh()
                loadSir.show<ErrorPage> { it ->
                    val hint = it.view.findViewById<TextView>(R.id.hint)
                    hint?.onClick {
                        viewModel.refresh()
                    }

                }
            }
        }
    }

    /**
     * Handle success state
     */
    private fun handleSuccessState(state: SampleLoadMoreUiState.Success<SampleUserInfo>) {
        when (state.loadState) {
            LoadState.REFRESHING -> {
                // refreshLayout will automatically handle pull-to-refresh UI
            }

            LoadState.LOADING_MORE -> {
                // BRVAH will automatically handle load more UI
            }

            LoadState.LOAD_MORE_COMPLETE -> {
                binding.refreshLayout.finishRefresh()
                adapter.setList(state.items)
                adapter.loadMoreModule.loadMoreComplete()
            }

            LoadState.LOAD_MORE_END -> {
                binding.refreshLayout.finishRefresh()
                adapter.setList(state.items)
                adapter.loadMoreModule.loadMoreEnd()
            }

            LoadState.LOAD_MORE_FAIL -> {
                binding.refreshLayout.finishRefresh()
                adapter.loadMoreModule.loadMoreFail()
            }
        }
    }
}

1.3 Adapter 实现

SampleListAdapter:

import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.BaseMultiItemQuickAdapter
import com.chad.library.adapter.base.module.LoadMoreModule
import com.chad.library.adapter.base.viewholder.BaseViewHolder

/**
 * Using https://github.com/CymChad/BaseRecyclerViewAdapterHelper v3.x
 * User information adapter
 */
class SampleLoadMoreAdapter :
    BaseQuickAdapter<SampleUserInfo, BaseViewHolder>(R.layout.item_sample_user),
    LoadMoreModule {

    override fun convert(holder: BaseViewHolder, item: SampleUserInfo) {
        holder.setText(R.id.tvName, item.name)
        holder.setText(R.id.tvAge, "Age: ${item.age}")
        holder.setText(R.id.tvEmail, item.email)

        holder.getView<View>(R.id.btnAction).setOnClickListener {
            Toast.makeText(context, "Clicked on ${item.name}", Toast.LENGTH_SHORT).show()
        }
    }
}


// Complex multi-type list adapter uses BaseMultiItemQuickAdapter
class InterestTagAdapter(private val chosenListListener: (List<InterestTagShowBean>) -> Unit) :
    BaseMultiItemQuickAdapter<InterestTagShowBean, BaseViewHolder>() {}

1.4 ViewModel 实现


package com.androidrtc.chat.samples.loadmore

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.androidrtc.chat.samples.data.SampleLoadMoreRepository
import com.androidrtc.chat.samples.data.SampleLoadMoreService
import com.androidrtc.chat.samples.data.SampleUserInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch

/**
 * ViewModel for pagination loading data
 * @author: est8
 * @date: 2025/2/28
 */
class SampleLoadMoreViewModel : ViewModel() {
    private val repository = SampleLoadMoreRepository.getSingleInstance()
    private var currentPage = 1
    private var currentId = ""

    private val _uiState =
        MutableStateFlow<SampleLoadMoreUiState<SampleUserInfo>>(SampleLoadMoreUiState.Loading)
    val sampleLoadMoreUiState = _uiState.asStateFlow()

    /**
     * Initialize data loading
     * @param id Data ID
     */
    fun initDataById(id: String) {
        if (currentId != id) {
            this.currentId = id
            this.currentPage = 1
        }
        loadData(isRefresh = true)
    }

    /**
     * Refresh data
     */
    fun refresh() {
        currentPage = 1
        loadData(isRefresh = true)
    }

    /**
     * Load more data
     */
    fun loadMore() {
        if (_uiState.value is SampleLoadMoreUiState.Success) {
            val currentState = _uiState.value as SampleLoadMoreUiState.Success<SampleUserInfo>
            // Only trigger load more when not in loading state and not in load complete state
            if (currentState.loadState != LoadState.LOADING_MORE &&
                currentState.loadState != LoadState.LOAD_MORE_END
            ) {
                loadData(isRefresh = false)
            }
        }
    }

    /**
     * Load data
     * @param isRefresh Whether it's a refresh operation
     */
    private fun loadData(isRefresh: Boolean = false) {
        viewModelScope.launch {
            updateLoadingState(isRefresh)

            repository.loadDataById(currentId, currentPage)
                .catch { exception ->
                    handleError(exception, isRefresh)
                }
                .collect { result ->
                    result.fold(
                        onSuccess = { data -> handleSuccess(data, isRefresh) },
                        onFailure = { exception -> handleError(exception, isRefresh) }
                    )
                }
        }
    }

    /**
     * Update loading state
     */
    private fun updateLoadingState(isRefresh: Boolean) {
        if (isRefresh) {
            _uiState.value = if (_uiState.value is SampleLoadMoreUiState.Success) {
                (_uiState.value as SampleLoadMoreUiState.Success<SampleUserInfo>).copy(
                    loadState = LoadState.REFRESHING
                )
            } else {
                SampleLoadMoreUiState.Loading
            }
        } else if (_uiState.value is SampleLoadMoreUiState.Success) {
            _uiState.value = (_uiState.value as SampleLoadMoreUiState.Success<SampleUserInfo>).copy(
                loadState = LoadState.LOADING_MORE
            )
        }
    }

    /**
     * Handle successfully loaded data
     */
    private fun handleSuccess(data: List<SampleUserInfo>, isRefresh: Boolean) {
        if (isRefresh) {
            // Handle empty data case
            if (data.isEmpty()) {
                _uiState.value = SampleLoadMoreUiState.Empty
                return
            }

            _uiState.value = SampleLoadMoreUiState.Success(
                items = data,
                loadState = if (data.size < SampleLoadMoreService.pageSize) LoadState.LOAD_MORE_END else LoadState.LOAD_MORE_COMPLETE,
                page = currentPage
            )
        } else if (_uiState.value is SampleLoadMoreUiState.Success) {
            val currentItems =
                (_uiState.value as SampleLoadMoreUiState.Success<SampleUserInfo>).items
            _uiState.value = (_uiState.value as SampleLoadMoreUiState.Success<SampleUserInfo>).copy(
                items = currentItems + data,
                loadState = if (data.size < SampleLoadMoreService.pageSize) LoadState.LOAD_MORE_END else LoadState.LOAD_MORE_COMPLETE,
                page = currentPage
            )
        }
        currentPage++ // Load next page next time
    }

    /**
     * Handle loading error
     */
    private fun handleError(exception: Throwable, isRefresh: Boolean) {
        if (isRefresh) {
            _uiState.value = SampleLoadMoreUiState.Error(exception.message ?: "Unknown error")
        } else if (_uiState.value is SampleLoadMoreUiState.Success) {
            _uiState.value = (_uiState.value as SampleLoadMoreUiState.Success<SampleUserInfo>).copy(
                loadState = LoadState.LOAD_MORE_FAIL
            )
        }
    }
}

1.5 LoadState 枚举

LoadState:

package com.androidtool.common.base.event

/**
 * Loading state enum
 */
enum class LoadState {
    REFRESHING,        // Pull-to-refresh in progress
    LOADING_MORE,      // Load more in progress
    LOAD_MORE_COMPLETE, // Load more completed
    LOAD_MORE_END,     // No more data
    LOAD_MORE_FAIL     // Load more failed
}

1.6 Contract 文件

SampleLoadMoreContract.kt:


sealed class SampleLoadMoreUiState<out T> {
    // Initial loading state
    data object Loading : SampleLoadMoreUiState<Nothing>()

    // Empty data state - only shown when no data is retrieved on first page
    data object Empty : SampleLoadMoreUiState<Nothing>()

    // Data state - contains all data-related states
    data class Success<T>(
        val loadState: LoadState = LoadState.LOAD_MORE_COMPLETE,
        val page: Int = 1,
        val items: List<T> = emptyList()
    ) : SampleLoadMoreUiState<T>()

    // Error state
    data class Error(val message: String) : SampleLoadMoreUiState<Nothing>()
}

2. TabLayout + ViewPager2

When you need to implement TabLayout and ViewPager2 in your Fragment, reference the following code that uses BaseRecyclerViewAdapterHelper v3.x for Adapter implementation:

2.1 布局文件

fragment_sample_dynamic_tab.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        style="@style/SampleDynamicTabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:background="@android:color/transparent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:tabGravity="start" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tabLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>

2.2 Fragment 实现

SampleDynamicTabFragment:

package com.androidrtc.chat.samples.dynamictab

import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.androidtool.common.base.BaseBindingFragment
import com.androidtool.common.base.page.EmptyPage
import com.androidtool.common.base.page.ErrorPage
import com.androidtool.common.base.page.LoadingPage
import com.androidtool.common.utils.collectWhenStarted
import com.google.android.material.tabs.TabLayoutMediator

/**
 * Pagination loading example Fragment
 */
class SampleDynamicTabFragment : BaseBindingFragment<FragmentSampleDynamicTabBinding>() {
    private val viewModel: SampleDynamicTabViewModel by viewModels()
    private var tabLayoutMediator: TabLayoutMediator? = null

    override fun initView() {
        observeData()
        viewModel.loadData()
    }

    override fun registerState(): View? {
        return binding.root
    }

    private fun observeData() {
        viewModel.tabCategoryListUiState.collectWhenStarted {
            when (it) {
                SampleTopicCategoryListUiState.Empty -> {
                    loadSir.show<EmptyPage>()
                }

                is SampleTopicCategoryListUiState.Error -> {
                    loadSir.show<ErrorPage>()
                }

                SampleTopicCategoryListUiState.Loading -> {
                    loadSir.show<LoadingPage>()
                }

                is SampleTopicCategoryListUiState.Success -> {
                    loadSir.showSuccess()
                    initTabLayoutAndViewPager(it.items)
                }
            }
        }
    }

    private fun initTabLayoutAndViewPager(categories: List<SampleTabCategory>) {
        val pagerAdapter = SampleTabPagerAdapter(this, categories)

        binding.viewPager.adapter = pagerAdapter
        tabLayoutMediator?.detach()
        tabLayoutMediator =
            TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
                tab.text = categories[position].name
            }.apply {
                attach()
            }

        if (categories.isNotEmpty()) {
            binding.viewPager.setCurrentItem(0, false)
        }
    }

    override fun onDestroy() {
        tabLayoutMediator?.detach()
        tabLayoutMediator = null
        super.onDestroy()
    }

    companion object {
    }

}

2.3 PagerAdapter 实现

SampleTabPagerAdapter:

class SampleTabPagerAdapter(
    fragment: Fragment,
    private val categories: List<SampleTabCategory>
) : FragmentStateAdapter(fragment) {

    override fun getItemCount(): Int = categories.size

    override fun createFragment(position: Int): Fragment {
        val category = categories[position]
        // Create different sub-Fragments based on different categories
        // return SampleLoadMoreFragment.newInstance(category)
        return SampleLoadMoreFragment()
    }
}

2.4 ViewModel 实现

SampleDynamicTabViewModel:


package com.androidrtc.chat.samples.dynamictab

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.androidrtc.chat.samples.data.SampleLoadMoreRepository
import com.androidrtc.chat.samples.data.SampleTabCategory
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch

class SampleDynamicTabViewModel : ViewModel() {
    private val repository = SampleLoadMoreRepository.getSingleInstance()

    private val _uiState =
        MutableStateFlow<SampleTopicCategoryListUiState<SampleTabCategory>>(
            SampleTopicCategoryListUiState.Loading
        )
    val tabCategoryListUiState = _uiState.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            updateLoadingState()

            repository.loadTabList()
                .catch { exception ->
                    handleError(exception)
                }
                .collect { result ->
                    result.fold(
                        onSuccess = { data ->
                            handleSuccess(
                                data
                            )
                        },
                        onFailure = { exception -> handleError(exception) }
                    )
                }
        }
    }

    /**
     * Update loading state
     */
    private fun updateLoadingState() {
        _uiState.value = SampleTopicCategoryListUiState.Loading
    }

    /**
     * Handle successfully loaded data
     */
    private fun handleSuccess(data: List<SampleTabCategory>) {
        // Handle empty data case
        if (data.isEmpty()) {
            _uiState.value = SampleTopicCategoryListUiState.Empty
            return
        }

        _uiState.value = SampleTopicCategoryListUiState.Success(
            items = data,
        )
    }

    /**
     * Handle loading error
     */
    private fun handleError(exception: Throwable) {
        _uiState.value = SampleTopicCategoryListUiState.Error(exception.message ?: "unknown error")
    }
}

2.5 Contract 文件

SampleTopicCategoryListContract.kt:

sealed class SampleTopicCategoryListUiState<out T> {
    // Initial loading state
    data object Loading : SampleTopicCategoryListUiState<Nothing>()

    // Empty data state
    data object Empty : SampleTopicCategoryListUiState<Nothing>()

    // Data state - contains all data-related states
    data class Success<T>(
        val items: List<T> = emptyList()
    ) : SampleTopicCategoryListUiState<T>()

    // Error state
    data class Error(val message: String) : SampleTopicCategoryListUiState<Nothing>()
}