I have a flow to fetch data from database via content provider.
fun getDataFlow(): Flow<Result> {
return flow {
emit(Result.Loading)
// fetchAll() is the method to fetch data via contentResolover.query()
val results = fetchAll()
emit(Result.Success(categories))
}.catch { e ->
emit(Result.Error(e))
}
}
So how to trigger refetching data when ContentProvider data changed (onChange get called)?
val contentObserver = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
}
}
you can use callback flow
#ExperimentalCoroutinesApi
fun ContentResolver.register(uri: Uri) = callbackFlow<Boolean> {
val observer = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
trySend(selfChange)
}
}
registerContentObserver(uri, false, observer)
invokeOnClose {
unregisterContentObserver(observer)
}
}
then u can do something like this
getDataFlow().combine(ContentResolver.register("YOUR_URI")){data, isSelfChange ->
}
Related
I'm just trying to find an answer how to pass the data from Repository to ViewModel without extra dependencies like RxJava. The LiveData seems as a not good solution here because I don't need to proceed it in my Presentation, only in ViewModel and it's not a good practice to use observeForever.
The code is simple: I use Firebase example trying to pass data with Flow but can't use it within a listener (Suspension functions can be called only within coroutine body error):
Repository
fun fetchFirebaseFlow(): Flow<List<MyData>?> = flow {
var ret: List<MyData>? = null
firebaseDb.child("data").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val data = dataSnapshot.getValue<List<MyData>>()
emit(data) // Error. How to return the data here?
}
override fun onCancelled(databaseError: DatabaseError) {
emit(databaseError) // Error. How to return the data here?
}
})
// emit(ret) // Useless here
}
ViewModel
private suspend fun fetchFirebase() {
repo.fetchFirebaseFlow().collect { data ->
if (!data.isNullOrEmpty()) {
// Add data to something
} else {
// Something else
}
}
You can use callbackFlow
#ExperimentalCoroutinesApi
fun fetchFirebaseFlow(): Flow<List<String>?> = callbackFlow {
val listener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val data = dataSnapshot.getValue<List<MyData>>()
offer(data)
}
override fun onCancelled(databaseError: DatabaseError) {
}
}
val ref =firebaseDb.child("data")
reef.addListenerForSingleValueEvent(listener)
awaitClose{
//remove listener here
ref.removeEventListener(listener)
}
}
ObservableField is like LiveData but not lifecycle-aware and may be used instead of creating an Observable object.
{
val data = repo.getObservable()
val cb = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(observable: Observable, i: Int) {
observable.removeOnPropertyChangedCallback(this)
val neededData = (observable as ObservableField<*>).get()
}
}
data.addOnPropertyChangedCallback(cb)
}
fun getObservable(): ObservableField<List<MyData>> {
val ret = ObservableField<List<MyData>>()
firebaseDb.child("events").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
ret.set(dataSnapshot.getValue<List<MyData>>())
}
override fun onCancelled(databaseError: DatabaseError) {
ret.set(null)
}
})
return ret
}
It is also possible to use suspendCancellableCoroutine for a single result. Thanks to Kotlin forum.
My Worker(for API call) starts from Service and I want to completion event send into Service class.
What should be best approach?
Calling from service:
PeriodicWorkRequest request = new PeriodicWorkRequest
.Builder(Worker.class, Constants.REPEAT_INTERVAL, TimeUnit.MINUTES)
.addTag(TAG)
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build();
WorkManager.getInstance(getApplicationContext()).enqueue(request);
Calling from WorkManager:
override fun doWork(): Result {
// API call
return Result.success()
}
Okay so what I would do is I would create common object for both Worker and Service class and utilize Observer pattern. This WorkObserver object would behave as a proxy between Service and Worker. Using Koin for example, it would look something like that:
class MyWorker: Worker(), KoinComponent {
private val workObserver: WorkObserver by inject()
override fun doWork(): Result {
val result = api.call().execute()
if(result.isSuccessful) {
workObserver.notifySuccess()
return Result.success()
} else {
workObserver.notifyError()
return Result.failure()
}
}
}
class MyService: Service(), KoinComponent {
private val workObserver: WorkObserver by inject()
override fun onCreate() {
workObserver.setOnResultListener { result ->
if(result) {
//do something
} else {
// do something else
}
}
override fun onDestroy() {
workObserver.setOnResultListener(null)
}
}
class WorkObserver {
private var onResultListener: ((Result) -> Unit)? = null
fun setOnResultListener(listener: ((Result) -> Unit)?) {
this.onResultListener = listener
}
fun notifySuccess() {
this.onResultListener?.invoke(true)
}
fun notifyError() {
this.onResultListener?.invoke(false)
}
}
Of course you can use other DI tools for that, you can have a list of listeners and remove particular ones, you can pass any other object through the listener in WorkObserver with the payload you need. I just created a simple boolean passing
For that simple case however if you don't want to use DI, simple Object would do the work. However when your codebase grows and you are dealing with multithreading issues, or even accessing this object in other parts of the application it may lead to problems. I am using it only to pass information between objects, I don't recommend using it for storing data etc:
class MyWorker: Worker() {
override fun doWork(): Result {
val result = api.call().execute()
if(result.isSuccessful) {
WorkObserver.notifySuccess()
return Result.success()
} else {
WorkObserver.notifyError()
return Result.failure()
}
}
}
class MyService: Service() {
override fun onCreate() {
WorkObserver.setOnResultListener { result ->
if(result) {
//do something
} else {
// do something else
}
}
override fun onDestroy() {
WorkObserver.setOnResultListener(null)
}
}
object WorkObserver {
private var onResultListener: ((Result) -> Unit)? = null
fun setOnResultListener(listener: ((Result) -> Unit)?) {
this.onResultListener = listener
}
fun notifySuccess() {
this.onResultListener?.invoke(true)
}
fun notifyError() {
this.onResultListener?.invoke(false)
}
}
My problem is, that when I try to get a document out of my database, that this document aka the object is always null. I only have this problem when I use Kotlin Coroutines to get the document out of my database. Using the standard approach with listeners do work.
EmailRepository
interface EmailRepository {
suspend fun getCalibratePrice(): Flow<EmailEntity?>
suspend fun getRepairPrice(): Flow<EmailEntity?>
}
EmailRepository Implementation
class EmailRepositoryImpl #Inject constructor(private val db: FirebaseFirestore) : EmailRepository {
fun hasInternet(): Boolean {
return true
}
// This works! When using flow to write a document, the document is written!
override fun sendEmail(email: Email)= flow {
emit(EmailStatus.loading())
if (hasInternet()) {
db.collection("emails").add(email).await()
emit(EmailStatus.success(Unit))
} else {
emit(EmailStatus.failed<Unit>("No Email connection"))
}
}.catch {
emit(EmailStatus.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
// This does not work! "EmailEntity" is always null. I checked the document path!
override suspend fun getCalibratePrice(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Kalibrieren").get().await()
emit(result.toObject<EmailEntity>())
}.catch {
}.flowOn(Dispatchers.Main)
// This does not work! "EmailEntity" is always null. I checked the document path!
override suspend fun getRepairPrice(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Reparieren").get().await()
emit(result.toObject<EmailEntity>())
}.catch {
}.flowOn(Dispatchers.Main)
}
Viewmodel where I get the data
init {
viewModelScope.launch {
withContext(Dispatchers.IO) {
if (subject.value != null){
when(subject.value) {
"Test" -> {
emailRepository.getCalibratePrice().collect {
emailEntity.value = it
}
}
"Toast" -> {
emailRepository.getRepairPrice().collect {
emailEntity.value = it
}
}
}
}
}
}
}
private val emailEntity = MutableLiveData<EmailEntity?>()
private val _subject = MutableLiveData<String>()
val subject: LiveData<String> get() = _subject
Fragment
#AndroidEntryPoint
class CalibrateRepairMessageFragment() : EmailFragment<FragmentCalibrateRepairMessageBinding>(
R.layout.fragment_calibrate_repair_message,
) {
// Get current toolbar Title and send it to the next fragment.
private val toolbarText: CharSequence by lazy { toolbar_title.text }
override val viewModel: EmailViewModel by navGraphViewModels(R.id.nav_send_email) { defaultViewModelProviderFactory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Here I set the data from the MutableLiveData "subject". I don't know how to do it better
viewModel.setSubject(toolbarText.toString())
}
}
One would say, that the Firebase rules are the problems here, but that should not be the case here, because the database is open and using the listener approach does work.
I get the subject.value from my CalibrateRepairMessageFragment. When I don't check if(subject.value != null) I get a NullPointerException from my init block.
I will use the emailEntitiy only in my viewModel and not outside it.
I appreciate every help, thank you.
EDIT
This is the new way I get the data. The object is still null! I've also added Timber.d messages in my suspend functions which also never get executed therefore flow never throws an error.. With this new approach I don't get a NullPointerException anymore
private val emailEntity = liveData {
when(subject.value) {
"Test" -> emailRepository.getCalibratePrice().collect {
emit(it)
}
"Toast" -> emailRepository.getRepairPrice().collect {
emit(it)
}
// Else block is never executed, therefore "subject.value" is either Test or toast and the logic works. Still error when using flow!
else -> EmailEntity("ERROR", 0F)
}
}
I check if the emailEntity is null or not with Timber.d("EmailEntity is ${emailEntity.value}") in one of my functions.
I then set the price with val price = MutableLiveData(emailEntity.value?.basePrice ?: 1000F) but because emailentity is null the price is always 1000
EDIT 2
I have now further researched the problem and made a big step forward. When observing the emailEntity from a fragment like CalibrateRepairMessageFragment the value is no longer null.
Furthermore, when observing emailEntity the value is also not null in viewModel, but only when it is observed in one fragment! So how can I observe emailEntity from my viewModel or get the value from my repository and use it in my viewmodel?
Okay, I have solved my problem, this is the final solution:
Status class
sealed class Status<out T> {
data class Success<out T>(val data: T) : Status<T>()
class Loading<T> : Status<T>()
data class Failure<out T>(val message: String?) : Status<T>()
companion object {
fun <T> success(data: T) = Success<T>(data)
fun <T> loading() = Loading<T>()
fun <T> failed(message: String?) = Failure<T>(message)
}
}
EmailRepository
interface EmailRepository {
fun sendEmail(email: Email): Flow<Status<Unit>>
suspend fun getCalibratePrice(): Flow<Status<CalibrateRepairPricing?>>
suspend fun getRepairPrice(): Flow<Status<CalibrateRepairPricing?>>
}
EmailRepositoryImpl
class EmailRepositoryImpl (private val db: FirebaseFirestore) : EmailRepository {
fun hasInternet(): Boolean {
return true
}
override fun sendEmail(email: Email)= flow {
Timber.d("Executed Send Email Repository")
emit(Status.loading())
if (hasInternet()) {
db.collection("emails").add(email).await()
emit(Status.success(Unit))
} else {
emit(Status.failed<Unit>("No Internet connection"))
}
}.catch {
emit(Status.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
// Sends status and object to viewModel
override suspend fun getCalibratePrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
emit(Status.loading())
val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
emit(Status.success(entity))
}.catch {
Timber.d("Error on getCalibrate Price")
emit(Status.failed(it.message.toString()))
}
// Sends status and object to viewModel
override suspend fun getRepairPrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
emit(Status.loading())
val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
emit(Status.success(entity))
}.catch {
Timber.d("Error on getRepairPrice")
emit(Status.failed(it.message.toString()))
}
}
ViewModel
private lateinit var calibrateRepairPrice: CalibrateRepairPricing
private val _calirateRepairPriceErrorState = MutableLiveData<Status<Unit>>()
val calibrateRepairPriceErrorState: LiveData<Status<Unit>> get() = _calirateRepairPriceErrorState
init {
viewModelScope.launch {
when(_subject.value.toString()) {
"Toast" -> emailRepository.getCalibratePrice().collect {
when(it) {
is Status.Success -> {
calibrateRepairPrice = it.data!!
_calirateRepairPriceErrorState.postValue(Status.success(Unit))
}
is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
}
}
else -> emailRepository.getRepairPrice().collect {
when(it) {
is Status.Success -> {
calibrateRepairPrice = it.data!!
_calirateRepairPriceErrorState.postValue(Status.success(Unit))
}
is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
}
}
}
price.postValue(calibrateRepairPrice.head!!.basePrice)
}
}
You can now observe the status in one of your fragments (but you dont need to!)
Fragment
viewModel.calibrateRepairPriceErrorState.observe(viewLifecycleOwner) { status ->
when(status) {
is Status.Success -> requireContext().toast("Price successfully loaded")
is Status.Loading -> requireContext().toast("Price is loading")
is Status.Failure -> requireContext().toast("Error, Price could not be loaded")
}
}
This is my toast extensions function:
fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, text, duration).show()
}
How to determine size of data returned before setting adapter?
How to use emptyview with paging library?
How to set emptyview if pagedlist returns null or no data?
Update[24/04/19]:
I just found out that the library already provide us a way to listen to empty initial load, using PagedList.BoundaryCallback<YourItem>.
*Note that my old answer is still a valid alternative.
val livedPageList = LivePagedListBuilder(sourceFactory, config)
.setBoundaryCallback(object: PagedList.BoundaryCallback<YourItem>() {
override fun onZeroItemsLoaded() {
super.onZeroItemsLoaded()
// Handle empty initial load here
}
override fun onItemAtEndLoaded(itemAtEnd: YourItem) {
super.onItemAtEndLoaded(itemAtEnd)
// Here you can listen to last item on list
}
override fun onItemAtFrontLoaded(itemAtFront: YourItem) {
super.onItemAtFrontLoaded(itemAtFront)
// Here you can listen to first item on list
}
})
.build()
Original Answer:
Based on this class on google sample Network State. Modify it to handle empty content in initialLoad.
#Suppress("DataClassPrivateConstructor")
data class NetworkState private constructor(
val status: Status,
val msg: String? = null
) {
enum class Status {
RUNNING,
SUCCESS_LOADED, // New
SUCCESS_EMPTY, // New
FAILED
}
companion object {
val EMPTY = NetworkState(Status.SUCCESS_EMPTY) // New
val LOADED = NetworkState(Status.SUCCESS_LOADED) // New
val LOADING = NetworkState(Status.RUNNING)
fun error(msg: String?) = NetworkState(Status.FAILED, msg)
}
}
Usage as follow:
class DataSource: PageKeyedDataSource<Long, Item>() {
val initialLoad: MutableLiveData<NetworkState> = MutableLiveData()
override fun loadInitial(params: LoadInitialParams<Long>, callback: LoadInitialCallback<Long, Item>) {
initialLoad.postValue(NetworkState.LOADING)
apiCallSource.subscribe({ items ->
if (items.isEmpty()) {
initialLoad.postValue(NetworkState.EMPTY)
} else {
initialLoad.postValue(NetworkState.LOADED)
}
}, { error ->
// handle error
})
}
}
And this is how the activity handle it:
class activity: AppCompatActivity() {
val viewModel = // init viewmodel
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.refreshState.observe(this, Observer { networkState ->
if (it == NetworkState.LOADING) {
// Show loading
} else {
// Hide loading
if (it.status == NetworkState.Status.SUCCESS_EMPTY) {
// Show empty state for initial load
}
}
}
}
}
For more details on how to connect DataSource with Activity, see this sample
Simply add a listener or callback function to your DataSourceFactory and your DataSource and call it if the list in loadInitial is empty:
class DataSourceFactory(
private val dataObservable: Observable<List<Data>>,
private val onEmptyAction: () -> Unit
) : DataSource.Factory<Int, Data >() {
override fun create(): DataSource {
return DataSource(observable, onEmptyAction)
}
}
class DataSource(
private val observable: Observable<List<Data>>,
private val onEmptyAction: () -> Unit
) : ItemKeyedDataSource<Int, Data>() {
private val data = mutableListOf<Data>()
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Data>) {
observable
.subscribe({ data ->
if (data.isEmpty()) {
// Inform someone that this list is empty from the
// beginning to be able to show an empty page
onEmptyAction()
}
// rest of your code & logic
}, { Timber.e(it) })
}
}
In your fragment/activity you are observing network state:
viewModel.getNetworkState1()?.observe(this, Observer {
// here you can handle you empty view
setEmptyView()
})
like this:
private fun setNoTransactionsLayout() {
if(viewModel.listIsEmpty()) {
yourTextView.visibility = View.VISIBLE
} else {
yourTextView.visibility = View.GONE
}
}
And in view model you have this function:
fun listIsEmpty(): Boolean {
return yourPagedList?.value?.isEmpty() ?: true
}
I have recently seen a weird issue that is acting as a barrier to my project.
Multiple calls to set the live data value does not invoke the observer in the view.
It seems that only the last value that was set actually invokes the Observer in the view.
Here is the code snippet for a review.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get(MainViewModelImpl::class.java)
viewModel.state().observe(this, Observer {
onStateChange(it!!)
})
viewModel.fetchFirstThree()
}
private fun onStateChange(state: MainViewModel.State) {
when (state) {
is One -> {
show(state.data)
}
is Two -> {
show(state.data)
}
is Three -> {
show(state.data)
}
}
}
private fun show(data: String) {
Log.d("Response", data)
}
}
MainViewModel.kt
abstract class MainViewModel : ViewModel() {
sealed class State {
data class One(val data: String) : State()
data class Two(val data: String) : State()
data class Three(val data: String) : State()
}
abstract fun state(): LiveData<State>
abstract fun fetchFirstThree()
}
MainViewModelImpl.kt
class MainViewModelImpl : MainViewModel() {
private val stateLiveData: MediatorLiveData<State> = MediatorLiveData()
override fun state(): LiveData<State> = stateLiveData
override fun fetchFirstThree() {
stateLiveData.value = State.One("One")
stateLiveData.value = State.Two("Two")
stateLiveData.value = State.Three("Three")
}
}
Expected output:
Response: One
Response: Two
Response: Three
Actual Output:
Response: Three
As per the output above, the Observer is not being called for the first two values.
I did some science, re-implementing LiveData and MutableLiveData to log out some data.
Check the source code here.
setValue value=Test1
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
Returned at !observer.active
setValue value=Test2
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
Returned at !observer.active
setValue value=Test3
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
Returned at !observer.active
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
ITEM: Test3
It looks like the observer hasn't reached an active state when you send the initial values.
private void considerNotify(LifecycleBoundObserver observer) {
// <-- Three times it fails here. This means that your observer wasn't ready for any of them.
if (!observer.active) {
return;
}
Once the observer reaches an active state, it sends the last set value.
void activeStateChanged(boolean newActive) {
if (newActive == active) {
return;
}
active = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += active ? 1 : -1;
if (wasInactive && active) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !active) {
onInactive();
}
if (active) {
// <--- At this point you are getting a call to your observer!
dispatchingValue(this);
}
}
I had such issue too.
To resolve it was created custom MutableLiveData, that contains a queue of posted values and will notify observer for each value.
You can use it the same way as usual MutableLiveData.
open class MultipleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
private val values: Queue<T> = LinkedList()
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(this::class.java.name, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, { t: T ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
//call next value processing if have such
if (values.isNotEmpty())
pollValue()
}
})
}
override fun postValue(value: T) {
values.add(value)
pollValue()
}
private fun pollValue() {
value = values.poll()
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#Suppress("unused")
#MainThread
fun call() {
value = null
}
}
You could use custom LiveData like this:
class ActiveMutableLiveData<T> : MutableLiveData<T>() {
private val values: Queue<T> = LinkedList()
private var isActive: Boolean = false
override fun onActive() {
isActive = true
while (values.isNotEmpty()) {
setValue(values.poll())
}
}
override fun onInactive() {
isActive = false
}
override fun setValue(value: T) {
if (isActive) {
super.setValue(value)
} else {
values.add(value)
}
}
}
FWIW I had the same problem but solved it like this...
I originally had some code similar to this...
private fun updateMonth(month: Int){
updateMonth.value = UpdateMonth(month, getDaysOfMonth(month))
}
updateMonth(1)
updateMonth(2)
updateMonth(3)
I experienced the same problem as described...
But when I made this simple change....
private fun updateMonth(month: Int) {
CoroutineScope(Dispatchers.Main).launch {
updateMonth.value = UpdateMonth(month, getDaysOfMonth(month))
}
}
Presumably, each updateMonth is going onto a different thread now, so all of the updates are observed.
You should call viewModel.fetchFirstThree() after Activity's onStart() method. for example in onResume() method.
Because in LiveData the Observer is wrapped as a LifecycleBoundObserver. The field mActive set to true after onStart().
class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
#Override
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);// return true after onStart()
}
#Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());// after onStart() change mActive to true
}
}
When the observer notify the change it calls considerNotify, before onStart it will return at !observer.mActive
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {// called in onCreate() will return here.
return;
}
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}