2-way databinding with switch inside custom relativelayout view - android

I am trying to implement some custom bindingadapters so that I can databind my viewmodel value to the switch.ischecked value in my custom view. I want the switch state to change if enabled in my viewmodel changes and visa-versa. I have looked at numerous articles on how to accomplish this, yet it still does nothing. I can see that my setSwitchChecked method is being used by the databinding implementation, but it doesn't seem to actually set anything. the other 2 adapters remain unused. Any help as so what I am missing or doing wrong is appreciated.
ViewModel
open class SettingsViewModel #Inject constructor(): ViewModel() {
var enabled: MutableLiveData<Boolean> = MutableLiveData()
}
Fragment
class SettingsFragment #Inject constructor(): Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var viewDataBinding: FragmentSettingsBinding
private lateinit var viewModel: SettingsViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_settings, container, false)
viewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(SettingsViewModel::class.java)
viewDataBinding = FragmentSettingsBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
return view
}
}
CustomView xml binding
<com.stinson.sleepcycles.views.SwitchRow
android:id="#+id/switch_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:switchLabel="#string/enabled"
app:switchChecked="#{viewmodel.enabled}"/>
Custom View Class
class SwitchRow constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) :
RelativeLayout(context, attrs, defStyle), View.OnClickListener {
constructor(context: Context, attrs: AttributeSet): this(context, attrs, 0)
init {
val view = inflate(context, R.layout.view_switch_row, this)
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.SwitchRow, defStyle, 0)
try {
view.text_label.text = a.getString(R.styleable.SwitchRow_switchLabel)
view.switch_toggle.isChecked = a.getBoolean(R.styleable.SwitchRow_switchChecked, false)
} finally {
a.recycle()
}
view.setOnClickListener {
view.switch_toggle.callOnClick()
}
view.switch_toggle.setOnClickListener {
toggleSwitch(view)
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP) {
this.callOnClick()
}
return super.dispatchTouchEvent(event)
}
override fun onClick(view: View?) {
if (view != null) view.callOnClick()
}
private fun toggleSwitch(view: View) {
view.switch_toggle.isChecked = !view.switch_toggle.isChecked
}
}
Custom View XML
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:padding="16dp">
<TextView
android:id="#+id/text_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="#dimen/text_size_medium"
tools:text="Label" />
<androidx.appcompat.widget.SwitchCompat
android:id="#+id/switch_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"/>
</RelativeLayout>
Binding Adapters
#BindingAdapter("switchCheckedAttrChanged")
fun setListener(switchRow: SwitchRow, listener: InverseBindingListener) {
switchRow.switch_toggle.setOnCheckedChangeListener { _, _ ->
listener.onChange()
}
}
#BindingAdapter("switchChecked")
fun setSwitchChecked(switchRow: SwitchRow, value: Boolean) {
if (value != switchRow.switch_toggle.isChecked) {
switchRow.switch_toggle.isChecked = value
}
}
#InverseBindingAdapter(attribute = "switchChecked")
fun getSwitchChecked(switchRow: SwitchRow): Boolean {
return switchRow.switch_toggle.isChecked
}

Your InverseBindingAdapter should have the following annotation:
#InverseBindingAdapter(attribute = "switchChecked", event = "switchCheckedAttrChanged")

Related

Problems with SearchView (items appearing and dissapearing when a button is clicked)

