I have two requests that depend on each other and I want to pass both results to the recyclerView adapter.
Unfortunately, following code does not return data_1 in collect. But when I commented out second collect with data_2 then I see result for data_1.
class MyViewModel(UUID: GUID): ViewModel() {
private val _data_1: MutableStateFlow<List<Example>> = MutableStateFlow(emptyList())
val data_1: StateFlow<List<Example>> get() = _data_1.asStateFlow()
private val _data_2: MutableStateFlow<List<Example_2>> = MutableStateFlow(emptyList())
val data_2: StateFlow<List<Example_2>> get() = _data_2.asStateFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
_repository.getData(UUID).collect { data_1 ->
_data_1.value = data_1
getSecondData(data_1)
}
}
private suspend fun getSecondData(item: List<Example>) {
val list = mutableListOf<Event>()
coroutineScope {
for (item in items) {
async(Dispatchers.IO) {
_repository.getSecondData(item).collect { it ->
list.add(it)
}
}.await()
}
_data_2.value = list
}
}
}
Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
myViewModel.data_1.collect { data_1 ->
myViewModel.data_2.collect { data_2 ->
withContext(Dispatchers.Main) {
binding.myRecyclerView.adapter = MyListAdapter(data_1, data_2)
}
}
}
}
}
}
I have check with Log.d that data_1 with data_2 returns records. I have tried storing data_1 in private variable in fragment before calling second collect. I have tried joinAll option but together with launch/collect it will never work.
Why is it not working and what should I do to fix it?
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 have a Fragment code -
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val safeArgs: PetDetailsViewArgs by navArgs()
val petId = safeArgs.petId
viewModel.getPetDetailsForId(petId).observe(viewLifecycleOwner, {
// ...
})
}
I have a ViewModel code -
private val viewState = PetDetailsViewState()
fun getPetDetailsForId(id: String?): LiveData<PetDetailsViewState> {
return if (id.isNullOrEmpty()) {
liveData {
emit(
viewState.copy(
loading = false,
error = ErrorType.PET_ID_NULL_OR_EMPTY
)
)
}
} else {
petDetailsLiveData
}
}
var petDetailsLiveData = petService.performPetAction(PetAction.GetPetDetails("2")).map {
when (it) {
// ...
}
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
As you see in my ViewModel, I am at the moment hardcoding the id in PetAction.GetPetDetails("2") which is not correct.
How do I pass the id from my view to viewModel?
You have two options, if the petId (from the Fragment) does not change, you could create / inject your ViewModel and pass the petId via Constructor.
Can your petId be null? If not you can then directly initialize your LiveData and observe it from your Fragment.
class PetViewModel(petId: String): ViewModel() {
val petDetailsLiveData = petService.performPetAction(PetAction.GetPetDetails(petId)).map {
// ...
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
}
Second option, as you showed in your question, if petId can change, create the LiveData within the function getPetDetailsForId(id: String?).
fun getPetDetailsForId(id: String?): LiveData<PetDetailsViewState> {
return if (id.isNullOrEmpty()) {
liveData {
emit(
viewState.copy(
loading = false,
error = ErrorType.PET_ID_NULL_OR_EMPTY
)
)
}
} else {
petService.performPetAction(PetAction.GetPetDetails("2")).map {
// ...
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
}
After discussion
You can consider some caching of your petId and the PetDetailsViewState to avoid duplicate api calls. Take this a a very simple example of getting the idea. There is much to improve here.
class PetViewModel : ViewModel() {
private val cachedPetDetailsViewState: PetDetailsViewState? = null
private val cachedPetId: String = ""
fun getPetDetailsForId(id: String?): LiveData<PetDetailsViewState> {
if (id == cachedPetId && cachedPetDetailsViewState != null) return MutableLiveData(cachedPetDetailsViewState)
cachedPetId == id
if (id.isNullOrEmpty() { ... }
else {
val petIdViewState = // make the API call
cachedPetDetailsViewState = petIdViewState
return MutableLiveData(petIdViewState)
}
}
}
Found a way to do with savedStateHandle -
Here is my Fragment -
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.petDetailsViewData.observe(viewLifecycleOwner, {
})
}
ViewModel -
class PetDetailsViewModel #ViewModelInject constructor(
private val petService: PetService,
#Assisted private val savedStateHandle: SavedStateHandle
) :
ViewModel() {
private val viewState = PetDetailsViewState()
var petDetailsViewData =
petService.performPetAction(PetAction.GetPetDetails(savedStateHandle.get<String>("petId")!!))
.map {
when (it) {
// ...
}
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
}
I basically use safeArgs key inside viewModel and access it via savedStateHandle. This way I don't need to bother my view with accessing ids and also on configuration change, I only call my service once.
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.
I have a small app I am using to try learn more about some of the newer Android components. I'm finding it difficult to find information and understand how best to do what I want.
Currently: Open app -> load data + stores in DB -> display data in list
I want to be able to query data again upon button press.
I have 2 buttons, 1 to fetch data again, 1 to delete the list data from the DB.
Problem is that it seems you cannot refresh if you are observing on an instance of LiveData, which I am. I understand that however the way I found to actually do a Network call and store in the Database returns an instance of LiveData and I am not sure how best to proceed.
Let me show you the code.
Fragment
private val viewModel: quoteViewModel by viewModels()
private lateinit var binding: FragmentHomeBinding
private lateinit var adapter: QuoteAdapter
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)
initRecyclerView()
setupRetrieveQuotesObserver()
setupDeleteDataListener()
setupFetchNewDataListener()
setupSwipeToRefresh()
}
private fun initRecyclerView() {
adapter = QuoteAdapter()
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
}
private fun setupDeleteDataListener() {
binding.removeQuotesButton.setOnClickListener {
viewModel.removeAllQuotes()
}
}
private fun setupFetchNewDataListener() {
binding.getQuotesButton.setOnClickListener {
viewModel.removeQuotes()
viewModel.getQuotes()
}
}
private fun setupRetrieveQuotesObserver() {
viewModel.quoteLiveDataList.observe(viewLifecycleOwner, Observer { result ->
when (result.status) {
NewResult.Status.SUCCESS -> {
result.data.let { adapter.setItems(ArrayList(result.data)) }
binding.progressBar.visibility = View.GONE
binding.swipeContainer.isRefreshing = false
}
NewResult.Status.ERROR -> {
binding.progressBar.visibility = View.GONE
Snackbar.make(binding.root, "Some error has occurred", Snackbar.LENGTH_SHORT)
.show()
}
NewResult.Status.LOADING -> {
binding.progressBar.visibility = View.VISIBLE
}
}
})
}
private fun setupSwipeToRefresh() {
binding.swipeContainer.setOnRefreshListener {
viewModel.getQuotes()
}
}
ViewModel
val quoteLiveDataList: LiveData<NewResult<List<Quote>>> = repository.quotes
fun getQuotes() = viewModelScope.launch {
repository.quotes
}
fun removeAllQuotes() = viewModelScope.launch {
repository.deleteAllQuotes()
}
Repository
val quotes = performGetOperation(
databaseQuery = { dao.getAllQuotes() },
networkCall = { remoteSource.getAllQuotes() },
saveCallResult = {
val quotesList = ArrayList<Quote>()
for (messageString in it.messages.non_personalized) {
quotesList.add(
Quote(
messageString,
FaceImageProvider().getRandomFacePicture(),
false
)
)
}
dao.insertQuotes(quotesList)
}
)
#WorkerThread
suspend fun deleteAllQuotes() = withContext(Dispatchers.IO) { dao.deleteAllQuotes() }
performGetOperation
This is a class I saw online for handling what I want to do. I think the issue stems from here as it is returning LiveData, I'm not sure how best to fix it
fun <T, A> performGetOperation(
databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> NewResult<A>,
saveCallResult: suspend (A) -> Unit
): LiveData<NewResult<T>> =
liveData(Dispatchers.IO) {
emit(NewResult.loading())
val source = databaseQuery.invoke().map { NewResult.success(it) }
emitSource(source)
val responseStatus = networkCall.invoke()
if (responseStatus.status == NewResult.Status.SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == NewResult.Status.ERROR) {
emit(NewResult.error(responseStatus.message!!))
emitSource(source)
}
}
RemoteDataSource
suspend fun getQuotes() = getResult { service.getQuotes() }
getResult
protected suspend fun <T> getResult(call: suspend () -> Response<T>): NewResult<T> {
try {
val response = call.invoke()
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
return NewResult.success(body)
}
}
return error("${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}
private fun <T> error(message: String): NewResult<T> {
Log.d("BaseDataSource", message)
return NewResult.error("Network called failed due to: $message")
}
NewResult
data class NewResult<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING,
}
companion object {
fun <T> success(data: T): NewResult<T> {
return NewResult(Status.SUCCESS, data, null)
}
fun <T> error(message: String, data: T? = null): NewResult<T> {
return NewResult(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): NewResult<T> {
return NewResult(Status.LOADING, data, null)
}
}
Apologies for the very long message, but I guess I need to show all the little bits and bobs I'm using.
I think the problem is in the Fragment where I do viewModel.quoteLiveDataList.observe, as it is returning a new LiveData if it is called again. So I'm not sure how I can do another server call and update the DB and return it here.
Appreciate any help!
Thanks
Use Transformations.switchMap on a MutableLiveData to trigger your repository call like it is done here in the GithubBrowserSample project. This will allow you to implement the refresh functionality -
private val _getQuotes = MutableLiveData<Boolean>()
val quotes: LiveData<NewResult<List<Quote>>> = _getQuotes.switchMap { getQuotes ->
repository.quotes
}
fun getQuotes() {
_getQuotes.value = true
}
fun refresh() {
_getQuotes.value?.let {
_getQuotes.value = it
}
}
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
})