Data binding LiveData from Transformation - Android Kotlin - android

I'm learning kotlin and android architecture components.
I have a simple use case of a map toggle button on a google map.
I want to use data binding to bind the map toggle button label to a MutableLiveData field in my ViewModel.
I set the mapType val in the MapViewModel from the onCreate method in the Activity. If I understand correctly, this should trigger the mapLabel val to change due to the use of Transformations.map.
Its not working... Why?
Here's my versions:
Android studio 3.2 Canary 4
kotlin_version = '1.2.21'
support = "27.1.0"
arch_core = "1.1.0"
databinding = "3.2.0-alpha04"
MapViewModel.kt
class MapViewModel : ViewModel() {
val mapType: MutableLiveData<MapType> = MutableLiveData()
val mapLabel: LiveData<String> = Transformations.map(mapType, {
if (it == MapType.MAP) "SAT" else "MAP"
})
}
enum class MapType {
SAT, MAP
}
activity_maps.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="vm"
type="uk.co.oliverdelange.wcr_android_kt.ui.map.MapViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MapsActivity">
<Button
android:id="#+id/map_toggle"
style="#style/Wcr_MapToggle"
android:layout_marginTop="110dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="7dp"
android:layout_gravity="top|end"
android:text="#{vm.mapLabel}" />
</fragment>
</FrameLayout>
</layout>
MapsActivity.kt
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
private lateinit var viewModel: MapViewModel
private lateinit var binding: ActivityMapsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_maps)
viewModel = ViewModelProviders.of(this).get(MapViewModel::class.java)
binding.vm = viewModel
val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
// I can do it this way, but I don't want to.
// viewModel.mapLabel.observe(this, Observer { map_toggle.text = it })
// Here is where i'm setting the MapType on the ViewModel.
viewModel.mapType.value = MapType.MAP
}
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
}
}
I've tested the binding with a MutableLiveData object where i set the string in the activity, and it works fine. The problem seems to be with the Transformations.map - have i just understood it wrong?
Also, whilst debugging, i see that the mapType val in my ViewModel has no observers (not sure if this is right or wrong, just interesting)

The issue here was that despite being bound to the mapLabel field, the view binding wasn't being updated when the value of the mapLabel field changed.
The reason is that I didn't set the lifecycle owner on the binding.
binding.setLifecycleOwner(this)
I realised my mistake after reading this blog post for the 10th time.
My new MapsActivity.kt
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
private lateinit var viewModel: MapViewModel
private lateinit var binding: ActivityMapsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_maps)
binding.setLifecycleOwner(this) //<- NEW!
viewModel = ViewModelProviders.of(this).get(MapViewModel::class.java)
binding.vm = viewModel
val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
// Here is where i'm setting the MapType on the ViewModel.
viewModel.mapType.value = MapType.MAP
}
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
}
}
On the plus side I learned a lot about how LiveData works internally!

Its not working... Why?
When you change the value of mapType, the value of mapLabel doesn't change immediately. You need to set value of mapLabel again. For ex, after you set value for mapType, you call method to update value of mapLabel
// Here is where i'm setting the MapType on the ViewModel.
viewModel.mapType.value = MapType.MAP
viewModel.updateMapLabel()
Your viewModel:
fun updateMapLabel() {
mapLabel.value =
if (it == MapType.MAP) "SAT" else "MAP"
}

Related

Android NullPointerException or null on Screen Orientation Change