I have a list of items on a Recyclerview, and I also have a SearchView to filther those items.
Every item has a favourite button, so when you click, the item adds to favorite table.
The problem is that, when I filter something and I start clicking those buttons, odd things happens: some items dissapear from the filtered list. It doesn't happen always, only sometimes. How can I fix this?
My code:
My class:
class CoasterFragment : Fragment() {
private val myAdapter by lazy { CoasterRecyclerViewAdapter(CoasterListenerImpl(requireContext(), viewModel),requireContext()) }
private lateinit var searchView: SearchView
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 recyclerView = binding.recyclerCoaster
recyclerView.adapter = myAdapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.coasters().observe(viewLifecycleOwner){myAdapter.setData(it)}
searchView = binding.search
searchView.clearFocus()
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
if(query != null){
searchDatabase(query)
searchView.clearFocus()
}
return true
}
override fun onQueryTextChange(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
})
return root
}
fun searchDatabase(query: String) {
val searchQuery = "%$query%"
viewModel.searchDatabase(searchQuery).observe(viewLifecycleOwner) { myAdapter.setData(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Here is my adapter code:
class CoasterRecyclerViewAdapter( val listener: CoasterListener,
val context: Context ) : RecyclerView.Adapter<CoasterRecyclerViewAdapter.ViewHolder>(){
private var coasterList = emptyList<CoasterFavorito>()
class ViewHolder private constructor(val binding: CoasterItemBinding, private val listener: CoasterListener,
private val context: Context): RecyclerView.ViewHolder(binding.root){
companion object{
fun crearViewHolder(parent: ViewGroup, listener: CoasterListener, 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, context)
override fun onBindViewHolder(holder: ViewHolder, position: Int){
holder.binding.nombre.text = coasterList[position].coaster.nombre
holder.binding.parque.text = coasterList[position].coaster.parque
holder.binding.ciudad.text = coasterList[position].coaster.ciudad
holder.binding.provincia.text = coasterList[position].coaster.provincia
holder.binding.comunidad.text = coasterList[position].coaster.comunidadAutonoma
Glide
.with(context)
.load(coasterList[position].coaster.imagen)
.centerCrop()
.into(holder.binding.imagen)
holder.binding.check.isChecked = coasterList[position].favorito
holder.binding.check.setOnClickListener{
if (coasterList[position].favorito) {
listener.delFavorito(coasterList[position].coaster.id)
holder.binding.check.isChecked = false
} else {
listener.addFavorito(coasterList[position].coaster.id)
holder.binding.check.isChecked = true
}
}
}
override fun getItemCount(): Int{
return coasterList.size
}
fun setData(coaster: List<CoasterFavorito>){
coasterList = coaster
notifyDataSetChanged()
}
}
interface CoasterListener {
fun addFavorito(id: Long)
fun delFavorito(id: Long)
}
The search Query:
#Query ("SELECT c.*, " + "EXISTS (SELECT * from montarse where usuario_id=:id and coaster_id = c.id) as favorito " + "FROM coasters c " + "WHERE nombre LIKE :searchQuery OR parque LIKE :searchQuery OR ciudad LIKE :searchQuery OR comunidadAutonoma LIKE :searchQuery OR provincia LIKE :searchQuery")
fun searchCoaster(id: Long, searchQuery: String): Flow<List<CoasterFavorito>>
My viewModel:
fun searchDatabase( searchQuery: String): LiveData<List<CoasterFavorito>> {
return coasterDao.searchCoaster( App.getUsuario()!!.id, searchQuery).asLiveData()
}
fun addFavorito( coasterId: Long) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
usuarioCoasterDao.create(UsuarioCoaster(App.getUsuario()!!.id, coasterId, null, null))
}
}
}
fun coasters(): LiveData<List<CoasterFavorito>> {
return coasterDao.findAllFav(App.getUsuario()!!.id).asLiveData()
}
my XML:
<?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:layout_height="match_parent"
tools:context=".ui.coaster.HomeCoasterFragment">
<androidx.appcompat.widget.SearchView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="#+id/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:iconifiedByDefault="false"
app:searchHintIcon="#null"
android:queryHint="Buscar..."
android:focusable="false"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerCoaster"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/search"
tools:listitem="#layout/coaster_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
I tried changind the clear focus().
Also I added some if else (for example, if the searchView is Empty, then load the list from the adapter as normal. If it is not empty, use the SearchView code to filter and load the list.

ViewHolder gets a null reference for a custom view when used in a RecycleView adapter

I have the following RecycleView adapter:
class PageViewerAdapter(val context: Context, private val pages: List<Page>) :
RecyclerView.Adapter<PageViewerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(context);
val view = inflater.inflate(R.layout.page_viewer_card, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.testView.text = position.toString()
holder.panelView.isEnabled = false
}
override fun getItemCount(): Int {
return pages.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val testView: TextView = itemView.test_text
val panelView: PanelView = itemView.page_preview
}
}
The problem is that itemView.page_preview, which is referencing my custom view in the XML, is null. Both in ViewHolder initialization and in the onBindViewHolder.
This is the RecyclerView card/item XML:
<?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"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:padding="10dp">
<TextView
android:id="#+id/test_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<my.package.PanelView
android:id="#+id/page_preview"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintDimensionRatio="W, 1:1.4142"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
If however, the page_preview references are removed from ViewHolder and onBindViewHolder, the views will load fine within a few seconds.
Update
This is the custom view class:
class PanelView:
(context: Context?, attrs: AttributeSet?) : SurfaceView(context), SurfaceHolder.Callback {
private var canvasThread: CanvasThread
init {
this.holder.addCallback(this)
canvasThread = CanvasThread(this.holder, this)
this.isFocusable = true
}
override fun surfaceCreated(holder: SurfaceHolder) {
resume()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
pause()
}
fun resume() {
if (!canvasThread.isAlive) {
canvasThread = CanvasThread(this.holder, this)
canvasThread.run = true
canvasThread.start()
}
}
fun pause() {
if (canvasThread.isAlive) {
var retry = true
canvasThread.run = false
while (retry) {
try {
canvasThread.join()
retry = false
} catch (e: InterruptedException) {
}
}
}
}
}
You are probably using kotlinx.android.synthetic import statements to access your specific within the parent view. This does not work for recycler views, instead you should do val panelView = itemView.findViewById(R.id.page_preview)
Kotlinx synthetic doesn't work by default in RecyclerView.ViewHolder class, you need to have it extend kotlinx.android.extensions.LayoutContainer:
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), LayoutContainer {
val testView: TextView = itemView.test_text //now this will work
val panelView: PanelView = itemView.page_preview
}
Anyway, you should really be careful with syntetic imports as they can be a headache to debug, especially in fragments. You might as well just use the fancy new findViewById<View>
Edit 1:
Since you've posted your custom view class, at first glance it seems that the super constructor invocation isn't right. You could use the following for all custom view kotlin classes:
class CustomView #JvmOverloads constructor(
ctx: Context,
attrs: AttributeSet? = null,
defStyleAttrs: Int = 0
) : View(ctx, attrs, defStyleAttrs)

