Search code examples
androidkotlinandroid-architecture-componentsandroid-livedataandroid-viewmodel

Data binding LiveData from Transformation - Android Kotlin


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)


Solution

  • 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!