Android Data Binding + Mediator Live Data - обработка событий жизненного цикла - PullRequest
1 голос
/ 24 октября 2019

В моем приложении для Android у меня есть фрагмент, где пользователь может одновременно просматривать и редактировать атрибуты некоторых объектов.

Я использую архитектуру MVVM с привязкой данных и посредническими живыми данными, в которых хранится объект Relation. изм. Вот как это работает:

  1. Фрагмент раздувает и связывает представление (макет xml).
  2. Во время этого процесса для фрагмента создается модель ViewModel.
  3. ViewModelбудет извлекать объект Relation (и его атрибуты) из базы данных и помещать его в MediatorLiveData.
  4. Благодаря адаптерам привязки данных и привязки поля editText автоматически устанавливаются на атрибуты объекта.
  5. Затем пользователь может редактировать эти поля editText и сохранять.
  6. После сохранения ViewModel получит тексты из editTexts и использует их для обновления объекта Relation в локальной базе данных

Вот в чем проблема: при повороте экрана фрагмент разрушается и воссоздается. Но у меня нет возможности восстановить содержимое editText. Привязка просто сбросит содержимое editText (поскольку мы еще не обновили атрибуты объекта Relation, мы делаем это только тогда, когда пользователь нажимает «сохранить»).

Я не могу использовать Bundle / saveInstanceStateпотому что привязка просто перезапишет это. Использование MediatorLiveData для хранения отредактированного содержимого также не будет работать, потому что ViewModel разрушается при вращении, поэтому мы теряем эти данные.

Часть макета фрагмента. Обратите внимание на переменную данных (viewmodel) и привязку данных в файле RelationsNameEditText:

<?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"
    tools:context=".presentation.relationdetail.RelationDetailFragment">

    <data>
        <variable
            name="relationDetailViewModel"
            type="be.pjvandamme.farfiled.presentation.relationdetail.RelationDetailViewModel" />
    </data>

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_margin="@dimen/relationDetailLayoutMargin">

                <TextView
                    android:id="@+id/nameLabelTextView"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="24dp"
                    android:layout_marginBottom="8dp"
                    android:text="@string/nameLabel"
                    app:layout_constraintBottom_toTopOf="@+id/relationNameEditText"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintHorizontal_bias="0.0"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />

                <EditText
                    android:id="@+id/relationNameEditText"
                    android:layout_width="@dimen/relationNameEditWidth"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:layout_marginBottom="16dp"
                    android:ems="10"
                    android:inputType="textPersonName"
                    app:layout_constraintBottom_toTopOf="@+id/editTextChips"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintHorizontal_bias="0.0"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/nameLabelTextView"
                    app:relationName="@{relationDetailViewModel.relation}" />

Сам фрагмент:

class RelationDetailFragment : Fragment() {


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding: FragmentRelationDetailBinding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_relation_detail,
            container,
            false
        )

        val application = requireNotNull(this.activity).application

        val arguments = RelationDetailFragmentArgs.fromBundle(arguments!!)

        val relationDataSource = FarFiledDatabase.getInstance(application).relationDao

        val relationLifeAreaDataSource = FarFiledDatabase.getInstance(application).relationLifeAreaDao

        val viewModelFactory =
            RelationDetailViewModelFactory(
                arguments.relationId,
                relationDataSource,
                relationLifeAreaDataSource,
                application
            )

        val relationDetailViewModel =
            ViewModelProviders.of(
                this, viewModelFactory).get(RelationDetailViewModel::class.java)

        binding.relationDetailViewModel = relationDetailViewModel

        binding.setLifecycleOwner(this)

        // stuff about chips

        val textWatcher = object: TextWatcher{ /* */ }

        binding.saveButton.isEnabled = false

        binding.relationNameEditText.addTextChangedListener(textWatcher)
        binding.relationSynopsisEditText.addTextChangedListener(textWatcher)
        binding.lifeAreaNowEditText.addTextChangedListener(textWatcher)
        // etc.

        relationDetailViewModel.enableSaveButton.observe(this, Observer{ /* */})

        relationDetailViewModel.showNameEmptySnackbar.observe(this, Observer{ /* */})

        relationDetailViewModel.navigateToRelationsList.observe(this, Observer{ /* */})

        return binding.root
    }
}

Модель представления:

class RelationDetailViewModel (
    private val relationKey: Long?,
    val relationDatabase: RelationDao,
    val relationLifeAreaDatabase: RelationLifeAreaDao,
    application: Application
): AndroidViewModel(application) {

    private var viewModelJob = Job()
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

    private val relation = MediatorLiveData<Relation?>()
    fun getRelation() = relation

    private val relationLifeAreas = MediatorLiveData<List<RelationLifeArea?>>()
    fun getRelationLifeAreas() = relationLifeAreas

    // other LiveData's with backing properties, to trigger UI events

    init {
        initializeRelation()
    }

    private fun initializeRelation(){
        if(relationKey == null || relationKey == -1L) {
            initializeNewRelation()
            getAdorableAvatarFacialFeatures()
        }
        else {
            retrieveAvatarUrl()
            relation.addSource(
                relationDatabase.getRelationWithId(relationKey),
                relation::setValue)
            relationLifeAreas.addSource(
                relationLifeAreaDatabase.getAllRelationLifeAreasForRelation(relationKey),
                relationLifeAreas::setValue)
        }
    }

    private fun initializeNewRelation(){
        uiScope.launch{
            var relationId = insert(Relation(0L,"","",null,false))
            initializeLifeAreasForRelation(relationId)
            relation.addSource(
                relationDatabase.getRelationWithId(
                        relationId!!),
                relation::setValue)
            relationLifeAreas.addSource(
                relationLifeAreaDatabase.getAllRelationLifeAreasForRelation(
                    relationId!!),
                relationLifeAreas::setValue)
        }
    }

    private fun initializeLifeAreasForRelation(relationId: Long?){
        if(relationId != null){
            enumValues<LifeArea>().forEach {
                uiScope.launch{
                    var relationLifeArea = RelationLifeArea(0L,relationId,it,"")
                    insert(relationLifeArea)
                }
            }
        }
    }

    private fun retrieveAvatarUrl(){
        uiScope.launch{
            var rel = get(relationKey!!)
            var avatarUrl = rel?.avatarUrl
            if (avatarUrl.isNullOrEmpty()){
                getAdorableAvatarFacialFeatures()
                _enableSaveButton.value = true
            }
            else
                _adorableAvatarString.value = rel?.avatarUrl
        }
    }

    private fun getAdorableAvatarFacialFeatures(){
        uiScope.launch{
            var getFeaturesDeferred = AdorableAvatarApi.retrofitService.getFacialFeatures()
            try{
                var result = getFeaturesDeferred.await()
                _adorableAvatarString.value = "https://api.adorable.io/avatars/face/" +
                        result.features.eyes.shuffled().take(1)[0] + "/" +
                        result.features.nose.shuffled().take(1)[0] + "/" +
                        result.features.mouth.shuffled().take(1)[0] + "/" +
                        result.features.COLOR_PALETTE.shuffled().take(1)[0]
                relation.value?.avatarUrl = _adorableAvatarString.value
            } catch(t:Throwable){
                // ToDo: what if this fails?? -> Try again later!!
                _adorableAvatarString.value = "Failure: " + t.message
            }
        }
    }

    fun onEditRelation(
        relationNameText: String,
        relationSynopsisText: String,
        lifeAreaNowText: String,
        // etc.
    ){
        _enableSaveButton.value = !compareRelationAttributes(
            relationNameText,
            relationSynopsisText,
            lifeAreaNow.Text,
            // etc
        )
    }

    private fun compareRelationAttributes(
        relationNameText: String,
        relationSynopsisText: String,
        lifeAreaNowText: String,
        // etc.
    ): Boolean {
        // checks if any of the attributes of the Relation object were changed
        // i.e. at least 1 of the editText fields has a text content that does
        // does not equal the corresponding attribute of the Relation object
    }

    fun onSave(
        name: String,
        synopsis: String,
        nowLA: String,
        // etc.
    ){
        if(!name.isNullOrEmpty()) {
            uiScope.launch {
                // update the DB
            }
            // TODO: this one should go away, need some sort of up button instead
            _navigateToRelationsList.value = true
        }
        else
            _showNameEmptySnackbar.value = true
    }

    // database suspend funs omitted

    // ui event handling functions

    override fun onCleared(){ /* cancel the viewModelJob */ }

}

адаптеры привязки:

@BindingAdapter("relationName")
fun TextView.setRelationName(item: Relation?){
    item?.let{
        text = item.name
    }
}

@BindingAdapter("relationSynopsis")
fun TextView.setRelationSynopsis(item: Relation?){
    item?.let{
        text = item.synopsis
    }
}

@BindingAdapter("relationLifeAreaNow")
fun TextView.setLifeAreaNowText(item: List<RelationLifeArea?>?){
    item?.let{
        item.forEach{
            if(it?.lifeArea == LifeArea.EPHEMERA){
                text = it.content
            }
        }
    }
}
<!-- etc. -->

Итак, мой вопрос: как мне с этим справиться? Я думаю, что единственным решением было бы: 1) держать объект Relation с атрибутами EDITED, обновляемыми всякий раз, когда пользователь редактирует editText, 2) сохранять его в базе данных.

Но я так не думаюбыло бы архитектурно обоснованным. И я не уверен, что это сработает.

1 Ответ

0 голосов
/ 14 ноября 2019

В этом случае вы можете попробовать Двухстороннее связывание данных

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...