AutoCompleteTextView dropdown not showing after device rotation

I have the following AutoCompleteTextView:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/offering_type_dropdown_layout"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/date_card_spacing"
android:layout_marginStart="4dp"
app:layout_constraintStart_toEndOf="#+id/offering_details_header_image"
app:layout_constraintEnd_toStartOf="#+id/offering_details_date_layout"
app:layout_constraintTop_toTopOf="parent"
android:hint="#string/offering_type_hint">
<AutoCompleteTextView
android:id="#+id/offering_details_type_dropdown"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textNoSuggestions"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:cursorVisible="false"/>
</com.google.android.material.textfield.TextInputLayout>
In my Activity's onCreate, I fill the AutoCompleteTextView like this:
String[] TYPES = new String[] {getString(R.string.burnt_offering), getString(R.string.meal_offering), getString(R.string.peace_offering), getString(R.string.sin_offering)};
ArrayAdapter<String> adapter = new ArrayAdapter<>(OfferingInputActivity.this, R.layout.offering_types_dropdown, TYPES);
mOfferingTypeCombo.setAdapter(adapter);
Then I populate the view using a Room database and preselect one of the values. In the Room callback, I do:
mOfferingTypeCombo.setText(getString(R.string.meal_offering)), false);
Everything works well on the initial run, and the dropdown is shown correctly:
Now I rotate the device to landscape. The very same code as above is executed but this time, the dropdown box only shows the current selection:
For some reason, all other entries in the adapter have disappeared. I have tried hacks such as setAdapter(null) before I set the adapter, but no success. Can someone tell me why after rotation, the dropdown is missing entries even though the exact same code is executed?
Currently there is a open bug on this topic.
You can use as workaround the setFreezesText method:
AutoCompleteTextView autoCompleteTextView =
view.findViewById(R.id.offering_details_type_dropdown);
autoCompleteTextView.setFreezesText(false);
The EditText set the freezesText=true. Due to this value after the rotation the TextView#onRestoreInstanceState(Parcelable) calls autoCompleteTextView.setText(value,true) which applies a filter to the adapter values.
This custom MaterialAutoCompleteTextView
resolves all problems:
class ExposedDropdownMenu : MaterialAutoCompleteTextView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun getFreezesText(): Boolean {
return false
}
init {
inputType = InputType.TYPE_NULL
}
override fun onSaveInstanceState(): Parcelable? {
val parcelable = super.onSaveInstanceState()
if (TextUtils.isEmpty(text)) {
return parcelable
}
val customSavedState = CustomSavedState(parcelable)
customSavedState.text = text.toString()
return customSavedState
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is CustomSavedState) {
super.onRestoreInstanceState(state)
return
}
setText(state.text, false)
super.onRestoreInstanceState(state.superState)
}
private class CustomSavedState(superState: Parcelable?) : BaseSavedState(superState) {
var text: String? = null
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeString(text)
}
}
}
Source
Note: It may not works correctly in older APIs like 23 or below.
one way is using a custom ArrayAdapter that prevents to Filter texts.
class NoFilterArrayAdapter : ArrayAdapter<Any?> {
constructor(context: Context, resource: Int) : super(context, resource)
constructor(context: Context, resource: Int, objects: Array<out Any?>) : super(context, resource, objects)
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults? {
return null
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {}
}
}
}
usage:
val adapter = NoFilterArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, items)
Like #Gabriele Mariotti mentioned, it's a bug. As mentioned in the posted link, I did this workaround which works well:
public class ExposedDropDown extends MaterialAutoCompleteTextView {
public ExposedDropDown(#NonNull final Context context, #Nullable final AttributeSet attributeSet) {
super(context, attributeSet);
}
#Override
public boolean getFreezesText() {
return false;
}
}
I solved this by deleting id from AutoCompleteTextView. This id is responsible for saving text after rotating.
Save string from AutoCompleteTextView in onSaveInstanceState method.
Code:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/inputAddress"
style="#style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/Address">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
/>
</com.google.android.material.textfield.TextInputLayout>
list_item.xml
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceSubtitle1"
/>
fragment.class
class CashierAddFragment : Fragment() {
var mBinding: FragmentCashierAddBinding? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentCashierAddBinding.inflate(inflater, container, false)
if(savedInstanceState == null) {
initAddressSpinner(binding, "")
} else {
initAddressSpinner(binding, savedInstanceState.getString(KEY_ADDRESS))
}
mBinding = binding
return binding.root
}
override fun onSaveInstanceState(outState: Bundle) {
val address = mBinding!!.inputAddress.getTrimText()
outState.putString(KEY_ADDRESS, address)
super.onSaveInstanceState(outState)
}
private fun initAddressSpinner(binding: FragmentCashierAddBinding, initValue: String?) {
val items = listOf("Option 1", "Option 2", "Option 3", "Option 4")
val adapter = ArrayAdapter(requireContext(), R.layout.list_item, items)
val autoTxtAddress = binding.inputAddress.editText as? AutoCompleteTextView
autoTxtAddress?.setText(initValue)
autoTxtAddress?.setAdapter(adapter)
}
}

How to open a fragment from another fragment using MVVM

I have a fragment ProductsFragment in which I have a button AddProduct when it is clicked I want to open a different fragment AddProductFragment.
I am using MVVM architecture
I went through this link and done the below mentioned implementation, but I did not quite understand or did not mention where fragment I want to navigate to
Error message
ProductsFragment - THE ISSUE IS HERE IN ONVIEWCREATED METHOD*
class ProductsFragment: Fragment() {
private lateinit var binding: ProductsBinding
private lateinit var navController: NavController
private lateinit var productsViewModel: ProductsViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.products, container, false)
val dao = SubscriberDatabase.getInstance(activity!!.applicationContext).productDAO
val repository = ProductRepository(dao)
val factory = ProductsViewModelFactory(repository, activity!!.applicationContext)
productsViewModel = ViewModelProvider(this, factory).get(ProductsViewModel::class.java)
binding.productsViewModel = productsViewModel
binding.lifecycleOwner = this
val view = binding.root
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
productsViewModel.navigateScreen.observe(activity!!, EventObserver {
navController.navigate(it) //issues is here
})
}
}
Products
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data class=".ProductsBinding">
<variable
name="productsViewModel"
type="com.rao.iremind.ProductsViewModel" />
</data>
<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Testing text"/>
<Button
android:id="#+id/btn_add_product"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add product"
android:onClick="#{() -> productsViewModel.addProduct()}"/>
<View
android:id="#+id/frgSpace"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</layout>
ProductViewModel
class ProductsViewModel (
private val repository: ProductRepository,
private val context: Context
): ViewModel() {
private val _navigateScreen = MutableLiveData<Event<Any>>()
val navigateScreen: LiveData<Event<Any>> = _navigateScreen
fun addProduct() {
Toast.makeText(context, "Products view model", Toast.LENGTH_LONG).show()
_navigateScreen.value = Event(R.id.frgSpace)
}
}
Event
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
class EventObserver<Int>(private val onEventUnhandledContent: (Int) -> Unit) : Observer<Event<Int>> {
override fun onChanged(event: Event<Int>?) {
event?.getContentIfNotHandled()?.let {
onEventUnhandledContent(it)
}
}
}
ProductsViewModelFactory
class ProductsViewModelFactory (
private val repository: ProductRepository,
private val context: Context
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ProductsViewModel::class.java)) {
return ProductsViewModel(repository, context) as T
}
throw IllegalArgumentException("Unknown View Model class")
}
}
I want to navigate to this fragment
class AddProductFragment: Fragment() {
private lateinit var binding: AddProductBinding
private lateinit var addProductViewModel: AddProductViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.add_product, container, false)
val dao = SubscriberDatabase.getInstance(activity!!.applicationContext).productDAO
val repository = ProductRepository(dao)
val factory = ProductsViewModelFactory(repository, activity!!.applicationContext)
addProductViewModel = ViewModelProvider(this, factory).get(AddProductViewModel::class.java)
binding.addProductViewModel = addProductViewModel
binding.lifecycleOwner = this
val view = binding.root
return view
}
}
Thanks
R
It seems that your EventObserver class is expecting an Int but you are sending Any in LiveData<Event<Any>>
Try changing
private val _navigateScreen = MutableLiveData<Event<Any>>()
val navigateScreen: LiveData<Event<Any>> = _navigateScreen
to
private val _navigateScreen = MutableLiveData<Event<Int>>()
val navigateScreen: LiveData<Event<Int>> = _navigateScreen
I would also recommend you to replace activity!! with viewLifecycleOwner in this line:
productsViewModel.navigateScreen.observe(viewLifecycleOwner, EventObserver {...})
so that your fragment does not receive any LiveData updates when its view is destroyed.

