RxSwift var-outlet binding organistaion - ios

I'm try to use RxSwift in my code and have a question:
I have a Settings class with different properties (staticValue for example):
class DeviceSettings: NSObject {
var staticValue = Variable<Int>(0)
}
I have an UI with UITextField which should be two-way connected with property, which works fine:
settings.staticValue.asObservable()
.map({ String($0) })
.bindTo(staticValueField.rx.text)
.addDisposableTo(disposeBag)
staticValueField.rx.controlEvent([.editingDidEnd]).asObservable()
.map({ Int(self.staticValueField.text!)! }) // Always Int
.subscribe(onNext: {
self.settings.staticValue.value = $0
}).addDisposableTo(disposeBag)
But actual Settings object will be loaded via network request and can be reloaded several times during VC lifecycle.
So:
How I should store my settings object? As usual variable or as
Variable<Settings> with ? or !?
Is my two-way-binding correct?
In which order I need to load data and call bind logic to enable binding and set start value at once?

Storing as usual variable. All observable properties is inside and stored as Variable<T>
It seems binding is correct and all works fine for now
I performed network request
Stored received object in my Setting object variable. If I will need to refresh data, I will update object properties values, but NOT the full object
Added .startWith( settings.staticValue.value ) to value -> tf binding for initial value
P.S. Correct me, if I made some mistake

Related

How to transform PagingData<T> into model in collectLatest() in UI

I am having a situation, where once I get pagingData <T: UIModel>, I need to get additional data from a different API. The second Api requires arguments that are there in first API response. Currently I am collecting in UI Layer in lifecyclescope as,
loadResults().collectLatest {
PagingResultAdapter.submitData(lifecycle, it)
// Extracting the data inside PagingData and setting in viewmodel.
it.map { uiModel ->
Timber.e("Getting data inside map function..")
viewModel.setFinalResults(uiModel)
}
}
}
But the problem is, the map{} function on pagingData won't run during data fetching. List is populated, ui is showing the items in recyclerview. But the map function not running..(I am not able see the log)
The UI layer loadResults() function in-turn calls the viewmodel.loadResults() with UI level variables. In terms of paging everything is working fine, but I cannot transform the pagingdata into UIModel in any layer.
Official site suggests to use map{} function only.
https://developer.android.com/topic/libraries/architecture/paging/v3-transform#basic-transformations
But I am not getting at which layer I should apply map{} and also before collecting or after collecting..Any help is good..
PagingData.map is a lazy transformation that runs during collection when you call .submitData(pagingData). Since you are only submitting the original un-transformed PagingData your .map transform will never run.
You should apply the .map to the PagingData you will actually end up submitting in order to have it run. Usually this is done from the ViewModel, so that the results are also cached in case you end up in a config change or cached scenario like when navigating between fragments.
You didn't share your ViewModel / place you are creating your Pager, but assuming this happens at a different layer you would have something like:
MyViewModel.kt
fun loadResults() = Pager(...) { ... }
.flow
.map {
Timber.e("Getting data inside map function..")
setFinalResults(uiModel)
it
}
.cachedIn(viewModelScope)
MyUi.kt
viewModel.loadResults().collectLatest {
pagingDataAdapter.submitData(it)
}
NOTE: You should use the suspending version of .submitData since you are using Flow / Coroutines, because it is able to propagate cancellation direction instead of relying on launched job + eager cancellation via the non-suspending version. There shouldn't be any visible impact, but it is more performant.
Try with:
import androidx.paging.map
.flow.map { item ->
item.map { it.yourTransformation() }
}

How to pass data down the reactive chain

Whenever I need to pass data down the reactive chain I end up doing something like this:
public Mono<String> doFooAndPassDtoAsMono(Dto dto) {
return Mono.just(dto)
.flatMap(dtoMono -> {
Mono<String> result = // remote call returning a Mono
return Mono.zip(Mono.just(dtoMono), result);
})
.flatMap(tup2 -> {
return doSomething(tup2.getT1().getFoo(), tup2.getT2()); // do something that requires foo and result and returns a Mono
});
}
Given the below sample Dto class:
class Dto {
private String foo;
public String getFoo() {
return this.foo;
}
}
Because it often gets tedious to zip the data all the time to pass it down the chain (especially a few levels down) I was wondering if it's ok to simply reference the dto directly like so:
public Mono<String> doFooAndReferenceParam(Dto dto) {
Mono<String> result = // remote call returning a Mono
return result.flatMap(result -> {
return doSomething(dto.getFoo(), result); // do something that requires foo and result and returns a Mono
});
}
My concern about the second approach is that assuming a subscriber subscribes to this Mono on a thread pool would I need to guarantee that Dto is thread safe (the above example is simple because it just carries a String but what if it's not)?
Also, which one is considered "best practice"?
Based on what you have shared, you can simply do following:
public Mono<String> doFooAndPassDtoAsMono(Dto dto) {
return Mono.just(dto.getFoo());
}
The way you are using zip in the first option doesn't solve any purpose. Similarly, the 2nd option will not work either as once the mono is empty then the next flat map will not be triggered.
The case is simple if
The reference data is available from the beginning (i.e. before the creation of the chain), and
The chain is created for processing at most one event (i.e. starts with a Mono), and
The reference data is immutable.
Then you can simple refer to the reference data in a parameter or local variable – just like in your second solution. This is completely okay, and there are no concurrency issues.
Using mutable data in reactive flows is strongly discouraged. If you had a mutable Dto class, you might still be able to use it (assuming proper synchronization) – but this will be very surprising to readers of your code.

how do I get non-server values mapped back from client?

I am setting properties in client-side Breeze entities. I see the EntityInfo UnmappedValuesMap collection, which I'd like to use. It would be great if I could get a few custom client values returned in the Unmapped collection to avoid adding these everywhere.
I am initializing the metastore with:
store.registerEntityTypeCtor("UserInfo", null, userInfoInitializer);
function userInfoInitializer(userinfo) {
userinfo.creatingId = ko.observable(0);
...
I was hoping 'creatingId' would get passed to server. But nothing extra appears in the net traffic.
I don't think it matters, but on the server I am using Breeze.ContextProvider.
Are there flags somewhere that govern this behavior? Thanks for any guidance.
If you wanted to add a 'creatingId' to every entity type you could do something like this:
metadataStore.getEntityTypes().forEach(
function(entityType) {
var ctor = function () {
this.creatingId = 0;
};
metadataStore.registerEntityTypeCtor(entityType.name, ctor, null);
});
Note- using a constructor instead of an initializer. This will ensure the value appears in the unmapped values collection. Don't worry, breeze will make this an observable property on your entity.

BreezeJS editing data not working

I was able to follow the instruction on adding data, that part was easy and understandable. But when I tried to follow instructions for editing data, I'm completely lost.
I am following the todo sample, which works quite well, but when I tried to add to my own project using the same principle, nothing works.
in my controller, I have the following:
function listenForPropertyChanged() {
// Listen for property change of ANY entity so we can (optionally) save
var token = dataservice.addPropertyChangeHandler(propertyChanged);
// Arrange to remove the handler when the controller is destroyed
// which won't happen in this app but would in a multi-page app
$scope.$on("$destroy", function () {
dataservice.removePropertyChangeHandler(token);
});
function propertyChanged(changeArgs) {
// propertyChanged triggers save attempt UNLESS the property is the 'Id'
// because THEN the change is actually the post-save Id-fixup
// rather than user data entry so there is actually nothing to save.
if (changeArgs.args.propertyName !== 'Id') { save(); }
}
}
The problem is that any time I change a control on the view, the propertyChanged callback function never gets called.
Here's the code from the service:
function addPropertyChangeHandler(handler) {
// Actually adds any 'entityChanged' event handler
// call handler when an entity property of any entity changes
return manager.entityChanged.subscribe(function (changeArgs) {
var action = changeArgs.entityAction;
if (action === breeze.EntityAction.PropertyChange) {
handler(changeArgs);
}
});
}
If I put a break point on the line:
var action = changeArgs.entityAction;
In my project, it never reaches there; in the todo sample, it does! It completely skips the whole thing and just loads the view afterwards. So none of my callback functions work at all; so really, nothing is subscribed.
Because of this, when I try to save changes, the manager.hasChanges() is always false and nothing happens in the database.
I've been trying for at least 3 days getting this to work, and I'm completely dumbfounded by how complicated this whole issue has been for me.
Note: I'm using JohnPapa's HotTowel template. I tried to follow the Todo editing functionality to a Tee.. and nothing is working the way I'd like it to.
Help would be appreciated.
The whole time I thought the problem was in the javascript client side end of things. Turned out that editing doesn't work when you created projected DTOs.
So in my server side, I created a query:
public IQueryable<PersonDTO> getPerson(){
return (from _person in ContextProvider.Context.Queries
select new PersonDTO
{
Id = _person.Id,
FirstName = _person.FirstName,
LastName = _person.LastName
}).AsQueryable();
}
Which just projected a DTO to send off to the client. This did work with my app in fetching data and populating things. So this is NOT wrong. Using this, I was able to add items and fetch items, but there's no information that allowed the entitymanager to know about the item. When I created an item, the entitymanager has a "createEntity" which allowed me to tell the entitymanager which item to use.. in my case:
manager.createEntity(person, initializeValues);
Maybe if there was a "manager.getEntity" maybe that would help?
Anyways, I changed the above query to get it straight from the source:
public IQueryable<Person> getPeople(){
return ContextProvider.Context.People;
}
Note ContextProvider is:
readonly EFContextProvider<PeopleEntities> ContextProvider =
new EFContextProvider<PeopleEntities>();
So the subscribe method in the javascript checks out the info that's retrieved straight from the contextual object.. interesting. Just wish I didn't spend 4 days on this.

Handling calculated properties with breezejs and web api

I'm experimenting with BreezeJS with Web API using the BreezeControllerAttribute. How should calculated properties on an entity be exposed? The only way I've found to do this reliably is to create an intermediate DTO that inherits from the entity or use a projection. Normally I would use a readonly property for this scenario, but those appear to be ignored.
When Breeze maps JSON property data to entities, it ignores properties that it does not recognize. That's why your server class's calculated property data are discarded even though you see them in the JSON on the wire.
Fortunately, you can teach Breeze to recognize the property by registering it as an unmapped property. I'll show you how. Let me give some background first.
Background
Your calculated property would be "known" to the Breeze client had it been a property calculated by the database. Database-backed properties (regular and calculated) are picked up in metadata as mapped properties.
But in your case (if I understand correctly) the property is defined in the logic of the server-side class, not in the database. Therefore it is not among the mapped properties in metadata. It is hidden from metadata. It is an unmapped instance property.
I assume you're not hiding it from the serializer. If you look at the network traffic for a query of the class, you can see your calculated property data arriving at the client. The problem is that Breeze is ignoring it when it "materializes" entities from these query results.
Solution with example
The solution is to register the calculated property in the MetadataStore.
I modified the entityExtensionTests.js of the DocCode sample to include this scenario; you can get that code from GitHub or wait for the next Breeze release.
Or just follow along with the code below, starting with this snippet from the Employee class in NorthwindModel.cs:
// Unmapped, server-side calculated property
[NotMapped] // Hidden from Entity Framework; still serialized to the client
public string FullName {
get { return LastName +
(String.IsNullOrWhiteSpace(FirstName)? "" : (", " + FirstName)); }
}
And here is the automated test in entityExtensionTests.js
test("unmapped property can be set by a calculated property of the server class", 2,
function () {
var store = cloneModuleMetadataStore(); // clones the Northwind MetadataStore
// custom Employee constructor
var employeeCtor = function () {
//'Fullname' is a server-side calculated property of the Employee class
// This unmapped property will be empty for new entities
// but will be set for existing entities during query materialization
this.FullName = "";
};
// register the custom constructor
store.registerEntityTypeCtor("Employee", employeeCtor);
var fullProp = store.getEntityType('Employee').getProperty('FullName');
ok(fullProp && fullProp.isUnmapped,
"'FullName' should be an unmapped property after registration");
var em = newEm(store); // helper creates a manager using this MetadataStore
var query = EntityQuery.from('Employees').using(em);
stop(); // going async
query.execute().then(success).fail(handleFail).fin(start);
function success(data) {
var first = data.results[0];
var full = first.FullName();
// passing test confirms that the FulllName property has a value
ok(full, "queried 'Employee' should have a fullname ('Last, First'); it is "+full);
}
});
What you need to do is in this small part of the test example:
var yourTypeCtor = function () {
this.calculatedProperty = ""; // "" or instance of whatever type is is supposed to be
};
// register your custom constructor
store.registerEntityTypeCtor("YourType", yourTypeCtor);

Resources