| name | android-dev-core |
| description | Android 功能开发核心规则。包含项目架构、MVI模式、Base类、Adapter、网络请求等基础规范。开发任何 Android 功能时都应加载此 skill。 |
| metadata | {"category":"android","platform":"android"} |
Android 功能开发核心规则
本 Skill 包含 Android 功能开发的核心规范,适用于所有功能开发场景。
1. 开发原则
You are an Android Developer that provides expert-level insights and solutions. Your responses
should include examples of code snippets (where applicable), best practices, and explanations of
underlying concepts.
Here are some rules:
- Adapt to existing project architecture while maintaining clean code principles; implement clean
architecture with domain, data, and presentation layers.
- Follow unidirectional data flow with ViewModel and UI State.
- Use the latest stable Android SDKs and Android Jetpack libraries.
- Prioritize the use of
Coroutine and suspend functions for asynchronous operations, and prioritize the use of suspendCancellableCoroutine or callbackFlow instead of the callback parameter. Improve code readability and maintainability.
- Prefer using Android's native tools and libraries over third-party dependencies.
- Follow the Android Kotlin Style Guide and official Android API Design Guidelines for style.
- Add code comments when logic is complex to aid understanding.
- Use Kotlin idioms and features such as coroutines and extension functions.
- Highlight any considerations, such as potential performance impacts, with advised solutions.
- Include links to reputable sources for further reading (when beneficial).
- Keep it simple, professional, and direct. Avoid unnecessary pleasantries (such as "okay" or "of
course").
- Use Markdown to format responses, with code blocks enclosed in backticks.
- Never fabricate information or guess the results of tool execution.
- When encountering issues, explain the situation and attempt to resolve it rather than just
apologizing.
- Provide necessary Chinese annotation explanations on complex logical functions and classes that require explanation of their functions.
- Please search and think about information in English, and finally translate the results into
Chinese.
2. Import 规范
NOTICE:
When using encoding, prioritize encoding and importing packages according to the following format,
never put a full package name in a code block(e.g.,com.package.VLog.d()), always put an import statement at the beginning of the file.
adhere to these project standards to maintain code consistency:
2.1 Click Handling
import com.androidtool.common.extension.onClick
button.onClick { }
button.onClick(delay = 600) { }
2.2 Dimension Conversion
import com.androidtool.common.extension.dp
val padding = 16.dp
2.3 Screen & Device Information
import com.androidtool.common.utils.ScreenUtil
ScreenUtil.getStatusBarHeight()
ScreenUtil.getScreenWidth()
ScreenUtil.getScreenHeight()
2.4 Logging
import com.androidtool.common.log.VLog
VLog.d("lilili", "Retrieved user ID: $userId")
2.5 String Resource
import com.androidtool.common.utils.TranslateResource
binding.title = TranslateResource.getStringResources("title")
2.6 collectWhenStarted
import com.androidtool.common.utils.collectWhenStarted
viewModel.sampleLoadMoreUiState.collectWhenStarted { state ->
handleUiState(state)
}
2.7 Common LoadError Page
import com.androidtool.common.base.page.EmptyPage
import com.androidtool.common.base.page.ErrorPage
import com.androidtool.common.base.page.LoadingPage
class GiftRankListFragment : BaseBindingFragment<FragmentGiftRankListBinding>() {
private val viewModel: GiftListViewModel by viewModels()
private lateinit var id: String
override fun registerState() = binding.recyclerView
private fun handleUiState(state: GiftListUiState<GiftRankListBean.GiftRankItem>) {
when (state) {
is GiftListUiState.Loading -> {
showState<LoadingPage>()
}
is GiftListUiState.Empty -> {
binding.refreshLayout.finishRefresh()
showState<EmptyPage> ()
}
is GiftListUiState.Success -> {
showSuccess()
handleSuccessState(state)
}
is GiftListUiState.Error -> {
binding.refreshLayout.finishRefresh()
showState<ErrorPage> {
val hint = view.findViewById<TextView>(R.id.hint)
hint?.onClick {
viewModel.refresh()
}
}
}
}
}
}
2.8 RecycleView List Adapter
import com.model.UserBean
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
class SampleLoadMoreAdapter : BaseQuickAdapter<SampleUserInfo, BaseViewHolder>(R.layout.item_sample_user),
LoadMoreModule {}
class InterestTagAdapter(private val chosenListListener: (List<UserBean>) -> Unit) :
BaseMultiItemQuickAdapter<InterestTagShowBean, BaseViewHolder>() {}
2.9 All model classes are in the com.model package, import using
import com.model.SomeBean
Always use project-provided utilities and extensions instead of creating custom implementations or
introducing unnecessary third-party dependencies.
3. 项目结构
Note: This is a recommended project structure, but be flexible and adapt to existing project structures.
Do not enforce these structural patterns if the project follows a different organization.
Assume I want to implement the feature:"one_feature":
app/src/main/java/com/package/one_feature
├── data
│ ├── repository
│ │ └── OneFeatureRepositoryImpl.kt
│ ├── datasource (optional)
│ │ ├── local
│ │ │ ├── OneFeatureDao.kt
│ │ │ └── OneFeatureDatabase.kt
│ │ └── remote
│ │ └── OneFeatureApiService.kt
│ ├── model
│ │ ├── OneFeatureEntity.kt
│ │ └── OneFeatureResponse.kt
│ └── mapper (optional)
│ └── OneFeatureMapper.kt
├── di (optional)
│ └── OneFeatureModule.kt
├── ui
│ ├── fragment
│ │ ├── OneFeatureFragment.kt
│ │ ├── TwoFeatureFragment.kt(optional)
│ │ ├── OneFeatureListAdapter.kt
│ │ ├── OneFeatureSubListViewModel.kt
│ │ └── OneFeatureListContract.kt(contain state,event,effect)
│ ├── view(optional)
│ │ ├── CustomViews.kt
│ │ └── OneFeatureItemView.kt
│ │── OneFeatureListContract.kt(contain state,event,effect)
│ ├── OneFeatureActivity.kt
│ └── OneFeatureViewModel.kt
└── domain(optional)
│ ├── usecase
│ │ ├── GetOneFeatureUseCase.kt
│ │ └── UpdateOneFeatureUseCase.kt
│ ├── repository
│ │ └── IOneFeatureRepository.kt
│ └── model
│ └── OneFeatureModel.kt
Note:
- If not specifically mentioned, we need to implement the domain layer, but the corresponding OneFeatureRepository doesn't need to generate an interface and implement it in the data layer; we don't need to use usecases.
- If I provide backend response types, please use the context-provided backend response data type to generate the ApiService. If I don't provide backend response types, please define a SampleBean to represent the return data type.
- If I provide a specific package file path, please replace
com/package/one_feature when importing.
- Due to project constraints, we currently don't use any DI tools for the di layer.
- Unless specifically mentioned, we don't need to distinguish between local and remote datasources in the data layer. Directly use OneFeatureApiService with retrofit suspend for remote fetching.
- Unless specifically mentioned, we don't need to implement mappers in the data layer.
4. 功能通用规范
4.1 布局与命名
- Prioritize the use of XML for layout, prefer ConstraintLayout, unless a simple layout is used
with FrameLayout/LinearLayout and RelativeLayout.
- Naming conventions:
- Class names: PascalCase (UserRepository)
- Variables/functions: camelCase (getUserData)
- Constants: UPPER_SNAKE_CASE (MAX_RETRY_COUNT)
- Use complete descriptive names
- Interface: Usually not prefixed with 'I', unless to eliminate ambiguity.
- Use complete and meaningful descriptive names (e.g., userProfileImageView instead of imgV).
4.2 Property Delegation with Lifecycle-Aware Components
When implementing complex logic in Activities and Fragments, use property delegation pattern to organize related functionality into specialized Impl classes. The delegate classes should implement DefaultLifecycleObserver and provide a bindLifecycle() method for automatic lifecycle management.
class FloatingVideoFragment : BaseBindingFragment<FragmentFloatingVideoBinding>(),
DraggableBehavior by DraggableBehaviorImpl(),
LoopVideoPlayerBehavior by LoopVideoPlayerBehaviorImpl() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindLifecycle(lifecycle)
}
}
interface LoopVideoPlayerBehavior {
fun bindLifecycle(lifecycle: Lifecycle)
fun setEventVideo(urls: List<DemoEventVideo>)
fun setupVideoPlayer(container: ViewPager2)
fun isLooping(): Boolean
fun startLoop()
fun pauseLoop()
fun releasePlayerResource()
}
class LoopVideoPlayerBehaviorImpl : LoopVideoPlayerBehavior, DefaultLifecycleObserver {
override {
lifecycle.addObserver()
}
{ }
{ }
{ releasePlayerResource() }
}
4.3 MVI Contract
State, Event, Effect should be integrated into a newly created OneFeatureContract.kt file
data class OneFeatureState(
val isLoading: Boolean = false,
val data: List<String> = emptyList(),
val error: String? = null,
val selectedItemId: String? = null
)
sealed interface OneFeatureEvent {
data object LoadData : OneFeatureEvent()
data object RefreshData : OneFeatureEvent()
data class DeleteItem(val itemId: String) : OneFeatureEvent()
}
sealed interface OneFeatureEffect {
data class ShowToast(val message: String) : OneFeatureEffect()
data class ShareContent(val content: String) : OneFeatureEffect()
}
4.4 MVI Data Flow Patterns
When implementing MVI architecture, follow these data flow patterns:
-
Effect: Use Channel to send one-time events (like navigation, Toast notifications) from
ViewModel to View layer
private val _effect = Channel<MyEffect>(Channel.BUFFERED)
val effect = _effect.receiveAsFlow()
fun showToast(message: String) {
viewModelScope.launch {
_effect.send(MyEffect.ShowToast(message))
}
}
import com.androidtool.common.utils.collectWhenStarted
viewModel.effect.collectWhenStarted { effect ->
when (effect) {
is MyEffect.ShowToast -> Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
is MyEffect.Navigate -> findNavController().navigate(effect.destination)
}
}
-
State: Use StateFlow to manage and expose UI state, ensuring consistency and observability
private val _state = MutableStateFlow(MyState())
val state = _state.asStateFlow()
-
Event: Trigger state updates by simply calling action methods on the ViewModel
fun onAction(event: MyEvent) {
when (event) {
is MyEvent.ItemClicked -> handleItemClick(event.item)
is MyEvent.RefreshRequested -> loadData()
}
}
binding.refreshButton.onClick {
viewModel.onAction(MyEvent.RefreshRequested)
}
recyclerView.adapter = adapter.apply {
setOnItemClickListener { adapter, view, position ->
val item = adapter.getItem(position)
viewModel.onAction(MyEvent.ItemClicked(item))
}
}
4.5 LoadSir 状态页框架
The project encapsulates LoadSir as a loading and error display framework, used as follows:
- First, extend BaseBindingFragment or BaseBindingActivity
- Second, override
open fun registerState(): View? = binding.recycleview or
open fun registerState(): View? = binding.root
- Third, use
showState<LoadingPage>, showState<ErrorPage>, showState<EmptyPage> to display state pages, and use
showSuccess() to display the page used in the XML
4.6 BaseBindingFragment
The project encapsulates BaseBindingFragment, package in package com.androidtool.common.base:
package com.androidtool.common.base
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import com.androidtool.common.loadsir.LoadLayout
import com.androidtool.common.loadsir.LoadSir
import com.scwang.smart.refresh.layout.SmartRefreshLayout
abstract class BaseBindingFragment<VB : ViewBinding> : Fragment(),
IViewBinding<VB> by ViewBindingDelegate() {
protected var lazyLoaded = false
protected var initView = false
lateinit var loadSir: LoadLayout
private set
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
lazyLoaded = false
initView = false
return createViewBinding(inflater, container, true)?.let { rootView ->
registerState()?.let { compatibleSmartRefresh(it) } ?: run { rootView }
}
}
override {
.onViewCreated(view, savedInstanceState)
initView()
}
{
.onDestroyView()
lazyLoaded =
}
{
.onResume()
(!lazyLoaded) {
lazyLoaded =
onLazyLoad()
}
}
{}
=
= lazyLoaded
: View {
loadSir = LoadSir.register(target, listener = { onStateClick() })
(loadSir.parent ? SmartRefreshLayout)?.let {
it.removeView(loadSir)
it.setRefreshContent(loadSir)
}
loadSir.rootView
}
: View? =
=
= loadSir.show<LoadingPage>()
= loadSir.showSuccess(useAnim)
{
isShowState = (ifShow) {
Collection<*> -> ifShow.isEmpty()
Array<*> -> ifShow.isEmpty()
-> ifShow
->
}
(isShowState) loadSir.show<T>(
useAnim = useAnim, block = block
) loadSir.showSuccess()
}
}
Usage example:
import com.androidtool.common.base.BaseBindingFragment
import com.androidtool.common.utils.collectWhenStarted
class SampleListFragment : BaseBindingFragment<FragmentGiftRankListBinding>() {
private val viewModel: SampleListViewModel by viewModels()
private lateinit var id: String
override fun registerState() = binding.recyclerView
private val adapter by lazy {
GiftRankListAdapter()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
id = arguments?.getString(DYNAMIC_ID) ?: "0"
viewModel.initDataById(id = id)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRefreshLayout()
setupRecyclerView()
observeUiState()
}
private fun setupRefreshLayout() {
binding.refreshLayout.setOnRefreshListener {
viewModel.refresh()
}
}
{
binding.recyclerView.layoutManager = LinearLayoutManager(context)
binding.recyclerView.adapter = adapter
adapter.setOnItemClickListener { _, _, position ->
uId = adapter.[position].uid
}
adapter.loadMoreModule.apply {
isAutoLoadMore =
isEnableLoadMoreIfNotFullPage =
setOnLoadMoreListener {
viewModel.loadMore()
}
}
}
{
viewModel.listUiState.collectWhenStarted { state ->
handleUiState(state)
}
viewModel.listEffect.collectWhenStarted { event ->
(event) {
GiftListUiEvent.UpdatePeopleCount -> {
handleUpdatePeopleCount(event.count)
}
}
}
}
{
DYNAMIC_ID =
: Fragment {
fragment = SampleListFragment()
fragment.arguments = Bundle().apply {
putString(DYNAMIC_ID, dynamicId)
}
fragment
}
}
4.7 BaseBindingActivity
The project encapsulates BaseBindingActivity, integrating LoadSir and ViewBinding. Note that this BaseBindingActivity does not implement TitleBar, you need to implement it yourself:
abstract class BaseBindingActivity<V : ViewBinding> : RootActivity(),
IViewBinding<V> by ViewBindingDelegate() {
lateinit var loadSir: LoadLayout
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootView = createViewBinding()
registerState()?.let { target ->
loadSir = LoadSir.register(target, listener = { onStateClick() })
(loadSir.parent as? SmartRefreshLayout)?.let {
it.removeView(loadSir)
it.setRefreshContent(loadSir)
}
}
setContentView((rootView.parent as? View) ?: rootView)
initView()
initData()
}
abstract fun initView()
open fun initData() = Unit
open fun registerState(): View? = null
open fun onStateClick() = Unit
fun = loadSir.show<LoadingPage>()
= loadSir.showSuccess(useAnim)
{
isShowState = (ifShow) {
Collection<*> -> ifShow.isEmpty()
Array<*> -> ifShow.isEmpty()
-> ifShow
->
}
(isShowState) loadSir.show<T>(
useAnim = useAnim, block = block
) loadSir.showSuccess()
}
}
Usage example using com.androidtool.common.widget.TitleBarView to integrate title bar:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.topic.TopicListActivity">
<com.androidtool.common.widget.TitleBarView
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
style="@style/TopicTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal=
=
=
=
=
=
=
=
= />
SampleBindingActivity:
class TopicListActivity : BaseBindingActivity<ActivityTopicListBinding>() {
private val viewModel: TopicListViewModel by viewModels()
private var tabLayoutMediator: TabLayoutMediator? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStatusBarTextColor(isLight = true)
initView()
observeData()
viewModel.loadData()
}
override fun initView() {
binding.titleBar.setTitle(TranslateResource.getStringResources("topic"))
binding.titleBar.setOnBackClickListener {
finish()
}
}
override fun registerState(): View {
return binding.contentView
}
private fun observeData() {
viewModel.topicCategoryListUiState.collectWhenStarted {
when (it) {
TopicCategoryListUiState.Empty -> {
showState<EmptyPage>()
}
is TopicCategoryListUiState.Error -> {
showState<ErrorPage>()
}
TopicCategoryListUiState.Loading -> {
showState<LoadingPage>()
}
is TopicCategoryListUiState.Success -> {
showSuccess()
initTabLayoutAndViewPager(it.items)
}
}
}
}
{
pagerAdapter = TopicPagerAdapter(, categories)
binding.viewPager.adapter = pagerAdapter
tabLayoutMediator?.detach()
tabLayoutMediator =
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tabView = layoutInflater.inflate(R.layout.tab_item_topic, )
tabTitle = tabView.findViewById<TextView>(R.id.tvTabTitle)
tabTitle.text = categories[position].name
tab.customView = tabView
}.apply {
attach()
}
(categories.isNotEmpty()) {
binding.viewPager.setCurrentItem(, )
}
}
{
tabLayoutMediator?.detach()
tabLayoutMediator =
.onDestroy()
}
{
{
intent = Intent(context, TopicListActivity::.java)
context.startActivity(intent)
}
}
}
4.8 TitleBarView API
binding.titleBar.setTitle(TranslateResource.getStringResources("topic"))
binding.titleBar.setOnBackClickListener {
finish()
}
TitleBarView API:
fun setTitle(title: CharSequence) {}
fun setOnBackClickListener(listener: () -> Unit) {}
fun setExtraButton(@DrawableRes resId: Int, clickListener: (v: View) -> Unit) {}
fun setExtraButtonLayout(view: View) {}
4.9 Adapter
Adapters should use BaseRecyclerViewAdapterHelper V3.x, as follows:
import com.model.UserBean
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
class SampleLoadMoreAdapter : BaseQuickAdapter<SampleUserInfo, BaseViewHolder>(R.layout.item_sample_user),
LoadMoreModule {}
class InterestTagAdapter(private val chosenListListener: (List<UserBean>) -> Unit) :
BaseMultiItemQuickAdapter<InterestTagShowBean, BaseViewHolder>() {}
4.10 Network Request
NOTICE: adhere to these project package import standards to maintain code consistency
import com.androidtool.common.extension.asResult
import com.androidtool.common.extension.requestToFlow
fun loadData(): Flow<Result<DataModel>> {
return requestToFlow { apiService.fetchData() }.asResult()
}
fun loadData() {
viewModelScope.launch {
updateLoadingState()
repository.loadData()
.catch { exception -> handleError(exception) }
.collect { result ->
result.fold(
onSuccess = { data -> handleSuccess(data) },
onFailure = { exception -> handleError(exception) }
)
}
}
}
import com.androidtool.common.troll.BaseResponse
import com.androidtool.common.net.Apis
interface ApiService {
@GET(Apis.ENDPOINT_DATA)
suspend fun getData(): BaseResponse<DataModel>
@GET(Apis.ENDPOINT_PARAMS_DATA)
suspend fun getParamsData(@QueryMap map: Map<String, String>): BaseResponse<List<DataModel>>
}