I want to databind both the activity and the recyclerview.
But I get this error for the recycler view.
E/RecyclerView: No adapter attached; skipping layout
Removing the code for activity data binding, the recyclerview works.
Activity.kt
class MainActivity : AppCompatActivity(), IActivity {
private lateinit var mIMainPresenter: IPresenter
private lateinit var mMainAdapter: MainAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mIMainPresenter = MainPresenter(this)
mIMainPresenter.getList()
}
/**
* setup UI widgets
*/
private fun setupList() {
val mLayoutManager = LinearLayoutManager(this)
mMainAdapter = MainAdapter(mIMainPresenter)
recyclerList.layoutManager = mLayoutManager
recyclerList.adapter = mMainAdapter
recyclerList.addItemDecoration(DividerItemDecoration(this, VERTICAL))
refresh_layout.setOnRefreshListener {
fetch(null)
refresh_layout.isRefreshing = false
}
}
/**
* fetches list from
*/
override fun fetch(view: View?) {
mIMainPresenter.getList()
}
/**
* sets the list items once data is fetched from network/database
*/
override fun setEvents(result: List<Events>) {
setupList()
mMainAdapter.setList(result)
mMainAdapter.notifyDataSetChanged()
}
override fun setPrompts(result: List<Prompts>) {
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.prompt = result[0]
}
}
Adapter.kt
class MainAdapter(private val mIClick: IClick) : RecyclerView.Adapter<MainAdapter.AutoViewHolder>() {
private var events: List<Events> = listOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AutoViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ListMainBinding>(inflater, R.layout.list_main, parent, false)
return AutoViewHolder(binding)
}
override fun onBindViewHolder(holder: AutoViewHolder, position: Int) {
val event = events.get(position)
val binding = holder.listMainBinding;
binding?.events = event
binding?.iClick = mIClick
binding?.executePendingBindings()
}
override fun getItemCount(): Int {
return events.size
}
fun setList(result: List<Events>) {
events = result
}
inner class AutoViewHolder : RecyclerView.ViewHolder {
var listMainBinding: ListMainBinding? = null
constructor(binding: ListMainBinding?) : super(binding?.root) {
listMainBinding = binding
}
}
}
If setPrompts is called after setEvents, I'm pretty sure it will have created a new RecyclerView and setupList won't be called after that new recycler view is created. That means the new recycler won't have an adapter.
What you will want to do instead is do the setContentView stuff inside of onCreate, keep a reference to the Binding, and then set prompt on that binding when you get the data from the db or network. You will probably also want to do similar with setEvents, and move the initial list setup into onCreate, and just change out the data when that comes in.
Related
First of all, I am Spanish so my english is not good.
I have an app with Kotlin and room, and it has a Recyclerview.
I have 3 tables: coaster, user and favorite.
The user can add coasters to favorite, and this is done succesfully.
The problem that I have is that when the user clicks on the button to add or delete from favorites, the recyclerview resets, it displays again. So it scrolls to the top of the Screen, and also some odd spaces appears after the element.
I also have a function to search, and it happens the same: spaces appears after each element when I am searching.
I have tried everything: notifyItemChanged,
notifyDataSetChanged... it doesnt work! I also tried removing the observer once from the recyclerview...
My main activity:
class CoasterFragment : Fragment() {
lateinit var coasterListener: CoasterListener
lateinit var usuarioCoaster: List\<UsuarioCoaster\>
private lateinit var searchView: SearchView
private lateinit var cAdapter: CoasterRecyclerViewAdapter
private var \_binding: FragmentCoasterBinding? = null
private val binding get() = \_binding!!
private val viewModel: CoastersViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCoasterBinding.inflate(inflater, container, false)
val root: View = binding.root
/* val livedata = viewModel.coasters()
livedata.observe(viewLifecycleOwner,object: Observer <List<CoasterFavorito>> {
override fun onChanged(it: List<CoasterFavorito>) {
createRecyclerView(it)
livedata.removeObserver(this)
}
})*/
viewModel.coasters().observe(viewLifecycleOwner){createRecyclerView(it)}
coasterListener = CoasterListenerImpl(requireContext(), viewModel)
searchView = binding.search
searchView.clearFocus()
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
override fun onQueryTextChange(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
})
return root
}
fun createRecyclerView(coasters: List<CoasterFavorito>) {
cAdapter =
CoasterRecyclerViewAdapter(
coasters as MutableList<CoasterFavorito>,
coasterListener,
requireContext()
)
val recyclerView = binding.recyclerCoaster
recyclerView.apply {
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
adapter = cAdapter
addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
cAdapter.notifyDataSetChanged()
}
}
fun searchDatabase(query: String) {
val searchQuery = "%$query%"
viewModel.searchDatabase(searchQuery).observe(viewLifecycleOwner) { createRecyclerView(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
my adapter:
class CoasterRecyclerViewAdapter(val coasters: List<CoasterFavorito>, val listener: CoasterListener,
val context: Context, ) : RecyclerView.Adapter<CoasterRecyclerViewAdapter.ViewHolder>(){
class ViewHolder private constructor(val binding: CoasterItemBinding, private val listener: CoasterListener,
private val context: Context): RecyclerView.ViewHolder(binding.root){
fun relleno(data: CoasterFavorito){
binding.nombre.text = data.coaster.nombre
binding.parque.text = data.coaster.parque
binding.ciudad.text = data.coaster.ciudad
binding.provincia.text = data.coaster.provincia
binding.comunidad.text = data.coaster.comunidadAutonoma
Glide
.with(context)
.load(data.coaster.imagen)
.centerCrop()
.into(binding.imagen)
binding.check.isChecked = data.favorito
binding.check.setOnClickListener{
if (data.favorito) {
listener.delFavorito(data.coaster.id)
binding.check.isChecked = false
} else {
listener.addFavorito(data.coaster.id)
binding.check.isChecked = true
}
}
}
companion object{
fun crearViewHolder(parent: ViewGroup, listener: CoasterListener, adapter: CoasterRecyclerViewAdapter, context: Context):ViewHolder{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CoasterItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding, listener, context )
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder.crearViewHolder(parent, listener, this, context)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.relleno(coasters[position])
override fun getItemCount() = coasters.size
}
interface CoasterListener {
fun addFavorito(id: Long)
fun delFavorito(id: Long)
}
I have tried everything: notifyItemChanged,
notifyDataSetChanged... it doesnt work! I also tried removing the observer once from the recyclerview...
Your createRecyclerView function should be invoked only once in a whole lifecycle of the Fragment. You should not create any new RecyclerView.Adapter, or set a LayoutManager to the RecyclerView every time your data set changes.
Therefore the Observer used in viewModel.coasters.observe() should only submit a new List to the existing Adapter and call .notifyDataSetChanged(), or other notifying functions.
READ FIRST:
Apologies, it seems I have played myself. I was using RecyclerView in my xml earlier, but switched it over for CardStackView (it still uses the exact same RecyclerView adapter). If I switch back to RecyclerView, the original code below works - the scroll position is saved and restored automatically on configuration change.
I'm using a MVVM viewmodel class which successfully retains list data for a RecyclerView after a configuration change. However, the previous RecyclerView position is not restored. Is this expected behaviour? What would be a good way to solve this?
I saw a blog post on medium briefly mentioning you can preserve scroll position by setting the adapter data before setting said adapter on the RecyclerView.
From what I understand, after a configuration change the livedata that was being observed earlier gets a callback. That callback is where I set my adapter data. But it seems this callback happens after the onCreate() function finishes by which point my RecyclerView adapter is already set.
class MainActivity : AppCompatActivity() {
private val adapter = MovieAdapter()
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Create or retrieve viewmodel and observe data needed for recyclerview
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
viewModel.movies.observe(this, {
adapter.items = it
})
binding.recyclerview.adapter = adapter
// If viewmodel has no data for recyclerview, retrieve it
if (viewModel.movies.value == null) viewModel.retrieveMovies()
}
}
class MovieAdapter :
RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {
var items: List<Movie> by Delegates.observable(emptyList()) { _, _, _ ->
notifyDataSetChanged()
}
class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val binding = ItemMovieCardBinding.bind(itemView)
fun bind(item: Movie) {
with(binding) {
imagePoster.load(item.posterUrl)
textRating.text = item.rating.toString()
textDate.text = item.date
textOverview.text = item.overview
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_movie_card, parent, false)
return MovieViewHolder(view)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
}
class MainViewModel : ViewModel() {
private val _movies = MutableLiveData<List<Movie>>()
val movies: LiveData<List<Movie>> get() = _movies
fun retrieveMovies() {
viewModelScope.launch {
val client = ApiClient.create()
val result: Movies = withContext(Dispatchers.IO) { client.getPopularMovies() }
_movies.value = result.movies
}
}
}
Set adapter only after its items are available.
viewModel.movies.observe(this, {
adapter.items = it
binding.recyclerview.adapter = adapter
})
This my adapter class which is trying to inflate the recycler view with the video_View and the arraylist is ending up with the value 0. I have tried a lot of method to make it working but every time it is everytime not inflating the recyclerView.
Adapter class:
class Video_adapter(var videolist: ArrayList<String>):RecyclerView.Adapter<Video_adapter.ViewHolder>(){
class ViewHolder(view: CardView):RecyclerView.ViewHolder(view){
val video:VideoView = view.findViewById(R.id.video)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.video_view,parent,false) as CardView
Log.i("OnCreateView","OnCreateViewHolder is working")
return ViewHolder(view)
}
override fun getItemCount(): Int {
return videolist.size
Log.i("Video_list1",videolist.size.toString())
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
var video = videolist[position]
holder.video.setVideoURI(video.toUri())
holder.video.setOnTouchListener( View.OnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN){
holder.video.start()
}
true
})
}
}
This is my main kotlin file
class MainActivity : AppCompatActivity() {
lateinit var toolbar: androidx.appcompat.widget.Toolbar
lateinit var recyclerView: RecyclerView
lateinit var layoutManager: RecyclerView.LayoutManager
lateinit var recycleradpter:Video_adapter
lateinit var videolist: ArrayList<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
toolbar = findViewById(R.id.toolbar)
recyclerView = findViewById(R.id.recycler_view)
layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
videolist = ArrayList()
getJson()
Log.i("Video_list",videolist.size.toString())
recycleradpter = Video_adapter(videolist)
Log.i("Video_list",videolist.size.toString())
recyclerView.adapter = recycleradpter
}
fun getJson(){
val url = "https://raw.githubusercontent.com/bikashthapa01/myvideos-android-app/master/data.json"
val queue = Volley.newRequestQueue(this)
//volley request
val request = JsonObjectRequest(Request.Method.GET,url,null,Response.Listener<JSONObject>{ response ->
try {
val categories = response.getJSONArray("categories")
for (i in 0 until categories.length()){
val category = categories.getJSONObject(i)
val videos = category.getJSONArray("videos")
for (i in 0 until videos.length()){
val video = videos.getJSONObject(i)
val source = video.getString("sources")
videolist.add(source)
Log.i("Url",source)
Toast.makeText(this,"Json request working",Toast.LENGTH_LONG).show()
}
}
}
catch (e:JSONException){
Log.i("error ",e.toString())
}
},Response.ErrorListener {error ->
Log.i("JSon_request_error",error.toString())
})
Log.i("Video_list",videolist.size.toString())
queue.add(request)
}
}
I am not able to find what is wrong with the code why it is not inflating the recycler view.
After the for loop in getJson method call
recycleradpter.notifyDataSetChanged() to refresh the RecyclerView.
When trying to get JSON data from an API and show it on the RecyclerView I'm getting following error:
I already use RecyclerView sometimes and I never had this problem.
MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(activity_main)
overwriteOnPostInteractionListener()
setupObservers()
}
private fun setupObservers(){
mServiceRequest.searchPostsFromAPI().observe(this, Observer { posts ->
if (posts != null){
loadRecyclerView()
mPostList = posts.toMutableList()
}
})
}
private fun loadRecyclerView() {
recyclerView.adapter = PostListAdapter(mPostList, mOnPostListInteractionListener)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
}
Adapter:
class PostListAdapter(private val postList: List<PostModel>,
private val onPostListInteractionListener: OnPostListInteractionListener):
RecyclerView.Adapter<PostViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
val inflate = LayoutInflater.from(parent.context)
val view = inflate.inflate(R.layout.posts , parent, false)
return PostViewHolder(view, parent.context, onPostListInteractionListener)
}
override fun getItemCount(): Int {
return postList.count()
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
holder.bindTask(postList[position])
}}
ViewHolder:
class PostViewHolder(itemView: View, private val context: Context,
private val onPostListInteractionListener: OnPostListInteractionListener)
: RecyclerView.ViewHolder(itemView) {
private val postTitle = itemView.findViewById<TextView>(R.id.titleTextViewMain)
private val postBody = itemView.findViewById<EditText>(R.id.bodyEditText)
fun bindTask(post: PostModel){
postTitle.text = post.title
postBody.setText(post.body)
postTitle.setOnClickListener {
onPostListInteractionListener.onListClick(post.id)
}
}}
I searched a lot how to solve this error but I can't.
You build your RecyclerView adapter before feeding the data to the list, so you need to call mPostList = posts.toMutableList() before instantiating your adapter.
So, change setupObservers() with:
private fun setupObservers(){
mServiceRequest.searchPostsFromAPI().observe(this, Observer { posts ->
if (posts != null){
mPostList = posts.toMutableList()
loadRecyclerView()
}
})
}
Also set your adapter after, you completely build the RecyckerView. So change the order of loadRecyclerView() as below.
private fun loadRecyclerView() {
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
recyclerView.adapter = PostListAdapter(mPostList, mOnPostListInteractionListener)
}
Setting RecyclerView adapter before layout can cause issues.
Side note:
You instantiate your adapter every time your list is changed, and that's not the right way, and it's better to create your stuff (including the adapter) only once in a lifecycle, and then make setters each time you want to change them instead of instantiating them over and over again.
So, you can instantiate your adapter only once in onCreate() method and create a method in your adapter that takes the posts setPosts(private val postList: List<PostModel>) and set them internally .. whenever you need to change adapter list, just call setPosts
I have created an Android app, which connects to GitHub API and shows a list of repositories of certain user. List is shown by recyclerView. I'm trying to save and restore a list, when I'm switching fragments. I have tried to save a list to a variable and add this to recyclerView. But it doesn't work.
My Fragment:
class MainFragment : Fragment() {
lateinit var recyclerView: RecyclerView
var responseSave:List<GitHubPOJO> = ArrayList()
var posts: MutableList<GitHubPOJO> = ArrayList()
lateinit var btn:Button
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_main, container, false)
btn = view.findViewById(R.id.button)
btn.setOnClickListener {
posts = ArrayList()
val name:String = view!!.findViewById<EditText>(R.id.editText).text.toString()
recyclerView = view!!.findViewById(R.id.posts_recycle_view)
val layoutManager = LinearLayoutManager(this.activity!!)
recyclerView.layoutManager = layoutManager
val adapter = PostsAdapter(posts)
recyclerView.adapter = adapter
//HIDE KEYBOARD
val inputMethodManager = this.activity!!.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
if(activity!!.currentFocus !=null) {
inputMethodManager.hideSoftInputFromWindow(this.activity!!.currentFocus!!.windowToken, 0)
}
val service = Retrofit.Builder()
.baseUrl("https://api.github.com/") // CHANGE API
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GitHubService::class.java)
service.retrieveRepositories(name)
.enqueue(object : Callback<List<GitHubPOJO>> {
override fun onResponse(call: Call<List<GitHubPOJO>>, response: Response<List<GitHubPOJO>>) {
responseSave = response.body()!!
posts.addAll(responseSave)
response.body()?.forEach { println ("TAG_: $it")}
recyclerView.adapter?.notifyDataSetChanged()
}
override fun onFailure(call: Call<List<GitHubPOJO>>, t: Throwable) {
//Toast.makeText(this#MainFragment, "Error occurred while networking", Toast.LENGTH_SHORT).show()
}
})
recyclerView.addOnItemTouchListener(
ClickListener(this.activity!!, recyclerView, object : ClickListener.OnItemClickListener {
override fun onLongItemClick(view: View?, position: Int) {
}
override fun onItemClick(view: View, position: Int) {
val url = posts!![position].htmlUrl
println("URL = $url")
view.findNavController().navigate(MainFragmentDirections.actionMainFragmentToWebFragment(url))
}
})
)
}
return view
}
And there is my code onResume:
override fun onResume() {
super.onResume()
val adapter = PostsAdapter()
adapter.updateAdapterList(responseSave.toMutableList())
println("RESUME")
println(responseSave)
}
When I print responseSave I see that my list is there. But it doesn't appears in RecyclerView.
Fragments are swithced by standart navigation library.
Activity code:
class MainActivity : AppCompatActivity(){
private lateinit var navController: NavController
private lateinit var mNavView:NavigationView
private lateinit var mDrawerLayout:DrawerLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mDrawerLayout = findViewById(R.id.drawer_layout)
mNavView = findViewById(R.id.nav_view)
navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this, navController,mDrawerLayout)
NavigationUI.setupWithNavController(mNavView, navController)
}
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.myNavHostFragment)
return NavigationUI.navigateUp(mDrawerLayout,navController)
}}
My RecyclerView Adapter code:
class PostsAdapter(private var posts: MutableList<GitHubPOJO>? = ArrayList()) : RecyclerView.Adapter<PostsAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.post_item, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = posts!![position]
holder.post.text = post.name
holder.site.text = post.fullName // Change what you wanna see
}
fun updateAdapterList(newList: MutableList<GitHubPOJO>) {
posts!!.clear()
posts!!.addAll(newList)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return posts?.size ?: 0
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var post: TextView = itemView.findViewById<View>(R.id.postitem_post) as TextView
var site: TextView = itemView.findViewById<View>(R.id.postitem_site) as TextView
}}
Add a method in your adapter thats something like;
fun updateAdapterList(newList: MutableList<GitHubPOJO>) {
oldList.clear()
oldList.addAll(newList)
notifyDataSetChanged()
}
Then call this in your onResume() and pass in your updated list
The problem is on your onResume method. You are not adding an Adapter and a LayoutManager to the RecylerView so it can't display the data. Try moving the RecylerView initalization code from the onClickListener to your onResume method:
recyclerView = view!!.findViewById(R.id.posts_recycle_view)
val layoutManager = LinearLayoutManager(this.activity!!)
recyclerView.layoutManager = layoutManager
val adapter = PostsAdapter(posts)
recyclerView.adapter = adapter
Although having this initialization code in your onCreateView method would be better since it's the first lifecycle method called when a fragment returns to the front view from the backstack.