Skip to content

Instantly share code, notes, and snippets.

@ildar2
Created August 27, 2021 11:23
Show Gist options
  • Save ildar2/9f51bbdb22e32ff397a11054b898845a to your computer and use it in GitHub Desktop.
Save ildar2/9f51bbdb22e32ff397a11054b898845a to your computer and use it in GitHub Desktop.
UI как список через DisplayDiffAdapter
//
// UI как список
//
// у нас есть единый лэйаут fragment_list_reusable.xml, который используется на 10 экранов
<androidx.constraintlayout.motion.widget.MotionLayout
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/motion_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
app:layoutDescription="@xml/fragment_list_reusable_scene">
<ImageView
android:id="@+id/iv_close"
style="@style/ButtonClose"
android:contentDescription="@string/desc_back"
android:focusable="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:importantForAccessibility="no"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/iv_close"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_close"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_header"
style="@style/TextTitle"
android:layout_width="0dp"
android:layout_marginHorizontal="@dimen/activity_horizontal_margin"
android:layout_marginTop="28dp"
android:gravity="left"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_close"
tools:text="Заголовок"
tools:visibility="visible" />
<ImageView
android:id="@+id/iv_empty"
android:layout_width="140dp"
android:layout_height="140dp"
android:contentDescription="@string/desc_empty_list"
android:src="@drawable/ic_empty_list"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_header"
app:layout_constraintVertical_bias="0.3"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/activity_horizontal_margin"
android:layout_marginTop="16dp"
android:gravity="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_empty"
tools:text="Сообщение о пустом списке"
tools:visibility="visible" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:enabled="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_header">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingBottom="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_notification" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>
// фрагмент отвечает за отображение UI, за навигацию и за пермишены
/**
* Профиль юзера - https://www.figma.com/file/TFTeD2l2RwdBTEU1Ffx5fG/KDIF-mobile.3?node-id=2009%3A6006
*
* открывается с верхнего меню на вкладках, если юзер залогинен
*/
class ProfileFragment : Fragment(R.layout.fragment_list_reusable) {
private val viewModel: ProfileViewModel by viewModel()
private val adapter = CommonAdapter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//motion_layout.setTransition(R.id.collapsed, R.id.collapsed) - можно убрать заголовок
tv_header.setText(R.string.profile_header)
iv_empty.hide()
tv_empty.hide()
iv_close.setOnClickListener {
activity?.onBackPressed()
}
rv.adapter = adapter
observe(viewModel.itemsLiveData) {
adapter.items = it
}
observe(AuthManager.loggedInLiveData) {
if (!it.loggedIn) {
open(
R.id.mobile_navigation,
bundleOf(MainFragment.PAGE_ARGUMENT to MainFragment.PAGE_HOME)
)
}
}
observeEvent(viewModel.errorLiveData) {
toast(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
rv.adapter = null
}
}
// во вьюмоделе находится вся логика
// элементы могут взаимодействовать друг с другом и с вьюмоделом напрямую
class ProfileViewModel(
private val repository: ProfileRepository
) : BaseViewModel() {
val itemsLiveData = MutableLiveData<List<DisplayDiffItem>>()
init {
loadItems()
}
private fun loadItems() {
val user = AuthManager.currentUser
itemsLiveData.value = listOf(
CommonInputDisplay(
locz.static(R.string.profile_fio),
user?.printFull(),
enabled = false
),
CommonInputDisplay(locz.static(R.string.profile_iin), user?.taxCode, enabled = false),
CommonInputDisplay(locz.static(R.string.profile_phone), "", enabled = false),
CommonSwitchDisplay(
locz.static(R.string.profile_receive_push),
user?.isPushSubscribed == true
) {
sendPushSettingsUpdate(it.checked)
},
CommonButtonAlternativeDisplay(R.string.profile_logout) {
AuthManager.logout()
}
)
}
private fun sendPushSettingsUpdate(checked: Boolean) {
coroutineJob.cancelChildren()
AuthManager.currentUser?.isPushSubscribed = checked
//todo shared prefs?
makeRequest({ repository.setPushEnabled(checked) }) {
when (it) {
is RequestResult.Success -> {
//do nothing
}
is RequestResult.Error -> {
setError(it.error)
}
}
}
}
}
// для более сложных экранов есть специальный интерфейс для вьюмоделов
/**
* Общая конструкция вьюмодели для экранов с UI в виде списка
*
* [itemsLiveData] - список элементов для [CommonAdapter]
* [actionLiveData] - события для фрагмента
* [T] - enum с типами событий и данными для них
*/
interface CommonItemViewModel<T> {
val itemsLiveData: MutableLiveData<List<DisplayDiffItem>>
val actionLiveData: MutableLiveData<EventWrapper<T>>
fun loadItems(update: Boolean = false)
}
// суперсложный UI: ClaimsCreatorViewModel, с отдельным AssetManagerImpl для работы со списком прикрепляемых файлов
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment