【问题标题】:Android Data Binding + Mediator Live Data - Handling Lifecycle EventsAndroid Data Binding + Mediator Live Data - 处理生命周期事件
【发布时间】:2019-10-24 18:48:06
【问题描述】:

在我的 Android 应用程序中,我有一个片段,用户可以在其中同时查看和编辑某些对象的属性。

我正在使用具有数据绑定的 MVVM 体系结构和包含正在编辑的 Relation 对象的中介实时数据。以下是它的工作原理:

  1. 片段膨胀并绑定视图(布局 xml)。
  2. 在此过程中,Fragment 制作了一个 ViewModel。
  3. ViewModel 将从数据库中获取 Relation 对象(及其属性),并将其放入 MediatorLiveData。
  4. 借助数据绑定和绑定适配器,editText 字段自动设置为对象的属性。
  5. 然后用户可以编辑这些 editText 字段并保存。
  6. 保存后,ViewModel 将从 editTexts 中获取文本并使用它们来更新本地数据库中的 Relation 对象

问题出在:旋转屏幕时,片段会被销毁并重新创建。但我无法恢复 editText 内容。绑定只会重置 editText 内容(因为我们实际上还没有更新 Relation 对象属性,所以我们只在用户按下“保存”时才这样做)。

我不能使用 Bundle / savedInstanceState 因为绑定只会覆盖它。使用 MediatorLiveData 来保存编辑的内容也不起作用,因为 ViewModel 在旋转时会被破坏,因此我们会丢失该数据。

片段布局的一部分。注意relationNameEditText中的数据变量(viewmodel)和数据绑定:

<?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) 持有一个具有 EDITED 属性的 Relation 对象,每当用户编辑 editText 时更新,2) 将其存储在数据库中。

但我不认为这在架构上是合理的。我也不确定它是否会起作用。

【问题讨论】:

  • 您找到解决方案了吗?我面临着类似的问题。
  • @IgorGanapolsky 是的。 ViewModel 基本上被设计为即使您的活动被破坏和重建也能保持活动状态(但请注意:如果您手动将活动设置为空,或替换您的片段,则视图模型也会被破坏)。因此,解决方案是拥有对象数据的 2 个版本:1 个 MediatorLiveData,它保存存储在数据库中的对象,1 个 LiveData,它是该对象的副本,并且是与布局绑定的数据。保存时,您只需将这些值复制到数据库绑定对象并持久保存。
  • 我什至不确定是否真的需要保存对 DB 对象的引用,因为您可以在保存时使用 id 检索它。

标签: android-fragments mvvm android-lifecycle android-databinding android-livedata


【解决方案1】:

这种情况可以试试Two Way Data Binding

【讨论】:

  • 不要只发布(可能的)解决方案的链接。请简要描述如何解决问题,如果可能的话,请举例说明
猜你喜欢
  • 2020-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-02
  • 2022-06-30
  • 2012-09-19
  • 2016-11-05
  • 1970-01-01
相关资源
最近更新 更多