Unconditional layout inflation from view adapter. kotlin - android-layout

I am getting following warning in Android Studio:
"Unconditional layout inflation from view adapter: Should use View Holder pattern (use recycled view passed into this method as the second parameter) for smoother scrolling." on inflater.inflate(R.layout.animal_ticket, null) line.
How do I fix the warning? I am not able to find solution for that problem.
Thanks!
override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
val animal = listOfAnimals[p0]
val inflater = LayoutInflater.from(context)
var holder = ViewHolder()
val myView = inflater.inflate(R.layout.animal_ticket, null)
myView.tvName.text = animal.name!!
myView.tvDes.text = animal.description!!
myView.jvAnimalImage.setImageResource(animal.image!!)
return myView
}

If there was previously another View of the same view type (getItemViewType(position: Int) returned the same value) that scrolled off screen, the list view may pass in that instance as the second parameter to getView(). It will be faster to reuse that view than to inflate a new one.
You should also use the ViewHolder to cache things about the view, such as the relatively expensive findViewById(). You can attach it to and retrieve it from the view by tag.
override fun getView(position: Int, convertView: View?, parent: ViewGroup?) {
val animal = listOfAnimals[position]
val myView = convertView ?:
LayoutInflater.from(context).inflate(R.layout.animal_ticket, parent, false)
val holder = myView.tag as? ViewHolder ?: ViewHolder(myView)
myView.tag = holder
holder.tvName.text = animal.name!! // etc
}
class ViewHolder(view: View) {
val tvName: TextView = view.name // etc
}

For any RecyclerView, you need your own ViewHolder class that has all of the views listed inside animal_ticket.
Basically, it works like this:
1) Create ViewHolder that 'holds' all of the views of the item you want to display;
2) Bind the ViewHolder to the RecyclerView and assign values to the views inside;
Here is a example adapter I wrote :
class MyActivity : Activity() {
//users is the list we're going to use to get information for the views
val users = ArrayList<User>()
//...getting user information
//.. your activity stuff here
//Creating our adapter
/*
Note that to extend the RecyclerView.Adapter, you need to specify a
ViewHolder. The code becomes much easier to manage if you just put the
ViewHolder inside your adapter.
*/
inner class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
//This is the view holder
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
//Here you declare your views and get them from the itemView
//The itemView is one that is passed each time to the RecyclerView
//(the items inside your XML layout file)
internal val userImageView = itemView.findViewById(R.id.userImage)
internal val userFullName = itemView.findViewById(R.id.userName)
}
//This is where you return your own ViewHolder with your layout
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
//user_list_item.xml is below
val itemView = layoutInflater.inflate(R.layout.user_list_item, parent, false))
return MyViewHolder(itemView)
}
//In here is where you want to set your values for the views
override fun onBindViewHolder(holder: SentRequestViewHolder, position: Int) {
val currentUser = users[position]
holder.userImage.drawable = currentUser.drawable
holder.userFullName.text = currentUser.name
}
//You must override this method as well for the adapter to work properly
override fun getItemCount() = users.size
}
You have to override these methods when using a RecyclerView.Adapter
Here is the user_list_item.xml
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp">
<ImageView
android:id="#+id/userImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="#+id/userName"
android:layout_below="#id/userImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>

Related

getDialog() in DialogFragment returns null

