Background
I understand the concept of callbacks from JS. Being on day #5 of flutter I don't really understand the interplay with callbacks at all in dart/flutter.
So please bear with me
What I'm trying to accomplish
I'm trying to build a single set of reusable code that can communicate from my app to a JSON site to make various different JSON requests via the same exact code base
What I have right now
I have a Future call which successfully polls a site for JSON data and sends it off for parsing by using a specific model.
Future<Result> getJSONfromTheSite(String thecall) async {
try {
//2
final response = await client.request(requestType: RequestType.GET,
path: thecall).timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
//3
return Result<MyModel>.success(MyModel.fromRawJson(response.body));
} else {
return Result.error(title:"Error",msg:"Status code not 200", errorcode:1);
}
} catch (error) {
(...)
}
}
I'm making the request via my UI page like this:
FutureBuilder(
future: _apiResponse.getJSONfromTheSite('latestnews'),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) { ...
Using the variable thecall which I feed in from the FutureBuilder on my various UI pages I can successfully change which http JSON request is being made to poll for different data.
What I'm stuck on
Where I'm stuck is that while I can successfully vary the request to change what data I'm polling for, I can't do anything with the result because my current code always wants to use MyModel to parse the JSON.
return Result<MyModel>.success(MyModel.fromRawJson(response.body));
The question
I essentially need to be able to switch the Model being used against this JSON data depending on what UI page is making the request, rather then hardcoding 10 versions of the same code.
I was thinking of feeding in the name of the model I want to use for that specific call via the FutureBuilder call. For example something like future: _apiResponse.getJSONfromTheSite('latestnews', MyModel2), but that didn't work at all.
Alternatively I was thinking of having the entire return Result<MyModel>.success(MyModel2.fromRawJson(response.body)); sent in as a callback.
The concept of callback makes sense from my JS days, but I'm not sure if I'm correctly applying the concept here. So if I'm going about this the wrong way I'm all ears to a more elegant solution.
Thank you!
You could simply pass as a callback the constructor you want to use and make your method getJSONfromTheSite dynamically typed. The only issue is that you won't be able to define fromRawJson as a factory constructor but instead as a static method returning an instance of your object.
Code Sample
// Your method fromRawJson should be implemented like this
class MyModel {
// ...
static MyModel fromRawJson(Map<String, dynamic> json) =>
MyModel(/* your parameters */);
}
/// T is the dynamic type which you will use to define if it is
/// MyModel or any other class.
Future<Result<T>> getJSONfromTheSite<T>(
String thecall,
T Function(Map<String, dynamic>) jsonFactory,
) async {
try {
// ...
return Result<T>.success(jsonFactory(response));
} catch (error) {
// ...
}
}
// Then you can call your method like this
await getJSONfromTheSite<MyModel>('latestnews', MyModel.fromRawJson);
// For another class such as MyModel2 it would be like this
await getJSONfromTheSite<MyModel2>('', MyModel2.fromRawJson);
Try the full code on DartPad
I have a flutter application which (simply put) list some data on various screens and can be modified. My current data approach works, but I feel it is not a best practice or optimal.
Currently, when a object is saved, it is converted to JSON (using dart:convert) and stored in a file on the device (using dart.io), overriding the file if it exist. Every screen that needs to display these objects reads the file to get the objects. Every time there is a change that needs to be saved, it exports everything (overwrites) again then imports it again to display it.
The reason I chose JSON over S is because I want to add a web portion later. Does this approach of reading/writing a best practice? I feel this much reading/writing of all the data for most screens could cause some performance issues.
Any advice is appreciated!
This is a possible way to keep data in-memory and write to disk when changes are made to your datamodel/settings.
I use RxDart myself. You don't need it per se, although it does make life easier. I'll be simplifying the examples below, so you get to know the concept and apply it to your own needs.
Let say you keep track of data in your settings class:
#JsonSerializable()
class Settings {
String someData1;
String someData2;
// json seriazable functions
}
You need a "handler"1 or something similar that manages changes made to your Settings and also to read/write data:
class SettingsHandler {
Settings _settings;
StreamController<Settings> _settingsController = BehaviorSubject<Settings>();
StreamController<String> _data1Controller = BehaviorSubject<String>();
StreamSink<String> get data1Input => _data1Controller.sink;
Observable<String> get data1Output => Observable(_data1Controller.stream);
Future<Settings> _readFromDisk() async {
// do your thing
}
Future<Settings> _writeToDisk(Settings settings) async {
// do your thing
}
Future<void> init() async {
// read from disk
_settings = await _readFromDisk();
_settingsController.sink.add(_settings);
// initialize data
data1Input.add(_settings.someData1);
data1Output
.skip(1) // we skip because we just added our initialization data above.
.listen((value) =>
// we must propagate through the update function
// otherwise nothing gets written to disk
update((settings) => settings.someData1 = value)
);
// when changes are made, it needs to notify this stream
// so everything can be written to disk
_settingsSaver.output
// save settings every 2.5 seconds when changes occur.
.debounceTime(const Duration(milliseconds: 2500))
// get the changes and write to disk
.listen((settings) => _writeToDisk(settings));
}
// this function is crucial as it allows changes to be made via the [adjustFunc]
// and then propagates this into the _settingsSaver stream.
void update(void Function(Settings input) adjustFunc) {
adjustFunc(_settings);
_settingsSaver.sink.add(_settings);
}
}
So now you can do something like
var handler = SettingsHandler();
await handler.init();
// this
handler.data1Input.add('NewData');
// or this
handler.update((settings) {
settings.someData1 = 'NewData';
});
Remember, this code only shows how the concept can work. You need to change it for your situation. You could also decide to not expose data1Input or the update(...) function, this is up to your own design.
1 I personally use BloC, your situation might require a different way.
I am trying to implement a grid with tree data using ag-grid. I am using the Enterprise Row Model. The problem is that when hard coding the data and setting it through setRowData the grid displays perfectly. However, when data is loaded through the enterprise row model, the grid does not render as a tree. In fact, the getDataPath callback is not even being called.
Did anyone manage to use the tree data feature with an enterprise data source as this does not seem to be documented?
Thanks
I am assuming that by Enterprise row model, you mean Serverside row model so you expect tree structured data from server. In that case, I have been able to combine following features in Ag-grid : Infinite scrolling + Tree data + server side row model.
I have even implemented custom filtering and it's working as expected.
Data flow:
We have to enable the server side row model on ag-grid using its configuration.
Implement a fake server and proxy data source objects in JavaScript. The object of data source must contain a method named "getRows" that ag-grid can call. Ag-grid will call this method every time user performs actions such as: scroll, filter, sorting, expanding a parent row to see child rows etc.
Implement a method called onGridReady() which will be called by ag-grid every time it's trying to render the grid first time, and then pass the server and dataSource objects to ag-grid's internal API inside onGridReady().
Implementation (using combination of ReactJS and plain JavaScript):
Enable serverSide row model in ag-grid.
<AgGridReact
columnDefs={this.columnDefs}
rowModelType={this.rowModelType}
treeData={true}
isServerSideGroup={this.isServerSideGroup}
getServerSideGroupKey={this.getServerSideGroupKey}
onGridReady={this.onGridReady}
cacheBlockSize={50}
/>
Implement a fake server and proxy data source objects in JavaScript.
To work with server side row model, you need to supply the data in an instance of ServerSideDataSource in JavaScript. Instance of ServerSideDataSource must have a method called getRows() which will be called by ag-grid every time user scrolls down to get next set of data or a row is expanded for retrieving its children records in Tree structure.
The constructor for ServerSideDataSource accepts a proxy data container, typcailly used in ag-grid example: a fakeServer instance. A singleton instance of fakeServer holds the data that was received from the real server and keeps it for following usage:
a) when ag-grid wants to display child records, it calls getRows. Because we supply a custom implementation of this ServerSideDataSource, we can write logic inside getRows to extract data from this fakeServer instance.
b) When ag-grid tries to display next set of data in infinite scrolling or paging, it checks how much data was last retrieved to ask for next block in infinite scrolling (using startRow and endRow variable).
Defining fake server and server side data source:
function createFakeServer(fakeServerData) {
function FakeServer(allData) {
this.data = allData;
}
FakeServer.prototype.getData = function(request) {
function extractRowsFromData(groupKeys, data) {
if (groupKeys.length === 0) {
return data; //child records are returned from here.
}
var key = groupKeys[0];
for (var i = 0; i < data.length; i++) {
if (data[i].employeeId === key) {
return extractRowsFromData(groupKeys.slice(1), data[i].children.slice());
}
}
}
return extractRowsFromData(request.groupKeys, this.data);
};
return new FakeServer(fakeServerData);
}
function createServerSideDatasource(fakeServer) {
function ServerSideDatasource(fakeServer) {
this.fakeServer = fakeServer;
}
ServerSideDatasource.prototype.getRows = function(params) {
console.log("ServerSideDatasource.getRows: params = ", params);
var rows = this.fakeServer.getData(params.request);
setTimeout(function() {
params.successCallback(rows, rows.length);
}, 200);
};
return new ServerSideDatasource(fakeServer);
}
Implement onGridReady()
Once this dataSource is ready, you have to supply this to ag-grid by calling its API method: params.api.setServerSideDataSource(). This API is available inside onGridReady() method that must be passed to Ag-grid as well. This method is mandatorily required if you're using serverSide row model.
onGridReady = params => {
...
var fakeServer = createFakeServer(jsonDataFromServer);
var dataSource = createServerSideDatasource(fakeServer);
params.api.setServerSideDatasource(dataSource);
}
Providing a key property that help ag-grid identify parent-child relationship. You have to supply these parameters to grid. Check the point (1) in Implementation that has HTML syntax and shows how to supply these methods to ag-grid.
var rowModelType = "serverSide";
var isServerSideGroup = function (dataItem) {
return !!dataItem.children;
};
var getServerSideGroupKey = function (dataItem) {
return dataItem.employeeId;
};
Notice that in getServerSideGroup(), we are returning a boolean value which checks whether children property of current row (i.e. dataItem) has any children or not.
I would request you to separately look through documents for server side row model for each feature and that means Tree data (client model) and Tree data (server side model) have two different approaches. We can't setup one model and expect it to work with data of other model.
Documentation for server side row model : https://www.ag-grid.com/javascript-grid-server-side-model/
Please try this and let me know. I had these requirements a month ago, so I contacted them for their help on Trial Support and they have prepared a plunker for this problem statement:
Working example for Tree data from server side with infinite scroll. https://next.plnkr.co/edit/XON5qvh93CpURbOJ?preview
Note:
Since the tree data is being retrieved from server, you can't use getDataPath.
Tree data would be in nested hierarchy per row unlike the client-side tree model. So unique column names are not encapsulated in an array.
Wrong :
var rowData = [
{orgHierarchy: ['Erica'], jobTitle: "CEO", employmentType: "Permanent"},
{orgHierarchy: ['Erica', 'Malcolm'], jobTitle: "VP", employmentType: "Permanent"}
...
]
Right :
[{
"employeeId": 101,
...
"children": [
{
"employeeId": 102,
...
"children": [
{
"employeeId": 103,
...
},
{
"employeeId": 104,
...
}
]},
]}
}]
There was one point when the grid was not being rendered at all in initial phase when I was just setting up the grid with Enterprise features and so it's their recommendation to use px instead of % for height and width of the wrapper DIV element that contains your Ag-grid element.
Edit:
If you prefer to fetch children record in separate API calls to save initial load time then you can make these API calls inside getRows() method. API call will have success and error callbacks. Inside the success callback method, once you receive the children data, you can pass them to ag-grid using:
params.successCallback(childrenData, childrenData.length);
Example: When a parent row is expanded, it sends a unique key of that parent row (which you must have configured already) through params.request.groupKeys. In this example, I am using JavaScript's fetch() to represent API calling. This method accepts ApiUrl, and optional request parameters' object in case of POST/PUT requests.
ServerSideDatasource.prototype.getRows = function(params) {
//get children data based on unique value of parent row from groupKeys
if(params.request.groupKeys.length > 0) {
fetch(API_URL, {...<required parameters>...})
.then(response => response.json(), error => console.log(error))
.then((childrenData) => {
params.successCallback(childrenData, childrenData.length);
});
}
else {
//this blocks means - get the parent data as usual.
params.successCallback(this.fakeServer.data, this.fakeServer.data.length);
}
};
Adding import 'ag-grid-enterprise' followed by initializing the enterprise key resolved the issue of getDataPath not working correctly.
Using example provided by Akshay Raut above, I was inspired to combine server side pagination, and client side grouping. Following his steps, with a slight change. On the getServerSideDatasource function, you can check if the parent node have the children, and rather than calling the server, just return the children directly. That way, you will be able to display the already loaded children as client side. Here is a sample code:
getServerSideDatasource(): IServerSideDatasource {
return {
getRows: (params) => {
if (params.request.groupKeys.length > 0) {
params.success({
rowData: params.parentNode.data.sales,
rowCount: params.parentNode.data.sales.length,
});
} else {
// Your regular server code to get next page
}
Here is a stackblitz:
https://stackblitz.com/edit/ag-grid-angular-hello-world-1gs4jx?file=src%2Fapp%2Fapp.component.ts
Make sure you are using gridOptions.treeData = true An example is here.
Hierarchy of data should be properly set when you implement the gridOptions.getDataPath(data)
Make sure you have implemented Enterperise.getRows Read more
If the above things don't work, share your code here to understand the overall picture better.
Infinite Scrolling or Enterprise/Serverside datasources are not compatible with Tree Data
https://www.ag-grid.com/javascript-grid-row-models/
So you have to either change your code to use Client Side Row Model or use Row Grouping (only available in enterprise)
Restangular offers a feature, extendModel, which lets you add functionality onto objects returned from the server. Is there any way to get these methods added to an empty / new model, that hasn't yet been saved to the server?
I wanted to do the same thing but didn't find an example. Here's how I ended up doing it:
models.factory('User', function(Restangular) {
var route = 'users';
var init = {a:1, b:2}; // custom User properties
Restangular.extendModel(route, function(model) {
// User functions
model.myfunc = function() {...}
return model;
});
var User = Restangular.all(route);
User.create = function(obj) {
// init provides default values which will be overridden by obj
return Restangular.restangularizeElement(null, _.merge({}, init, obj), route);
}
return User;
}
Some things to be aware of:
Use a function like _.merge() instead of angular.extend() because it clones the init variable rather than simply assigning its properties.
There is a known issue with Restangular 1.x that causes the Element's bound data to not be updated when you modify its properties (see #367 and related). The workaround is to call restangularizeElement() again before calling save(). However this call will always set fromServer to false which causes a POST to be sent so I wrote a wrapper function that checks if id is non-null and sets fromServer to true.
after I change to Backbone-Relational my model stopped to work when I call destroy().. I have to ensure that when it removes success on server there will be no more id at client side, so then when I try to save it again my model wont request PUT (update) - throws Record Not Found on server side.
Coffeescript side
save: ->
if isBlank #model.get("text")
#model.destroy() # after success it still with same attributes including id!!
else
#model.save()
Rails side
def destroy
#note = Note.find(params[:id])
#note.destroy
respond_with #note # callback is empty
end
Bug from Backbone-Relational perhaps? Does Backbone.js update id after destroy?
Backbone does not look like it modifies the Model on destruction in any way. Except removing it from its collection if any.
Check the code
What it does is triggering the event destroy so you easily can listen this event and do whatever you think is the proper behavior in destruction:
// code simplified and no tested
var MyModel = Backbone.Model.extend({
initialize: function(){
this.on( "destroy", this.afterDestroy, this );
},
afterDestroy: function(){
this.set( "id", null );
}
});