Hide progressBar when Fragment resumed

I have this code in ViewModel class to display a progressBar when data is loading :
class DetailViewModel(
context: Application,
private val schedulerProvider: BaseSchedulerProvider,
private val dataSource: RemoteDataSource
) : AndroidViewModel(context) {
val isFeedsLoading = ObservableBoolean(false)
fun showFeeds(goal: SavingsGoal): Disposable? {
EspressoIdlingResource.increment() // App is busy until further notice
isFeedsLoading.set(true)
return dataSource.getFeeds(goal.id)
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement()
}
isFeedsLoading.set(false)
}
?.subscribe({ feeds ->
}
) {
Timber.e(it)
}
}
And the layout :
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
app:items="#{vm.feeds}"/>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:visibleGone="#{vm.isFeedsLoading}"/>
</FrameLayout>
Problem : When we RESUME the Fragment. It shows ProgressBar and the visibility is not Gone as expected. What could be the reason?
And my Fragment :
#ActivityScoped
class DetailFragment #Inject
constructor() // Required empty public constructor
: DaggerFragment() {
#Inject
lateinit var viewModelFactory: DetailViewModel.DetailViewModelFactory
#Inject
lateinit var goal: SavingsGoal
private val compositeDisposable = CompositeDisposable()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val viewModel = ViewModelProviders.of(this, viewModelFactory)[DetailViewModel::class.java]
val root = inflater.inflate(R.layout.fragment_detail, container, false)
val binding = FragmentDetailBinding.bind(root).apply {
setVariable(BR.vm, viewModel)
goal = this#DetailFragment.goal
lifecycleOwner = this#DetailFragment
}
with(root) {
with(activity as AppCompatActivity) {
setupActionBar(binding.toolbar) {
setDisplayShowTitleEnabled(false)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
}
}
binding.vm?.showFeeds(goal)?.let { compositeDisposable.add(it) }
return root
}
override fun onDestroyView() {
super.onDestroyView()
compositeDisposable.clear()
}
}
BindingAdapter :
#BindingAdapter("visibleGone")
fun View.visibleGone(visible: Boolean) {
visibility = if (visible) View.VISIBLE else View.GONE
}
Not sure if this is what you were looking for..?
android:visibility="#{vm.isFeedsLoading} ? View.VISIBLE : View.GONE"
https://stackoverflow.com/a/47746579/7697633

Categories

Resources