How to call liveData observer from another liveData observer - android

I am working on livedata. I have two apis but one api is dependent on another api. Based on first api response i am calling another api using livedata observer. I am calling from inside observer is this a right approach or any other alternative
mainViewModel.getListLiveData().observe(MainActivity.this, new Observer<List<Student>>() {
#Override
public void onChanged(#Nullable List<Student> list) {
if(list.size() > 0){
mainViewModel.getStudentLiveData().observe(MainActivity.this, new Observer<Student>() {
#Override
public void onChanged(#Nullable Student student) {
}
});
}
}
});

Observe on the student LiveData exposed by the mainViewModel. In the viewmodel make student live data change with change in List LiveData using Transformations or using a MediatorLiveData
In your activity:
mainViewModel.getStudentLiveData().observe(this, new Observer<Student>() {
student -> {}
});
In your viewmodel:
private MutableLiveData<List< Student>> studentListLiveData = new MutableLiveData(); // this will hold result of your first api call
private MutableLiveData<Student> studentLiveData = new MutableLiveData(); // this will hold result of your second api call
private void fetchData() {
fetchStudentList(new Callback {
result -> {
studentListLiveData.value = result;
fetchStudent(new Callback {
result -> { studentLiveData.value = result; }
});
}
})
}
public LiveData<Student> getStudentLiveData() {
return studentLiveData;
}

Related

MVVM - MediatorLiveData onChanged not being called

I'm not sure where I'm implementing MediatorLiveData incorrectly.
I have a Repository class that exposes a mutable live data object:
private MutableLiveData<List<UserTransaction>> mTransactionsLiveData
= new MutableLiveData<>();
public MutableLiveData<List<UserTransaction>> getTransactionsLiveData() {
return mTransactionsLiveData;
}
I have passed reference of this MutableLiveData to my ViewModel class with a getter:
public class UserTransactionsViewModel extends AndroidViewModel {
private LiveData<List<UserTransaction>> mTransactionsLiveData;
private SharedRepo mRepo;
public UserTransactionsViewModel(#NonNull Application application) {
super(application);
mRepo = new SharedRepo(application);
mTransactionsLiveData = mRepo.getTransactionsLiveData();
}
public LiveData<List<UserTransaction>> getTransactionsLiveData() {
return mTransactionsLiveData;
}
public void getUserTransactions(int userId) {
mRepo.getUserTransactions(userId);
}
}
And in my Fragment class, I am observing it:
mViewModel.getTransactionsLiveData().observe(getViewLifecycleOwner(), new Observer<List<UserTransaction>>() {
#Override
public void onChanged(List<UserTransaction> list) {
//Observing it here
loadDataIntoRecycler(list);
}
});
Where I am confused:
I am now trying to add a MediatorLiveData to format the Data that the ViewModel receives from the Repository class.
So I first added a new new MediatorLiveData<>() in the ViewModel
private MediatorLiveData<List<UserTransaction>> mediatorLiveData
= new MediatorLiveData<>();
and attached the .addSource
private MediatorLiveData<List<UserTransaction>> mediatorLiveData = new MediatorLiveData<>();
public UserTransactionsViewModel(#NonNull Application application) {
super(application);
mRepo = new SharedRepo(application);
mTransactionsLiveData = mRepo.getTransactionsLiveData();
mediatorLiveData.addSource(mTransactionsLiveData, new Observer<List<UserTransaction>>() {
#Override
public void onChanged(List<UserTransaction> userTransactions) {
Log.d(TAG, "onChanged: Formatting data...");
for(int i = 0; i < list.size(); i++){
list.get(i).setTime("Changing time");
}
}
});
}
I am confused, do I make the observer in the Fragment listen to the MediatorLiveData, instead of the LiveData? Because the .onChanged method on mediatorLiveData is never called.
The .onChanged method on the mediatorLiveData is never called because it's not observed.
And, if you have already tried to observe the mediatorLiveData from the Fragment and .onChanged method is still not called, it means the observee is never changed. Never changed means never being setValue or postValue.
Like this:
mediatorLiveData.addSource(mTransactionsLiveData,
new Observer<List<UserTransaction>>() {
#Override
public void onChanged(List<UserTransaction> userTransactions) {
Log.d(TAG, "onChanged: Formatting data...");
for(int i = 0; i < list.size(); i++){
list.get(i).setTime("Changing time");
}
mediatorLiveData.setValue(list);
}
});
Or, using Transformations.map (and Java 8 lambda) may be cleaner (while observe userTransactions from the Fragment):
public LiveData<List<UserTransaction>> userTransactions;
userTransactions = Transformations.map(mTransactionsLiveData, list -> {
Log.d(TAG, "onChanged: Formatting data...");
for(int i = 0; i < list.size(); i++){
list.get(i).setTime("Changing time");
}
return list;
});
MediatorLiveData only observes its sources while it's active (has an active observer) so yes, you should observe it in your Fragment. When there are no active observers, onChanged will not be called.

Android observe data not being called

Hi I am new to android development and am trying to get my head around the architecture but have spent the past 2 days trying to figure out LiveData
I am using an SDK which allows me to scan some sensors, I display the sensors on the device and then toggle a switch to connect the sensor. Once the sensor is connected I have created a button which runs a funcion called startMeasuring() everytime some data is measured a callback is hit and this is where my struggle begins.
In my MainActivity I have the following code which is ran once I toggle the switch to connect to the sensor.
public void onConnectedSensorClick(BluetoothDevice sensor, Integer position, Boolean checked) {
XsensDotDevice xsDevice = new XsensDotDevice(this, sensor, new XsDevice(this));
if (checked) {
xsDevice.connect();
mMainActivityViewModel.addConnectedSensor(xsDevice);
}
}
The XsensDotDevice() expects 3 parameters the context, scanned sensor and the callback class.
In my callback calss the following callback function is overridden
#Override
public void onXsensDotDataChanged(String s, XsensDotData xsensDotData) {
}
This function is the one which gets triggered when I start measuring and the sensor sends the device a measurement.
I am have created a ViewModel and Repository which is what I want to use to store this data so I can access it back in my MainActivity using an Observer
I got the ViewModel and Repository working for my scanned devices but I'm not sure how to get this working for the measurement data because I can't access the ViewModel in my callback class XsDevice() to pass the data to the Repository
What I want to do is somehow pass the XsensDotData (measurement data) to the SensorDataRepository and then create an Observer in my MainActivity like so.
mMainActivityViewModel.getSensorData().observe(this, new Observer<XsensDotData>() {
#Override
public void onChanged(XsensDotData xsensDotData) {
for(int i = 0; i< xsensDotData.getFreeAcc().length; i++){
Log.d("Sensor Data Acceleration " + i, String.valueOf(xsensDotData.getFreeAcc()[i]));
}
}
});
I have already created a Repository and ViewModel which i will show below
Repository
public class SensorDataRepository {
private static SensorDataRepository instance;
private XsensDotData dataSet = new XsensDotData();
public static SensorDataRepository getInstance() {
if (instance == null) {
instance = new SensorDataRepository();
}
return instance;
}
public MutableLiveData<XsensDotData> getSensorData() {
MutableLiveData<XsensDotData> data = new MutableLiveData<>();
data.setValue(dataSet);
return data;
}
public void addSensorData(XsensDotData data) {
dataSet = data;
}
}
ViewModel
public class MainActivityViewModel extends ViewModel {
private MutableLiveData<ArrayList<BluetoothDevice>> mScannedSensors;
private ScannedSensorRepository mScannedSensorRepo;
private MutableLiveData<ArrayList<XsensDotDevice>> mConnectedSensors;
private ConnectedSensorRepository mConnectedSensorRepo;
private MutableLiveData<XsensDotData> mSensorData;
private SensorDataRepository mSensorDataRepo;
public void init() {
if (mScannedSensors != null) {
return;
}
mScannedSensorRepo = ScannedSensorRepository.getInstance();
mScannedSensors = mScannedSensorRepo.getScannedSensors();
if (mConnectedSensorRepo != null) {
return;
}
mConnectedSensorRepo = ConnectedSensorRepository.getInstance();
mConnectedSensors = mConnectedSensorRepo.getConnectedSensors();
if (mSensorDataRepo != null) {
return;
}
mSensorDataRepo = SensorDataRepository.getInstance();
mSensorData = mSensorDataRepo.getSensorData();
}
public LiveData<ArrayList<BluetoothDevice>> getScannedSensors() {
return mScannedSensors;
}
public void addScannedSensor(BluetoothDevice device) {
mScannedSensorRepo.addScannedSensors(device);
}
public LiveData<ArrayList<XsensDotDevice>> getConnectedSensors() {
return mConnectedSensors;
}
public void addConnectedSensor(XsensDotDevice device) {
mConnectedSensorRepo.addConnectedSensors(device);
}
public LiveData<XsensDotData> getSensorData() {
return mSensorData;
}
public void addSensorData(XsensDotData data) {
mSensorDataRepo.addSensorData(data);
}
}
I included the code for the scanned and connect devices in the ViewModel in case it come in handy and helps explain whats going on.
Thank you for any help!
Here is a simple example of how I use LiveData. In my view model I will have a value as so :
var isInternetAvailable = MutableLiveData<Boolean>().apply { value = true }
in my activity I will have the code:
viewmodel.isInternetAvailable.observe(this, Observer {
// execute your logic here
var theValue = viewmodel.isInternetAvailable.value!!
}
Then in my viewModel when the internet has changed I will use
viewmodel.isInternetAvailable.postValue(true)
So for your code - as far as I can see, you're observing the function but not posting to it in order to trigger your observer function
You can use
MutableLiveData<XsensDotData> data = new MutableLiveData<>();
and then
data.postValue(dataSet)
That should hopefully trigger your observer

How to prevent Live date from duplication in on back pressed from another fragment and on recall

hello i have two problems regarding live data with view model and navigation component first one is when i go from fragment A with live data to fragment B and then from B to A the data in my list gets duplicated,
the other problem is one i re call viewModel.loadList() in my fragment after making and event to filter the data also gets duplicated
here is my view model
public class HomeViewModel extends ViewModel {
MutableLiveData<ArrayList<HomeResponseModel>> homeLiveData = new MutableLiveData<>();
ArrayList<HomeResponseModel> homeList = new ArrayList<>();
public MutableLiveData<ArrayList<HomeResponseModel>> geHomeList(HomeRequestModel homeRequestModel, Context context, ApiInterface apiInterface, LottieAnimationView lottieAnimationView) {
if (homeLiveData == null) {
homeLiveData = new MutableLiveData<ArrayList<HomeResponseModel>>();
loadHomeList(homeRequestModel);
}
return homeLiveData;
}
public void loadHomeList(HomeRequestModel homeRequestModel) {
Call<List<HomeResponseModel>> call = apiInterface.getHomeList(homeRequestModel, );
call.enqueue(new Callback<List<HomeResponseModel>>() {
#Override
public void onResponse(Call<List<HomeResponseModel>> call, Response<List<HomeResponseModel>> response) {
if (response.isSuccessful()) {
homeList.addAll(response.body());
homeLiveData.setValue(homeList);
}
}
#Override
public void onFailure(Call<List<HomeResponseModel>> call, Throwable t) {
}
});
}
my observer in onCreateView
viewModel.geHomeList(homeRequestModel).observe(getViewLifecycleOwner(), new Observer<ArrayList<HomeResponseModel>>() {
#Override
public void onChanged(ArrayList<HomeResponseModel> homeResponse) {
homeResponseModels.addAll(homeResponse);
homeAdapter.notifyDataSetChanged();
}
});
}
how i recall load method after a filter event
viewModel.loadHomeList(homeRequestModel);
Clear the list before adding the new models:
viewModel.geHomeList(homeRequestModel).observe(getViewLifecycleOwner(), new Observer<ArrayList<HomeResponseModel>>() {
#Override
public void onChanged(ArrayList<HomeResponseModel> homeResponse) {
homeResponseModels.clear();
homeResponseModels.addAll(homeResponse);
homeAdapter.notifyDataSetChanged();
}
});
}
Or, even better:
In case your adapter holds a List or HomeResponseModel you could create a method to update it:
public update(List<HomeResponseModel> homeResponse) {
this.homeResponseModels = homeResponse;
notifydatasetchanged();
}
and then change the observe method to call it:
viewModel.geHomeList(homeRequestModel).observe(getViewLifecycleOwner(), new Observer<ArrayList<HomeResponseModel>>() {
#Override
public void onChanged(ArrayList<HomeResponseModel> homeResponse) {
homeAdapter.update(homeResponse);
}
});
}
Besides that, in your ViewModel in the loadHomeList method in the onResponse callback you could assign the data received to the liveData:
if (response.isSuccessful()) {
homeLiveData.setValue(response.body());
}
no need to save it in the homeList var, you can get rid of that var. Otherwise, perform homeList.clear(); before adding to it all the received data to avoid duplicates.

How can I perform LiveData transformations on a background thread?

I have a need to transform one type of data, returned by a LiveData object, into another form on a background thread to prevent UI lag.
In my specific case, I have:
MyDBRow objects (POJOs consisting of primitive longs and Strings);
a Room DAO instance emitting these via a LiveData<List<MyDBRow>>; and
a UI expecting richer MyRichObject objects (POJOs with the primitives inflated into e.g. date/time objects)
so I need to transform my LiveData<List<MyDBRow>> into a LiveData<List<MyRichObject>>, but not on the UI thread.
The Transformations.map(LiveData<X>, Function<X, Y>) method does this needed transformation, but I can't use this because it executes the transformation on the main thread:
Applies the given function on the main thread to each value emitted by source LiveData and returns LiveData, which emits resulting values.
The given function func will be executed on the main thread.
What is a clean way to make LiveData transformations occur:
somewhere off the main thread, and
only as needed (i.e. only when something is observing the intended transformation)?
The original, “source” LiveData can be monitored by a new Observer instance.
This Observer instance, when source LiveData is emitted, can prepare a background thread to perform the needed transformation and then emit it via a new, “transformed” LiveData.
The transformed LiveData can attach the aforementioned Observer to the source LiveData when it has active Observers, and detach them when it doesn't, ensuring that the source LiveData is only being observed when necessary.
The question gives an example source LiveData<List<MyDBRow>> and needs a transformed LiveData<List<MyRichObject>>. A combined transformed LiveData and Observer could look something like this:
class MyRichObjectLiveData
extends LiveData<List<MyRichObject>>
implements Observer<List<MyDBRow>>
{
#NonNull private LiveData<List<MyDBRow>> sourceLiveData;
MyRichObjectLiveData(#NonNull LiveData<List<MyDBRow>> sourceLiveData) {
this.sourceLiveData = sourceLiveData;
}
// only watch the source LiveData when something is observing this
// transformed LiveData
#Override protected void onActive() { sourceLiveData.observeForever(this); }
#Override protected void onInactive() { sourceLiveData.removeObserver(this); }
// receive source LiveData emission
#Override public void onChanged(#Nullable List<MyDBRow> dbRows) {
// set up a background thread to complete the transformation
AsyncTask.execute(new Runnable() {
#Override public void run() {
assert dbRows != null;
List<MyRichObject> myRichObjects = new LinkedList<>();
for (MyDBRow myDBRow : myDBRows) {
myRichObjects.add(MyRichObjectBuilder.from(myDBRow).build());
}
// use LiveData method postValue (rather than setValue) on
// background threads
postValue(myRichObjects);
}
});
}
}
If multiple such transformations are needed, the above logic could be made generic like this:
abstract class TransformedLiveData<Source, Transformed>
extends LiveData<Transformed>
implements Observer<Source>
{
#Override protected void onActive() { getSource().observeForever(this); }
#Override protected void onInactive() { getSource().removeObserver(this); }
#Override public void onChanged(#Nullable Source source) {
AsyncTask.execute(new Runnable() {
#Override public void run() {
postValue(getTransformed(source));
}
});
}
protected abstract LiveData<Source> getSource();
protected abstract Transformed getTransformed(Source source);
}
and the subclass for the example given by the question could look something like this:
class MyRichObjectLiveData
extends TransformedLiveData<List<MyDBRow>, List<MyRichObject>>
{
#NonNull private LiveData<List<MyDBRow>> sourceLiveData;
MyRichObjectLiveData(#NonNull LiveData<List<MyDBRow>> sourceLiveData) {
this.sourceLiveData = sourceLiveData;
}
#Override protected LiveData<List<MyDBRow>> getSource() {
return sourceLiveData;
}
#Override protected List<MyRichObject> getTransformed(List<MyDBRow> myDBRows) {
List<MyRichObject> myRichObjects = new LinkedList<>();
for (MyDBRow myDBRow : myDBRows) {
myRichObjects.add(MyRichObjectBuilder.from(myDBRow).build());
}
return myRichObjects;
}
}
It may be eaiser to do using MediatorLiveData. Transformations.map() is implemented with MediatorLiveData under the hood.
#MainThread
public static <X, Y> LiveData<Y> mapAsync(
#NonNull LiveData<X> source,
#NonNull final Function<X, Y> mapFunction) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
#Override
public void onChanged(#Nullable final X x) {
AsyncTask.execute(new Runnable() {
#Override
public void run() {
result.postValue(mapFunction.apply(x));
}
});
}
});
return result;
}
Listen to a MediatorLiveData<T> that listens to two other LiveData<T>s.
For example:
val exposed: LiveData<List<T>> = MediatorLiveData<List<T>>().apply {
addSource(aLiveDataToMap) { doWorkOnAnotherThread(it) }
addSource(aMutableLiveData) { value = it }
}
private fun doWorkOnAnotherThread(t: T) {
runWorkOnAnotherThread {
val t2 = /* ... */
aMutableLiveData.postValue(t2)
}
}
Whenever aLiveDataToMap changes, it will trigger doWorkOnAnotherThread() which will then set the value of aMutableLiveData, which finally sets to value of exposed, which a lifecycle-owner will be listening to. Replace Ts with your desired type.
Thanks to #jaychang0917
Kotlin Form :
#MainThread
fun <X, Y> mapAsync(source: LiveData<X>, mapFunction: androidx.arch.core.util.Function<X, Y>): LiveData<Y> {
val result = MediatorLiveData<Y>()
result.addSource(source) { x -> AsyncTask.execute { result.postValue(mapFunction.apply(x)) } }
return result
}
A solution with coroutines:
class RichLiveData(val rows: LiveData<List<MyDBRow>>) : LiveData<List<MyRichObject>>(),
CoroutineScope by CoroutineScope(Dispatchers.Default) {
private val observer = Observer<List<MyDBRow>> { rows ->
launch {
postValue(/*computationally expensive stuff which returns a List<MyRichObject>*/)
}
}
override fun onActive() {
rows.observeForever(observer)
}
override fun onInactive() {
rows.removeObserver(observer)
}
}
Another possible solution with coroutines:
object BackgroundTransformations {
fun <X, Y> map(
source: LiveData<X>,
mapFunction: (X) -> Y
): LiveData<Y> {
val result = MediatorLiveData<Y>()
result.addSource(source, Observer<X> { x ->
if (x == null) return#Observer
CoroutineScope(Dispatchers.Default).launch {
result.postValue(mapFunction(x))
}
})
return result
}
fun <X, Y> switchMap(
source: LiveData<X>,
switchMapFunction: (X) -> LiveData<Y>
): LiveData<Y> {
val result = MediatorLiveData<Y>()
result.addSource(source, object : Observer<X> {
var mSource: LiveData<Y>? = null
override fun onChanged(x: X) {
if (x == null) return
CoroutineScope(Dispatchers.Default).launch {
val newLiveData = switchMapFunction(x)
if (mSource == newLiveData) {
return#launch
}
if (mSource != null) {
result.removeSource(mSource!!)
}
mSource = newLiveData
if (mSource != null) {
result.addSource(mSource!!) { y ->
result.setValue(y)
}
}
}
}
})
return result
}
}
Hope it helps
How about like this:
#Query("SELECT * FROM " + PeriodicElement.TABLE_NAME)
abstract fun getAll(): LiveData<List<PeriodicElement>>
fun getAllElements(): LiveData<HashMap<String, PeriodicElement>> {
return Transformations.switchMap(getAll(), ::transform)
}
private fun transform(list: List<PeriodicElement>): LiveData<HashMap<String, PeriodicElement>> {
val map = HashMap<String, PeriodicElement>()
val liveData = MutableLiveData(map)
AsyncTask.execute {
for (p in list) {
map[p.symbol] = p
if (!liveData.hasObservers()) {
//prevent memory leak
break
}
}
liveData.postValue(map)
}
return liveData
}

