【发布时间】:2019-10-24 18:48:06
【问题描述】:
在我的 Android 应用程序中,我有一个片段,用户可以在其中同时查看和编辑某些对象的属性。
我正在使用具有数据绑定的 MVVM 体系结构和包含正在编辑的 Relation 对象的中介实时数据。以下是它的工作原理:
- 片段膨胀并绑定视图(布局 xml)。
- 在此过程中,Fragment 制作了一个 ViewModel。
- ViewModel 将从数据库中获取 Relation 对象(及其属性),并将其放入 MediatorLiveData。
- 借助数据绑定和绑定适配器,editText 字段自动设置为对象的属性。
- 然后用户可以编辑这些 editText 字段并保存。
- 保存后,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