ViewModel在屏幕旋转时更新

问题描述 投票:2回答:1

我创建了一个简单的项目来研究Kotlin和Android体系结构

https://github.com/AOreshin/shtatus

该屏幕由RecyclerView和三个EditText组成。

对应的ViewModel公开了7个LiveData:

  • 与过滤器相对应的三个LiveData
  • 通知用户找不到条目的事件
  • 通知用户没有条目的事件
  • SwipeRefreshLayout的状态
  • 根据过滤器输入显示的连接列表

当用户在过滤器中键入文本时,ViewModel的LiveData会收到有关更改的通知并更新数据。我读过,将MutableLiveData暴露给Activity / Fragments是一个不好的做法,但是它们必须以某种方式通知ViewModel有关更改。根据用户输入未找到任何条目时,将显示Toast。

问题

当用户输入不匹配的过滤器值时,将显示Toast。如果用户随后旋转设备,则Toast将再次显示。

我已阅读这些文章:

https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

https://proandroiddev.com/livedata-with-single-events-2395dea972a8

但是我不明白如何将这些应用于我的用例。我认为我执行更新的问题

private val connections = connectionRepository.allConnections()
private val mediatorConnection = MediatorLiveData<List<Connection>>().also {
    it.value = connections.value
}

private val refreshLiveData = MutableLiveData(RefreshStatus.READY)
private val noMatchesEvent = SingleLiveEvent<Void>()
private val emptyTableEvent = SingleLiveEvent<Void>()

val nameLiveData = MutableLiveData<String>()
val urlLiveData = MutableLiveData<String>()
val actualStatusLiveData = MutableLiveData<String>()

init {
    with(mediatorConnection) {
        addSource(connections) { update() }
        addSource(nameLiveData) { update() }
        addSource(urlLiveData) { update() }
        addSource(actualStatusLiveData) { update() }
    }
}

fun getRefreshLiveData(): LiveData<RefreshStatus> = refreshLiveData
fun getNoMatchesEvent(): LiveData<Void> = noMatchesEvent
fun getEmptyTableEvent(): LiveData<Void> = emptyTableEvent
fun getConnections(): LiveData<List<Connection>> = mediatorConnection

private fun update() {
    if (connections.value.isNullOrEmpty()) {
        emptyTableEvent.call()
    } else {
        mediatorConnection.value = connections.value?.filter { connection -> getPredicate().test(connection) }

        if (mediatorConnection.value.isNullOrEmpty()) {
            noMatchesEvent.call()
        }
    }
}

[update()由于新订阅mediatorConnection而在屏幕旋转时触发,并调用了MediatorLiveData.onActive()。这是故意的行为

Android live data - observe always fires after config change

显示吐司的代码

package com.github.aoreshin.shtatus.fragments

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.github.aoreshin.shtatus.R
import com.github.aoreshin.shtatus.ShatusApplication
import com.github.aoreshin.shtatus.viewmodels.ConnectionListViewModel
import javax.inject.Inject

class ConnectionListFragment : Fragment() {
    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    private lateinit var refreshLayout: SwipeRefreshLayout

    private lateinit var nameEt: EditText
    private lateinit var urlEt: EditText
    private lateinit var statusCodeEt: EditText

    private lateinit var viewModel: ConnectionListViewModel

    private lateinit var recyclerView: RecyclerView
    private lateinit var viewAdapter: ConnectionListAdapter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_connection_list, container, false)

        val application = (requireActivity().application as ShatusApplication)
        application.appComponent.inject(this)

        val viewModelProvider = ViewModelProvider(this, viewModelFactory)
        viewModel = viewModelProvider.get(ConnectionListViewModel::class.java)

        bindViews(view)
        setupObservers()
        setupListeners()
        addFilterValues()
        setupRecyclerView()
        return view
    }

    private fun setupObservers() {
        with(viewModel) {
            getConnections().observe(viewLifecycleOwner, Observer { viewAdapter.submitList(it) })

            getRefreshLiveData().observe(viewLifecycleOwner, Observer { status ->
                when (status) {
                    ConnectionListViewModel.RefreshStatus.LOADING -> refreshLayout.isRefreshing = true
                    ConnectionListViewModel.RefreshStatus.READY -> refreshLayout.isRefreshing = false
                    else -> throwException(status.toString())
                }
            })

            getNoMatchesEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_matches) })
            getEmptyTableEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_connections) })
        }
    }

    private fun setupRecyclerView() {
        viewAdapter = ConnectionListAdapter(parentFragmentManager, ConnectionItemCallback())
        recyclerView.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = viewAdapter
        }
    }

    private fun addFilterValues() {
        with(viewModel) {
            nameEt.setText(nameLiveData.value)
            urlEt.setText(urlLiveData.value)
            statusCodeEt.setText(actualStatusLiveData.value)
        }
    }

    private fun bindViews(view: View) {
        with(view) {
            recyclerView = findViewById(R.id.recycler_view)
            refreshLayout = findViewById(R.id.refresher)
            nameEt = findViewById(R.id.nameEt)
            urlEt = findViewById(R.id.urlEt)
            statusCodeEt = findViewById(R.id.statusCodeEt)
        }
    }

    private fun setupListeners() {
        with(viewModel) {
            refreshLayout.setOnRefreshListener { send() }
            nameEt.addTextChangedListener { nameLiveData.value = it.toString() }
            urlEt.addTextChangedListener { urlLiveData.value = it.toString() }
            statusCodeEt.addTextChangedListener { actualStatusLiveData.value = it.toString() }
        }
    }

    private fun throwException(status: String) {
        throw IllegalStateException(getString(R.string.error_no_such_status) + status)
    }

    private fun showToast(resourceId: Int) {
        Toast.makeText(context, getString(resourceId), Toast.LENGTH_SHORT).show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        with(viewModel) {
            getNoMatchesEvent().removeObservers(viewLifecycleOwner)
            getRefreshLiveData().removeObservers(viewLifecycleOwner)
            getEmptyTableEvent().removeObservers(viewLifecycleOwner)
            getConnections().removeObservers(viewLifecycleOwner)
        }
    }
}

