I request data from server by bunches and store it in the array.To track fetching of the next bunch of the data I have this class.In the addItems method I notify diffObservers and pass list of new items:
class PackItems:MutableLiveData<ArrayList<GetPacksResponse.PackData>>() {
private var diffObservers=ArrayList<Observer<List<GetPacksResponse.PackData>>>()
private var active=false
fun observeItems(owner: LifecycleOwner, valueObserver:Observer<List<GetPacksResponse.PackData>>,diffObserver:Observer<List<GetPacksResponse.PackData>>) {
super.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
override fun removeObservers(owner: LifecycleOwner) {
super.removeObservers(owner)
diffObservers= ArrayList()
}
fun addItems(toAdd:List<GetPacksResponse.PackData>) {
value?.addAll(toAdd)
if (active)
for (observer in diffObservers)
observer.onChanged(toAdd)
}
override fun onActive() {
super.onActive()
active=true
}
override fun onInactive() {
super.onInactive()
active=false
}
}
The problem is PackItems is MutableLiveData and it's not good practice to expose it.Is there way to cast it to LiveData?Like usually we do:
private val _items = MutableLiveData<List<Int>>()
val items: LiveData<List<Int>> = _items
UPD:Ideally would be if I could expose completely immutable LiveData.But I can't just write
private val _packs:PackItems=PackItems()
val packs:LiveData<ArrayList<GetPacksResponse.PackData>>
get()=_packs
Because in this case packs won't contain observeItems method.Therefore there must be custom class derived from LiveData like:
open class PackItems: LiveData<ArrayList<GetPacksResponse.PackData>>() {
protected var active=false
protected var diffObservers = ArrayList<Observer<List<GetPacksResponse.PackData>>>()
fun observeItems(owner: LifecycleOwner, valueObserver: Observer<List<GetPacksResponse.PackData>>, diffObserver: Observer<List<GetPacksResponse.PackData>>) {
super.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
//...
}
class MutablePackItems: PackItems() {
fun addItems(toAdd:List<GetPacksResponse.PackData>) {
value?.addAll(toAdd)
if (active)
for (observer in diffObservers)
observer.onChanged(toAdd)
}
}
But in this case I won't be able to set data because now MutablePackItems is LiveData(immutable) :)
I'd consider using composition instead of inheritance:
class PackItems() {
private val mutableData = MutableLiveData<ArrayList<GetPacksResponse.PackData>>()
val asLiveData: LiveData<ArrayList<GetPacksResponse.PackData>> get() = mutableData
...
fun observeItems(owner: LifecycleOwner, valueObserver:Observer<List<GetPacksResponse.PackData>>,diffObserver:Observer<List<GetPacksResponse.PackData>>) {
mutableData.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
fun removeObservers(owner: LifecycleOwner) {
mutableData.removeObservers(owner)
diffObservers = ArrayList()
}
// etc
}
EDIT: to set active as in your original code, may be a bit nastier:
private val mutableData = object : MutableLiveData<ArrayList<GetPacksResponse.PackData>>() {
override fun onActive() {
super.onActive()
active = true
}
override fun onInactive() {
super.onInactive()
active = false
}
}
EDIT 2:
but the main problem is I need to return custom LiveData class with custom observeItems method
The point is that you don't necessarily. Whenever you'd call LiveData's method (e.g. observe), just call items.asLiveData.observe(...) instead. If you want to pass it to another method foo accepting LiveData, call foo(items.asLiveData).
In principle, you could modify this approach by extending LiveData and delegating all calls to mutableData:
class PackItems(): LiveData<ArrayList<GetPacksResponse.PackData>>() {
private val mutableData = MutableLiveData<ArrayList<GetPacksResponse.PackData>>()
...
fun observeItems(owner: LifecycleOwner, valueObserver:Observer<List<GetPacksResponse.PackData>>,diffObserver:Observer<List<GetPacksResponse.PackData>>) {
mutableData.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
override fun observe(owner: LifecycleOwner, observer: ArrayList<GetPacksResponse.PackData>) {
mutableData.observe(owner, observer)
}
override fun removeObservers(owner: LifecycleOwner) {
mutableData.removeObservers(owner) // not super!
diffObservers = ArrayList()
}
// etc
}
but I don't think it's a good idea.
Related
I have view model like this:
class SimpleViewModel : ViewModel() {
private val _state = MutableStateFlow(false)
val state: StateFlow<Boolean> = _state
}
How can I collect this state's values and call methods from another class like this:
class AnotherClass {
fun doWhenViewModelStateUpdateToTrue()
fun doWhenViewModelStateUpdateToFalse()
}
Your other class needs a reference to the state flow and to a CoroutineScope to run the collection in.
The CoroutineScope should have a lifecycle matching that of this class. So if it's a class you create in an Activity, for example, you would pass lifecycleScope.
class AnotherClass(
private val coroutineScope: CoroutineScope,
private val flowToCollect: Flow<Boolean>
) {
init {
coroutineScope.launch {
flowToCollect.collect {
if (it) doWhenViewModelStateUpdateToTrue()
else doWhenViewModelStateUpdateToFalse()
}
}
}
fun doWhenViewModelStateUpdateToTrue() {
//...
}
fun doWhenViewModelStateUpdateToFalse() {
//...
}
}
I use the following Code A to query records ,the data are wrapped with sealed class Result<out R>.
The val queryList is assigned with Result.Loading first, then it is assigned with Result.Success and wrapped data, the different UI will be loaded based the different value of queryList.
I think the queryList is only assigned with Result.Loading onetime, the queryList will keep return Result.Success when I launch mViewMode.listRecord() again and again, right?
So I hope the queryList is always assigned with Result.Loading before I launch mViewMode.listRecord() and return Result.Success , how can I fix the code?
Maybe do I need to modify Code B? or do I need to redesign data structure? or is there the better solution?
Code A
#Composable
fun Greeting() {
Column( ) {
val aResult: Result<Flow<List<MRecord>>> = Result.Loading
val queryList by produceState(initialValue = aResult) {
value = mViewMode.listRecord()
}
when (queryList){
is Result.Error -> { ...}
is Result.Loading -> { ... }
is Result.Success -> { ... }
}
}
}
class SoundViewModel #Inject constructor(...): ViewModel()
{
fun listRecord(): Result<Flow<List<MRecord>>>{
return aRecordRepository.listRecord()
}
}
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao){
fun listRecord(): Result<Flow<List<MRecord>>> {
val temp = mRecordDao.listRecord()
return Result.Success(temp)
}
}
interface RecordDao {
#Query("SELECT * FROM record_table ORDER BY createdDate desc")
fun listRecord(): Flow<List<MRecord>>
}
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
}
Code B
...
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao){
fun listRecord(): Result<Flow<List<MRecord>>> {
val temp = mRecordDao.listRecord()
return Result.Success(temp) //How can I return Result.Loading first, then return Result.Success(temp)?
}
}
...
You can create a StateFlow in your view model representing Result and connect it to your RecordRepository as follows and then convert it to compose state using collectAsState
#Composable
fun Greeting(soundViewModel: SoundViewModel = SoundViewModel()) {
LaunchedEffect(Unit) {
soundViewModel.listRecord()
}
Column {
val queryList: Result by soundViewModel.dataResult.collectAsState()
when (queryList) {
is Result.Error -> {
...
}
is Result.Loading -> {
...
}
is Result.Success -> {
...
}
}
}
}
class SoundViewModel {
private val _dataResult: MutableStateFlow<Result> = MutableStateFlow(Result.Loading) // private mutable state flow
val dataResult = _dataResult.asStateFlow() // publicly exposed as read-only state flow
private val recordRepository = RecordRepository()
suspend fun listRecord() {
recordRepository.listRecord().collect {
_dataResult.value = Result.Success(it)
}
}
}
class RecordRepository {
fun listRecord(): Flow<List<Int>> = flow {
emit(listOf(1))
delay(1000L)
emit(listOf(2, 3))
}
}
sealed interface Result {
object Loading : Result
data class Success(val lst: List<Int>) : Result
data class Error(val err: Throwable) : Result
}
The tricky thing is: When you expose a Flow from Room, it only emits each list after there's a database change and a new query is completed. There is no in-between signal from the flow to indicate that the database change is detected but the new query isn't completed yet.
One possible solution is if you create a flow in your repository that when something that happens modifies the database, it restarts with a new emission of Result.Loading and then emits the DAO flow again. This way, your Flow is protected from missing any changes, even if you somehow miss showing a loading state.
You could use a shared flow in the Repository if there's more than one flow you want to handle this way. Use it with flatMapLatest, so every time you do something that is likely to cause a database change, the existing upstream listRecord flow from the DAO will be cancelled so you can get a new Loading state before collecting it again.
Disclaimer: I haven't tested this. It's only an idea.
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao){
private expectedChangeTicker = MutableSharedFlow<Unit>(replay = 1, bufferOverflow = BufferOverflow.DROP_OLDEST)
suspend fun addSomething(someThing: SomeThing) {
// Call this in every repository function that might cause listRecord to change
expectedChangeTicker.emit(Unit)
mRecordDao.addSomething(someThing)
}
val listRecord: Flow<Result<List<MRecord>> =
expectedChangeTicker.flatMapLatest {
flow {
emit(Result.Loading)
emitAll(mRecordDao.listRecord().map { Result.Success(it) })
}
}
}
I don't know Compose, so I'm not sure how you should expose this Flow in your ViewModel. Notice I changed it from Result<Flow...> to Flow<Result...>, which I think is more likely what you need. Here is my guess at how it should be done:
class SoundViewModel #Inject constructor(...): ViewModel()
{
val listRecord: Flow<Result<List<MRecord>>> =
aRecordRepository.listRecord
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
}
#Composable
fun Greeting() {
Column( ) {
val aResult: Result<List<MRecord>> = Result.Loading
val queryList by produceState(initialValue = Result.Loading) {
value = mViewMode.listRecord
}
when (queryList){
is Result.Error -> { ...}
is Result.Loading -> { ... }
is Result.Success -> { ... }
}
}
}
I don't think you need produceState. You can simply collect the flow returned by Dao in your composable using collectAsState() extension function.
#Composable
fun Greeting() {
Column( ) {
val queryList by viewModel.listRecord.collectAsState(Result.Loading)
when (queryList){
is Result.Error -> { ...}
is Result.Loading -> { ... }
is Result.Success -> { ... }
}
}
}
class SoundViewModel #Inject constructor(...): ViewModel() {
val listRecord = aRecordRepository.listRecord()
}
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao) {
fun listRecord(): Flow<List<MRecord>> {
return mRecordDao.listRecord()
}
}
Edit:
If you want to emit the loading state from the flow itself, you can do something like this:
class RecordRepository #Inject constructor(private val mRecordDao: RecordDao) {
fun listRecord(): Flow<Result<List<MRecord>>> {
return flow { // Create a new flow
emit(Result.Loading) // Emit loading state right away
mRecordDao.listRecord().collect {
emit(Result.Success(it)) // Emit success state upon receiving data from dao
}
}
}
}
I am trying to listen to my ViewModels MutableStateFlow from my FlutterSceneView. But I get the following error when trying to set the listener from the views init:
Suspend function 'listenToBackgroundColor' should be called only from a coroutine or another suspend function
class FlutterSceneView(context: Context, private val viewModel: FlutterSceneViewModelType): PlatformView {
private val context = context
private val sceneView = SceneView(context)
init {
listenToBackgroundColor() // Error here
}
private suspend fun listenToBackgroundColor() {
viewModel.colorFlow.collect {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}
}
}
My ViewModel:
interface FlutterSceneViewModelType {
var colorFlow: MutableStateFlow<String>
}
class FlutterSceneViewModel(private val database: Database): FlutterSceneViewModelType, ViewModel() {
override var colorFlow = MutableStateFlow<String>("#FFFFFF")
init {
listenToBackgroundColorFlow()
}
private fun listenToBackgroundColorFlow() {
database.backgroundColorFlow.watch {
colorFlow.value = it.hex
}
}
}
the .watch call is a helper I have added so that this can be exposed to iOS using Kotlin multi-platform, it looks as follows but I can use collect instead if necessary:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
I resolved this by using viewModel context:
private fun listenToBackgroundColor() {
viewModel.colorFlow.onEach {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}.launchIn(viewModel.viewModelScope)
}
I had to import the following into my ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
from:
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")
class ModelFactory {
fun setA() : ModelFactory {
// blabla...
}
fun setB() : ModelFactory {
// blabla...
}
fun setC() : ModelFactory {
// blabla...
}
fun build() : Model {
// An error occurs if any of setA, setB, and setC is not called.
}
}
//example
fun successTest() {
ModelFactory().setA().setB().setC().build() // No error occurs at compile time
}
fun failTest() {
ModelFactory().setA().build() // An error occurs at compile time because setB and setC are not called.
}
It's awkward grammatically, but I think it's been expressed what I want.
I have already implemented an error-raising runtime for this requirement, but I want to check this at compile time.
If possible, I think I should use annotations. But is this really possible at compile time?
With Kotlin, I have been avoiding builder pattern, as we can always specify default values for non-mandatory fields.
If you still want to use a builder pattern, you can use Step builder pattern that expects all mandatory fields to be set before creating the object. Note that each setter method returns the reference of next setter interface. You can have multiple Step builders based on the combination of mandatory fields.
class Model(val a: String = "", val b: String = "", val c: String = "")
class StepBuilder {
companion object {
fun builder(): AStep = Steps()
}
interface AStep {
fun setA(a: String): BStep
}
interface BStep {
fun setB(b: String): CStep
}
interface CStep {
fun setC(c: String): BuildStep
}
interface BuildStep {
//fun setOptionalField(x: String): BuildStep
fun build(): Model
}
class Steps : AStep, BStep, CStep, BuildStep {
private lateinit var a: String
private lateinit var b: String
private lateinit var c: String
override fun setA(a: String): BStep {
this.a = a
return this
}
override fun setB(b: String): CStep {
this.b = b
return this
}
override fun setC(c: String): BuildStep {
this.c = c
return this
}
override fun build() = Model(a, b , c)
}
}
fun main() {
// cannot build until you call all three setters
val model = StepBuilder.builder().setA("A").setB("B").setC("C").build()
}
I am trying to proxy calls for Observables and LiveData (similar to the Mediator pattern), but I could not find a typesafe solution. This is the problem:
class Proxy {
private val backupMap = HashMap<LiveData<Any>, Observer<Any>>()
fun <T> add(liveData : LiveData<T>, observer : Observer<T>) {
// !This is the issue LiveData<Any> is expected
backupMap.put(liveData, observer)
}
fun attach() {
backupMap.forEach { (key, value) ->
key.observeForever(value)
}
}
}
fun addSome() {
Proxy().apply {
add(MutableLiveData<String>(), Observer { })
}
}
I could cast backupMap.put to backupMap.put(liveData as LiveData<Any>, observer as Observer<Any>) but this causes an Unchecked Cast.
Solution I found is to use an intermediate object to hold the typesafe binding:
private val backupMap: MutableMap<LiveData<*>, Attacher<*>>
private class Attacher<A>(private val lifeData: LiveData<A>, private val observer : Observer<A>) {
fun attach() {
lifeData.observeForever(observer)
}
fun detach() {
lifeData.removeObserver(observer)
}
}