LiveData.getValue() returns null with Room

Java POJO Object
public class Section {
#ColumnInfo(name="section_id")
public int mSectionId;
#ColumnInfo(name="section_name")
public String mSectionName;
public int getSectionId() {
return mSectionId;
}
public void setSectionId(int mSectionId) {
this.mSectionId = mSectionId;
}
public String getSectionName() {
return mSectionName;
}
public void setSectionName(String mSectionName) {
this.mSectionName = mSectionName;
}
}
My Query method
#Query("SELECT * FROM section")
LiveData<List<Section>> getAllSections();
Accessing DB
final LiveData<List<Section>> sections = mDb.sectionDAO().getAllSections();
On the next line I am checking sections.getValue() which is always giving me null although I have data in the DataBase and later I am getting the value in the onChanged() method.
sections.observe(this, new Observer<List<Section>>() {
#Override
public void onChanged(#Nullable List<Section> sections){
}
});
But when I omit LiveData from the query I am getting the data as expected.
Query Method:
#Query("SELECT * FROM section")
List<Section> getAllSections();
Accessing DB:
final List<Section> sections = mDb.sectionDAO().getAllSections();
On the next line I am checking sections.getValue() which is always giving me null although I have data in the DataBase and later I am getting the value in the onChanged() method.
This is normal behavior, because queries that return LiveData, are working asynchronously. The value is null at that moment.
So calling this method
LiveData<List<Section>> getAllSections();
you will get the result later here
sections.observe(this, new Observer<List<Section>>() {
#Override
public void onChanged(#Nullable List<Section> sections){
}
});
from documentation:
Room does not allow accessing the database on the main thread unless you called allowMainThreadQueries() on the builder because it might potentially lock the UI for long periods of time. Asynchronous queries (queries that return LiveData or RxJava Flowable) are exempt from this rule since they asynchronously run the query on a background thread when needed.
I solve this problem through this approach
private MediatorLiveData<List<Section>> mSectionLive = new MediatorLiveData<>();
.
.
.
#Override
public LiveData<List<Section>> getAllSections() {
final LiveData<List<Section>> sections = mDb.sectionDAO().getAllSections();
mSectionLive.addSource(sections, new Observer<List<Section>>() {
#Override
public void onChanged(#Nullable List<Section> sectionList) {
if(sectionList == null || sectionList.isEmpty()) {
// Fetch data from API
}else{
mSectionLive.removeSource(sections);
mSectionLive.setValue(sectionList);
}
}
});
return mSectionLive;
}
LiveData is an asynchronous query, you get the LiveData object but it might contain no data. You could use an extra method to wait for the data to be filled and then extract the data.
public static <T> T getValue(LiveData<T> liveData) throws InterruptedException {
final Object[] objects = new Object[1];
final CountDownLatch latch = new CountDownLatch(1);
Observer observer = new Observer() {
#Override
public void onChanged(#Nullable Object o) {
objects[0] = o;
latch.countDown();
liveData.removeObserver(this);
}
};
liveData.observeForever(observer);
latch.await(2, TimeUnit.SECONDS);
return (T) objects[0];
}
I resolved the similar issue as follows
Inside your ViewModel class
private LiveData<List<Section>> mSections;
#Override
public LiveData<List<Section>> getAllSections() {
if (mSections == null) {
mSections = mDb.sectionDAO().getAllSections();
}
return mSections;
}
This is all required. Never change the LiveData's instance.
I would suggest creating another query without LiveData if you need to synchronously fetch data from the database in your code.
DAO:
#Query("SELECT COUNT(*) FROM section")
int countAllSections();
ViewModel:
Integer countAllSections() {
return new CountAllSectionsTask().execute().get();
}
private static class CountAllSectionsTask extends AsyncTask<Void, Void, Integer> {
#Override
protected Integer doInBackground(Void... notes) {
return mDb.sectionDAO().countAllSections();
}
}
if sections.getValue() is null I have to call api for data and insert
in into the database
You can handle this at onChange method:
sections.observe(this, new Observer<List<Section>>() {
#Override
public void onChanged(#Nullable List<Section> sections){
if(sections == null || sections.size() == 0) {
// No data in your database, call your api for data
} else {
// One or more items retrieved, no need to call your api for data.
}
}
});
But you should better put this Database/Table initialization logic to a repository class. Check out Google's sample. See DatabaseCreator class.
For anyone that comes across this. If you are calling LiveData.getValue() and you are consistently getting null. It is possible that you forgot to invoke LiveData.observe(). If you forget to do so getValue() will always return null specially with List<> datatypes.

Categories

Resources