Irbisa · Шпаргалка

View, разметка и RecyclerView

Разметка, ConstraintLayout, ViewBinding, свои View, measure/layout/draw, RecyclerView, DiffUtil

Middle-разработчик · 25 вопросов

  1. 01

    Чем отличаются View.VISIBLE, View.INVISIBLE и View.GONE?

    INVISIBLE прячет view, но оставляет за ней место в разметке; GONE прячет её и убирает из разметки, так что соседи занимают освободившееся место.

  2. 02

    Чем отличаются px, dp и sp, и что где использовать?

    px обозначает физический пиксель, dp обозначает независимый от плотности пиксель, который имеет один и тот же физический размер на любом экране, а sp работает как dp, который ещё и масштабируется вместе с настройкой размера шрифта.

  3. 03

    Чем стиль отличается от темы, и что означает ?attr/colorPrimary?

    Стиль представляет собой набор значений атрибутов для одной view; тема является набором, применённым к приложению, Activity или поддереву view, и она же задаёт атрибуты темы вроде colorPrimary, которые любая view может найти через ?attr/.

    <!-- res/values/themes.xml: a theme, applied to the whole app --><style name="Theme.App" parent="Theme.Material3.DayNight.NoActionBar">    <item name="colorPrimary">@color/brand</item></style> <!-- A style: values for one view, colours taken from the theme -->…
  4. 04

    Чем отличаются ViewBinding, DataBinding и findViewById?

    findViewById ищет view в рантайме и может вернуть null или не тот тип; ViewBinding генерирует на каждую разметку класс с типизированным полем для каждого android:id (nullable, только если view есть не во всех вариантах); DataBinding добавляет выражения и двустороннее связывание в XML.

    // build.gradle.kts: android { buildFeatures { viewBinding = true } } // res/layout/activity_main.xml -> ActivityMainBindingclass MainActivity : AppCompatActivity() {  // Fine in an Activity: the Activity and its views die together  private lateinit var binding: ActivityMainBinding…
  5. 05

    Как работает RecyclerView, и когда вызываются onCreateViewHolder и onBindViewHolder?

    RecyclerView создаёт ровно столько строк, сколько нужно для экрана, плюс несколько запасных, и переиспользует их при скролле: onCreateViewHolder инфлейтит новую строку, только когда в пуле нет свободной строки этого view type, а onBindViewHolder заполняет строку данными для позиции.

    class OrderAdapter(  private val items: List<Order>,  private val onClick: (Order) -> Unit,) : RecyclerView.Adapter<OrderAdapter.Holder>() {   class Holder(val binding: ItemOrderBinding) : RecyclerView.ViewHolder(binding.root)…
  6. 06

    Что добавляют RecyclerView классы ItemDecoration, ItemTouchHelper и SnapHelper?

    Они выносят оформление и жесты списка из разметки строки: ItemDecoration добавляет отступы и разделители, ItemTouchHelper добавляет смахивание для удаления и перетаскивание для сортировки, а SnapHelper останавливает флинг так, чтобы элемент был выровнен.

    class SpacingDecoration(private val space: Int) : RecyclerView.ItemDecoration() {  override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {    val pos = parent.getChildAdapterPosition(view)    if (pos == RecyclerView.NO_POSITION) return    outRect.bottom = space    if (pos == 0) outRect.top = space        // equal gaps, no doubling between rows…
  7. 07

    Какие проблемы решает ConstraintLayout, и что делают 0dp, цепочки, guideline и barrier?

    Он позволяет собрать сложный экран одним плоским уровнем вместо вложенных LinearLayout и RelativeLayout.

    <!-- right: 0dp fills what start..end leaves; match_parent would ignore the end constraint --><TextView    android:id="@+id/title"    android:layout_width="0dp"    android:layout_height="wrap_content"    app:layout_constraintTop_toTopOf="parent"…
  8. 08

    Как работает инфлейт разметки, и что меняют <include>, <merge> и ViewStub?

    LayoutInflater проходит по скомпилированному XML на главном потоке и создаёт объект View для каждого тега; <include> переиспользует другой файл разметки, <merge> убирает лишний корень этого файла, а ViewStub откладывает инфлейт части экрана до момента, когда она понадобится.

    <!-- res/layout/view_header.xml: <merge>, so both views attach to the including parent --><merge xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    tools:parentTag="android.widget.LinearLayout">  <ImageView      android:id="@+id/avatar"…
  9. 09

    Этот Fragment держит binding в lateinit var, и LeakCanary сообщает об утечке. Что не так и как это исправить?

    class ProfileFragment : Fragment(R.layout.fragment_profile) {  private lateinit var binding: FragmentProfileBinding   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {    binding = FragmentProfileBinding.bind(view)    lifecycleScope.launch {      viewModel.state.collect { binding.title.text = it.title }    }  }}

    Fragment живёт дольше своей view: после onDestroyView он остаётся в back stack, и поле binding удерживает мёртвое дерево view.

  10. 10

    Какой жизненный цикл у View, от создания до onDetachedFromWindow?

    View создаётся (из XML или в коде), присоединяется к окну, проходит measure, layout и draw в каждом кадре, где это нужно, и в конце отсоединяется.

  11. 11

    Почему view.width равен 0 в onCreate, и как получить настоящий размер?

    В onCreate дерево view только заинфлейчено; measure и layout выполняются позже, в первом кадре после присоединения окна, так что до этого любой размер равен 0.

    // binding: ViewBinding for this Activity, as in the ViewBinding questionoverride fun onCreate(savedInstanceState: Bundle?) {  super.onCreate(savedInstanceState)  setContentView(binding.root)   Log.d(TAG, "width=${binding.chart.width}")      // 0: not measured yet…
  12. 12

    Как работают onMeasure, onLayout и onDraw в кастомной View, и когда вызывать invalidate(), а когда requestLayout()?

    Родитель измеряет каждого ребёнка с помощью MeasureSpec, расставляет его через layout(), а затем каждая view рисует себя в onDraw.

    class MeterView @JvmOverloads constructor(  context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {   private val paint = Paint(Paint.ANTI_ALIAS_FLAG)   // created once, not in onDraw  private val bar = RectF()…
  13. 13

    Как добавить View собственные XML-атрибуты и сделать так, чтобы она сохраняла состояние при повороте?

    Объявите атрибуты в <declare-styleable> и прочитайте их в конструкторе через obtainStyledAttributes; для состояния переопределите onSaveInstanceState и onRestoreInstanceState с подклассом BaseSavedState.

    // res/values/attrs.xml// <declare-styleable name="RatingView">//   <attr name="rating" format="integer" />//   <attr name="starColor" format="color" />// </declare-styleable>…
  14. 14

    Когда стоит использовать SurfaceView или TextureView вместо обычной View?

    Когда кадры производятся вне главного потока и с высокой частотой: видео, превью камеры, игра или рендерер карты.

  15. 15

    Какие API анимации есть у View, и почему анимация translationX плавная, а анимация высоты дёргается?

    View анимируют через property-аниматоры (ValueAnimator, ObjectAnimator, view.animate()), Transitions, MotionLayout и spring/fling-анимации.

    // wrong: requestLayout and a re-measure of the branch on every frameValueAnimator.ofInt(0, targetHeight).apply {  addUpdateListener { panel.updateLayoutParams { height = it.animatedValue as Int } }}.start() // right: RenderNode properties, no measure or layout…
  16. 16

    Что дают DiffUtil и ListAdapter по сравнению с notifyDataSetChanged(), и как обновить одну view в одной строке?

    notifyDataSetChanged() говорит «изменилось всё»: каждая видимая строка биндится заново, ничего не анимируется, позиция скролла теряется.

    class ChatAdapter : ListAdapter<Chat, ChatAdapter.Holder>(Diff) {   override fun onBindViewHolder(holder: Holder, position: Int) {    val item = getItem(position)    holder.binding.title.text = item.title    holder.binding.unread.text = item.unread.toString()…
  17. 17

    Найдите баг: после нажатия на лайк сердечко не закрашивается, пока строка не уйдёт за экран и не вернётся.

    data class Post(val id: Long, val text: String, var liked: Boolean) object PostDiff : DiffUtil.ItemCallback<Post>() {  override fun areItemsTheSame(old: Post, new: Post) = old.id == new.id  override fun areContentsTheSame(old: Post, new: Post) = old == new} class FeedViewModel : ViewModel() {  private val _posts = MutableStateFlow<List<Post>>(emptyList())  val posts: StateFlow<List<Post>> = _posts // the Fragment calls adapter.submitList(it)  fun toggleLike(id: Long) {    val copy = _posts.value.toMutableList()    copy.first { it.id == id }.apply { liked = !liked }    _posts.value = copy  }}

    toggleLike копирует список, но меняет общий Post на месте, поэтому меняется и список, который уже лежит в адаптере.

  18. 18

    После скролла строки этого списка показывают бейдж предыдущего элемента и чужое фото. Что не так в onBindViewHolder?

    override fun onBindViewHolder(holder: Holder, position: Int) {  val item = items[position]  holder.binding.title.text = item.title  if (item.isUrgent) {    holder.binding.badge.visibility = View.VISIBLE  }  holder.binding.root.setOnClickListener { onClick(items[position]) }  imageLoader.load(item.photoUrl) { bitmap ->    holder.binding.photo.setImageBitmap(bitmap)  }}

    Holder представляет собой переиспользованную строку, на которой всё ещё видно то, что выставил предыдущий элемент, поэтому bind обязан задавать каждую view на каждой ветке.

  19. 19

    Найдите баг: после прокрутки списка задач те, которых вы не касались, сами становятся отмеченными или снимаются.

    class TaskAdapter(  private val onToggle: (Task, Boolean) -> Unit,) : ListAdapter<Task, TaskAdapter.Holder>(TaskDiff) {   class Holder(val binding: ItemTaskBinding) : RecyclerView.ViewHolder(binding.root)   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =    Holder(ItemTaskBinding.inflate(LayoutInflater.from(parent.context), parent, false))   override fun onBindViewHolder(holder: Holder, position: Int) {    val task = getItem(position)    holder.binding.title.text = task.title    holder.binding.done.isChecked = task.done    holder.binding.done.setOnCheckedChangeListener { _, checked -> onToggle(task, checked) }  }}

    Bind присваивает isChecked, пока на переиспользованной строке ещё висит листенер предыдущего элемента, а setChecked вызывает его синхронно.

  20. 20

    Как показать в одном RecyclerView несколько типов строк: заголовки, рекламу и карточки?

    Возвращайте свой getItemViewType для каждого вида строки и создавайте соответствующий ViewHolder в onCreateViewHolder.

    // HeaderVH, ArticleVH, AdVH: ViewHolder subclasses, omittedsealed interface Row {  val id: Long  data class Header(override val id: Long, val title: String) : Row  data class Article(override val id: Long, val headline: String) : Row  data class Ad(override val id: Long, val slot: String) : Row…
  21. 21

    RecyclerView дёргается при скролле. Что проверять и в каком порядке?

    Сначала измерьте: трасса в Perfetto или профайлер Android Studio покажет, какие слайсы RecyclerView (RV OnBindView, RV CreateView, RV OnLayout) не укладываются в кадр.

  22. 22

    Как CoordinatorLayout и сворачивающийся тулбар работают вместе с RecyclerView?

    Дети CoordinatorLayout реагируют друг на друга через Behavior, а список сообщает о своём скролле через nested scrolling, поэтому AppBarLayout может поглотить скролл и свернуться первым.

    <androidx.coordinatorlayout.widget.CoordinatorLayout    android:layout_width="match_parent"    android:layout_height="match_parent">   <com.google.android.material.appbar.AppBarLayout      android:layout_width="match_parent"…
  23. 23

    Как touch-событие проходит по иерархии View, и как родитель забирает жест у ребёнка?

    MotionEvent спускается через dispatchTouchEvent: каждая ViewGroup может перехватить его в onInterceptTouchEvent, иначе оно уходит ребёнку под пальцем, а необработанные события всплывают обратно через onTouchEvent.

    import kotlin.math.abs class CarouselHost(ctx: Context, attrs: AttributeSet?) : FrameLayout(ctx, attrs) {  private val slop = ViewConfiguration.get(ctx).scaledTouchSlop  private var downX = 0f  private var downY = 0f…
  24. 24

    Почему этот код падает при нажатии на кнопку, но обычно не падает, когда похожая строка выполняется в onCreate?

    override fun onCreate(savedInstanceState: Bundle?) {  super.onCreate(savedInstanceState)  setContentView(binding.root)   thread { binding.status.text = "Loading" }            // usually no crash   binding.refresh.setOnClickListener {    thread { binding.status.text = repository.load() }  // crashes  }}

    View не потокобезопасны, а проверка живёт в ViewRootImpl.checkThread(), которая работает только после присоединения view к окну.

  25. 25

    После повышения targetSdk до 35 и выше тулбар уехал под статус-бар, а клавиатура закрывает поле ввода. Что изменилось и как это исправить?

    Приложения с targetSdk 35 на Android 15 работают в режиме edge-to-edge принудительно: окно заходит под статус-бар и панель навигации, setStatusBarColor игнорируется, и каждая view у края должна сама применять WindowInsets.

    // binding: ViewBinding for this Activity, as in the ViewBinding questionoverride fun onCreate(savedInstanceState: Bundle?) {  enableEdgeToEdge()                 // same behaviour below API 35  super.onCreate(savedInstanceState)  setContentView(binding.root)…