When app is on portrait, everything is fine, but when I use landscape (I create 2 vision of the app, but only portrait works), the app suddenly closes and shows that java.lang.NullPointerException: findViewById(R.id.roll_button) must not be null or Unable to start activity ComponentInfo{MainActivity}: java.lang.NullPointerException.
I added some solutions from internet including adding android:configchanges or android:scrrenOrientation="portrait", it didnot help.
The code is show below:
MainActivity:
class MainActivity : AppCompatActivity() {
lateinit var diceImage: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rollButton: Button = findViewById(R.id.roll_button)!!
rollButton.setOnClickListener {
rollDice()
}
diceImage = findViewById(R.id.dice_image)
}
private fun rollDice() {
val randomInt = Random().nextInt(6) + 1
val drawableResource = when (randomInt) {
1 -> R.drawable.dice_1
2 -> R.drawable.dice_2
3 -> R.drawable.dice_3
4 -> R.drawable.dice_4
5 -> R.drawable.dice_5
else -> R.drawable.dice_6
}
diceImage.setImageResource(drawableResource)
}
}
Code below is activity_main.xml: (Most would be fine below)
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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"
android:configChanges="orientation|screenSize"
tools:context=".MainActivity"
>
<ImageView
android:id="#+id/dice_image"
android:layout_width="123dp"
android:layout_height="96dp"
android:layout_marginStart="144dp"
android:layout_marginTop="112dp"
android:configChanges="orientation|screenSize"
android:src="#drawable/empty_dice"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="TODO" />
<Button
android:id="#+id/roll_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="#string/roll"
android:configChanges="orientation|screenSize"
app:layout_constraintBottom_toTopOf="#+id/textView2"
app:layout_constraintStart_toEndOf="#+id/textView2"/>
</android.support.constraint.ConstraintLayout>
Please Add your LogCat Error and LandscapeMode layout details.
I have suggestion for you use below solution.
Use View Binding Or Data Binding
avoid to findViewById() more easy to use in development.
Just Follow Some Step
add below line in build.gradle app
android {
buildFeatures {
viewBinding true
}
}
2.Use this keyword tools:viewBindingIgnore="true" in parent Layout. Like this.
<LinearLayout
tools:viewBindingIgnore="true" >
</LinearLayout>
Your onCreate() of activity be like this.
private lateinit var binding: ResultProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
Now you can access all id easily.
binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }
For more details visit
https://developer.android.com/topic/libraries/view-binding
For DataBinding Visit
https://developer.android.com/topic/libraries/data-binding
never ever use !!(not null assertion opeartor)..No good production app usage it..the problem is that the id is not found in file activity_main.are you using 2 different XML files, one for landscape and one for portrait. if yes, just check if the configuration in which u get crash has that id, otherwise, there is no reason for crash..data binding and view binding is a good way to avoid null pouiter exception
One of the tags of the question is Kotlin, so here's a simple example demonstrating savedInstanceState(). It's a very simple slot machine 'game' that preserves the state of the fruit images and the score.
MainActivity.kt
// I've missed out the imports, just for the sake of brievity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var images: IntArray
private val spinButton by lazy {binding.spinButton}
private val scoreText by lazy {binding.scoreTextView}
private val imageViews by lazy {
listOf<ImageView>(binding.imageView1, binding.imageView2, binding.imageView3)
}
private var score = 10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
images = savedInstanceState?.getIntArray(IMAGES)
?: intArrayOf( R.drawable.cherries, R.drawable.cherries, R.drawable.cherries)
score = savedInstanceState?.getInt(SCORE)
?: 10
drawFruit()
spinButton.setOnClickListener { drawslotMachine() }
}
// when the screen is rotated (I think onDestroy is called) the app saves images and score in a
// key value pair, IMAGES and SCORE being the keys
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putIntArray(IMAGES, images)
outState.putInt(SCORE, score)
}
private fun drawslotMachine() {
score += 10
setRandomImages()
drawFruit()
}
private fun drawFruit() {
for (i in 0..(imageViews.size - 1)) {
imageViews[i].setImageResource(images[i])
}
scoreText.text = score.toString()
}
private fun setRandomImages() {
images = intArrayOf(randomImage(), randomImage(), randomImage())
}
private fun randomImage(): Int{
val r = Random.nextInt(15)
return when(r){
in 0..4 -> R.drawable.apple
in 5..8 -> R.drawable.green_apple
9 -> R.drawable.cherries
in 10..12 -> R.drawable.mushroom
in 13..14 -> R.drawable.orange
else -> R.drawable.mushroom
}
}
}
This is the Constants.kt file, which contains the keys for saveInstanceState with some dummy values ("Int Array" & "Score"). It's in the same directory as MainActivity.kt
package com.example.gambling
const val IMAGES = "Int Array"
const val SCORE = "Score"
A mistake I made was blithely accepting Android Studios code completion and using:
override fun onSaveInstanceState(outState: Bundle, outPersistentState:
PersistableBundle)
instead of just:
onSaveInstanceState(outState: Bundle)
This is also an answer to this question which goes into some of the potential pitfalls
Also a Massive Credit to Joed Goh, this answer is based on this work

Kotlin databinding shared viewmodel not updating both views