我应如何解决此问题?

android kotlin android-recyclerview android-livedata android-architecture-components
1个回答
0
投票

在挠头之后,我决定使用内部ViewModel状态,这样可以将Activity / Fragment中的逻辑保持在最低水平。

所以现在我的ViewModel看起来像这样:

package com.github.aoreshin.shtatus.viewmodels

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.aoreshin.shtatus.events.SingleLiveEvent
import com.github.aoreshin.shtatus.room.Connection
import io.reactivex.FlowableSubscriber
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.subscribers.DisposableSubscriber
import okhttp3.ResponseBody
import retrofit2.Response
import java.util.function.Predicate
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ConnectionListViewModel @Inject constructor(
    private val connectionRepository: ConnectionRepository
) : ViewModel() {
    private var tableStatus = TableStatus.OK

    private val connections = connectionRepository.allConnections()
    private val mediatorConnection = MediatorLiveData<List<Connection>>()

    private val stopRefreshingEvent = SingleLiveEvent<Void>()
    private val noMatchesEvent = SingleLiveEvent<Void>()
    private val emptyTableEvent = SingleLiveEvent<Void>()

    private val nameLiveData = MutableLiveData<String>()
    private val urlLiveData = MutableLiveData<String>()
    private val statusLiveData = MutableLiveData<String>()

    init {
        with(mediatorConnection) {
            addSource(connections) { update() }
            addSource(nameLiveData) { update() }
            addSource(urlLiveData) { update() }
            addSource(statusLiveData) { update() }
        }
    }

    fun getStopRefreshingEvent(): LiveData<Void> = stopRefreshingEvent
    fun getNoMatchesEvent(): LiveData<Void> = noMatchesEvent
    fun getEmptyTableEvent(): LiveData<Void> = emptyTableEvent
    fun getConnections(): LiveData<List<Connection>> = mediatorConnection
    fun getName(): String? = nameLiveData.value
    fun getUrl(): String? = urlLiveData.value
    fun getStatus(): String? = statusLiveData.value
    fun setName(name: String) { nameLiveData.value = name }
    fun setUrl(url: String) { urlLiveData.value = url }
    fun setStatus(status: String) { statusLiveData.value = status }

    private fun update() {
        if (connections.value != null) {
            if (connections.value.isNullOrEmpty()) {
                if (tableStatus != TableStatus.EMPTY) {
                    emptyTableEvent.call()
                    tableStatus = TableStatus.EMPTY
                }
            } else {
                mediatorConnection.value = connections.value?.filter { connection -> getPredicate().test(connection) }

                if (mediatorConnection.value.isNullOrEmpty()) {
                    if (tableStatus != TableStatus.NO_MATCHES) {
                        noMatchesEvent.call()
                        tableStatus = TableStatus.NO_MATCHES
                    }
                } else {
                    tableStatus = TableStatus.OK
                }
            }
        }
    }

    fun send() {
        if (!connections.value.isNullOrEmpty()) {
            val singles = connections.value?.map { connection ->
                val id = connection.id
                val description = connection.description
                val url = connection.url
                var message = ""

                connectionRepository.sendRequest(url)
                    .doOnSuccess { message = it.code().toString() }
                    .doOnError { message = it.message!! }
                    .doFinally {
                        val result = Connection(id, description, url, message)
                        connectionRepository.insert(result)
                    }
            }

            Single.mergeDelayError(singles)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doFinally { stopRefreshingEvent.call() }
                .subscribe(getSubscriber())
        } else {
            stopRefreshingEvent.call()
        }
    }

    private fun getSubscriber() : FlowableSubscriber<Response<ResponseBody>> {
        return object: DisposableSubscriber<Response<ResponseBody>>() {
            override fun onComplete() { Log.d(TAG, "All requests sent") }
            override fun onNext(t: Response<ResponseBody>?) { Log.d(TAG, "Request is done") }
            override fun onError(t: Throwable?) { Log.d(TAG, t!!.message!!) }
        }
    }

    private fun getPredicate(): Predicate<Connection> {
        return Predicate { connection ->
            connection.description.contains(nameLiveData.value.toString(), ignoreCase = true)
                    && connection.url.contains(urlLiveData.value.toString(), ignoreCase = true)
                    && connection.actualStatusCode.contains(
                statusLiveData.value.toString(),
                ignoreCase = true
            )
        }
    }

    private enum class TableStatus {
        NO_MATCHES,
        EMPTY,
        OK
    }

    companion object {
        private const val TAG = "ConnectionListViewModel"
    }
}

