Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)

like image 531
OliverDeLange Avatar asked Mar 29 '18 08:03

OliverDeLange


2 Answers

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!

like image 118
OliverDeLange Avatar answered Nov 10 '22 04:11

OliverDeLange


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"
}
like image 20
RoShan Shan Avatar answered Nov 10 '22 03:11

RoShan Shan