I'm trying to use a shared viewmodel for an activity and a fragment displayed in the activity because both need to be updated by the viewmodel. The fragment is constantly updated using MutableLiveData, but the activity is not and I don't really understand why. For readability reasons I did cut out the layout parameters in the .xml files which are irrelevant to the problem.
My activity code looks like the following:
class MainActivity : AppCompatActivity() {
private lateinit var _binding : MainActivityBinding
private val _viewModel : MySharedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
_binding.viewModel = _viewModel
_binding.lifecycleOwner = this
...
}
}
The lifecycle owner of the binding is set, yet MutableLiveData does not seem to update the activity.
In the activity layout file is the following:
<layout>
<data>
<variable
name="viewModel"
type="com.customApp.viewModels.MySharedViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.appbar.MaterialToolbar>
<TextView
android:id="#+id/totalTime"
android:text="#{viewModel.topBarText}"
.../>
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.bottomnavigation.BottomNavigationView
.../>
<androidx.fragment.app.FragmentContainerView
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
So I have the topbar with the text view that always shall show the text, the bottom navigation view as tabbar and the fragment container view that contains the fragments.
The fragment code is:
class FirstFragment : Fragment() {
private val _viewModel : MySharedViewModel by viewModels()
private lateinit var _binding : FirstFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = DataBindingUtil.inflate(inflater, R.layout.first_fragment, container, false)
_binding.viewModel = _viewModel
_binding.lifecycleOwner = viewLifecycleOwner
return _binding.root
}
}
Also here the lifecycleowner is set for the binding and in the fragment layout it is bound to the viewmodel:
<layout>
<data>
<variable
name="viewModel"
type="com.customApp.viewModels.MySharedViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
...
android:text="#{viewModel.fragmentText}" />
<Button
android:id="#+id/startButton"
android:onClick="#{() -> viewModel.startIteration()}"
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And in my viewmodel I simply update texts that shall be displayed:
class MySharedViewModel : ViewModel() {
var topBarText : MutableLiveData<String> = MutableLiveData<String>("Hi")
var fragmentText : MutableLiveData<String> = MutableLiveData<String>("There")
private val stringsForTop = arrayOf("Hi","How", "You")
private val stringsForFragment = arrayOf("There","Are", "?")
private var index = 0
fun startIteration() {
kotlin.concurrent.fixedRateTimer(initialDelay = 1L, period = 1000L) {
viewModelScope.launch(Dispatchers.Main) {
topBarText.value = stringsForTop[index]
fragmentText.value = stringsForFragment[index]
index++
}
}
}
}
Now while the fragment text is updated, the text in the top bar is not updated and always just displays "Hi". I have the feeling, that the activity as lifecycleobserver for the view model is overwritten when the fragment is initialized and its viewLifecycleOwner is bound to the view model.
Am I missing something or is there another way to register both, activity and fragment with their lifecycleowners, at the viewmodel? Any help appreciated.
If you are using by viewmodels<>() in Fragment, it will create new instance of ViewModel coupled to Fragment life cycle. if you want same instance of ViewModel as it is Activity use by activityViewModels<>(), try below code in Fragment
private val _viewModel : MySharedViewModel by activityViewModels()

Bottom Navigation View Null

I am trying to set a badge to a BottomNavigationView by following this straightforward approach.
However, when I initialize the BottomNavigationView I get:
java.lang.IllegalStateException: view.findViewById(R.id.bottom_navigation_view) must not be null
I am initializing the BottomNativigationView from a fragment. I am guessing that is the issue, but I cannot figure out the solution.
private lateinit var bottomNavigation: BottomNavigationView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
bottomNavigation = view.findViewById(R.id.bottom_navigation_view)
}
Here is the BottomNavigationView xml for the Activity that sets up navigation for the fragments.
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorWhite"
app:itemIconTint="#color/navigation_tint"
app:itemTextColor="#color/navigation_tint"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/bottom_navigation" />
It feels like I am missing something simple, but I cannot figure out what. Thanks!
You have many options to communicate betwean fragments - activity and between fragment's itself..
You should not try access activity views from fragment.
Solution 1: Share data with the host activity
class ItemViewModel : ViewModel() {
private val mutableSelectedItem = MutableLiveData<Item>()
val selectedItem: LiveData<Item> get() = mutableSelectedItem
fun selectItem(item: Item) {
mutableSelectedItem.value = item
}
}
class MainActivity : AppCompatActivity() {
// Using the viewModels() Kotlin property delegate from the activity-ktx
// artifact to retrieve the ViewModel in the activity scope
private val viewModel: ItemViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.selectedItem.observe(this, Observer { item ->
// Perform an action with the latest item data
})
}
}
class ListFragment : Fragment() {
// Using the activityViewModels() Kotlin property delegate from the
// fragment-ktx artifact to retrieve the ViewModel in the activity scope
private val viewModel: ItemViewModel by activityViewModels()
// Called when the item is clicked
fun onItemClicked(item: Item) {
// Set a new item
viewModel.selectItem(item)
}
}
Solution 2: Receive results in the host activity
button.setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager
.setFragmentResultListener("requestKey", this) { requestKey, bundle ->
// We use a String here, but any type that can be put in a Bundle is supported
val result = bundle.getString("bundleKey")
// Do something with the result
}
}
}
There is many more ways but these are latest approaches from Google.
Check this reference: https://developer.android.com/guide/fragments/communicate
You can access the activity from its fragment by casting activity to your activity class, and inflate the views then.
bottomNavigation = (activity as MyActivityName).findViewById(R.id.bottom_navigation_view)

