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
}
Related
As a developer one needs to adapt to change, I read somewhere it says:
If you don’t choose the right architecture for your Android project, you will have a hard time maintaining it as your codebase grows and your team expands.
I wanted to implement Clean Architecture with MVVM
My app data flow will look like this:
Model class
data class Note(
val title: String? = null,
val timestamp: String? = null
)
Dtos
data class NoteRequest(
val title: String? = null,
val timestamp: String? = null
)
and
data class NoteResponse(
val id: String? = null,
val title: String? = null,
val timestamp: String? = null
)
Repository layer is
interface INoteRepository {
fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit)
fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit)
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteRepositoryImpl is:
class NoteRepositoryImpl: INoteRepository {
private val mFirebaseFirestore = Firebase.firestore
private val mNotesCollectionReference = mFirebaseFirestore.collection(COLLECTION_NOTES)
private val noteList = mutableListOf<NoteResponse>()
private var getNoteListSuccessListener: ((List<NoteResponse>) -> Unit)? = null
private var deleteNoteSuccessListener: ((List<NoteResponse>) -> Unit)? = null
override fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit) {
getNoteListSuccessListener = success
}
override fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit) {
deleteNoteSuccessListener = success
}
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
override fun deleteNoteById(noteId: String) {
mNotesCollectionReference.document(noteId)
.delete()
.addOnSuccessListener {
deleteNoteSuccessListener?.invoke(noteList)
}
}
}
ViewModel layer is:
interface INoteViewModel {
val noteListStateFlow: StateFlow<List<NoteResponse>>
val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteViewModelImpl is:
class NoteViewModelImpl: ViewModel(), INoteViewModel {
private val mNoteRepository: INoteRepository = NoteRepositoryImpl()
private val _noteListStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteListStateFlow: StateFlow<List<NoteResponse>>
get() = _noteListStateFlow.asStateFlow()
private val _noteDeletedStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
get() = _noteDeletedStateFlow.asStateFlow()
init {
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener {
viewModelScope
.launch {
_noteListStateFlow.emit(it)
Log.e("NOTE_G_VM", "$it")
}
}
// deleteNoteSuccessListener
mNoteRepository
.deleteNoteSuccessListener {
viewModelScope
.launch {
_noteDeletedStateFlow.emit(it)
Log.e("NOTE_D_VM", "$it")
}
}
}
override fun getNoteList() {
// Get all notes
mNoteRepository.getNoteList()
}
override fun deleteNoteById(noteId: String) {
mNoteRepository.deleteNoteById(noteId = noteId)
}
}
and last but not least Fragment is:
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private val viewModel: INoteViewModel by viewModels<NoteViewModelImpl>()
private lateinit var adapter: NoteAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = binding.recyclerViewNotes
recyclerView.addOnScrollListener(
ExFABScrollListener(binding.fab)
)
adapter = NoteAdapter{itemView, noteId ->
if (noteId != null) {
showMenu(itemView, noteId)
}
}
recyclerView.adapter = adapter
// initView()
fetchFirestoreData()
binding.fab.setOnClickListener {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment()
findNavController().navigate(action)
}
}
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
//In the showMenu function from the previous example:
#SuppressLint("RestrictedApi")
private fun showMenu(v: View, noteId: String) {
val menuBuilder = MenuBuilder(requireContext())
SupportMenuInflater(requireContext()).inflate(R.menu.menu_note_options, menuBuilder)
menuBuilder.setCallback(object : MenuBuilder.Callback {
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
return when(item.itemId){
R.id.option_edit -> {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment(noteId = noteId)
findNavController().navigate(action)
true
}
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
true
} else -> false
}
}
override fun onMenuModeChange(menu: MenuBuilder) {}
})
val menuHelper = MenuPopupHelper(requireContext(), menuBuilder, v)
menuHelper.setForceShowIcon(true) // show icons!!!!!!!!
menuHelper.show()
}
}
With all the above logic I'm facing TWO issues
issue - 1
As mentioned here, I have added SnapshotListener on collection as:
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
with it if I change values of a document from Firebase Console, I get updated values in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same.
issue - 2
If I delete any item from list/recyclerview using:
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
still I get updated list(i.e new list of notes excluding deleted note) in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same, no and exclusion of deleted item.
Question Where exactly I'm making mistake to initialize/update adapter? because ViewModel and Repository are working fine.
Make following changes:
In init{} block of NoteViewModelImpl :
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener{noteResponseList ->
viewModelScope.launch{
_noteListStateFlow.emit(it.toList())
}
}
you must add .toList() if you want to emit list in StateFlow to get notified about updates, and in HomeFragment
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Impose StateFlow
lifecycleScope.launch {
viewModel.noteListStateFlow.collect { list ->
adapter.submitList(list.toMutableList())
}
}
}
That's it, I hope it works fine.
Try to remove additional lists of items in the fetchFirestoreData() and showMenu() (for item R.id.option_delete) methods of the HomeFragment fragment and see if it works:
// remove `val noteList:MutableList<NoteResponse>` in `fetchFirestoreData()` method
private fun fetchFirestoreData() {
...
// remove this line
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
adapter.submitList(data)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
And the same for the delete menu item (R.id.option_delete).
I am implementing an "Add to favourites" functionality in a detail screen. If the user taps the FAB, I want to set the fab as selected and update my database. How could I use the same value that I am sending to the database to be used in my fragment (to be consistent, in case there is some issue while updating the DB)
Fragment
class BeerDetailsFragment : Fragment(R.layout.fragment_beer_details) {
private val viewModel by viewModels<BeerDetailsViewModel>()
private val args by navArgs<BeerDetailsFragmentArgs>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeToObservers()
viewModel.getBeer(args.beerId)
}
private fun subscribeToObservers() {
viewModel.beer.observe(viewLifecycleOwner, { resource ->
when(resource.status) {
Status.SUCCESS -> {
loadData(resource.data)
}
Status.ERROR -> {
showError(resource.message)
}
Status.LOADING -> {}
}
})
}
private fun loadData(beerDetails: BeerDomainModel?) {
if (beerDetails != null) {
Glide.with(requireContext())
.load(beerDetails.imageMedium)
.placeholder(R.drawable.ic_beer)
.error(R.drawable.ic_beer)
.fallback(R.drawable.ic_beer)
.into(beerDetailsImage)
beerDetailsName.text = beerDetails.name
beerDetailsDescription.text = beerDetails.description
fab.isSelected = beerDetails.isFavourite
fab.setOnClickListener {
viewModel.updateBeer(beerDetails)
// I shouldn't do it like this in case there is an issue while updating the DB
fab.isSelected = !beerDetails.isFavourite
}
}
}
...
View Model class
class BeerDetailsViewModel #ViewModelInject constructor(private val repository: BreweryRepository) : ViewModel() {
private val beerId = MutableLiveData<String>()
fun getBeer(id: String) {
beerId.value = id
}
var beer = beerId.switchMap { id ->
liveData {
emit(Resource.loading(null))
emit(repository.getBeer(id))
}
}
fun updateBeer(beer: BeerDomainModel) {
viewModelScope.launch {
repository.updateBeer(beer)
}
}
}
Repository
class BreweryRepository #Inject constructor(private val breweryApi: BreweryApi, private val beerDao: BeerDao, private val responseHandler: ResponseHandler) {
...
suspend fun getBeer(id: String): Resource<BeerDomainModel> {
return try {
withContext(IO) {
val isInDB = beerDao.isInDB(id)
if (!isInDB) {
val response = breweryApi.getBeer(id).beer.toDomainModel()
beerDao.insert(response.toBeerEntity())
responseHandler.handleSuccess(response)
} else {
val beer = beerDao.get(id).toDomainModel()
responseHandler.handleSuccess(beer)
}
}
} catch (e: Exception) {
responseHandler.handleException(e)
}
}
suspend fun updateBeer(beer: BeerDomainModel) {
withContext(IO) {
val dbBeer = with(beer) {
copy(isFavourite = !isFavourite)
toBeerEntity()
}
beerDao.update(dbBeer)
}
}
}
I would prefer to use a unidirectional flow with the following implementation:
Not sure how is your DAO implemented, but if you are using Room you could update your get method to return a Flow instead. That way whenever your data is updated, you will get back the updated data.
Then in your VM you just get that Flow or stream of data and subscribe to the updates. Flow has a very convenient method: asLiveData() so your code will look much cleaner.
If you are not using Room, then what I'd do is either construct a Flow or some type of stream and on updates successful updates send out the new data.
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()
}
I can't observe the LiveData<PagedList> change in activity, but the UI is updated(the list has grown in activty).
I can only observe it once when the livedata is initialized.
when the paging library call loadAfter method, the ui is updated, but didn't call pageList.observe{}
Firstly, I put the process of data request into the Kotlin Coroutines, I can't observe the data change, then I used asynchronous requests instead.It still didn't work.
Here is my code:
PlayActivity main code
private val commentAdapter =
object : BasePagedAdapter(diffCallback, this) {
// just bind recycleview item and corresponding view model. etc.
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_play)
binding.vm = vm
binding.lifecycleOwner = this
val workId = intent.getLongExtra(WORK_ID, 0)
vm.listComment(workId)
play_rv_comment.adapter = commentAdapter
/* herer is the problem*/
vm.commentList.observe(this, Observer {
/*only log once when called loadInitial*/
LogUtils.e("observe", it)
commentAdapter.submitList(it)
})
PlayViewModel
class PlayViewModel : BaseViewModel() {
var workId: Long = 0
// the data which I want to observe
lateinit var commentList: LiveData<PagedList<WorkComment>>
private val commentPageSize = 15
fun listComment(workId: Long) {
// init by DataSource.Factory in android paging library
commentList = BaseDataSourceFactory(workId).toLiveData(commentPageSize)
}
DataSource.Factory in Android paging
class BaseDataSourceFactory(
val workId: Long
) :
DataSource.Factory<Long, WorkComment>() {
override fun create(): DataSource<Long, WorkComment> {
return object : PageKeyedDataSource<Long, WorkComment>() {
override fun loadInitial(
params: LoadInitialParams<Long>,
callback: LoadInitialCallback<Long, WorkComment>
) {
try {
val res = RetrofitUtil.getInstanceWithJwt().create(WorkCommentApi::class.java)
.listComment(
workId, 1, params.requestedLoadSize
)
res.enqueue(object : retrofit2.Callback<TResult> {
override fun onFailure(call: Call<TResult>, t: Throwable) {
}
override fun onResponse(call: Call<TResult>, response: Response<TResult>) {
callback.onResult(
response.body()!!.toList(WorkComment::class.java),
null, 2)
}
})
} catch (e: SocketTimeoutException) {
ToastUtils.showShort("请稍候重试")
} catch (e: Exception) {
LogUtils.e(e.localizedMessage)
}
}
// called many times, but I can't observe the PagedList change
override fun loadAfter(
params: LoadParams<Long>,
callback: LoadCallback<Long, WorkComment>
) {
val res = RetrofitUtil.getInstanceWithJwt().create(WorkCommentApi::class.java)
.listComment(
workId, 1, params.requestedLoadSize
)
res.enqueue(object : retrofit2.Callback<TResult> {
override fun onFailure(call: Call<TResult>, t: Throwable) {
}
override fun onResponse(call: Call<TResult>, response: Response<TResult>) {
callback.onResult(
response.body()!!.toList(WorkComment::class.java),
params.key + 1
)
}
})
}
override fun loadBefore(
params: LoadParams<Long>,
callback: LoadCallback<Long, WorkComment>
) {
}
}
}
}
Retrofit Api
interface WorkCommentApi {
/**
* list comment
*/
#GET("public/work/comment")
fun listComment(#Query("workId") workId: Long, #Query("current") current: Long, #Query("size") size: Int): Call<TResult>
}
I want to know what should I do to observe the LiveData<PagedList> change
This is happening because each time you call vm.listComment(workId), the object you first bound in activity is killed and new object is created.
You can use Transformations with MediatorLiveData.
Activity:
viewModel.logout().observe(this, Observer {
// do here
})
ViewModel:
class RepackViewModel(app: Application) : BaseViewModel(app) {
// IMPORTANT - Mediator
val logout = MediatorLiveData<PagedList<WorkComment>>()
fun logout() : LiveData<PagedList<WorkComment>> = logout
init {
// IMPORTANT - passes repo update to activity
logout.addSource(repo.getLogoutResponse()) { logout.postValue(it) }
}
}
Repository:
class BaseRepository(val app: Application) {
private val logout = MutableLiveData<PagedList<WorkComment>>()
fun getLogoutResponse(): LiveData<PagedList<WorkComment>> = logout
override fun create(): DataSource<Long, WorkComment> {
//when you get your data
logout.value = // your value
}
You need to have your work id be mutable data to be observed by the transformation. so whenever you update your work, id, it will fetch comments. Like Thus...
ViewModel:
val workIdMutableLiveData: MutableLiveData<Int> = MutableLiveData(workId)
//This performs the meat of the work to display the items in the recyclerview
var commentsList = Transformations.switchMap(workIdMutableLiveData) { workId ->
val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(pagingLimit)
.build()
val pagedListBuilder = LivePagedListBuilder<Long, WorkComment>(BaseDataSourceFactory(workId), config)
pagedListBuilder.build()
}
Then in your activity, observe
yourViewModel.commentsList.observe(this, Observer { list ->
list ?: return#Observer
adapter.submitList(list)
yourRecyclerView.adapter = adapter
})
Whenever you update the workIdMutableLiveData by doing a
workIdMutableLiveData.postValue(workId)
...The recyclerview will update. Your recyclerview must inherit from PagedListAdapter.
After testing, I knew list couldn't be observed when it has inner data change, like add(), remove(). etc.
It only be observed when its reference has been changed, like create or assignment operation:
list.value = null
So I couldn't observe the data change of LiveData<List>
I'm using Paging Library and Android Architecture Components. I simply want to observe pagedlist livedata and update my RecyclerView when there is a change.
I'm observing isLoadingLiveData, isEmptyLiveData and errorLiveData objects which are MediatorLiveData objects created in my ViewModel and observed in my fragment. And also observing resultLiveData which returns the fetched Gist list from remote.
In my ViewModel, I created a PagedList LiveData and whenever it's data changed, I wanted to update isLoadingLiveData, isEmptyLiveData, errorLiveData and PagedListAdapter. Therefore, I defined isLoadingLiveData, isEmptyLiveData, errorLiveData and resultLiveData as MediatorLiveData objects. I added resultLiveData as a source of these objects. So when resultLiveData has changed, these objects' onChanged methods will be called. And resultLiveData is depend on userNameLiveData, so when userNameLiveData has changed, allGistsLiveData will be called and it will fetch the data. For example when the user swipe the list, I'm setting userNameLiveData and doing network call again.
My ViewModel:
private val userNameLiveData = MutableLiveData<String>()
private var gists: LiveData<PagedList<Gist>>? = null
val allGistsLiveData: LiveData<PagedList<Gist>>
get() {
if (null == gists) {
gists = GistModel(NetManager(getApplication()), getApplication()).getYourGists(userNameLiveData.value!!).create(0,
PagedList.Config.Builder()
.setPageSize(PAGED_LIST_PAGE_SIZE)
.setInitialLoadSizeHint(PAGED_LIST_PAGE_SIZE)
.setEnablePlaceholders(PAGED_LIST_ENABLE_PLACEHOLDERS)
.build())
}
return gists!!
}
val resultLiveData = MediatorLiveData<LiveData<PagedList<Gist>>>().apply {
this.addSource(userNameLiveData) {
gists = null
this.value = allGistsLiveData
}
}
val isLoadingLiveData = MediatorLiveData<Boolean>().apply {
this.addSource(resultLiveData) { this.value = false }
}
val isEmptyLiveData = MediatorLiveData<Boolean>().apply {
this.addSource(resultLiveData) { this.value = false }
}
val errorLiveData = MediatorLiveData<Boolean>().apply {
this.addSource(resultLiveData) {
if (it == null) {
this.value = true
}
}
}
fun setUserName(userName: String) {
userNameLiveData.value = userName
}
and my fragment:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.isEmptyLiveData.observe(this#YourGistsFragment, Observer<Boolean> { isVisible ->
emptyView.visibility = if (isVisible!!) View.VISIBLE else View.GONE
})
viewModel.isLoadingLiveData.observe(this#YourGistsFragment, Observer<Boolean> {
it?.let {
swipeRefreshLayout.isRefreshing = it
}
})
viewModel.errorLiveData.observe(this#YourGistsFragment, Observer<Boolean> {
it?.let {
showSnackBar(context.getString(R.string.unknown_error))
}
})
viewModel.setUserName("melomg")
viewModel.resultLiveData.observe(this#YourGistsFragment, Observer { it -> gistAdapter.setList(it?.value) })
}
override fun onRefresh() {
viewModel.setUserName("melomg")
}
my repository:
fun getYourGists(userName: String): LivePagedListProvider<Int, Gist> {
return remoteDataSource.getYourGists(userName, GitHubApi.getGitHubService(context))
}
my remoteDataSource:
fun getYourGists(username: String, dataProvider: GitHubService): LivePagedListProvider<Int, Gist> {
return object : LivePagedListProvider<Int, Gist>() {
override fun createDataSource(): GistTiledRemoteDataSource<Gist> = object : GistTiledRemoteDataSource<Gist>(username, dataProvider) {
override fun convertToItems(items: ArrayList<Gist>?): ArrayList<Gist>? {
return items
}
}
}
}
I tried to create this solution from this project. But my problem is
resultLiveData has been changing without waiting the network call response and therefore the result of response ignored and my ui is being updated before the data has arrived. Since resultLiveData is changing before request and therefore there is no data yet. Simply how can I observe pagedlist livedata?
Finally, I found a working solution but I don't think it is a best solution. So if you have any ideas please don't hesitate to answer. Here is my solution:
I gave up from wrapping my PagedList data with MediatorLiveData. I still leave my all other live data(isLoadingLiveData, isEmptyLiveData, errorLiveData) as MediatorLiveData but added their source when my gists PagedList LiveData initialized. Here is the code:
private var gists: LiveData<PagedList<Gist>>? = null
private val userNameLiveData = MutableLiveData<String>()
val isLoadingLiveData = MediatorLiveData<Boolean>()
val isEmptyLiveData = MediatorLiveData<Boolean>()
val errorLiveData = MediatorLiveData<ErrorMessage>()
val allGistsLiveData: LiveData<PagedList<Gist>>
get() {
if (gists == null) {
gists = GistModel(NetManager(getApplication()), getApplication()).getYourGists(userNameLiveData.value!!).create(0,
PagedList.Config.Builder()
.setPageSize(Constants.PAGED_LIST_PAGE_SIZE)
.setInitialLoadSizeHint(Constants.PAGED_LIST_PAGE_SIZE)
.setEnablePlaceholders(Constants.PAGED_LIST_ENABLE_PLACEHOLDERS)
.build())
isLoadingLiveData.addSource(gists) { isLoadingLiveData.value = false }
isEmptyLiveData.addSource(gists) {
if (it?.size == 0) {
isEmptyLiveData.value = true
}
}
errorLiveData.addSource(gists) {
if (it == null) {
errorLiveData.value = ErrorMessage(errorCode = ErrorCode.GENERAL_ERROR)
}
}
}
}
return gists!!
}
fun setUserName(userName: String) {
isLoadingLiveData.value = true
userNameLiveData.value = userName
gists = null
allGistsLiveData
}
There is a better way to do this. Look this example:
https://github.com/android/architecture-components-samples/tree/master/PagingWithNetworkSample
The trick is putting inside your DataSource a mutable live data like this:
enum class NetworkState { LOADING, LOADED, FAILURE }
val networkState = MutableLiveData<NetworkState>()
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<String, OrderService.Resource>) {
networkState.post(NetworkState.LOADING)
ApiClient.listMyObjects(
onSuccess = { list ->
networkState.post(NetworkState.LOADED)
callback.onResult(list)
}
onFailure = {
networkState.post(NetworkState.FAILURE)
}
)
}
Then you can listen to your paged list and network state with:
val myDataSource = MyDataSource.Factory().create()
myDataSource.toLiveData().observe(viewLifeCycle, Observer { pagedList ->
// Update your recycler view
myRecyclerViewAdapter.submitList(pagedList)
})
myDataSource.networkState.observe(viewLifeCycle, Observer { state ->
// Show errors, retry button, progress view etc. according to state
})