Here's the code of my DialogFragment.kt
class CustomDialogCalendarFragment: DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding: FragmentCustomDialogCalendarBinding = DataBindingUtil.inflate(
inflater, R.layout.fragment_custom_dialog_calendar, container, false)
val application = requireNotNull(this.activity).application
//Create an instance of the ViewModelFactory
val dataSource = ReservationDatabase.getInstance(application).ReservationDatabaseDao
val viewModelFactory = CustomDialogCalendarViewModelFactory(dataSource)
//Get a reference to the ViewModel associated with this fragment
val customDialogViewModel =
ViewModelProvider(
this, viewModelFactory).get(CustomDialogCalendarViewModel::class.java)
// To use the View Model with data binding, you have to explicitly
// give the binding object a reference to it.
binding.customDialogCalendarViewModel = customDialogViewModel
customDialogViewModel.navigateToSharedResourceCalendar.observe(viewLifecycleOwner) {
if (it == true) {
customDialogViewModel.doneNavigating()
}
}
binding.submitCreateEventButton.setOnClickListener { view: View ->
customDialogViewModel.onSetReservation(binding.nameEvent.text.toString())
this.findNavController().navigate(CustomDialogCalendarFragmentDirections.actionCustomDialogCalendarFragmentToSharedResourceCalendarFragment())
}
//HERE'S THE NULL POINTER EXCEPTION
getDialog()!!.getWindow()?.setBackgroundDrawableResource(R.drawable.round_corner)
return binding.root
}
override fun onStart() {
super.onStart()
val width = (resources.displayMetrics.widthPixels * 0.85).toInt()
//HERE'S THE NULL POINTER EXCEPTION
dialog!!.window?.setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
I don't understand why the getDialog() method returns null in every point of code on the onCreateView.
This class is called by another Fragment.
I have the same kind of DialogFragment in the project that does the same operations with no error.
So I don't know why this DialogFragment doesn't do the same.
Thank you!

Kotlin: Live data does not change Fragment UI when data changes

I am struggling to use Live data on an MVVM pattern. The app is supposed to:
Fetch data from an API (which it does correctly)
Store that data in the Live data object from the ViewModel
Then the fragment calls the Observer method to fill the recyclerView.
The problem comes in point 3, it does nothing, and I cannot find the solution.
Here is the relevant code. (If I'm missing something, I will try to answer as quickly as possible)
Main Activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: SharedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Custom button to fetch data from api and log the Live Data value.
binding.refreshFab.setOnClickListener {
viewModel.fetchPlayerData()
Log.d("gabs", "${viewModel.livePlayerlist.value}")
}
}
}
ViewModel:
class SharedViewModel(app: Application): AndroidViewModel(app) {
// val playerDao = LaRojaDB.getDatabase(app).playerDao()
lateinit var playerList: Players
val livePlayerlist: MutableLiveData<MutableList<Players.PlayersItem>> by lazy {
MutableLiveData<MutableList<Players.PlayersItem>>()
}
fun fetchPlayerData() {
CoroutineScope(Dispatchers.IO).launch {
val response = MyService.getLaRojaService().getAllPlayers()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
val body = response.body()
if(!body.isNullOrEmpty()){
playerList = body
val playerArrayList = mutableListOf<Players.PlayersItem>()
playerList.forEach {
playerArrayList.add(it)
}
livePlayerlist.value = playerList
}
}
}
}
}
}
The fragment that displays the recycler view: (Fragment is already showing, I set up a textView as a title to make sure since I'm new using fragments as well.)
class PlayerListFragment : Fragment() {
private var _binding: FragmentPlayerListBinding? = null
private val binding get() = _binding!!
private val model: SharedViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentPlayerListBinding.inflate(inflater, container, false)
binding.rvPlayerList.layoutManager = LinearLayoutManager(activity)
----> // This is the observer that does not update the UI** <----
model.livePlayerlist.observe( viewLifecycleOwner, {
binding.rvPlayerList.adapter = PlayerAdapter(it)
})
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_player_list, container, false)
}
}
Thank you all in advance, hope I can finally learn what is causing the issue!
I think you don't need to switch Coroutine contexts. A few changes I'd expect if I were reviewing this code:
This should all be in the same IO context. You then postValue to your liveData.
fun fetchPlayerData() {
viewModelScope.launch(Dispatchers.IO) {
val xx = api.fetch()
...
_playerState.postValue(xx) //see below
}
}
Additionally, it's preferred not to expose mutable state, so your ViewModel should not expose the MutableLiveData (which shouldn't really be lazy). But it's also better to encapsulate the state in a sealed class:
//delete this
val livePlayerlist: MutableLiveData<MutableList<Players.PlayersItem>> by lazy {
MutableLiveData<MutableList<Players.PlayersItem>>()
}
Should be: (names are just pseudo code, I have no idea what this code is about)
sealed class PlayerDataState {
data class ListAvailable(data: List<Players.PlayersItem>>): PlayerDataState
object Loading(): PlayerDataState
}
And your new LiveData:
private val _playerState = MutableLiveData<PlayerDataState>()
val playerState: LiveData<PlayerDataState>() get() = _playerState
Finally when observing from the UI, you just...
model.playerState.observe(viewLifecycleOwner, {
when (it) {
is Loading -> ...
is ListAvailable -> binding.rvPlayerList.adapter = PlayerAdapter(it.data)
}
}

Kotlin AddOnPageChangeListener not working

I wanted to connect a viewPager with an adapter (like this: viewPager.addOnPageChangeListener()), but the letters of the pageChangeListener just turn red like it wasn't a valid code...What am I doing wrong?? Heres a screenshot:
[1]: https://i.stack.imgur.com/IOYJY.png
Context: I'm currently working on a game with a few fragments where you can choose your game cards. I need the pageChangeListener to change the pictures of them cards. Maybe there could be another way to do this but i don't know how...
package com.suffv1
import android.os.Bundle
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.viewpager.widget.ViewPager
import com.example.suff_02.Adapter2
import com.example.suff_02.R
import com.example.suff_02.kartenmodell
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity(){
private lateinit var actionbar: ActionBar
private lateinit var liste: ArrayList<kartenmodell>
private lateinit var myAdapter: Adapter2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
super.setContentView(R.layout.activity_main)
actionbar = this.supportActionBar!!
loadCards()
viewpager2.addOnPageChangeListener(object: ViewPager.OnPageChangeListener{
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
val title = liste[position].KartenImage
actionbar.title = title
}
})
}
private fun loadCards() {
liste = ArrayList()
liste.add(kartenmodell(R.drawable.bier_radler_klein_level_1))
liste.add(kartenmodell(R.drawable.bier_hopfentrunk_klein_level_1))
liste.add(kartenmodell(R.drawable.bier_butt_light_klein_level_1))
liste.add(kartenmodell(R.drawable.bier_becks_klein_level_1))
liste.add(kartenmodell(R.drawable.bier_tyskie_klein_level_1))
myAdapter = Adapter2(this, liste)
viewpager2.adapter = myAdapter
viewpager2.setPadding(100, 0, 100, 0)
}
}
Looks like you are using ViewPager2 not the original Viewpager and Viewpager2 does not have Page Change Listeners it instead has Page Change Callbacks
So you are using the wrong method to get notified when a pages is changed.
Instead do something like
var myPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
Toast.makeText(this#MainActivity, "Selected position: ${position}",
Toast.LENGTH_SHORT).show()
}
}
viewpager.registerOnPageChangeCallback(myPageChangeCallback)
Though architecturally a lot of times it is bad form to use a OnPageChangeCallback as you are likely breaking the encapsulation idea of the Fragment and it can be better to use the lifecycle state change to Resumed of the Fragment to do things when a page(Fragment) is selected. e.g. put the code in the Fragments onResume method.
Though in this case of setting the actionbar title it is probably ok architecturally to use a OnPageChangeCallback
To create a AddOnPageChangeListener(), you do it as:
//See how, you have to use object to create an Object of the interface OnPageChangeListener.
//This is how you do for other listeners/interfaces/class as well when you've to implement its member functions instead of new in java.
viewPager.addOnPageChangeListener(object: ViewPager.OnPageChangeListener{
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
TODO("Not yet implemented")
}
override fun onPageSelected(position: Int) {
TODO("Not yet implemented")
}
override fun onPageScrollStateChanged(state: Int) {
TODO("Not yet implemented")
}
})
But, further in Kotlin, you can use Kotlin functions as
//This is for onPageSelected only.
viewPager.onPageChangeListener{ position: Int ->
//This position is the selected Page's position
}