Changing toolbar title in each fragment using MVVM and Databinding

I am looking to change the toolbar title,which is in my main activity, in my fragments page. My project is based on MVVM Architecture, with databinding.
This is my main_activity.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="viewModel"
type="com.lalsoft.toolbar_mvvm_databinding.viewmodel.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp"
android:theme="#style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:minHeight="?attr/actionBarSize"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleTextColor="#android:color/white"
app:navigationIcon="#drawable/ic_arrow_back"
android:background="?attr/colorPrimary"
app:popupTheme="#style/AppTheme.PopupOverlay"
app:navigationOnClickListener="#{()->viewModel.navBackClicked()}"
app:title="#{viewModel.toolbarTitle}"/>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
This is my mainActivity.kt
private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var dataBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
//setSupportActionBar(dataBinding.toolbar)
//dataBinding.toolbar.setNavigationIcon(R.drawable.ic_arrow_back)
viewModel.navClicked.observe(this, navClickObserver)
viewModel.toolbarTitle.observe(this, toolbarTitleObserver)
dataBinding.viewModel = viewModel
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction().replace(
R.id.fragment_container,
FirstFragment()
).commit()
}
}
private val navClickObserver = Observer<Boolean> {
supportFragmentManager.popBackStack()
Log.e(TAG, "Nav Back clicked")
}
private val toolbarTitleObserver = Observer<String> {
Log.e(TAG, "Title set : $it")
}
}
And this is my MainViewModel
private const val TAG = "MainViewModel"
open class MainViewModel : ViewModel() {
val toolbarTitle: MutableLiveData<String> = MutableLiveData()
private val _navClicked: MutableLiveData<Boolean> = MutableLiveData()
val navClicked: LiveData<Boolean> = _navClicked
init {
Log.e(TAG, "Inside Init")
//toolbarTitle.value ="Main Activity"
}
fun navBackClicked() {
_navClicked.value = true
}
}
Now i am trying to change the toolbar title in FragmentViewModel by changing the mutable toolbarTitle of my mainActivityViewModel.
private const val TAG = "FirstViewModel"
class FirstViewModel : MainViewModel() {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToFragment: LiveData<Event<String>>
get() = _navigateToDetails
init {
Log.e(TAG, "Inside Init")
toolbarTitle.value="First Fragment"
}
fun onBtnClick() {
_navigateToDetails.value = Event("Second Fragment")
}
}
This is my fragment class
private const val TAG = "FirstFragment"
class FirstFragment : Fragment() {
private lateinit var viewModel: FirstViewModel
private lateinit var dataBinding: FirstFragmentBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dataBinding = DataBindingUtil.inflate(inflater, R.layout.first_fragment, container, false)
return dataBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(FirstViewModel::class.java)
viewModel.toolbarTitle.observe(viewLifecycleOwner, toolbarTitleObserver)
viewModel.navigateToFragment.observe(viewLifecycleOwner, navigateToFragmentObserver)
//(activity as MainActivity?)!!.toolbar.title = "Check"
dataBinding.viewModel = viewModel
}
private val toolbarTitleObserver = Observer<String> {
Log.e(TAG, "Title set : $it")
//(activity as MainActivity?)!!.toolbar.title = "Check"
//Log.e(TAG, "Title set : Check")
}
private val navigateToFragmentObserver = Observer<Event<String>> { it ->
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
Log.i(TAG, "checkIt string $it")
parentFragmentManager.beginTransaction().replace(
R.id.fragment_container,
SecondFragment()
).addToBackStack(null).commit()
}
}
}
Eventhough its observing the toolbarTitle correctly,the Title in my program is not changing..
Hope to get some help to get out of this issue.
This is my sample git project where i am trying to do this : github
I struggle with the same!
I think the problem is the lifecycleowner. But could not find an answer.
Currently I observe the values from the the ViewModel and assign the values inside the observer.
But I think there is a better way!
If your Fragment is using a ViewModel should be scoped to a host Activity, use by activityViewModels() delegate:
#AndroidEntryPoint
class HomeFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
}
I think also this answer will help.
https://stackoverflow.com/a/62560605
When you use by viewModels, you are creating a ViewModel scoped to that individual Fragment - this means each Fragment will have its own individual instance of that ViewModel class. If you want a single ViewModel instance scoped to the entire Activity, you'd want to use by activityViewModels

