パスカルから転がってから半年が経ちましたkotlinでAndroidの開発に夢中になり、今では自分のアイデアを他の人の修道院に公に登ることをすでに許可しています。しかし、それには理由があります。プロフィールチャットで、初心者だけでなく、Android開発者にとって最もよくある質問を見て、チャットからの同僚の説明が理解できないため、ほとんどの場合、理解できないエラーが発生した場合に気づきました。彼らの主な質問である理由は、既製のコードやライブラリを無意識に使用していることです。ただし、それらでは機能しない既製のコード例に依存します(この領域では、デフォルトで1年以上前に記述されたコードは、更新または一般的にやり直しが必要です。これは、スタックオーバーフローのあるコード、ライブラリガイドに適用されます。 、そしてグーグル自体からのガイドでさえ)、彼らはエラーが発生する理由や異なる振る舞いを理解していません、のようなライブラリに依存しているのでその建築と仕事の原則を理解しようとせずに、中国の部屋。
リサイクラービューの問題が頻繁に発生するため、アプリケーションで複数項目のリストを表示するために、拡張可能でクリーンなコードを自分で作成する方法について少し理解したいと思います。
アンドロイド開発のアーキテクチャパターンを研究して、私は最初にグーグル開発者ガイドサーバーで答えを探すように自分自身を訓練しました。ただし、特にトレーニングコードラボでは、汎用性、純度、拡張性のために設計されたものよりも単純化されたコードの例が存在する場合があります。
この場合、内部のマークアップとロジックが異なるアイテムのリストを表示するために、豪華なリサイクラービューを使用する必要がありました。インスタントメッセンジャーやソーシャルメディアフィードから銀行のアプリケーションまで、すべての最新のアプリケーションはこのアイデアに基づいています。さらに、手動レイアウトマークアップの代わりに、リサイクラービューリストのさまざまな視覚要素のリアクティブアプローチを使用してオンザフライで組み合わせると、JetpackComposeで提供される宣言型機能UIの世界への架け橋になります。後でGoogleは穏やかに切り替えることを提案します。
Codelab, recycler view , sealed . . , ,- , , . , /, ( , SOLID, ).
, Google id data- : id Long.MIN_VALUE, id data-. : data-, , . recycler view .
. adapter delegates, groupie epoxy. , . , , . , , , .
:
, , 10%, ;
: , - data- .
, , , , , , .
, , , , recycler view , . , .
, .
recycler view, ListAdapter, , :
getItemType - , ( , Google );
onCreateViewHolder - , ViewHolder , ( );
onBindViewHolder - , ( ) ViewHolder, .
recycler view , recycler view , , , , DiffUtil-.
DiffCallback,
class BaseDiffCallback : DiffUtil.ItemCallback<HasStringId>() {
override fun areItemsTheSame(oldItem: HasStringId, newItem: HasStringId): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: HasStringId, newItem: HasStringId): Boolean = oldItem == newItem
}
, areContentsTheSame , areItemsTheSame true. HasStringId, id String equals, data- , view. Data- id, DiffUtil , ui- id .
, , . , :
interface ViewHoldersManager {
fun registerViewHolder(itemType: Int, viewHolder: ViewHolderVisitor)
fun getItemType(item: Any): Int
fun getViewHolder(itemType: Int): ViewHolderVisitor
}
recycler view:
object ItemTypes {
const val UNKNOWN = -1
const val HEADER = 0
const val TWO_STRINGS = 1
const val ONE_LINE_STRINGS = 2
const val CARD = 3
}
"" adapter delegates, . .
hilt data binding, : ui. , , :
@Module
@InstallIn(FragmentComponent::class)
object DiModule {
@Provides
@FragmentScoped
fun provideAdaptersManager(): ViewHoldersManager = ViewHoldersManagerImpl().apply {
registerViewHolder(ItemTypes.HEADER, HeaderViewHolder())
registerViewHolder(ItemTypes.ONE_LINE_STRINGS, OneLine2ViewHolder())
registerViewHolder(ItemTypes.TWO_STRINGS, TwoStringsViewHolder())
registerViewHolder(ItemTypes.CARD, CardViewHolder())
}
}
:
ard item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="card" type="ru.alexmaryin.recycleronvisitor.data.ui_models.CardItem" />
</data>
<androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_margin="8dp"
card_view:cardBackgroundColor="@color/cardview_shadow_end_color"
card_view:cardCornerRadius="15dp">
<ImageView
android:id="@+id/card_background_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"
tools:ignore="ContentDescription"
tools:src="@android:mipmap/sym_def_app_icon" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:drawable/screen_background_dark_transparent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/card_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:textAllCaps="true"
android:textColor="#FFFFFF"
android:textStyle="bold"
tools:text="Cart title"
android:text="@{card.title}"/>
<TextView
android:id="@+id/txt_discription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textColor="#FFFFFF"
tools:text="this is a simple discription with losts of text lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
android:text="@{card.description}"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>
One line item
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable name="model" type="ru.alexmaryin.recycleronvisitor.data.ui_models.OneLineItem2" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:text="@{model.left}"
android:textAlignment="textEnd"
android:textAppearance="?attr/textAppearanceListItem"
android:textColor="@color/cardview_dark_background"
app:layout_constraintEnd_toStartOf="@+id/divider"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlSymmetry,TextContrastCheck"
tools:text="Left text" />
<ImageView
android:id="@+id/divider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.6"
android:padding="5dp"
android:scaleType="center"
android:scaleX="0.5"
android:scaleY="0.9"
android:src="@drawable/ic_outline_waves_24"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@+id/text1"
app:layout_constraintEnd_toStartOf="@+id/text2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/text1"
app:layout_constraintTop_toTopOf="@+id/text1"
app:srcCompat="@drawable/ic_outline_waves_24"
tools:ignore="ContentDescription"
tools:visibility="visible" />
<TextView
android:id="@id/text2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingEnd="8dp"
android:text="@{model.right}"
android:textAppearance="?attr/textAppearanceListItem"
app:layout_constraintBottom_toBottomOf="@+id/divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/divider"
app:layout_constraintTop_toTopOf="@+id/divider"
tools:ignore="RtlSymmetry"
tools:text="Right text" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Two line item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="model" type="ru.alexmaryin.recycleronvisitor.data.ui_models.TwoStringsItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/listPreferredItemHeight"
android:mode="twoLine"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
<TextView
android:id="@+id/text1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{model.caption}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:textAppearance="?attr/textAppearanceListItem" />
<TextView
android:id="@id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{model.details}"
app:layout_constraintTop_toBottomOf="@id/text1"
app:layout_constraintStart_toStartOf="parent"
android:textAppearance="?attr/textAppearanceListItemSecondary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Header item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="headerItem"
type="ru.alexmaryin.recycleronvisitor.data.ui_models.RecyclerHeader" />
</data>
<TextView
style="@style/regularText"
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#591976D2"
android:textAlignment="center"
android:textStyle="italic"
android:text="@{headerItem.text}"/>
</layout>
, :
interface ViewHolderVisitor {
val layout: Int
fun acceptBinding(item: Any): Boolean
fun bind(binding: ViewDataBinding, item: Any, clickListener: AdapterClickListenerById)
}
( acceptVisitor execute, , ) - acceptBinding bind, layout, .
accept : ( ) , , , accept, , true. , , , . - (accept = true), - , .
, , . :
class ViewHoldersManagerImpl : ViewHoldersManager {
private val holdersMap = emptyMap<Int, ViewHolderVisitor>().toMutableMap()
override fun registerViewHolder(itemType: Int, viewHolder: ViewHolderVisitor) {
holdersMap += itemType to viewHolder
}
override fun getItemType(item: Any): Int {
holdersMap.forEach { (itemType, holder) ->
if(holder.acceptBinding(item)) return itemType
}
return ItemTypes.UNKNOWN
}
override fun getViewHolder(itemType: Int) = holdersMap[itemType] ?: throw TypeCastException("Unknown recycler item type!")
}
( ):
class CardViewHolder : ViewHolderVisitor {
override val layout: Int = R.layout.card_item
override fun acceptBinding(item: Any): Boolean = item is CardItem
override fun bind(binding: ViewDataBinding, item: Any, clickListener: AdapterClickListenerById) {
with(binding as CardItemBinding) {
card = item as CardItem
Picasso.get().load(item.image).into(cardBackgroundImage)
}
}
}
as . -, , : accept , CardItem, bind . : layout, binding data binding . -, , idea android studio ?
, recycler view,- , , , :
class BaseListAdapter(
private val clickListener: AdapterClickListenerById,
private val viewHoldersManager: ViewHoldersManager
) : ListAdapter<HasStringId, BaseListAdapter.DataViewHolder>(BaseDiffCallback()) {
inner class DataViewHolder(
private val binding: ViewDataBinding,
private val holder: ViewHolderVisitor
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: HasStringId, clickListener: AdapterClickListenerById) =
holder.bind(binding, item, clickListener)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder =
LayoutInflater.from(parent.context).run {
val holder = viewHoldersManager.getViewHolder(viewType)
DataViewHolder(DataBindingUtil.inflate(this, holder.layout, parent, false), holder)
}
override fun onBindViewHolder(holder: DataViewHolder, position: Int) = holder.bind(getItem(position), clickListener)
override fun getItemViewType(position: Int): Int = viewHoldersManager.getItemType(getItem(position))
}
view, :
// - :
// private val viewModel: MainViewModel by viewModels()
// private lateinit var recycler: RecyclerView
// @Inject lateinit var viewHoldersManager: ViewHoldersManager
// private val items = mutableListOf<HasStringId>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recycler = requireActivity().findViewById(R.id.recycller)
val itemsAdapter = BaseListAdapter(AdapterClickListenerById {}, viewHoldersManager)
itemsAdapter.submitList(items)
recycler.apply {
layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(DividerItemDecoration(requireContext(), (layoutManager as LinearLayoutManager).orientation))
adapter = itemsAdapter
}
populateRecycler()
}
private fun populateRecycler() {
lifecycleScope.launch {
viewModel.getItems().flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { items.add(it) }
}
}
"" , recycler view . :
-;
sealed ;
data- / , view data ;
- ;
, SOLID ;
, (YAGNI).
もちろん、私の実装にはまだ改善と拡張の方法があります。グルーピーのように、要素のグループ化とそれらの視覚的な崩壊を追加できます。データバインディングを破棄するか、ビューバインディングのオプションをアダプターに追加するか、ビューホルダー内のすべてのお気に入りのfindViewByIdで通常のマークアップを膨らませることができます。そして、コードは同じライブラリに変わりますが、そのライブラリにはすでにたくさんのライブラリがあります。私の特定の目的のために、必要が生じた瞬間に、単純な訪問者によるオプションで十分です:
これはアンドロイドの世界での私の最初の誕生なので、厳密に判断しないでください。記事のテキストからの完全なサンプルコードは、githubリポジトリで入手できます。