In my app i'm saving all data into realm now here when data is loaded into recycler view in my recycler view item i'm having a button there on click of button i'm setting the status field bool of realm database to true and thereafter on dataset changed i'm trying to set an item button background if boolean inside database is true but here i'm having no luck whenever my recyclerview is refreshed in not getting my desired output maybe something is wrong with my data fetching query any help will be appreciated.
Here is my recycler view on bindview holder code:
//this is the code where i'm facing problem i want the else want to execute but that's not happening
val `object` = realm!!.where(Devicesdatabase::class.java).equalTo("status", false).findFirst()
if (`object` != null) {
holder.btn.setBackgroundResource(R.drawable.list_button)
holder.btn.text = "Stranger"
}
else
{
holder.btn.setBackgroundResource(R.drawable.list_button_grey)
holder.btn.setText("known")
}
Here's what i'm doing inside on button click:
holder.btn.setOnClickListener {
holder.btn.setBackgroundResource(R.drawable.list_button_grey)
holder.btn.setText("known")
val `object` = realm!!.where(Devicesdatabase::class.java)
.equalTo("devicename", host.hostname)
.findFirst()
if (`object` != null) {
realm!!.executeTransaction {
var item = realm!!.createObject(Devicesdatabase::class.java)
item.status = true
notifyDataSetChanged()
}
}
}
This is my realm model class:
#RealmClass
open class Devicesdatabase : RealmModel {
var wifi:String?=null
var devicename: String? = null
var macAddress: String? = null
var status:Boolean = false
var type:String?=null
}
Related
I'm newbie use Kotlin on my dev apps android,
and now, I on step learn to implement Update UI with LiveData, Retrofit, Coroutine on Recyclerview. The My Apps:
MainActivity > MainFragment with 3 Tab fragment > HomeFragment, DashboardFragment, and SettingsFragment
I call function to get data from server on onCreateView HomeFragment, and observe this with show shimmer data on my Recylerview when is loading, try update RecyclerView when success, and show view Failed Load -button refresh when error.
The problem is:
Adapter Recyclerview not Update when success get Data from Server. Adapter still show shimmer data
With case error (no Internet), i show view Failed Load, with button refresh. Tap to refresh, i re-call function to get data server, but fuction not work correct. Recyclerview show last data, not show Failed Load again.
Bellow my code
HomeFragment
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private lateinit var adapterNews: NewsAdapter
private var shimmerNews: Boolean = false
private var itemsDataNews = ArrayList<NewsModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHomeBinding.inflate(inflater, container, false)
......
newsViewModel = ViewModelProvider(requireActivity()).get(NewsViewModel::class.java)
//news
binding.frameNews.rvNews.setHasFixedSize(true)
binding.frameNews.rvNews.layoutManager = llmh
adapterNews = NewsAdapter(itemsDataNews, shimmerNews)
binding.frameNews.rvNews.adapter = adapterNews
// Observe
//get News
newsViewModel.refresh()
newsViewModel.newsList.observe(
viewLifecycleOwner,
androidx.lifecycle.Observer { newsList ->
newsList?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = true
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
if (it.size == 0)
binding.frameNews.root.visibility = View.GONE
else
getDataNews(it)
}
})
newsViewModel.loading.observe(viewLifecycleOwner) { isLoading ->
isLoading?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = false
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
}
newsViewModel.loadError.observe(viewLifecycleOwner) { isError ->
isError?.let {
binding.frameNews.rvNews.visibility = View.INVISIBLE
binding.frameNews.itemNewsLayoutFailed.visibility = View.VISIBLE
binding.frameNews.btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
}
....
return binding.root
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNewsShimmer() {
shimmerNews = true
itemsDataNews.clear()
itemsDataNews.addAll(NewsData.itemsShimmer)
adapterNews.notifyDataSetChanged()
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNews(list: List<NewsModel>) {
Toast.makeText(requireContext(), list.size.toString(), Toast.LENGTH_SHORT).show()
shimmerNews = false
itemsDataNews.clear()
itemsDataNews.addAll(list)
adapterNews.notifyDataSetChanged()
}
override fun onDestroyView() {
super.onDestroyView()
_binding=null
}
NewsViewModel
class NewsViewModel: ViewModel() {
val newsService = KopraMobileService().getNewsApi()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
val newsList = MutableLiveData<List<NewsModel>>()
val loadError = MutableLiveData<String?>()
val loading = MutableLiveData<Boolean>()
fun refresh() {
fetchNews()
}
private fun fetchNews() {
loading.postValue(true)
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
} else {
onError("Error : ${response.message()} ")
}
}
}
loadError.postValue("")
loading.postValue( false)
}
private fun onError(message: String) {
loadError.postValue(message)
loading.postValue( false)
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
NewsAdapter
NewsAdapter(
var itemsCells: List<NewsModel?> = emptyList(),
var shimmer: Boolean ,
) :
RecyclerView.Adapter<ViewHolder>() {
private lateinit var context: Context
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsHomeViewHolder {
context = parent.context
return NewsHomeViewHolder(
NewsItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: NewsHomeViewHolder, position: Int) {
holder.bind(itemsCells[position]!!)
}
inner class NewsHomeViewHolder(private val binding: NewsItemHomeBinding) :
ViewHolder(binding.root) {
fun bind(newsItem: NewsModel) {
binding.newsItemFlat.newsTitle.text = newsItem.name
binding.newsItemFlatShimmer.newsTitle.text = newsItem.name
binding.newsItemFlat.newsSummary.text = newsItem.name
binding.newsItemFlatShimmer.newsSummary.text = newsItem.name
if (shimmer) {
binding.layoutNewsItemFlat.visibility = View.INVISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.startShimmer()
} else {
binding.layoutNewsItemFlat.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.INVISIBLE
}
}
}
override fun getItemCount(): Int {
return itemsCells.size
}
I hope someone can help me to solve the problem. thanks, sorry for my English.
You have 3 different LiveDatas, right? One with a list of news data, one with a loading error message, and one with a loading state.
You set up an Observer for each of these, and that observer function gets called whenever the LiveData's value updates. That's important, because here's what happens when your request succeeds, and you get some new data:
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
}
which means newsList updates, then loadError updates, then loading updates.
So your observer functions for each of those LiveDatas run, in that order. But you're not testing the new values (except for null checks), so the code in each one always runs when the value updates. So when your response is successful, this happens:
newsList observer runs, displays as successful, calls getDataNews
loadError observer runs, value is null so nothing happens
loading observer runs, value is false but isn't checked, displays as loading, calls getDataNewsShimmer
So even when the response is successful, the last thing you do is display the loading state
You need to check the state (like loading) before you try to display it. But if you check that loading is true, you'll have a bug in fetchNews - that sets loading = true, starts a coroutine that finishes later, and then immediately sets loading = false.
I'd recommend trying to create a class that represents different states, with a single LiveData that represents the current state. Something like this:
// a class representing the different states, and any data they need
sealed class State {
object Loading : State()
data class Success(val newsList: List<NewsModel>?) : State()
data class Error(val message: String) : State()
}
// this is just a way to keep the mutable LiveData private, so it can't be updated
private val _state = MutableLiveData<State>()
val state: LiveData<State> get() = _state
private fun fetchNews() {
// initial state is Loading, until we get a response
_state.value = State.Loading
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
// if you're using postValue I don't think you need to switch to Dispatchers.Main?
_state.postValue(
// when you get a response, the state is now either Success or Error
if (response.isSuccessful) State.Success(response.body()?.data)
else State.Error("Error : ${response.message()} ")
)
}
}
And then you just need to observe that state:
// you don't need to create an Observer object, you can use a lambda!
newsViewModel.state.observe(viewLifecycleOwner) { state ->
// Handle the different possible states, and display the current one
// this lets us avoid repeating 'binding.frameNews' before everything
with(binding.frameNews) {
// You could use a when block, and describe each state explicitly,
// like your current setup:
when(state) {
// just checking equality because Loading is a -singleton object instance-
State.Loading -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = false
itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
// Error and Success are both -classes- so we need to check their type with 'is'
is State.Error -> {
rvNews.visibility = View.INVISIBLE
itemNewsLayoutFailed.visibility = View.VISIBLE
btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
is State.Success -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = true
itemNewsLayoutFailed.visibility = View.GONE
// Because we know state is a Success, we can access newsList on it
// newsList can be null - I don't know how you want to handle that,
// I'm just treating it as defaulting to size == 0
// (make sure you make this visible when necessary too)
if (state.newsList?.size ?: 0 == 0) root.visibility = View.GONE
else getDataNews(state.newsList)
}
}
// or, if you like, you could do this kind of thing instead:
itemNewsLayoutFailed.visibility = if (state is Error) VISIBLE else GONE
}
}
You also might want to break that display code out into separate functions (like showError(), showList(state.newsList) etc) and call those from the branches of the when, if that makes it more readable
I hope that makes sense! When you have a single value representing a state, it's a lot easier to work with - set the current state as things change, and make your observer handle each possible UI state by updating the display. When it's Loading, make it look like this. When there's an Error, make it look like this
That should help avoid bugs where you update multiple times for multiple things, trying to coordinate everything. I'm not sure why you're seeing that problem when you reload after an error, but doing this might help fix it (or make it easier to see what's causing it)
Good day. So I currently have data in my recycler view. It is for now only static data. I still have to do the code where I import. My problem however is I have a button that changes the background of a text view. This happens in my adapter. And when I scroll through my list the bg color change gets reverted back to what it was before the button click. I have read a lot of similar problems but could not really find one that explains clearly or work for me. From what I read the data gets reset to the static data because it is currently happening in my onBindViewHolder and I think this changes the data on every new data read(scrolling). I read that I should create a link or a listener and then call it. But It does not make sense to me because if a link is called the same amount of times as the code is executed then it will be the same will it not. Maybe having a condition listener but not sure if this is the way to go.
I am somewhat new to android and kotlin. Have been working with it for a month now. I dont know everything I am doing but I got given a deadline. So sadly there was no time to go and learn the basics. Thank you for any and all help. Please let me know if you need any additional code/information
my adapter
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RowViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.table_list_item, parent, false)
return RowViewHolder(itemView)
}
private fun setHeaderBg(view: View) {
view.setBackgroundResource(R.drawable.table_header_cell_bg)
}
private fun setContentBg(view: View) {
view.setBackgroundResource(R.drawable.table_content_cell_bg)
}
override fun onBindViewHolder(holder: RowViewHolder, position: Int) {
// (TableViewAdapter.DataviewHolder) .bind()
val rowPos = holder.adapterPosition
if (rowPos == 0) {
// Header Cells. Main Headings appear here
holder.itemView.apply {
setHeaderBg(txtWOrder)
setHeaderBg(txtDElNote)
setHeaderBg(txtCompany)
// setHeaderBg(txtAddress)
setHeaderBg(txtWeight)
setHeaderBg(txtbutton1)
setHeaderBg(txtbutton2)
setHeaderBg(txttvdone)
txtWOrder.text = "WOrder"
txtDElNote.text = "DElNote"
txtCompany.text = "Company"
// txtAddress.text = "Address"
txtWeight.text = "Weight"
txtbutton1.text = "Delivered"
txtbutton2.text = "Exception"
txttvdone.text = ""
}
} else {
val modal = Tripsheetlist[rowPos - 1]
holder.itemView.apply {
setContentBg(txtWOrder)
setContentBg(txtDElNote)
setContentBg(txtCompany)
// setContentBg(txtAddress)
setContentBg(txtWeight)
setContentBg(txtbutton1)
setContentBg(txtbutton2)
setContentBg(txttvdone)
txtWOrder.text = modal.WOrder.toString()
txtDElNote.text = modal.DElNote.toString()
txtCompany.text = modal.Company.toString()
// txtAddress.text = modal.Address.toString()
txtWeight.text = modal.Weight.toString()
txtbutton1.text = modal.Button1.toString()
txtbutton2.text = modal.Button2.toString()
txttvdone.text = modal.tvdone.toString()
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
txttvdone.setBackgroundResource(R.color.green)
txttvdone.setText("✓")
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
txttvdone.setBackgroundResource(R.color.orange)
txttvdone.setText("x")
}
}
}
class RowViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val txttvdone:TextView = itemView.findViewById<TextView>(R.id.txttvdone)
val txtbutton1:Button = itemView.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
} class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view){
var txtbutton1 = view.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
var txttvdone = view.findViewById<TextView>(R.id.txttvdone)
}
I tried (TableViewAdapter.DataviewHolder) .bind() doing this and creating another class as I saw that was done in another thread(Why do values disappear after scrolling in Recycler View?) Its a lot like my problem. I just can't seem to implement his solution to make mine work. ( don't understand his solution fully)
//I am also aware that I am using android extensions which will expire at the end of the year. But for now it works and once I have the code up and running I will start to move over to the newer versions of kotlin.
A RecyclerView, as its name implies, will recycle the views when they go off screen. This means that when the view for an item comes into view, it gets recreated and the onBindViewHolder() is called to fill in the details.
Your onClickListener inside your adapter changes the background of one of the subviews for your cell view. However, that cell will be redrawn if it leaves the screen and comes back.
To get around this, your onClickListener should be changing a property on the data item, and your onBindViewHolder should check that property to determine what background color to display for the subview:
enum class DataState {
Unselected,
Success,
Failure
}
data class DataItem(var state: DataState = DataState.Unselected)
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
var dataItems: List<DataItem> = emptyList()
fun updateData(data: List<DataItem>) {
dataItems = data
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val dataItem = dataItems[position]
holder.txttvdone.apply {
setBackgroundResource(when (dataItem.state) {
DataState.Unselected -> android.R.color.transparent
DataState.Success -> R.color.green
DataState.Failure -> R.color.orange
})
text = when (dataItem.state) {
DataState.Unselected -> ""
DataState.Success -> "✓"
DataState.Failure -> "x"
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
dataItem.state = DataState.Success
notifyDataSetChanged()
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
dataItem.state = DataState.Failure
notifyDataSetChanged()
}
}
}
}
Here is my Adapter class code:
class SearchPeopleAdapter(user: FirestoreRecyclerOptions<User>) :
FirestoreRecyclerAdapter<User, SearchPeopleAdapter.ViewHolder>(user) {
private var mUser : FirestoreRecyclerOptions<User>? = null
private var mOptions: FirestoreRecyclerOptions<User>? = null
private var mSnapshots: ObservableSnapshotArray<User>? = null
init {
mUser = user
}
fun firestoreRecyclerAdapter(user: FirestoreRecyclerOptions<User>?) {
mOptions = user
mSnapshots = user!!.snapshots
if (mOptions!!.owner != null) {
mOptions!!.owner!!.lifecycle.addObserver(this)
}
}
override fun startListening() {
if (!mSnapshots!!.isListening(this)) {
mSnapshots!!.addChangeEventListener(this);
}
}
override fun stopListening() {
mSnapshots!!.removeChangeEventListener(this)
notifyDataSetChanged()
}
//Inflate the xml
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.search_people_list_item, parent, false))
}
//Bind every dataView to the xml based on the Int value
override fun onBindViewHolder(viewHolder: ViewHolder, holderNumber: Int, user: User) {
viewHolder.apply {
itemView.search_people_person_list_name.text = user.Name
itemView.search_people_person_username.text = user.UserName
}
viewHolder.bind(user)
}
override fun getItemCount(): Int {
return mSnapshots!!.size
}
//Adds functionality to each View (aka ViewHolder) which is every person downloaded
inner class ViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView!!), View.OnClickListener {
//We are downloading User Objects so this Variable will be assigned to the UserObject downloaded
var currentUser : User? = null
//When the class is initiated this function is called which sets an OnClickListener to each View
init {
itemView!!.setOnClickListener(this)
}
//The Data from the Object is used to Populate the TextViews
fun bind(model: User) {
currentUser = model
itemView.search_people_person_list_name.text = model.Name
itemView.search_people_person_username.text = model.UserName
//If the user has set a profilePhoto then download & populate it with Glide
if ( model.ProfilePhotoChosen ) {
CompanionObjects.getPersonProfilePhotoStorageRef(model.Uid).downloadUrl.addOnSuccessListener {
val downloadUrl = it.toString()
Glide.with(itemView)
.load(downloadUrl)
.into(itemView.search_people_list_profile_image)
}.addOnFailureListener {
Timber.i("unable to retrieve your profile photo")
}
}
//Clear the View with the Glide.with(View).clear() method as the view will be reused and the photo
//might also get reused unnecessarily
else {
Glide.with(itemView).clear(itemView.search_people_list_profile_image)
}
}
//The onClick function is called when the View is clicked, in this case we are starting
// an Intent with the Intent Extra of userId to the PersonProfile Activity
// which will check for the IntentExtras and Populate the elements
override fun onClick(v: View?) {
val userId = currentUser!!.Uid
Timber.i("The click is $userId")
val intent = Intent(v!!.context, PersonProfileActivity::class.java)
intent.putExtra(CompanionObjects.USER_ID_INTENT_EXTRA, userId)
v.context.startActivity(intent)
}
}
}
Here are 2 of the methods in the Activity class, Inspite of calling the onStart and onStop method, the itemCount method always returns 0
override fun onStop() {
adapter!!.stopListening()
super.onStop()
}
//Retrieves data from Firestore and assigns the retrieved data to the search People Adapter
private fun retrieveDataFromFirestore(searchQuery : String) {
mFirestore = FirebaseFirestore.getInstance()
//Assigns the Collection Name from which needs to be queried
//the where conditions ensure to query a userDocument whose userName starts with the query entered in the searchField
val userNameQuery = mFirestore.collection(CompanionObjects.USERS_COLLECTION_NAME)
.whereGreaterThanOrEqualTo("userName", searchQuery)
.whereLessThanOrEqualTo("userName", "$searchQuery\uF7FF")
//Assigns the query to the User Objects that are related to Firestore
users = FirestoreRecyclerOptions.Builder<User>()
.setQuery(userNameQuery, User::class.java)
.build()
//Assigns the Firestore queried data to the search People Adapter
adapter = SearchPeopleAdapter(users!!)
registerAdapterObserver()
adapter!!.firestoreRecyclerAdapter(users!!)
adapter!!.startListening()
search_people_list.setHasFixedSize(true)
search_people_list.hasFixedSize()
search_people_list.layoutManager = LinearLayoutManager(this)
search_people_list.adapter = adapter
persistSearchQueryString(searchQuery)
}
When I call getItemCount method in the Activity class. It always returns 0 even thou the adapter does hold Views. How do I retrieve the exact count in the Adapter
Please make sure mSnapshots = user!!.snapshots produces items.
In fun retrieveDataFromFirestore(searchQuery : String) after adding items in adapter!!.firestoreRecyclerAdapter(users!!), you are not notifying RecyclerView to update.
So, don't forget to call
adapter!!.notifyDataSetChanged();
Your code might looks like this:
private fun retrieveDataFromFirestore(searchQuery : String) {
mFirestore = FirebaseFirestore.getInstance()
//Assigns the Collection Name from which needs to be queried
//the where conditions ensure to query a userDocument whose userName starts with the query entered in the searchField
val userNameQuery = mFirestore.collection(CompanionObjects.USERS_COLLECTION_NAME)
.whereGreaterThanOrEqualTo("userName", searchQuery)
.whereLessThanOrEqualTo("userName", "$searchQuery\uF7FF")
//Assigns the query to the User Objects that are related to Firestore
users = FirestoreRecyclerOptions.Builder<User>()
.setQuery(userNameQuery, User::class.java)
.build()
//Assigns the Firestore queried data to the search People Adapter
adapter = SearchPeopleAdapter(users!!)
registerAdapterObserver()
adapter!!.firestoreRecyclerAdapter(users!!)
adapter!!.startListening()
adapter!!.notifyDataSetChanged(); //<------- Add this line---------
search_people_list.setHasFixedSize(true)
search_people_list.hasFixedSize()
search_people_list.layoutManager = LinearLayoutManager(this)
search_people_list.adapter = adapter
persistSearchQueryString(searchQuery)
}
Ok I solved this, Here is the code:
private fun retrieveDataFromFirestore(searchQuery : String) {
mFirestore = FirebaseFirestore.getInstance()
//Assigns the Collection Name from which needs to be queried
//the where conditions ensure to query a userDocument whose userName
starts with the query entered in the searchField
val userNameQuery = mFirestore.collection(CompanionObjects.USERS_COLLECTION_NAME)
.whereGreaterThanOrEqualTo(userName, searchQuery.toLowerCase())
.whereLessThanOrEqualTo(userName, "${searchQuery.toLowerCase()}\uF7FF")
//Assign the Query to the user variable
users = FirestoreRecyclerOptions.Builder<User>()
.setQuery(userNameQuery, User::class.java)
.build()
//Assigns the Firestore queried data to the search People Adapter
adapter = object : SearchPeopleAdapter(users!!) {
//Need to create a class Body because it is open and this gives
//option to override its onDataChanged method in the Activity rather than in its adapter class
override fun onDataChanged() {
if ( itemCount == 0 ) {
search_people_list.visibility = View.INVISIBLE
retrieving_progress.visibility = View.INVISIBLE
empty_search_users_text.visibility = View.VISIBLE
//If was not searching with name field then prompt search with name field
if ( !searchingWithNameField ) {
//Code to add formatting options to the text like underline it
val content = SpannableString(getString(R.string.search_with_name_instead))
content.setSpan(UnderlineSpan(), 0, content.length, 0)
change_search_field_text.text = content
} else
//If was not searching with username field then prompt search with username field
{
//Code to add formatting options to the text like underline it
val content = SpannableString(getString(R.string.search_with_username_instead))
content.setSpan(UnderlineSpan(), 0, content.length, 0)
change_search_field_text.text = content
}
change_search_field_text.visibility = View.VISIBLE
} else {
//The adapter count is not 0 so show the recyclerView and hide the progress bar, emptyText, Change Search field Text etc.
search_people_list.visibility = View.VISIBLE
retrieving_progress.visibility = View.INVISIBLE
empty_search_users_text.visibility = View.INVISIBLE
change_search_field_text.visibility = View.INVISIBLE
//Should persist only if there is a result from the query ofCourse
persistSearchQueryStringAndSearchField(searchQuery)
}
}
}
adapterCreated = true
adapter!!.startListening()
adapter!!.notifyDataSetChanged()
//Make the progress bar visible and invisible soon as a document is added to the adapter
registerAdapterObserver()
search_people_list.setHasFixedSize(true)
search_people_list.hasFixedSize()
search_people_list.layoutManager = LinearLayoutManager(this)
search_people_list.adapter = adapter
}
So, What I did was made the adapter class open, then created an instance of the Adapter in the Activity and called the OnDataChanged() method in the Activity which watches for the itemCount in the Adapter. This way I am able to retrieve the correct the adapterCount value.
I'm currently using RecyclerView with MVVM databinding(well the databinding might not matter).
Anyway, what I'm trying to do is to show a certain layout view when the data in the recyclerView
is empty. It works wen I use the AdapterDataObserver onChanged(the following code). But the problem is that when I
open my app, my Empty time view shows up only for a split second(like a glitch / flicks) then show the lists normally. I'm unable to fix the
problem to not show this empty time view when a open the screen. Some sample example or code will be awesome!
I would love to hear from you !
Part of the code below
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = activity?.run {
ViewModelProviders.of(this, viewModelFactory).get(ShopViewModel::class.java)
} ?: throw Exception("Invalid Activity")
val adapter = ShopRecyclerAdapter(viewModel, mIndex))
val emptyObserver = object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
val isEmpty = adapter.itemCount == 0
val isfavImage = if (mIndex == 2) R.drawable.img_fav else R.drawable.img_none
val isNoData = if (isEmpty || adapter == null) View.VISIBLE else View.GONE
val visibility = if (isEmpty || adapter == null) View.GONE else View.VISIBLE
binding.layoutSwipeRefresh.visibility = visibility
binding.noneLayout.visibility = isNoData
binding.noneLayout.item_coupon_none_im.setImageResource(isfavImage)
}
}
adapter.registerAdapterDataObserver(emptyObserver)
emptyObserver.onChanged()
val recyclerView: RecyclerView = binding.shopRecycleView
val layoutManager = GridLayoutManager(activity, 1, RecyclerView.VERTICAL, false)
recyclerView.layoutManager = layoutManager
How do you get data for the list? Say you have a loading state that has just a loading spinner. After you get data for the list, register observer to display either empty view or populate the list
Let's define one variable in the begin, it will be more easier to read:
sqlite3 column KEY_TITLE = A
I have activity in conduction with cursor loader. When there are no rows in column A I want activity to display an emptyView. To do that, I created a method which checks all rows in column A if there is any data in it.
fun hideLayout(){
val isEmpty = emptyViewObserver(applicationContext).isEmpty()
if (isEmpty) {
recyclerView.visibility = View.GONE
emptyView.visibility = View.VISIBLE
} else {
recyclerView.visibility = View.VISIBLE
emptyView.visibility = View.GONE
}
}
class emptyViewObserver with isEmpty method:
class emptyViewObserver(val context: Context){
fun isEmpty():Boolean{
val db = WalletDbHelper(context).readableDatabase
val projection = arrayOf(KEY_TITLE)
val cursor = db.query(
WalletEntry.TABLE_NAME,
projection,
null,
null,
null,
null,
null
)
val names = mutableListOf<String>()
with(cursor){
while (moveToNext()){
val name = getString(cursor.getColumnIndexOrThrow(WalletEntry.KEY_TITLE))
names.add(name)
}
}
db.close()
return names.isEmpty()
}
}
Whenever I perform any CRUD operation in MainActivity class I call hideLayout method to check out if emptyView should be shown or not. It works fine. But I'm facing problem after I perform delete operation from my adapter to call hideLayout() method. For obvious reasons it throws me error when calling it directly from adapter so I though about implementing interface and overriding this in MainActivity. I read some tutorials but I still didn't figure out how to implement this properly.
This is my delete method from adapter:
holder.delete.setOnClickListener { v ->
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
inflater.inflate(R.menu.delete, popup.menu)
popup.show()
popup.setOnMenuItemClickListener { v ->
when(v.itemId) {
R.id.delete -> {
delete(uri, walletName)
true
}
else -> {
false
}
}
}
}
and delete():
fun delete(uri: String, walletName: String){
val newUri: Uri = Uri.parse(uri)
context.contentResolver.delete(newUri, WalletEntry.KEY_TITLE, arrayOf(walletName))
}
Do you have any ideas how can I write this code?
You need a callback, When you delete some item, check if is the last one and if it is true call that.
Add this interface on your adapter:
interface OnListIsEmpty{
fun onListIsEmpty()
}
And create a object of this like global variable in your adapter too:
private var mOnListIsEmpty: OnListIsEmpty? = null
Add a setter method:
fun setOnListIsEmpty(onListIsEmpty: OnListIsEmpty){
this.mOnListIsEmpty = onListIsEmpty
}
And in your delete method do the validation if is empty if it is true then:
mOnListIsEmpty?.onListIsEmpty()
And not forget set the callback when you initilize the adapter with:
adapter.setOnListIsEmpty(object : OnListIsEmpty{
override fun onListIsEmpty() {
//your action
}
})