I encountered a problem when using databinding and viewpager2. I used viewpager2 in a fragment. FragmentA in viewpager2 wanted to share the viewmodel of the fragment.
A TextView attribute text is bound in fragmentA, but postValue in viewmodel cannot change the text of TextView
But fragmentA is bound to a click event (TextView has a click, triggering the sendCode() function), which can be triggered in the viewmodel
fragmentA:
class SignUpMainFragment(val vm:SignUpFragmentVM):Fragment() {
private var mBinding:FragmentSignUpMainBinding?=null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_sign_up_main,container,true)
mBinding?.signUp = vm
return mBinding?.root!!
}
}
Layout of fragmentA:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="signUp"
type="com.xxx.SignUpFragmentVM" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="#+id/et_email"
android:layout_width="468dp"
android:layout_height="94dp"
android:layout_marginStart="40dp"
android:hint="Email"
android:text="#={signUp.txtEmail}"
android:textSize="30sp"
android:textColor="#color/color_aaa280"
android:drawableStart="#mipmap/img_mail"
android:paddingStart="26dp"
android:drawablePadding="18dp"
android:inputType="textEmailAddress"
android:background="#drawable/shape_edit_bg_e7e7e7"
app:layout_constraintTop_toTopOf="#id/et_name"
app:layout_constraintStart_toEndOf="#id/et_name" />
<TextView
android:id="#+id/tv_send_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{signUp.txtSendEmail}"
android:textSize="30.5sp"
android:textColor="#color/color_71cbc0"
android:onClick="#{signUp.click}"
android:clickable="true"
android:layout_marginStart="25dp"
app:layout_constraintTop_toTopOf="#id/et_lock_psw"
app:layout_constraintBottom_toBottomOf="#id/et_lock_psw"
app:layout_constraintStart_toEndOf="#id/et_lock_psw"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
SignUpFragmentVM:
class SignUpFragmentVM constructor():ViewModel() {
val txtSendEmail = MutableLiveData("Get verified.")
....
private fun sendCode(){
viewModelScope.launch {
...
//TODO:60s Countdown
txtSendEmail.postValue("60")
}
}
}
You need more
mBinding?.signUp = vm
mBinding?.lifecycleOwner = this // => add
Related
I have a problem using viewmodel in fragment with data binding.
In my programm i'm using only one activity and fragments. I organized transitions between fragments using navigation. And i want to use data binding in my fragment, but when running the application only with binding, everything works well when I pass to the binding viewmodel when switching to this fragment of the application, it crashes.
I would be very grateful for your help!
My fragment
class TimeSettings : Fragment() {
private lateinit var viewModel: TimeSettingsViewModel
private lateinit var binding: FragmentTimeSettingsBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentTimeSettingsBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
binding.viewmodel = viewModel;
return binding.root
}
}
Object for viewmodel
object TimeData {
var inhale: Int = 4
private val _currentTimeInhale = MutableLiveData<Int>()
val currentTimeInhale: LiveData<Int>
get() = _currentTimeInhale
init{
_currentTimeInhale.value = inhale
}
fun changeCurrentTimeInhale(){
_currentTimeInhale.value = 5
}
}
Viewmodel
class TimeSettingsViewModel : ViewModel() {
val currentInhale: LiveData<Int>
get() = TimeData.currentTimeInhale
fun onChange() = TimeData.changeCurrentTimeInhale()
}
xml file for fragment
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="viewmodel"
type="ivakin.first.my_first_app.viewmodels.TimeSettingsViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textView"
android:layout_width="95dp"
android:layout_height="59dp"
android:text="#{viewmodel.currentInhale}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.452" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Set"
android:onClick="#{()->viewmodel.onChange()"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
U Forgot Initialize ViewModel :
viewmodel = TimeSettingsViewModel()
Also dont use return binding.root
Why is val rv: RecyclerView = findViewById(R.id.material_list_rv) returning null?
MainActivity
class MainActivity : AppCompatActivity() {
var adaptor: MaterialListAdaptor = MaterialListAdaptor()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<MaterialListFragment>(R.id.fragment_container_view)
}
}
initRecyclerView()
setRecyclerViewData()
}
private fun initRecyclerView(){
val rv: RecyclerView = findViewById(R.id.material_list_rv)
rv.layoutManager = LinearLayoutManager(this#MainActivity)
rv.adapter = adaptor
}
private fun setRecyclerViewData(){
val l = DataSource.createMaterialList()
adaptor.setDataSet(l)
}
}
activity_main
<?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=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MaterialListFragment
class MaterialListFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_material_list, container, false)
}
}
fragment_material_list
<?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=".MaterialListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/material_list_rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="#layout/material_list_rv_list_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
The problem
You placed the call to findViewById in MainActivity.onCreate. It's too early for that, the Fragment's view hasn't been created at that point.
You should be looking for the view in your MaterialListFragment. If you call findViewById in onCreateView, it will work. Like this:
val root = inflater.inflate(R.layout.fragment_material_list, container, false)
val rv = root.findViewById(R.id. material_list_rv)
// Do whatever (eg assign rv to a field in the Fragment to use it later)
return root
Some advice
Even if your code worked, try to have Activities, Fragments and custom Views manage their own children.
If the upper containers dive deep into their children, it's very easy to make mistakes (like the one here!) and you lose the ability to update internal details of Fragments and Views without breaking the containing Activity.
Activity/Fragment lifecycle chart
To orient yourself, keep this in mind (though this diagram is EXTREMELY simplified):
You are looking for RecylerView in MainActivity, where it does not exist.
Instead you should try to access it in FragmentActivity.
I'm trying to make a simple button that changes the fragment, it doesn't give any error, it just doesn't work.
XML code:
<?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=".ui.follow.FollowFragment">
<ImageView
android:id="#+id/imageView4"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="background"
android:scaleType="fitXY"
android:src="#drawable/background" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<Button
android:id="#+id/loginButton"
android:layout_width="250dp"
android:layout_height="60dp"
android:layout_marginTop="50dp"
android:backgroundTint="#color/btn_col"
android:text="Log In" />
</LinearLayout>
</FrameLayout>
Fragment code:
class FollowFragment : Fragment() {
companion object {
fun newInstance() = FollowFragment()
}
private lateinit var viewModel: FollowViewModel
private lateinit var loginButton: Button
private lateinit var viewFollow: View
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_follow, container, false)
viewFollow = root
loginButton = viewFollow.findViewById(R.id.loginButton)
var loginFragment = LoginFragment()
println("Hello :(")
loginButton.setOnClickListener {
println("Hello :)")
//parentFragmentManager.beginTransaction().replace(R.id.nav_host_fragment, loginFragment).commit()
}
return inflater.inflate(R.layout.fragment_follow, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(FollowViewModel::class.java)
// TODO: Use the ViewModel
}
}
Only the sad hello is printed, it seems to recognize the button, but the listener don't work. The app has a bottom navigation bar, and the screens are fragments.
By returning inflater.inflate(R.layout.fragment_follow, ...) you're inflating a second layout to be used for your fragment instead of the one that you're using to set the listener.
You should be returning root.
I have a string variable in my fragment which I want to use in XML with data binding
This is my fragment
class HomeFragment : Fragment() {
var struc: String = "Organization Structure"
lateinit var binding: FragmentHomeBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.getRoot()
}}
This is the XML
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="data"
type="com.mountmeru.view.HomeFragment" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.HomeFragment">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/tvOrganization"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/ivOrganization"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="#{data.struc}"
android:textAlignment="center"
android:textColor="#color/mountmerured"
android:textSize="15sp" />
</FrameLayout></layout>
When I run the application the textview text is never set.
What is wrong here?
First way :
This not direct way which you asked. But you can also do using data class, this is little bit long way but you can add multiple data.
Make data class
data class Data(var struc: String = "Organization Structure")
Add this in xml
<data>
<variable
name="dataModel"
type="Data" /> // here data class path of step1
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.HomeFragment">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/tvOrganization"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/ivOrganization"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="#{dataModel.struc}" // use like this
android:textAlignment="center"
android:textColor="#color/mountmerured"
android:textSize="15sp" />
</FrameLayout></layout>
In Activity or fragment
private fun setData() {
val data = Data(struc= "Organization")
bindObject.dataModel = data
}
Second way :
Or Else direct way is
<data>
<variable
name="struc"
type="String" />
</data>
and from activity/fragment
bindObject.struc = "Organization Structure";
Third way :
Or In your case you need to do this in onCreateView and everything works
binding.data = this;
I think you have mistaken while the layout inflating. Please try below code
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater, layoutId, container, false)
return binding.getRoot()
}}
I have an object that will fill some fields in my xml, but a mutablelivedata return a T value and I can't use the T properties. What I need to do to use the T object in my xml?
I don't want to create a mutablelivedata for each object member.
Sorry for my bad english..
class ExampleFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val viewModel = ViewModelProviders.of(this).get(MyVm::class.java)
val binding = DataBindingUtil.inflate<FragmentExampleBinding>(
inflater,
R.layout.fragment_house,
container,
false
)
binding.myVm = viewModel
binding.lifecycleOwner = this
return binding.root
}
}
class MyVm: ViewModel(){
val house: MutableLiveData<House> by lazy { MutableLiveData<House>().apply{ value =
House().apply{
door = "First door"
window = "My window"
}
} }
val thing: MutableLiveData<String> by lazy { MutableLiveData<String>().apply{ value = "something" } }
}
class House(){
var door: String = ""
var window: String = ""
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="my_vm" type="br.com.MyVm"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
orientation="vertical"
>
<!--I need this work-->
<TextView
android:layout_width="wrap_parent"
android:layout_height="wrap_parent"
android:text="my_vm.house.door"
/>
<!--I need this work-->
<TextView
android:layout_width="wrap_parent"
android:layout_height="wrap_parent"
android:text="my_vm.house.window"
/>
<TextView
android:layout_width="wrap_parent"
android:layout_height="wrap_parent"
android:text="my_vm.thing"
/>
</LinearLayout>
<layout>
This code is correcty! A tried run and worked, I thounght it didn't work because the android studio didn't show the attributes when press ctrl + space.