RecyclerView(Kotlin): Adding an swipe to delete functionality on a ToDo app with data in SQL database

I am new to kotlin and android studio and currently I am trying to build an todo list applications with my own ideas. Its mostly done but I have to add edit and delete functionality to the tasks that user adds. The tasks that user adds are stored on the device using SQLiteDatabase. This is the base swipe to delete class that I wrote:
abstract class SwipeToDelete(context: Context, dragDir: Int, swipeDir: Int): ItemTouchHelper.SimpleCallback(dragDir, swipeDir) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
TODO("Not yet implemented")
}
}
This is the delete functionality that I have added which works great with a button:
fun deleteToDo(todoId: Long){
val db = writableDatabase
db.delete(TABLE_TODO_ITEM,"$COL_TODO_ID=?", arrayOf(todoId.toString()))
db.delete(TABLE_TODO,"$COL_ID=?", arrayOf(todoId.toString()))
}
This is the recyclerview adapter that I am using:
class ItemAdapter(val context: Context,val dbHandler: DBHandler, val list: MutableList<ToDoItem>) :
RecyclerView.Adapter<ItemAdapter.ViewHolder>(){
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(context).inflate(R.layout.rv_child_item,p0,false))
}
override fun onBindViewHolder(holder: ViewHolder, p1: Int) {
holder.itemName.text = list[p1].itemName
holder.itemName.isChecked = list[p1].isCompleted
holder.itemName.setOnClickListener{
list[p1].isCompleted = !list[p1].isCompleted
dbHandler.updateToDoItem(list[p1])
}
}
override fun getItemCount(): Int {
return list.size
}
class ViewHolder(v : View) : RecyclerView.ViewHolder(v){
val itemName : CheckBox = v.findViewById(R.id.cb_item)
}
}
but for some reason I cannot make it work when I try to call the delete function in this Swipetodelete object:
val item = object : SwipeToDelete(this,0,ItemTouchHelper.LEFT){
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}
I also want to add the edit functionality but if i can get this delete functionality to work i can add it.
Since the SwipeToDelete is an abstract class, you can override the onSwiped function on your Fragment/Activity Class.
You can modify your SwipeToDelete class to:
abstract class SwipeToDelete(context: Context, dragDir: Int, swipeDir: Int): ItemTouchHelper.SimpleCallback(dragDir, swipeDir) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
}
And then override the onSwiped function inside your fragment/activity and attach it to your recyclerview:
val item = object : SwipeToDelete(this,0,ItemTouchHelper.LEFT){
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
deleteToDo(todoId)
}
}
ItemTouchHelper(item).attachToRecyclerView(recycler)

