I have a Java project with three fragments that works perfectly. I am trying to translate the project into Kotlin. Everything works so far, except the first (and only complete so far) fragment. The fragments do load as they should but the first fragment's adapter's functions
onCreateViewHolder(parent: ViewGroup, viewType: Int)
and
onBindViewHolder(viewHolder: MyViewHolder, i: Int)
never execute so the empty fragment is displayed in the user's interface. What am I missing, please?
Here is my FragmentMainEntries:
class FragmentMainEntries() : Fragment() {
private lateinit var binding: FragmentMainEntriesBinding
private var foodList = ArrayList<Item>()
private var tableRowAdapter = MainFragAdapter(foodList)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?
): View? {
prepareList()
//Toast.makeText(activity, "foodList 9 = " foodList.size, Toast.LENGTH_LONG).show()
binding = FragmentMainEntriesBinding.inflate(layoutInflater)
val view = binding.root
val recyclerView = view.findViewById<RecyclerView>(R.id.recycleView)
recyclerView.layoutManager = LinearLayoutManager(view.context)
tableRowAdapter = MainFragAdapter(foodList)
binding.recycleView.adapter = tableRowAdapter
return inflater.inflate(R.layout.fragment_main_entries, container, false)
}
private fun prepareList()
{
//Toast.makeText(activity, "prepareList()", Toast.LENGTH_SHORT).show()
try {
foodList.clear()
for (i in Tables.finalTable.indices) {
val item = Item(Tables.finalTable[i]?.get(0) as Int,
Tables.finalTable[i]?.get(2) as String,
Tables.finalTable[i]?.get(3).toString(),
Tables.finalTable[i]?.get(4).toString().toFloat(),
Tables.finalTable[i]?.get(8).toString().toFloat())
foodList.add(item)
}
// Toast.makeText(activity, "foodList8 = " foodList.size, Toast.LENGTH_LONG).show()
}
catch(e: Exception)
{
Toast.makeText(activity, "MAIN ACTIVITY EXCEPTION002\n" e.message , Toast.LENGTH_LONG).show()
Thread.sleep(6000)
}
}
companion object {
@JvmStatic
fun newInstance() = FragmentMainEntries().apply {
}
}
}
Here is my adapter:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
class MainFragAdapter(private var foodArrayList: MutableList<Item>) :
RecyclerView.Adapter<MainFragAdapter.MyViewHolder>()
{
lateinit var view: View
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent, false)
Toast.makeText(view.context, "foodArrayList5 = " foodArrayList.size, Toast.LENGTH_SHORT).show()
return MyViewHolder(view)
}
override fun onBindViewHolder(viewHolder: MyViewHolder, i: Int) {
if(i<1) // For debugging
{
Toast.makeText(view.context, "foodArrayList7 = " foodArrayList.size, Toast.LENGTH_SHORT).show()
}
foodArrayList[i]
viewHolder.tvIndex.text = foodArrayList[i].index.toString()
viewHolder.tvDescription.text = foodArrayList[i].description
viewHolder.tvUnits.text = foodArrayList[i].units
viewHolder.tvPortion.text = foodArrayList[i].portion.toString()
viewHolder.tvCalories.text = foodArrayList[i].calories.toString()
}
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvIndex: TextView = itemView.findViewById(R.id.tvIndex)
val tvDescription: TextView = itemView.findViewById(R.id.tvDescription)
val tvUnits: TextView = itemView.findViewById(R.id.tvUnits)
val tvPortion: TextView = itemView.findViewById(R.id.tvPortion)
val tvCalories: TextView = itemView.findViewById(R.id.tvCalories)
}
override fun getItemCount(): Int
{
return foodArrayList.size
}
companion object{ }
}
Here is my MainActivity class:
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.WindowInsets
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator
import com.neels.newarcadiafragments.databinding.ActivityMainBinding
import com.neels.newarcadiafragments.model.Tables
import java.io.File
import java.io.FileInputStream
import java.io.ObjectInputStream
class MainActivity : AppCompatActivity() {
private var binding: ActivityMainBinding? = null
var logTag = "Observer"
var message: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding!!.getRoot())
val decorView = window.decorView
decorView.windowInsetsController!!.hide(WindowInsets.Type.statusBars())
supportActionBar!!.setDisplayShowCustomEnabled(true)
supportActionBar!!.setCustomView(R.layout.custom_action_bar)
supportActionBar!!.elevation = 1f
val view = supportActionBar!!.customView
message = view.findViewById(R.id.name)
message!!.setOnClickListener { v: View? ->
message!!.text = getString(R.string.message_clicked)
Toast.makeText(this@MainActivity, "You have clicked message", Toast.LENGTH_LONG).show()
}
setContentView(R.layout.activity_main)
setupTabs()
readMainTableFromSdCard()
}
private fun setupTabs() {
val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
val viewPager = findViewById<ViewPager2>(R.id.viewPager)
viewPager.adapter = ViewPagerAdapter(this)
TabLayoutMediator(tabLayout, viewPager
) { tab: TabLayout.Tab, position: Int ->
if (position == 0) {
tab.text = "Main Entries"
tab.view.setBackgroundColor(Color.parseColor("#A52A2A"))
} else if (position == 1) {
tab.text = "Meal Entries"
tab.view.setBackgroundColor(Color.parseColor("#A52A2A"))
} else {
tab.text = "Label Entry"
tab.view.setBackgroundColor(Color.parseColor("#A52A2A"))
}
}.attach()
tabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
tab!!.view.setBackgroundColor(Color.CYAN)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
tab!!.view.setBackgroundColor(Color.parseColor("#A52A2A"))
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
}
private fun readMainTableFromSdCard() {
val externalStorageVolumes =
ContextCompat.getExternalFilesDirs(this.applicationContext, null)
val sdCardStorage = externalStorageVolumes[1]
try {
val f2 = File("$sdCardStorage/condensed.mst")
val readData = FileInputStream(f2)
val readStream = ObjectInputStream(readData)
Tables.finalTable = readStream.readObject() as Array<Array<Any?>?>
readStream.close()
// Toast.makeText(getApplicationContext(), "Rows = " Tables.finalTable.size, Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(applicationContext, "EXCEPTION: " readMainTableFromSdCard(), Toast.LENGTH_LONG).show()
return
}
//Toast.makeText(getApplicationContext(), "Rows = " Tables.finalTable.length, Toast.LENGTH_LONG).show();
}
companion object {
class ViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
override fun createFragment(position: Int): Fragment {
return if (position == 0) {
FragmentMainEntries.newInstance()
} else if (position == 1) {
FragmentMealEntries.newInstance()
} else {
FragmentLabelEntry.newInstance()
}
}
override fun getItemCount(): Int {
return 3
}
}
}
}
Here is the fragment layout file:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="@color/very_light_background"
android:layout_height="match_parent"
tools:context=".FragmentMainEntries">
<androidx.viewpager2.widget.ViewPager2
android:id="@ id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@ id/recycleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@ id/viewPager" />
</androidx.constraintlayout.widget.ConstraintLayout>
Here is my Item class:
class Item internal constructor(
var index: Int,
var description: String,
var units: String,
var portion: Float,
var calories: Float,
)
Here is my item row layout file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@ id/itemLayout"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="34dp"
android:focusable="true"
android:clickable="true"
android:layout_margin="0dp">
<TableLayout
android:id="@ id/table_heading_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="192dp"
android:layout_marginTop="0dp"
android:foreground="?selectableItemBackground">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_gravity="bottom"
android:background="@color/brown"/>
<TableRow
android:layout_width="match_parent"
android:layout_height="34dp">
<TextView
android:id="@ id/tvIndex"
android:layout_width="50dp"
android:layout_height="34dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_column="0"
android:background="@color/brown"
android:textAlignment="center" />
<View
android:layout_width="1dip"
android:background="@color/brown"/>
<TextView
android:id="@ id/tvDescription"
android:layout_width="720dp"
android:layout_height="34dp"
android:layout_column="1"
android:textColor="@color/brown"
android:background="@color/very_light_background"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<View
android:layout_width="1dip"
android:background="@color/brown"/>
<TextView
android:id="@ id/tvPortion"
android:layout_width="100dp"
android:layout_height="34dp"
android:layout_column="2"
android:textColor="@color/brown"
android:background="@color/very_light_background"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textAlignment="center" />
<View
android:layout_width="1dip"
android:background="@color/brown"/>
<TextView
android:id="@ id/tvUnits"
android:layout_width="100dp"
android:layout_height="34dp"
android:layout_column="3"
android:textColor="@color/brown"
android:background="@color/very_light_background"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textAlignment="center" />
<View
android:layout_width="1dip"
android:background="@color/brown"/>
<TextView
android:id="@ id/tvCalories"
android:layout_width="108dp"
android:layout_height="34dp"
android:layout_column="4"
android:textColor="@color/brown"
android:background="@color/very_light_background"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textAlignment="center" />
<View
android:layout_width="1dip"
android:background="@color/brown"/>
</TableRow>
<View
android:layout_height="1dip"
android:background="@color/brown"/>
</TableLayout>
</LinearLayout>
CodePudding user response:
You're inflating a layout and setting stuff up on the View
s in it, but then you inflate another copy of the layout (where nothing's set up) and return that for display. So your stuff might be working, just in the layout you can't see!
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?
): View? {
// inflating layout copy A, storing the Binding class it creates
binding = FragmentMainEntriesBinding.inflate(layoutInflater)
// you're storing the `View` you should be returning here!
val view = binding.root
// setting things up on layout A
val recyclerView = view.findViewById<RecyclerView>(R.id.recycleView)
recyclerView.layoutManager = LinearLayoutManager(view.context)
tableRowAdapter = MainFragAdapter(foodList)
binding.recycleView.adapter = tableRowAdapter
// inflating layout copy B and returning that for display!
return inflater.inflate(R.layout.fragment_main_entries, container, false)
}
You're doing it again in MainActivity
too:
binding = ActivityMainBinding.inflate(layoutInflater)
...
// this tells the system to inflate this layout file and display it
setContentView(R.layout.activity_main)
For the Activity, you can either do
binding = ActivityMainBinding.inflate(layoutInflater)
// pass your inflated hierarchy for display
setContentView(binding.root)
or
// inflate and display a view hierarchy
setContentView(R.layout.activity_main)
// now the layout is displayed as 'view' (aka getView()) you can bind to that
// hierarchy - basically looking up all the view IDs in the binding class
binding = ActivityMainBinding.bind(view)
I prefer the first version since you just display what you inflated from the view binding, you don't need to match the correct layout XML file and view binding class, but it's up to you
Also, make your binding
field lateinit
like you did in your Fragment. Don't make it nullable just to have a temporary null value, using !!
in your code is generally bad and a sign something shouldn't be nullable at all!
For the Fragment, do something like this:
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?
): View? {
// inflating layout copy A, storing the Binding class it creates
binding = FragmentMainEntriesBinding.inflate(layoutInflater)
// don't use findViewById with view binding, part of the point is you don't
// have to do that! All your Views with IDs are already in the Binding object.
// by using 'with(binding)' we can refer to things in binding without having to
// prefix them all with 'binding.'
with(binding) {
// really binding.recyclerView - 'binding' is 'this' inside the 'with' block
recyclerView.layoutManager = LinearLayoutManager(requireContext())
tableRowAdapter = MainFragAdapter(foodList)
recyclerView.adapter = tableRowAdapter
}
// pass back the inflated layout - 'root' is a special property of the created binding
// object, either what it inflated through #inflate, or bound to through #bind
return binding.root
}
edit also just FYI, you can use View Binding with your Adapter too (might as well since you're already using it!)
class MainFragAdapter(private var foodArrayList: MutableList<Item>) :
RecyclerView.Adapter<MainFragAdapter.MyViewHolder>()
{
// you're only using this as a temp thing in onCreateViewHolder
// but it shouldn't really be here! Safer to use a local variable in the function
lateinit var view: View
// make this take an ItemViewBinding instead, and make the parameter
// a val so it's a property on the object. Pass the view root to the super constructor
inner class MyViewHolder(val binding: ItemViewBinding) : RecyclerView.ViewHolder(binding.root) {
// no need to look anything up! It's all in the binding object
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ItemRowBinding.inflate(inflater, parent, false)
Toast.makeText(parent.context, "foodArrayList5 = " foodArrayList.size, Toast.LENGTH_SHORT).show()
return MyViewHolder(binding)
}
override fun onBindViewHolder(viewHolder: MyViewHolder, i: Int) {
val item = foodArrayList[i]
// see the ViewHolder class below - working with a binding object now
with(viewHolder.binding) {
tvIndex.text = item.index.toString()
tvDescription.text = item.description
tvUnits.text = item.units
tvPortion.text = item.portion.toString()
tvCalories.text = item.calories.toString()
}
}
// you can make this an expression if you like, it's neat
override fun getItemCount() = foodArrayList.size
}