DataBinding & LiveData : two implementations (Kotlin & Java) and can't make the Java impl working

I'm facing issues while using DataBinding and LiveData in a Java projet. I followed a previous course in Kotlin and when I try to implement the same behaviors I just can't make it work. I'm clearly missing something in terms of understanding so I'd like to have you thoughts.
I'll paste the code from the Kotlin (working) example and then the Java (not working) one.
KOTLIN
score_fragment.xml
...
<data>
<variable
name="scoreViewModel"
type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/score_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".screens.score.ScoreFragment">
<TextView
android:id="#+id/score_text"
...
android:text="#{String.valueOf(scoreViewModel.score)}"
.../>
...
ScoreFragment.kt
class ScoreFragment : Fragment() {
private lateinit var viewModelFactory: ScoreViewModelFactory
private lateinit var viewModel: ScoreViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate view and obtain an instance of the binding class.
val binding: ScoreFragmentBinding = DataBindingUtil.inflate(
inflater,
R.layout.score_fragment,
container,
false
)
// Get args using by navArgs property delegate
val scoreFragmentArgs by navArgs<ScoreFragmentArgs>()
viewModelFactory = ScoreViewModelFactory(scoreFragmentArgs.score)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ScoreViewModel::class.java)
binding.scoreViewModel = viewModel
binding.lifecycleOwner = this
return binding.root
}
}
ScoreViewModel.kt
class ScoreViewModel(finalScore: Int) : ViewModel() {
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
get() = _score
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
get() = _eventPlayAgain
init {
Timber.i("ScoreViewModel created")
_score.value = finalScore
}
fun onPlayAgain() {
_eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
_eventPlayAgain.value = false
}
override fun onCleared() {
super.onCleared()
Timber.i("ScoreViewModel cleared")
}
}
Explanations : let's focus only on the score value. In ScoreViewModel the value is of type LiveData. When the fragment's launched, the value is correctly displayed on the screen through "#{String.valueOf(scoreViewModel.score)}". This works correctly.
JAVA
activity_main.xml
<data>
<variable
name="noteViewModel"
type="com.example.architectureapp.viewModel.NoteViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/textview"
android:text="#{noteViewModel.test}" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private NoteViewModel mNoteViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
binding = ActivityMainBinding.inflate(getLayoutInflater());
mNoteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);
binding.setNoteViewModel(mNoteViewModel);
binding.setLifecycleOwner(this);
}
}
NoteViewModel
public class NoteViewModel extends AndroidViewModel {
public MutableLiveData<String> test = new MutableLiveData<>("TesT");
public NoteViewModel(#NonNull Application application) {
super(application);
}
}
Explanations : here I'm setting a MutableLiveData test whith a value of "TesT" and then I intent to display it using android:text="#{noteViewModel.test}". But the text is never displayed and remains blank.
Obviously there is something wrong but despite the syntaxic differences between the two implementations I just can't figure out why the Java version is not displaying the value in the Textview.
EDIT
Thanks to Rajnish suryavanshi I was not getting my binding the right way, I had to only use :
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
This one set the content view with the layout provided AND return the binding.
Alternatively you can do :
binding = ActivityMainBinding.inflate(getLayoutInflater()); (returns the binding but does not set the content view)
setContentView(binding.getRoot()); (set the content view with the binding root view)
I found this misleading -> https://developer.android.com/topic/libraries/data-binding/expressions
It states that we can replace
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
by
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
which is not the same !
Happy to get the subtility now !
Take a look on this two lines.
setContentView(R.layout.activity_main);
binding = ActivityMainBinding.inflate(getLayoutInflater());
You are not creating your binding while inflating the layout. Instead of setContentView use DataBindingUtil.setContentView(this, R.layout.activity_main);
And now you can get the view using layout inflater
binding = ActivityMainBinding.inflate(getLayoutInflater());

Categories

Resources