Navigation with View Binding

I'm trying to replace all findViewById using View Binding. But, I can't change my NavController code line using View Binding.
val navController = findNavController(this, R.id.mainHostFragment)
to
var binding : ActivityMainBinding
val navController = findNavController(this, binding.mainHostFragment)
How can I do that?
You can't replace it with View Binding.
findNavController does more than finding the view in the layout.
Have a look at the source code here
/**
* Find a {#link NavController} given a local {#link Fragment}.
*
* <p>This method will locate the {#link NavController} associated with this Fragment,
* looking first for a {#link NavHostFragment} along the given Fragment's parent chain.
* If a {#link NavController} is not found, this method will look for one along this
* Fragment's {#link Fragment#getView() view hierarchy} as specified by
* {#link Navigation#findNavController(View)}.</p>
*
* #param fragment the locally scoped Fragment for navigation
* #return the locally scoped {#link NavController} for navigating from this {#link Fragment}
* #throws IllegalStateException if the given Fragment does not correspond with a
* {#link NavHost} or is not within a NavHost.
*/
#NonNull
public static NavController findNavController(#NonNull Fragment fragment) {
Fragment findFragment = fragment;
while (findFragment != null) {
if (findFragment instanceof NavHostFragment) {
return ((NavHostFragment) findFragment).getNavController();
}
Fragment primaryNavFragment = findFragment.getParentFragmentManager()
.getPrimaryNavigationFragment();
if (primaryNavFragment instanceof NavHostFragment) {
return ((NavHostFragment) primaryNavFragment).getNavController();
}
findFragment = findFragment.getParentFragment();
}
// Try looking for one associated with the view instead, if applicable
View view = fragment.getView();
if (view != null) {
return Navigation.findNavController(view);
}
throw new IllegalStateException("Fragment " + fragment
+ " does not have a NavController set");
}
It does more than just finding the controller. It traverses, creates fragment, creates views and throw an exception.
View binding just generates a binding class with all the views of your layout in it. It is not meant for finding the navigation controller of the app.
Here is my sample code, using view binding & navigation.
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.zeddigital.zigmaster.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/*
Use view binding in activities
Call the static inflate() method included in the generated binding class.
This creates an instance of the binding class for the activity to use.
Get a reference to the root view by either calling the getRoot() method or using Kotlin property syntax.
Pass the root view to setContentView() to make it the active view on the screen.*/
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
setSupportActionBar(binding.appBarMain.toolbar)
//val navController = findNavController(R.id.nav_host_fragment)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
appBarConfiguration = AppBarConfiguration(setOf(
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow), binding.drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration)
binding.navView.setupWithNavController(navController)
}
}
With help in this answer, i find a simple implementation
binding?.apply {
setContentView(root)
setSupportActionBar(toolbar)
navController = (supportFragmentManager
.findFragmentById(fragmentHost.id) as NavHostFragment)
.navController
setupActionBarWithNavController(navController, appBarConfiguration)
bottom.setupWithNavController(navController)
}
findNavController(R.id.nav_host_fragment) should come after attaching view to activity like this:
setContentView(binding.root)
val navController: NavController = findNavController(R.id.nav_host_fragment)
More Here

Resources