Created
August 27, 2021 11:23
-
-
Save ildar2/9f51bbdb22e32ff397a11054b898845a to your computer and use it in GitHub Desktop.
UI как список через DisplayDiffAdapter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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