Whenever I use the Notification Shade to switch between the device dark and light themes, (my app when running) always seems to crash for some reason. My app's minimum API is 29 (Android 10). The logcat points to a line of code where the reason for the error isn't obvious. (import android.view.*). How can I prevent this from happening again?
java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter view
Activity class
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) supportFragmentManager.beginTransaction()
.replace(R.id.detail_container, MainFragment())
.commitNow()
}
}
Main fragment class
package com.example.vp2
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.text.TextUtils
import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.app.NavUtils
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import java.util.*
class MainFragment : androidx.fragment.app.Fragment() {
private lateinit var mySpinnerItems: Array<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val v = inflater.inflate(R.layout.fragment_main, container, false)
super.onCreate(savedInstanceState)
return v
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
val mSpinner = requireView().findViewById<Spinner>(R.id.mSpinner)
val mViewPager2 = requireView().findViewById<ViewPager2>(R.id.mViewPager2)
// Spinner items array
mySpinnerItems = arrayOf(
"Item 1",
"Item 2",
"Item 3",
)
val arrayAdapter = ArrayAdapter(requireView().context, android.R.layout.simple_spinner_item, mySpinnerItems)
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_item)
mSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
when {
mySpinnerItems[position] == "Item 1" -> {
mViewPager2.setCurrentItem(0, false)
}
mySpinnerItems[position] == "Item 2" -> {
mViewPager2.setCurrentItem(1, false)
}
else -> {
mViewPager2.setCurrentItem(2, false)
}
}
}
override fun onNothingSelected(parent: AdapterView<*>) {
mViewPager2.setCurrentItem(0, false)
}
}
mSpinner.adapter = arrayAdapter
mViewPager2.adapter = MySpinnerFragmentAdapter(this)
mViewPager2.orientation = ViewPager2.ORIENTATION_HORIZONTAL
mViewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
when (position) {
0 -> {
mSpinner.setSelection(0)
}
1 -> {
mSpinner.setSelection(1)
}
else -> {
mSpinner.setSelection(2)
}
}
super.onPageSelected(position)
}
})
super.onActivityCreated(savedInstanceState)
}
private class MySpinnerFragmentAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
private val intItems = 3
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> Fragment1()
1 -> Fragment2()
else -> Fragment3()
}
}
override fun getItemCount(): Int {
return intItems
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
menu.clear()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == android.R.id.home) {
val intent = activity?.let { NavUtils.getParentActivityIntent(it) }
when {
intent != null -> {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
NavUtils.navigateUpTo(requireActivity(), intent)
}
}
true
} else super.onOptionsItemSelected(item)
}
}
Main activity layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/detail_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
</LinearLayout>
Main fragment layout
<?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"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/detail_container">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/mViewPager2"
android:layout_height="0dp"
android:layout_width="match_parent"
android:layout_weight="1" />
<!-- divider (start)-->
<View
android:id="#+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="5dp"
android:background="?android:attr/textColorSecondary" />
<!-- divider (end)-->
<Spinner
android:id="#+id/mSpinner"
style="#style/Widget.AppCompat.Spinner.Underlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnerMode="dropdown"/>
</LinearLayout>
UPDATE
cactustictacs' suggestion
mSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when {
mySpinnerItems[position] == "Item 1" -> {
mViewPager2.setCurrentItem(0, false)
}
mySpinnerItems[position] == "Item 2" -> {
mViewPager2.setCurrentItem(1, false)
}
else -> {
mViewPager2.setCurrentItem(2, false)
}
}
}
override fun onNothingSelected(parent: AdapterView<*>) {
// Code to perform some action when nothing is selected
mViewPager2.setCurrentItem(0, false)
}
}
The stacktrace (please post the actual text in future not a screenshot!) is saying some parameter called view is null when its type is specified as non-null (View instead of View?). And that's being caused by AdapterView.onItemSelected running (so it's the view parameter in that method), which is declared in onActivityCreated
Basically that onItemSelected method needs to have nullable types for the first two parameters, AdapterView<*>? and View?.
That's what you get if you let the IDE auto-implement the methods (with ctrl+I) - the docs have that View! type which means because it's coming from Java, it could be null, might not, don't know - so the safe default is View?. When you specify a non-null type (like View) Kotlin does a null check to make sure - that's what that Intrinsics.checkNotNullParameter call is. You got a null, so it threw an exception!
So yeah, make them nullable, then null-check them before you access them. Also make sure appcompat is up to date (at least 1.2.0) because they had an issue where Activitys weren't being recreated when you used setDefaultNightMode
Related
When trying to write setHasOptionsMenu(true) in onCreate and override fun onCreateOptionsMenu as usual, Android Studio crosses out these functions saying that they are deprecated.
I looked at what they suggest
https://developer.android.com/jetpack/androidx/releases/activity?authuser=5#1.4.0-alpha01
and it turns out that they are asking to insert some new functions in Activity (MainActivity.kt) and some in Fragment (DogListFragment.kt). But in my app, all menu customization was done only in Fragment, so Activity can't do that. Activity simply doesn't have access to the RecyclerView, which is in the layout (fragment_god_list.xml) that belongs to Fragment. Activity only has androidx.fragment.app.FragmentContainerView in its activity_main.xml
Does anyone know how this can be done in Fragment without having to do anything with the menus in Activity?
GitHub project: https://github.com/theMagusDev/DogglersApp
MainActivity.kt:
package com.example.dogglers
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
import com.example.dogglers.databinding.ActivityMainBinding
private lateinit var navController: NavController
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Setup view binding
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Setup navController
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
DogListFragment.kt:
package com.example.dogglers
import android.os.Bundle
import android.view.*
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.dogglers.adapter.DogCardAdapter
import com.example.dogglers.const.Layout
import com.example.dogglers.databinding.FragmentDogListBinding
class DogListFragment : Fragment() {
private var _binding: FragmentDogListBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var recyclerView: RecyclerView
private var layoutType = Layout.VERTICAL
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDogListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.verticalRecyclerView
setUpAdapter()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.layout_manu, menu)
val layoutButton = menu.findItem(R.id.action_switch_layout)
// Calls code to set the icon
setIcon(layoutButton)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_switch_layout -> {
layoutType = when (layoutType) {
Layout.VERTICAL -> Layout.HORIZONTAL
Layout.HORIZONTAL -> Layout.GRID
else -> Layout.VERTICAL
}
setUpAdapter()
return true
}
// Otherwise, do nothing and use the core event handling
// when clauses require that all possible paths be accounted for explicitly,
// for instance both the true and false cases if the value is a Boolean,
// or an else to catch all unhandled cases.
else -> return super.onOptionsItemSelected(item)
}
}
fun setUpAdapter() {
recyclerView.adapter = when(layoutType){
Layout.VERTICAL -> {
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
DogCardAdapter(
context,
Layout.VERTICAL
)
}
Layout.HORIZONTAL -> {
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
DogCardAdapter(
context,
Layout.HORIZONTAL
)
}
else -> {
recyclerView.layoutManager = GridLayoutManager(context, 2, RecyclerView.VERTICAL, false)
DogCardAdapter(
context,
Layout.GRID
)
}
}
}
private fun setIcon(menuItem: MenuItem?) {
if (menuItem == null)
return
menuItem.icon = when(layoutType) {
Layout.VERTICAL -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_vertical_layout)
Layout.HORIZONTAL -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_horizontal_layout)
else -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
}
}
}
ActivityMain.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph"/>
</FrameLayout>
FragmentDogList:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/vertical_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/vertical_list_item"/>
</FrameLayout>
When you call setUpActionBarWithNavController() method , you are setting up toolbar inside the activity. Your Fragment is inside this activity. Your fragment has this actionBar too. To use Menu provider inside fragment, you need to call below method inside of onViewCreated() method of fragment.
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
Also, You need to make your fragment implement MenuProvider Interface
class DogListFragment : Fragment(),MenuProvider {...
IDE will ask you to implement its provider method i.e onCreateMenu and onMenuItemSelected
Inside OnCreateMenu, use menu Inflator to inflate menu layout
example:-
menuInflater.inflate(R.menu.search_menu,menu)
I'm using the Jetpack navigation component with a navhost and navgraph. In my categoriesFragment, I have a RecyclerView that displays the current list of categories in the database. It's attached to a LiveData implementation of the SQL that grabs all the categories from the database.
Whenever I add a category to the database from a dialog window, I navigate back to the categoriesFragment while passing data back to it and insert the category into the database from within the categoriesFragment. It shows the updated list of categories in the RecyclerView of the categoriesFragment fine, but the problem is I have to press the back button 2 times to go back to the previous fragment. What could be the issue? Thanks.
Fragment:
package com.example.pomoplay.ui.main
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.pomoplay.CategoriesRecyclerAdapter
import com.example.pomoplay.PomoPlayObservablesSingleton
import com.example.pomoplay.R
import kotlinx.android.synthetic.main.fragment_categories.*
class CategoriesFragment : Fragment(), SearchView.OnQueryTextListener{
private var newCategoryCreated: Boolean = false
lateinit var navController: NavController
private var adapter: CategoriesRecyclerAdapter? = null
private val viewModel: CategoriesFragmentViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.i("Lifecycle-Fragment", "OnCreateView() called")
return inflater.inflate(R.layout.fragment_categories, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.i("Lifecycle-Fragment", "OnViewCreated() called")
Log.i("Lifecycle-Frag-Bundle", savedInstanceState.toString())
categories_searchview.isIconified = false
categories_searchview.isFocusable = true
categories_searchview.clearFocus()
navController = Navigation.findNavController(view)
observerSetup()
recyclerSetup()
var searchView = categories_searchview
searchView.setOnQueryTextListener(this)
fab_new_category.setOnClickListener {
navController.navigate(R.id.action_categoriesFragment_to_newCategoryDialogFragment)
}
if(!arguments?.isEmpty!! && newCategoryCreated){
var args = CategoriesFragmentArgs.fromBundle(arguments!!)
if(args.fromNewCategoryDialog){
var category = args.category
viewModel.insertCategory(category)
PomoPlayObservablesSingleton.newCategoryCreatedSubject.onNext(false)
}
}
searchView.setOnCloseListener {
viewModel.setLastSearchQuery("")
navController.navigate(R.id.categoriesFragment)
true
}
}
override fun onResume() {
super.onResume()
if(viewModel.getLastSearchQuery() != null && viewModel.getLastSearchQuery().toString() != "")
{
categories_searchview.requestFocusFromTouch()
categories_searchview.setQuery(viewModel.getLastSearchQuery().toString(), true)
}
Log.i("Lifecycle-Fragment", "OnResume() called")
}
override fun onPause() {
super.onPause()
categories_searchview.clearFocus()
Log.i("Lifecycle-Fragment", "OnPause() called")
}
override fun onStop() {
super.onStop()
Log.i("Lifecycle-Fragment", "OnStop() called")
}
override fun onDestroyView() {
super.onDestroyView()
Log.i("Lifecycle-Fragment", "OnDestroyView() called")
//PomoPlayObservablesSingleton.newCategoryCreatedSubject.onNext(false)
}
override fun onDestroy() {
super.onDestroy()
Log.i("Lifecycle-Fragment", "OnDestroy() called")
}
override fun onDetach() {
super.onDetach()
Log.i("Lifecycle-Fragment", "OnDetach() called")
}
private fun observerSetup() {
Log.i("Lifecycle-Fragment", "observerSetup() called")
viewModel.getSearchCategoriesNameResults().observe(this,androidx.lifecycle.Observer { categories ->
Log.i("Lifecycle-Fragment", "getSearchCategoriesNameResults observable value received")
adapter?.setCategoryList(categories)
})
viewModel.getAllCategories()?.observe(this, androidx.lifecycle.Observer { categories ->
Log.i("Lifecycle-Fragment", "getAllCategories observable value received")
if(categories.isNotEmpty()){
adapter?.setCategoryList(categories.sortedBy { category -> category.name?.toLowerCase() })
category_not_found_bubble.visibility = View.GONE
category_not_found_text.visibility = View.GONE
}
else{
category_not_found_bubble.visibility = View.VISIBLE
category_not_found_text.visibility = View.VISIBLE
}
})
PomoPlayObservablesSingleton.newCategoryCreatedSubject.subscribe{comp -> newCategoryCreated = comp }
}
private fun recyclerSetup() {
Log.i("Lifecycle-Fragment", "recyclerSetup() called")
adapter = context?.let { CategoriesRecyclerAdapter(it) }
categories_list?.layoutManager = LinearLayoutManager(context)
categories_list?.adapter = adapter
}
override fun onQueryTextSubmit(query: String?): Boolean {
Log.i("Lifecycle-Fragment", "onQueryTextSubmit() called")
var q = query?.toLowerCase()?.trim()?.replace("\\s+".toRegex(), " ")
setLastSearchQuery(q.toString())
viewModel.searchCategoriesByName(viewModel.getLastSearchQuery().toString())
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
Log.i("Lifecycle-Fragment", "onQueryTextChange() called")
return false
}
private fun setLastSearchQuery(q: String?) {
Log.i("Lifecycle-Fragment", "setLastSearchQuery() called")
viewModel.setLastSearchQuery(q.toString())
}
RecyclerView Adapter:
package com.example.pomoplay
import android.content.Context
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import com.example.pomoplay.ui.main.CategoriesFragmentDirections
class CategoriesRecyclerAdapter(private val context: Context) : RecyclerView.Adapter<CategoriesRecyclerAdapter.ViewHolder>(){
private var categoriesList: List<Category> = emptyList()
private val layoutInflater = LayoutInflater.from(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = layoutInflater.inflate(R.layout.categories_list_item, parent, false)
return ViewHolder(itemView)
}
fun setCategoryList(categories: List<Category>) {
categoriesList = categories
notifyDataSetChanged()
}
override fun getItemCount() = categoriesList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val category = categoriesList[position]
holder.textCategoryTitle?.text = category?.name
holder.textCategoryDescription?.text = category?.desc
holder.optMenu.setOnClickListener {
val popup = PopupMenu(context, holder.optMenu)
//inflating menu from xml resource
//inflating menu from xml resource
popup.inflate(R.menu.category_menu)
//adding click listener
//adding click listener
popup.setOnMenuItemClickListener(object : MenuItem.OnMenuItemClickListener,
PopupMenu.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.app_settings -> {
Toast.makeText(context, "it works from recyclerview! :)", Toast.LENGTH_SHORT).show()
true
}
else -> false
}
}
})
//displaying the popup
//displaying the popup
popup.show()
}
holder.textCategoryTitle?.setOnClickListener {
var navController = Navigation.findNavController(it)
var action = CategoriesFragmentDirections.actionCategoriesFragmentToCategoryFragment(category, fromCategoriesFragmentTitle = true)
PomoPlayObservablesSingleton.newCategoryCreatedSubject.onNext(false)
navController.navigate(action)
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textCategoryTitle = itemView.findViewById<TextView?>(R.id.categories_list_item_title)
val textCategoryDescription = itemView.findViewById<TextView?>(R.id.categories_list_item_description)
val optMenu: ImageView = itemView.findViewById(R.id.optmenu)
}
RecyclerView Layout:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/categories_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:clipToPadding="false"
android:paddingBottom="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/categories_searchview"
tools:listitem="#layout/categories_list_item">
</androidx.recyclerview.widget.RecyclerView>
New Category Dialog:
package com.example.pomoplay.ui.main
import android.app.Dialog
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.navigation.NavController
import androidx.navigation.Navigation
import com.example.pomoplay.Category
import com.example.pomoplay.PomoPlayObservablesSingleton
import com.example.pomoplay.R
class NewCategoryDialogFragment : DialogFragment() {
private lateinit var categoryNameEditText: EditText
private lateinit var categoryDescEditText: EditText
private lateinit var navController: NavController
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreateDialog(savedInstanceState)
navController = Navigation.findNavController(parentFragment?.view!!)
var catList = ArrayList<String>()
catList.add("Test Item 1")
catList.add("Test Item 2")
val view =
requireActivity().layoutInflater.inflate(R.layout.fragment_new_category_dialog, null)
categoryNameEditText = view.findViewById(R.id.dialog_new_category_name) as EditText
categoryDescEditText = view.findViewById(R.id.dialog_new_category_desc) as EditText
return activity?.let { it ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(it)
builder.setTitle("Testing")
.setPositiveButton(
"ok"
) { _, id ->
var cat = Category(categoryNameEditText.text.toString().trim().replace("\\s+".toRegex(), " "), categoryDescEditText.text.toString().trim().replace("\\s+"," "))
var action = NewCategoryDialogFragmentDirections.actionNewCategoryDialogFragmentToCategoriesFragment(cat, fromNewCategoryDialog = true)
PomoPlayObservablesSingleton.newCategoryCreatedSubject.onNext(true)
navController.navigate(action)
}
.setNegativeButton("cancel") { _, id ->
}
.setView(view)
var spinner = view.findViewById<Spinner>(R.id.dialog_new_category_spinner)
var spinnerAdapter =
context?.let {
ArrayAdapter<String>(
it,
android.R.layout.simple_spinner_item,
catList
)
}
spinnerAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = spinnerAdapter
val dialog = builder.create()
dialog.setOnShowListener { dialog ->
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = categoryNameEditText.text.isNotBlank() && categoryNameEditText.text.isNotEmpty()
}
categoryNameEditText.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(
s: CharSequence, start: Int, before: Int,
count: Int
) {
}
override fun beforeTextChanged(
s: CharSequence, start: Int, count: Int,
after: Int
) {
}
override fun afterTextChanged(s: Editable) { // Check if edittext is empty
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
!(categoryNameEditText.text.isBlank() || categoryNameEditText.text.isEmpty())
}
})
dialog
} ?: throw IllegalStateException("Activity cannot be null")
}
}
NavGraph:
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/nav_graph_main"
app:startDestination="#id/categoriesFragment">
<fragment
android:id="#+id/clockFragment"
android:name="com.example.pomoplay.ui.main.ClockFragment"
android:label="Pomo Clock"
tools:layout="#layout/fragment_clock" />
<fragment
android:id="#+id/categoryFragment"
android:name="com.example.pomoplay.ui.main.CategoryFragment"
android:label="Category"
tools:layout="#layout/fragment_category">
<action
android:id="#+id/action_categoryFragment_to_clockFragment"
app:destination="#id/clockFragment" />
<argument
android:name="category"
app:argType="com.example.pomoplay.Category"
app:nullable="true"
android:defaultValue="#null" />
<action
android:id="#+id/action_categoryFragment_to_newTaskDialogFragment"
app:destination="#id/newTaskDialogFragment" />
<argument
android:name="pomotask"
app:argType="com.example.pomoplay.PomoTask"
app:nullable="true"
android:defaultValue="#null" />
<argument
android:name="fromNewTaskDialog"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="fromCategoriesFragmentTitle"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<fragment
android:id="#+id/categoriesFragment"
android:name="com.example.pomoplay.ui.main.CategoriesFragment"
android:label="Categories"
tools:layout="#layout/fragment_categories">
<action
android:id="#+id/action_categoriesFragment_to_newCategoryDialogFragment"
app:destination="#id/newCategoryDialogFragment" />
<argument
android:name="category"
app:argType="com.example.pomoplay.Category"
app:nullable="true" />
<action
android:id="#+id/action_categoriesFragment_to_categoryFragment"
app:destination="#id/categoryFragment" />
<argument
android:name="fromNewCategoryDialog"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<dialog
android:id="#+id/newCategoryDialogFragment"
android:name="com.example.pomoplay.ui.main.NewCategoryDialogFragment"
tools:layout="#layout/fragment_new_category_dialog">
<action
android:id="#+id/action_newCategoryDialogFragment_to_categoriesFragment"
app:destination="#id/categoriesFragment" />
</dialog>
<dialog
android:id="#+id/newTaskDialogFragment"
android:name="com.example.pomoplay.ui.main.NewTaskDialogFragment"
android:label="fragment_new_task_dialog"
tools:layout="#layout/fragment_new_task_dialog" >
<action
android:id="#+id/action_newTaskDialogFragment_to_categoryFragment"
app:destination="#id/categoryFragment" />
</dialog>
</navigation>
You are calling navController.navigate(action) again which will create CategoryFragment again.
To back to previous fragment, you should use navController()?.popBackStack().
I found a solution. If I want to pass data/arguments back to the previous fragment from a dialog fragment and, at the same time, solve the problem of having to press the back button twice, I could use the code in my original question unchanged and just use the Pop Behavior options in the Action attributes of the Action that goes from the newCategoryDialogFragment to the categoriesFragment.
To test it out, I kept the original code in place and in the Pop Behavior section, I set the Pop To attribute to categoriesFragment and checked the Inclusive checkbox.
I had to check the Inclusive checkbox, because when I navigate to the categoresFragment from the newCategoryDialogFragment with navcontroller.navigate(action), it adds the categoriesFragment again to the backstack; the inclusive property removes this duplicate. Here's a screenshot of the Pop Behavior section:
I have a Fragment MyFragment currently which has a Spinner my_spinner. For testing my app, I originally populated the contents of my_spinner manually by observing the property myLiveDataList in the AndroidViewModel MyViewModel as below:
my_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.MyFragment">
<Spinner
android:id="#+id/my_spinner"
android:layout_width="match_parent"
android:layout_height="100dp" />
</FrameLayout>
MyFragment.kt
import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.lifecycle.Observer
import com.example.app.R
import com.example.app.data.room.entities.MyEntity
import com.example.app.ui.viewmodels.MyViewModel
import kotlinx.android.synthetic.main.my_fragment.*
class MyFragment : Fragment() {
companion object {
fun newInstance() = MyFragment()
}
private lateinit var viewModel: MyViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
val myAdapter = ArrayAdapter<MyEntity>(this.context!!, android.R.layout.simple_spinner_item)
// This is where I populate my_spinner
viewModel.myLiveDataList.observe(this, Observer<List<MyEntity>> { data ->
data?.forEach {
myAdapter.add(it)
}
})
my_spinner.adapter = myAdapter
}
}
MyViewModel.kt
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.toLiveData
import com.example.app.data.repositories.MyRepository
import com.example.app.data.room.entities.MyEntity
class MyViewModel(application: Application) : AndroidViewModel(application) {
private val myRepository = MyRepository(application)
val myLiveDataList: LiveData<List<MyEntity>>
get() = myRepository.getAllData().toLiveData()
}
This fills my_spinner successfully when I navigate to MyFragment:
Since it populates as expected, I went ahead to make the following changes to my_fragment.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewmodel"
type="com.example.app.ui.viewmodels.MyViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.MyFragment">
<Spinner
android:id="#+id/my_spinner"
android:layout_width="match_parent"
android:layout_height="100dp"
app:entries="#{viewmodel.myLiveDataList}"/>
</FrameLayout>
</layout>
I've added in a Binding Adapter file BindingAdapterUtil (following code was copied from this article):
import android.R
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import com.example.app.ui.adapter.SpinnerExtensions.getSpinnerValue
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerEntries
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerInverseBindingListener
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerItemSelectedListener
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerValue
#BindingAdapter("entries")
fun Spinner.setEntries(entries: List<Any>?) {
setSpinnerEntries(entries)
}
#BindingAdapter("onItemSelected")
fun Spinner.setItemSelectedListener(itemSelectedListener: SpinnerExtensions.ItemSelectedListener?) {
setSpinnerItemSelectedListener(itemSelectedListener)
}
#BindingAdapter("newValue")
fun Spinner.setNewValue(newValue: Any?) {
setSpinnerValue(newValue)
}
#BindingAdapter("selectedValue")
fun Spinner.setSelectedValue(selectedValue: Any?) {
setSpinnerValue(selectedValue)
}
#BindingAdapter("selectedValueAttrChanged")
fun Spinner.setInverseBindingListener(inverseBindingListener: InverseBindingListener?) {
setSpinnerInverseBindingListener(inverseBindingListener)
}
#InverseBindingAdapter(attribute = "selectedValue", event = "selectedValueAttrChanged")
fun Spinner.getSelectedValue(): Any? {
return getSpinnerValue()
}
object SpinnerExtensions {
fun Spinner.setSpinnerEntries(entries: List<Any>?) {
if (entries != null) {
val arrayAdapter = ArrayAdapter(context, R.layout.simple_spinner_item, entries)
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
adapter = arrayAdapter
}
}
fun Spinner.setSpinnerItemSelectedListener(listener: ItemSelectedListener?) {
if (listener == null) {
onItemSelectedListener = null
} else {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
if (tag != position) {
listener.onItemSelected(parent.getItemAtPosition(position))
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
}
fun Spinner.setSpinnerInverseBindingListener(listener: InverseBindingListener?) {
if (listener == null) {
onItemSelectedListener = null
} else {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
if (tag != position) {
listener.onChange()
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
}
fun Spinner.setSpinnerValue(value: Any?) {
if (adapter != null ) {
val position = (adapter as ArrayAdapter<Any>).getPosition(value)
setSelection(position, false)
tag = position
}
}
fun Spinner.getSpinnerValue(): Any? {
return selectedItem
}
interface ItemSelectedListener {
fun onItemSelected(item: Any)
}
}
And I've modified the onActivityCreated in MyFragment like so:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
DataBindingUtil.setContentView<MyFragmentBinding>(
this.activity!!, R.layout.my_fragment
).apply {
this.setLifecycleOwner(this#MyFragment)
this.viewmodel = viewModel
}
}
The result of this is that my_spinner is no longer populating with the contents of MyViewModel.myLiveDataList. To try to ascertain if the property was at fault, I created a new property in MyViewModel like so:
val myList: List<String>?
get() = listOf("First", "Second", "Third")
And I have bound this property to my_spinner just like MyViewModel.myLiveDataList above with success this time.
The function in MyRepository.getAllData() (which myLiveDataList returns) returns a Flowable<List<MyEntity>> (RxJava), which calls a Room DAO to get the data. My assumption here is that myLiveDataList doesn't have anything to serve when it tries to bind the values for the first time, and never tries again.
Am I missing something when trying to bind a LiveData datasource to a Spinner?
After reading this answer, I've modified my_fragment.xml to the following:
...
<data>
<import type="java.util.List" />
<import type="com.example.app.data.room.entities.MyEntity" />
<import type="androidx.lifecycle.LiveData" />
<variable name="viewmodel"
type="com.example.app.ui.viewmodels.MyViewModel" />
<variable name="myTestList"
type="LiveData<List<MyEntity>>" />
</data>
...
<Spinner
android:id="#+id/my_spinner"
android:layout_width="match_parent"
android:layout_height="100dp"
app:entries="#{myTestList}"/>
...
I've also removed the contents of MyFragment.onActivityCreated and modified MyFragment.onCreateView as followed:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
val binding = MyFragmentBinding.inflate(inflater, container, false)
binding.setLifecycleOwner(this)
binding.viewmodel = viewModel
binding.myTestList = viewModel.myLiveDataList
return binding.root
}
Not a perfect solution, and I still don't know why my original bout at this problem didn't yield the desired results, but it will do. If there is a better way of binding a Spinner to LiveData in this fashion, please let me know.
I am getting null pointers (sometimes) on views within fragments using synthetic.
I do not know what is wrong because I am declaring the fragments in XML and I think that when I call the populate method from the Activity, the root view is already created
Anyway, I do not believe is correct check this each time I call the populate...
I write an example code of what happens (the null pointer would be on tv):
The fragment:
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_foo.*
class FooFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_foo, container, false)
}
fun populate(fooName: String) {
tv.text = fooName
}
}
And the XML related within the XML of the Activity related
<fragment
android:id="#+id/fFoo"
android:name="com.foo.FooFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout="#layout/fragment_foo"/>
This would the Activity related:
class FooActivity : AppCompatActivity() {
private lateinit var fooFragment: FooFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_foo)
fooFragment = fFoo as FooFragment
}
private fun loadDataProductionInfo(productionId: Long) {
FooAPIParser().getFoo {
initFoo(it.fooName)
}
}
private fun initFoo(fooName: String) {
fooFragment.populate(fooName)
}
}
And finally the XML of the specific fragment:
<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:layout_height="wrap_content">
<TextView
android:id="#+id/tvFoo"
style="#style/Tv"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Kotlin synthetic properties are not magic and work in a very simple way. When you access btn_K, it calls for getView().findViewById(R.id.tv)
The problem is that you are accessing it too soon getView() returns null in onCreateView. Try doing it in the onViewCreated method:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//Here
}
}
Using views outside onViewCreated.
tv?.text = "kotlin safe call"
If you are going to use Fragments you need to extend FragmentActivity
Thinking in your answers I have implemented a patch. I do not like very much, but I think it should work better than now. What do you think?
I would extend each Fragment I want to check this from this class:
import android.os.Bundle
import android.os.Handler
import android.view.View
import androidx.fragment.app.Fragment
open class BaseFooFragment : Fragment() {
private var viewCreated = false
private var attempt = 0
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewCreated = true
}
fun checkViewCreated(success: () -> Unit) {
if (viewCreated) {
success()
} else {
initLoop(success)
}
}
private fun initLoop(success: () -> Unit) {
attempt++
Handler().postDelayed({
if (viewCreated) {
success()
} else {
if (attempt > 3) {
return#postDelayed
}
checkViewCreated(success)
}
}, 500)
}
}
The call within the Fragment would be more or less clean:
fun populate(fooName: String) {
checkViewCreated {
tv.text = fooName
}
}
Finally, I have received help to find out a better answer.
open class BaseFooFragment : Fragment() {
private var listener: VoidListener? = null
private var viewCreated = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewCreated = true
listener?.let {
it.invoke()
listener = null
}
}
override fun onDestroyView() {
viewCreated = false
super.onDestroyView()
}
fun doWhenViewCreated(success: VoidListener) {
if (viewCreated) {
success()
} else {
listener = success
}
}
}
The VoidListener is simply this:
typealias VoidListener = () -> Unit
One way to do this more generic (for example, when you want to use more than one listener) could be like this:
open class BaseFooFragment : Fragment() {
private val listeners: MutableList<VoidListener> = mutableListOf()
private var viewCreated = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewCreated = true
listeners.forEach { it() }
listeners.clear()
}
override fun onDestroyView() {
viewCreated = false
super.onDestroyView()
}
fun doWhenViewCreated(success: VoidListener) {
if (viewCreated) {
success()
} else {
listeners.add(success)
}
}
}
I'm trying to override the OnItemSelected method of a Spinner in my Fragment, the same way I did with OnClick on Button, but it's not responding anymore
Inside the OnCreate, the commented block works perfectly, but I want to clean it because I have more than one spinner in this layout and this way it is getting very polluted.
Another question: If possible , how this OnNothingSelected method works and how to use it more effectively?
In the expression "if (itemSelected! =" ... ")" is because the first element of my spinner is a string with "....", so as not to have any item selected at the beginning, I'm using this wonderfull technique... How could I improve this?
My code is as follows:
Vetarano2.kt
com.mtsa.escudeiro_rpghelper.fragments
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.Button
import android.widget.Spinner
import android.widget.Toast
import com.mtsa.escudeiro_rpghelper.R
class Veterano2 : Fragment(), View.OnClickListener, AdapterView.OnItemSelectedListener {
private lateinit var spinner: Spinner
private lateinit var button: Button
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
val fragView = inflater.inflate(R.layout.fragment_veterano2, container, false)
initViews(fragView)
initListeners()
// SPINNER RAÇA
/*
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val selecionado = parent.getItemAtPosition(position) as String
Toast.makeText(context, "Opção escolhida: $selecionado", Toast.LENGTH_SHORT).show()
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
*/
return fragView
}
private fun initViews(v: View) {
spinner = v.findViewById(R.id.spinner2)
button = v.findViewById(R.id.button2)
}
private fun initListeners() {
spinner.onItemSelectedListener = this
button.setOnClickListener(this)
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (view?.id) {
R.id.spinner2 -> {
val selecionado = parent?.getItemAtPosition(position) as String
Toast.makeText(context, "Opção escolhida: $selecionado", Toast.LENGTH_SHORT).show()
}
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.button2 -> {
Toast.makeText(context, "SALVAR", Toast.LENGTH_SHORT).show()
}
}
}
}
fragment_veterano2.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="64dp">
<Spinner
android:id="#+id/spinner2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="#array/array_example"
android:spinnerMode="dropdown" />
<Button
android:id="#+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="64dp"
android:text="Button" />
</LinearLayout>
arrays_diversos.xml
<resources>
<string-array name="array_example">
<item>AAAAA</item>
<item>BBBBB</item>
<item>CCCCC</item>
<item>DDDDD</item>
<item>EEEEE</item>
</string-array>
</resources>
Many thanks in advance!
EDIT: Reformatted code for better visualization, but the problem persists
As the doc says, if you have multiple spinners, you should judge the id of parent like below, sorry,I just know java.
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
int iid = parent.getId();
switch (iid) {
case R.id.spinner1:
Toast.makeText(getActivity(), "hello,spinner1", Toast.LENGTH_SHORT).show();
break;
case R.id.spinner2:
Toast.makeText(getActivity(), "hello,spinner2", Toast.LENGTH_SHORT).show();
break;
break;
default:
break;
}
}