In my Activity, I have a function which observes data in Firestore database and updates a LiveData ArrayList in the ViewModel:
businessViewModel.listenToAllSites().observe(this, Observer { allSites ->
businessViewModel.updateAllSitesVMLiveData(allSites)
})
The ViewModel returns LiveData from Repository as below:
fun listenToAllSites(): LiveData<ArrayList<SiteObject>> {
return businessRepository.listenToAllSites()
}
Which returns data from Firestore database Snaphot Listener in the form of LiveData as below:
fun listenToAllSites(): LiveData<ArrayList<SiteObject>> {
if (allSitesRegistration == null) {
allSitesRegistration = firestore.collection(SITES)
.addSnapshotListener { snapshot, e ->
if (e != null) {
Log.w(TAG, "listenToAllSites(), listen error: ", e)
return#addSnapshotListener
}
val source = if (snapshot != null && snapshot.metadata.hasPendingWrites())
"Local"
else
"Server"
if (snapshot != null) {
Log.d(TAG, "listenToAllSites(): $source data: ${snapshot.size()}")
} else {
Log.d(TAG, "listenToAllSites(): $source data: null")
}
val allSites = ArrayList<SiteObject>()/**/
if (snapshot != null) {
for (document in snapshot) {
val site = document.toObject(SiteObject::class.java)
site.siteID = document.id
allSites.add(site)
}
}
Log.d(TAG, "updated 'allSitesMutableLiveData' (${allSites.size} x SiteObject)")
allSitesMutableLiveData.value = allSites
}
} else Log.d(TAG, "'allSites Listener' already exists (${allSitesRegistration.toString()})")
return allSitesMutableLiveData
}
In the ViewModel, the list gets updated as below:
fun updateAllSitesVMLiveData (allSitesList: ArrayList<SiteObject>) {
_allSites.value = allSitesList
}
I then have this and other similar lists to observe in my Fragments to populate RecyclerViews and which update on database changes - so for example:
// Observe All Sites & Filter for Current User
businessViewModel.allSites.observe(viewLifecycleOwner, Observer { allSites ->
if (allSites != null) {
if(businessViewModel.filterSitesForUser.value == true) {
sitesAdapter.setList(allSites.filter { site -> site.users.contains(businessViewModel.currentUser.value?.userID)} as ArrayList<SiteObject>)
} else sitesAdapter.setList(allSites as ArrayList<SiteObject>)
sitesAdapter.notifyDataSetChanged()
binding.sitesSearchFilterEditTextView.setText(searchViewInputText) // to reset filtered list after process death!
}
})
I also have a button which allows user to update list to see just their data or all data by toggling a LiveData<Boolean> in ViewModel, the change of which is observed in the Fragment as below:
// Observe sites filter status
businessViewModel.filterSitesForUser.observe(viewLifecycleOwner, Observer { filterStatus ->
if (filterStatus && !businessViewModel.allSites.value.isNullOrEmpty()) {
binding.sitesUserIconImageView.visibility = View.VISIBLE
binding.sitesUsersIconImageView.visibility = View.GONE
sitesAdapter.setList(businessViewModel.allSites.value?.filter { site -> site.users.contains(businessViewModel.currentUser.value?.userID)} as ArrayList<SiteObject>)
} else if (!filterStatus && !businessViewModel.allSites.value.isNullOrEmpty()){
binding.sitesUserIconImageView.visibility = View.GONE
binding.sitesUsersIconImageView.visibility = View.VISIBLE
sitesAdapter.setList(businessViewModel.allSites.value as ArrayList<SiteObject>)
}
sitesAdapter.filter.filter(searchViewInputText)
sitesAdapter.notifyDataSetChanged()
})
But what I am struggling with is best way to sort this data without disrupting the flow of data.. The user has the ability to sort list in different ways (sort by field, sort direction) and I have so far done this by updating the list in the repository (so the original observer is triggered) but I'm sure this sort of logic should not be in the repo (and I am creating a second list of data which is not ideal) but I can't see how to do it in the ViewModel:
fun updateSitesOrderBy(field: String) {
val sortedSites = ArrayList<SiteObject>()
allSites.let { sortedSites.addAll(it) }
if (field != orderBySites.value) {
when (field) {
DATE_CREATED -> orderBySites.value = DATE_CREATED
DATE_EDITED -> orderBySites.value = DATE_EDITED
SITE_TASKS -> orderBySites.value = SITE_TASKS
SITE_RATING -> orderBySites.value = SITE_RATING
else -> orderBySites.value = SITE_REFERENCE
}
}
)
sortedSites.sortBy { it ->
when (orderBySites.value) {
DATE_CREATED -> it.dateCreatedTimestamp.toString()
DATE_EDITED -> it.dateEditedTimestamp.toString()
SITE_TASKS -> it.siteTask.toString()
SITE_RATING -> it.siteRating.toString()
else -> it.siteReference
}
}
)
allSitesMutableLiveData.value = sortedSites
}
I have now amended code to use MediatorLiveData as recommended by mfkw1 as described in the below follow up question, which works except for the issue explained:
Observing MediatorLiveData Issue
Related
I would like to make simple widget for my weather app, to show local temperature. My question is: how to get the LocationTracker in my widget class?
class Widget: GlanceAppWidget() {...}
I found the solution, I use fusedLocationProviderClient
//client
val locationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
So, my method get location:
> //get location
private suspend fun getCurrentLocation(
context: Context,
locationClient: FusedLocationProviderClient
): Location? {
val hasAccessFineLocationPermission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val hasAccessCoarseLocationPermission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ||
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
if (!hasAccessCoarseLocationPermission || !hasAccessFineLocationPermission || !isGpsEnabled) {
return null
}
return suspendCancellableCoroutine { cont ->
locationClient.lastLocation.apply {
if (isComplete) {
if (isSuccessful) {
cont.resume(result)
} else {
cont.resume(null)
}
return#suspendCancellableCoroutine
}
addOnSuccessListener {
cont.resume(it)
}
addOnFailureListener {
cont.resume(null)
}
addOnCanceledListener {
cont.cancel()
}
}
}
}
I have similar class structure.
class Request {
Level1 level1;
}
class Level1 {
Level2 level2;
String data;
}
class Level2 {
Level3 level3;
String data;
}
class Level3 {
Level4 level4;
String data;
}
class Level4 {
String data;
}
Request r = new Request();
r.level1 = new Level1();
r.level1.level2 = new Level2();
r.level1.level2.level3 = null;//new Level3();
//r.level1.level2.level3.level4 = new Level4();
//r.level1.level2.level3.level4.data = "level4Data";
and to get data from nested fields I do following
Benefit of using Optional being I don't have to worry about checking null at each level in object hierarchy
String level4Data = Optional.ofNullable(r)
.map(req -> req.level1)
.map(l1 -> l1.level2)
.map(l2 -> l2.level3)
.map(l3 -> l3.level4)
.map(l4 -> l4.data)
.orElse(null);
System.out.println("level4Data: " + level4Data);
but again if I want to log reason behind level4Data being null, I have to do following/I don't any better
if (level4Data == null) {
if (r == null) System.out.println("request was null");
else if (r.level1 == null) System.out.println("level1 was null");
else if (r.level1.level2 == null) System.out.println("level2 was null");
else if (r.level1.level2.level3 == null) System.out.println("level3 was null");
else if (r.level1.level2.level3.level4 == null) System.out.println("level4 was null");
else if (r.level1.level2.level3.level4.data == null) System.out.println("level4.data was null");
}
is there more elegant/efficient way of doing this as it defeats benefits of using Optional in first place
Thank you for your time and inputs
Optional doesn't have a peek method like in Stream API.
For your use case, you can write a wrapper that will do the additional job:
// logging wrapper
static <T, R> Function<T, R> logIfNull(Function<? super T, ? extends R> function, String message) {
return input -> {
R result;
if ((result = function.apply(input)) == null)
System.out.println("logIfNull :: Null found. " + message);
return result;
};
}
// usage
String level4Data = Optional.ofNullable(r)
.map(logIfNull(req -> req.level1, "req.level1"))
.map(logIfNull(l1 -> l1.level2, "l1.level2"))
.map(logIfNull(l2 -> l2.level3, "l2.level3"))
.map(logIfNull(l3 -> l3.level4, "l3.level4"))
.map(logIfNull(l4 -> l4.data, "l4.data"))
.orElse(null);
I'm using the webflux framework for spring boot, the behavior I'm trying to implement is creating a new customer in the database, if it does not already exist (throw an exception if it does)
and also maintain another country code database (if the new customer is from a new country, add to the database, if the country is already saved, use the old information)
This is the function in the service :
public Mono<Customer> createNewCustomer(Customer customer) {
if(!customer.isValid()) {
return Mono.error(new BadRequestException("Bad email or birthdate format"));
}
Mono<Customer> customerFromDB = customerDB.findByEmail(customer.getEmail());
Mono<Country> countryFromDB = countryDB.findByCountryCode(customer.getCountryCode());
Mono<Customer> c = customerFromDB.zipWith(countryFromDB).doOnSuccess(new Consumer<Tuple2<Customer, Country>>() {
#Override
public void accept(Tuple2<Customer, Country> t) {
System.err.println("tuple " + t);
if(t == null) {
countryDB.save(new Country(customer.getCountryCode(), customer.getCountryName())).subscribe();
customerDB.save(customer).subscribe();
return;
}
Customer cus = t.getT1();
Country country = t.getT2();
if(cus != null) {
throw new CustomerAlreadyExistsException();
}
if(country == null) {
countryDB.save(new Country(customer.getCountryCode(), customer.getCountryName())).subscribe();
}
else {
customer.setCountryName(country.getCountryName());
}
customerDB.save(customer).subscribe();
}
}).thenReturn(customer);
return c;
}
My problem is, the tuple returns null if either country or customer are not found, while I need to know about them separately if they exist or not, so that I can save to the database correctly.
country == null is never true
I also tried to use customerFromDB.block() to get the actual value but I receive an error that it's not supported, so I guess that's not the way
Is there anyway to do two queries to get their values?
Solved it with the following solution:
public Mono<Customer> createNewCustomer(Customer customer) {
if(!customer.isValid()) {
return Mono.error(new BadRequestException("Bad email or birthdate format"));
}
return customerDB.findByEmail(customer.getEmail())
.defaultIfEmpty(new Customer("empty", "", "", "", "", ""))
.flatMap(cu -> {
if(!cu.getEmail().equals("empty")) {
return Mono.error(new CustomerAlreadyExistsException());
}
return countryDB.findByCountryCode(customer.getCountryCode())
.defaultIfEmpty(new Country(customer.getCountryCode(), customer.getCountryName()))
.flatMap(country -> {
customer.setCountryName(country.getCountryName());
customerDB.save(customer).subscribe();
countryDB.save(country).subscribe();
return Mono.just(customer);});
});
}
Instead of doing both queries simulatneaously, I queried for one result and then queries for the next, I think this is the reactive way of doing it, but I'm open for corrections.
I subscribed to ids and search in the ui but i wasn't getting any results so i stepped through with the debugger and found out that the transformation is not getting triggered after the first time. So when i call setIds the first time ids gets updated but for every call after the first one the transformation won't trigger. Same goes for the search.
Any ideas what might possible go wrong?
class MyViewModel : ViewModel() {
private val repository = Repository.sharedInstance
var recentRadius: LiveData<List<RecentRadius>>?
var recentRoute: LiveData<List<RecentRoute>>?
init {
recentRadius = repository.recentRadius()
recentRoute = repository.recentRoute()
}
private val idsInput = MutableLiveData<String>()
fun setIdsInput(textId: String) {
idsInput.value = textId
}
val ids: LiveData<List<String>> = Transformations.switchMap(idsInput) { id ->
repository.ids(id)
}
private val searchInput = MutableLiveData<Search>()
fun setSearchInput(search: Search) {
searchInput.value = search
}
val search: LiveData<SearchResult> = Transformations.switchMap(searchInput) { search ->
when (search.type) {
SearchType.ID -> repository.id(search)
SearchType.RADIUS -> repository.radius(search)
SearchType.ROUTE -> repository.route(search)
}
}
}
The most common reason why transformation don't get triggered is when there is no Observer observing it or the input LiveData is not getting changed.
Below example illustrates use of map when observer is attached in the activity.
Activity
class MainActivity : AppCompatActivity() {
lateinit var mBinding : ActivityMainBinding
private val mViewModel : MainViewModel by lazy {
getViewModel { MainViewModel(this.application) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
mBinding.vm = mViewModel
// adding obeserver
mViewModel.videoName.observe(this, Observer<String> { value ->
value?.let {
//Toast.makeText(this, it, Toast.LENGTH_LONG).show()
}
})
}
}
ViewModel with map
class MainViewModel(val appContext : Application) : AndroidViewModel(appContext) {
private val TAG = "MainViewModel"
var videoData = MutableLiveData<VideoDownload>()
var videoName : LiveData<String>
init {
// Update the data
videoName = Transformations.map(videoData) { "updated : "+it.webUrl }
}
fun onActionClick(v : View) {
// change data
videoData.value = VideoDownload(System.currentTimeMillis().toString())
}
fun onReActionClick(v : View) {
// check data
Toast.makeText(appContext, videoName.value, Toast.LENGTH_LONG).show()
}
}
ViewModel with switchMap
class MainViewModel(val appContext : Application) : AndroidViewModel(appContext) {
private val TAG = "MainViewModel"
var videoData = MutableLiveData<VideoDownload>()
var videoName : LiveData<String>
init {
// Update the data
videoName = Transformations.switchMap(videoData) { modData(it.webUrl) }
}
private fun modData(str: String): LiveData<String> {
val liveData = MutableLiveData<String>()
liveData.value = "switchmap : "+str
return liveData
}
fun onActionClick(v : View) {
// change data
videoData.value = VideoDownload(System.currentTimeMillis().toString())
}
fun onReActionClick(v : View) {
// check data
Toast.makeText(appContext, videoName.value, Toast.LENGTH_LONG).show()
}
}
for me, it was because the observer owner was a fragment. It stopped triggering when navigating to different fragments. I changed the observer owner to the activity and it triggered as expected.
itemsViewModel.items.observe(requireActivity(), Observer {
The view model was defined as a class property:
private val itemsViewModel: ItemsViewModel by lazy {
ViewModelProvider(requireActivity()).get(ItemsViewModel::class.java)
}
If you really want it to be triggered.
fun <X, Y> LiveData<X>.forceMap(
mapFunction: (X) -> Y
): LiveData<Y> {
val result = MutableLiveData<Y>()
this.observeForever {x->
if (x != null) {
result.value = mapFunction.invoke(x)
}
}
return result
}
I have a use case which I would assume is pretty standard, however I haven't been able to find an example on exactly how to do this, or if it's possible.
Let's assume I have the following TableView
First Name Last Name Street NewRecord
Tom Smith Main St. Yes
Mike Smith First St. No
In this case, the grid should have the first three cells editable since the record is new, however when the record is not new then the Last Name cell should be disabled.
I tried this in the CellFactory and RowFactory - but haven't seen a way to accomplish this.
Thanks for your help.
The easiest way to do this is with a third-party binding library: ReactFX 2.0 has this functionality built-in, as described here. Using that you can do
TableColumn<Person, String> lastNameColumn = new TableColumn<>("Last Name");
lastNameColumn.setEditable(true);
lastNameColumn.setCellFactory(tc -> {
TableCell<Person, String> cell = new TextFieldTableCell<>();
cell.editableProperty().bind(
// horrible cast needed because TableCell.tableRowProperty inexplicably returns a raw type:
Val.flatMap(cell.tableRowProperty(), row -> (ObservableValue<Person>)row.itemProperty())
.flatMap(Person::newRecordProperty)
.orElseConst(false));
return cell ;
});
(assumes a Person table model object with the obvious JavaFX properties and methods).
Without the library, you need a pretty miserable nested list of listeners:
TableColumn<Person, String> lastNameColumn = new TableColumn<>("Last Name");
lastNameColumn.setEditable(true);
lastNameColumn.setCellFactory(tc -> {
TableCell<Person, String> cell = new TextFieldTableCell<>();
ChangeListener<Boolean> newRecordListener = (obs, wasNewRecord, isNewRecord) -> updateEditability(cell);
ChangeListener<Person> rowItemListener = (obs, oldPerson, newPerson) -> {
if (oldPerson != null) {
oldPerson.newRecordProperty().removeListener(newRecordListener);
}
if (newPerson != null) {
newPerson.newRecordProperty().addListener(newRecordListener);
}
updateEditability(cell);
};
ChangeListener<TableRow> rowListener = (obs, oldRow, newRow) -> {
if (oldRow != null) {
((ObservableValue<Person>)oldRow.itemProperty()).removeListener(rowItemListener);
if (oldRow.getItem() != null) {
((Person)oldRow.getItem()).newRecordProperty().removeListener(newRecordListener);
}
}
if (newRow != null) {
((ObservableValue<Person>)newRow.itemProperty()).addListener(rowItemListener);
if (newRow.getItem() != null) {
((Person)newRow.getItem()).newRecordProperty().addListener(newRecordListener);
}
}
updateEditability(cell);
};
cell.tableRowProperty().addListener(rowListener);
return cell ;
});
and then
private void updateEditability(TableCell<Person, String> cell) {
if (cell.getTableRow() == null) {
cell.setEditable(false);
} else {
TableRow<Person> row = (TableRow<Person>) cell.getTableRow();
if (row.getItem() == null) {
cell.setEditable(false);
} else {
cell.setEditable(row.getItem().isNewRecord());
}
}
}
An alternative using a "legacy style" API is
TableColumn<Person, String> lastNameColumn = new TableColumn<>("Last Name");
lastNameColumn.setEditable(true);
lastNameColumn.setCellFactory(tc -> {
TableCell<Person, String> cell = new TextFieldTableCell<>();
cell.editableProperty().bind(
Bindings.selectBoolean(cell.tableRowProperty(), "item", "newRecord"));
return cell ;
});
I dislike this option, because it lacks any type safety (or indeed any compiler checks at all), and additionally in some earlier versions of JavaFX would generate almost endless warning messages if any of the properties in the "chain" had null values (which they will, frequently, in this case). I believe the latter issue is fixed, but the ReactFX version of this is far better, imho.
You can always stop an editing by setting event handler to check if the action is legal.
columnName.setOnEditStart(
new EventHandler<CellEditEvent<ItemClass, String>>(){
#Override
public void handle(CellEditEvent<ItemClass, String> event) {
if(event.getTableColumn().getCellData(3).compareTo("yes") == 0) {
event.getTableView().edit(-1, null);
//this prevents the editing in "progress"
}
}
}
);