相应的片段看起来像这样:

package com.github.aoreshin.shtatus.fragments

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.github.aoreshin.shtatus.R
import com.github.aoreshin.shtatus.ShatusApplication
import com.github.aoreshin.shtatus.viewmodels.ConnectionListViewModel
import javax.inject.Inject

class ConnectionListFragment : Fragment() {
    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    private lateinit var refreshLayout: SwipeRefreshLayout

    private lateinit var nameEt: EditText
    private lateinit var urlEt: EditText
    private lateinit var statusCodeEt: EditText

    private lateinit var viewModel: ConnectionListViewModel

    private lateinit var recyclerView: RecyclerView
    private lateinit var viewAdapter: ConnectionListAdapter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_connection_list, container, false)

        val application = (requireActivity().application as ShatusApplication)
        application.appComponent.inject(this)

        val viewModelProvider = ViewModelProvider(this, viewModelFactory)
        viewModel = viewModelProvider.get(ConnectionListViewModel::class.java)

        bindViews(view)
        setupObservers()
        setupListeners()
        addFilterValues()
        setupRecyclerView()
        return view
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        if (savedInstanceState != null) {
            refreshLayout.isRefreshing = savedInstanceState.getBoolean(REFRESHING, false)
        }
    }

    private fun setupObservers() {
        with(viewModel) {
            getConnections().observe(viewLifecycleOwner, Observer { viewAdapter.submitList(it) })
            getStopRefreshingEvent().observe(viewLifecycleOwner, Observer { refreshLayout.isRefreshing = false })
            getNoMatchesEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_matches) })
            getEmptyTableEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_connections) })
        }
    }

    private fun setupRecyclerView() {
        viewAdapter = ConnectionListAdapter(parentFragmentManager, ConnectionItemCallback())
        recyclerView.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = viewAdapter
        }
    }

    private fun addFilterValues() {
        with(viewModel) {
            nameEt.setText(getName())
            urlEt.setText(getUrl())
            statusCodeEt.setText(getStatus())
        }
    }

    private fun bindViews(view: View) {
        with(view) {
            recyclerView = findViewById(R.id.recycler_view)
            refreshLayout = findViewById(R.id.refresher)
            nameEt = findViewById(R.id.nameEt)
            urlEt = findViewById(R.id.urlEt)
            statusCodeEt = findViewById(R.id.statusCodeEt)
        }
    }

    private fun setupListeners() {
        with(viewModel) {
            refreshLayout.setOnRefreshListener { send() }
            nameEt.addTextChangedListener { setName(it.toString()) }
            urlEt.addTextChangedListener { setUrl(it.toString()) }
            statusCodeEt.addTextChangedListener { setStatus(it.toString()) }
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putBoolean(REFRESHING, refreshLayout.isRefreshing)
    }

    private fun showToast(resourceId: Int) {
        Toast.makeText(context, getString(resourceId), Toast.LENGTH_SHORT).show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        with(viewModel) {
            getNoMatchesEvent().removeObservers(viewLifecycleOwner)
            getEmptyTableEvent().removeObservers(viewLifecycleOwner)
            getStopRefreshingEvent().removeObservers(viewLifecycleOwner)
            getConnections().removeObservers(viewLifecycleOwner)
        }
    }

    companion object {
        private const val REFRESHING = "isRefreshing"
    }
}

Pros

  • 没有其他依赖项
  • 广泛使用SingleLiveEvent的用途
  • 非常容易实现

缺点

即使在这种简单情况下,条件逻辑也很快失去控制,肯定需要重构。不确定这种方法是否可以在现实生活中复杂的场景中使用。

如果有解决此问题的更简洁,更简洁的方法,我将很高兴听到它们!

© www.soinside.com 2019 - 2024. All rights reserved.