Why IngDao?

IngDao is a data management framework for flutter mobile application. It encapsulates how the data source coming and persistance way from the service layer. The data source can be file, database, network or hybrid mode. Programmer just need to configure all the available data sources in one time and activate it, then programmer no need to know the data source exactly during data manipulating in the rest of the functions. They just need know the common data manipulate method like, fetch, delete, save, count...

Framework adopted the factory/repository concept. Factory will expose out the generic data function: save, delete, fetch and count. Repository will be used by the factory to provide actual implementation.

Currently, the framework supported sqlite database and network data sources as repository term. For sqlite database, it will auto detect the sql table changes without user specify the migration plan and version. Currently the support mode is able to detect new field/new table be added. In future, will detect field/table be removed or even field type change. It support table joining, pagination, grouping and allow user select multiple fields from different table at once time without write pure sql.

For sqlitedb repository, its allow you snapshot the database and get the snapshot version. Later you can restore the db to that version if any ermengency found.

Framework also supported database encryption with AES and SSL pinning for web request. In future, AES/DH for data network flow will be supported too.
*Backend server need to follow the same way to receive and decrypt the AES data.

Programmer can create own repostiory by extends from DfBaseRepository, and provide the implementation. After that, programmer need to register the repository to factory.

The framework architecture concept is provided as below: -

Custom Repository

You can create your own repository by implementing the following functions: -

import 'package:ingdao/dataflow/model/DfBaseModel.dart';
import 'package:ingdao/dataflow/model/DfBaseRepository.dart';

class DfMyCustomRepositoryModel extends DfBaseModel<DfMyCustomEntity> {
  String extraParamINeed;

  DfDbModel({@required String name, @required bool encrypt, @required this.extraParamINeed, @required List<DfMyCustomEntity> entities}) : super(name: name, encrypt: encrypt, entities: entities);

}

class DfMyCustomRepository extends DfBaseRepository<DfMyCustomRepositoryModel> {

  DfMyCustomRepository({@required DfMyCustomRepositoryModel model}) : super(model: model);

  Future<void> startInit() async {

  }

  Future<void> stop() async {

  } 

  Future<DfResponse> fetch(DfQuery query) async {

  }

  Future<List<DfResponse>> save(List<DfQuery> queries) async {

  }

  Future<List<DfResponse>> delete(List<DfQuery> queries) async {

  }
}

Then register and activate your new repository via DfFactory: -

  DfFactory instance = DfFactory();
  DfMyCustomRepository repMyCustom = DfMyCustomRepository(model: modelMyCustom); 
  instance.registerRepository("myRepositoyName", repMyCustom);
  await instance.activateRepository("myRepositoyName");

How to use repositories.

  1. Define all the available repositories in Enum
const REPO_NAME_SQLITEDB = "sqlitedb";
const REPO_NAME_WEB = "web";
const REPO_NAME_ONLINE_SYNC = "onlinesync";

enum RepositoryType {
  sqlitedb,
  web,
  onlinesync
}

extension RepositoryTypeExtension on RepositoryType {

  String get name {
    switch (this) {
      case RepositoryType.sqlitedb:
        return REPO_NAME_SQLITEDB;
      case RepositoryType.web:
        return REPO_NAME_WEB;
      case RepositoryType.onlinesync:
        return REPO_NAME_ONLINE_SYNC;                
      default:
        return null;
    }
  }

  String desp() {
    return this.name;
  }
}

2. Start plan out the fields for the table entity and web http json response. Example, we have an object called "TankTimesheet" which stores the timesheet event against the oil tank maintenance.

So, create a 'TankTimesheetField' class that represent the tank timesheet fields container. The data source of tank timesheet object can be db table entity or HTTP response. This class also have one method call createFieldBasedOnRepo(DfBaseRepository repo). It will based on pass in repository to return the related entity accordingly. One is the db entity and another one is web entity. The entity will be implemented at below later

import 'package:ingdao/dataflow/field/dfbasefield.dart';
import 'package:ingdao/dataflow/repository/dfbaserepository.dart';
import '../../data_factory.dart';
import '../../entity/tanktimesheet/tanktimesheet.dart';
import '../../entity/tanktimesheet/tanktimesheetweb.dart';

abstract class TankTimesheetField {

  DfBaseField fId;
  DfBaseField fTankId;
  DfBaseField fName;
  DfBaseField fMarkOn;
  DfBaseField fActive;

  static TankTimesheetField createFieldBasedOnRepo(DfBaseRepository repo) {
    switch (repo.model.name) {
      case REPO_NAME_SQLITEDB :
        return TankTimesheet();
      case REPO_NAME_WEB :
        return TankTimesheetWeb();
    }
    return null;
  }
}

3. Create a tank timesheet entity class. This class will be for database repository. The class will extends DfDbEntity and TankTimesheetField together. Please look at the commented 'fTankName' field. This field is not the db field under timesheet table. But declare here is to store the sql fetch result for the tank name of tank table. so the sql look like: "select tanktimesheet.id, tanktimesheet.tankid, tanktimesheet.name, tanktimesheet.markon, tanktimesheet.active, tank.name from tanktimesheet inner join tank", if this field be uncommented.

super(name: "tanktimesheet") - indicate the table name is "tanktimesheet"

The data types available are: text, datetime, boolean, real, blob and int

DfDateTimeValidator, DfStringValidator can be use as field validator before the data saving.

Data Mapping. Declare yours variables to store/retrieve the data such as id, tankid, name, markOn and active. Use the getObjectValue and setObjectValue in DfBaseField (inheritance: DfDbField and DfWebField) to get/fetch the value from db query result or web json response.

parseExecRawJsonResultToEntity(DfQueryPurpose purpose, Mapper map) method determines the json response/query result will be rendered to which entity class.

customFieldJsonMap() method determines the json response/query result attribute how render/map to an entity custom field.

import 'package:ingdao/dataflow/entity/dfbaseentity.dart';
import 'package:ingdao/dataflow/entity/dfdbentity.dart';
import 'package:ingdao/dataflow/field/dfbasefield.dart';
import 'package:ingdao/dataflow/field/dfdbfield.dart';
import 'package:ingdao/dataflow/field/dffieldtype.dart';
import 'package:ingdao/dataflow/control/dfquery.dart';
import 'package:ingdao/dataflow/field/dfstringvalidator.dart';
import 'package:ingdao/dataflow/field/dfdatetimevalidator.dart';
import '../../field/tanktimesheet/tanktimesheet_field.dart';
import 'package:object_mapper/object_mapper.dart';

class TankTimesheet extends DfDbEntity with TankTimesheetField {

  String id;
  String tankId;
  String name;
  DateTime markOn;
  bool active;
    
  TankTimesheet ({String pId, String pTankId, String pName, DateTime pMarkOn, bool pActive}) : super(name: "tanktimesheet") {
    fId = DfDbField(entity: this, name: "id", primaryKey: true, joinKey: false, fieldType: DfFieldType.text(), getObjectValue: () => id, setObjectValue: (v) => id = v, value: pId);
    fTankId = DfDbField(entity: this, name: "tankid", primaryKey: false, joinKey: true, fieldType: DfFieldType.text(), getObjectValue: () => tankId, setObjectValue: (v) => tankId = v, value: pTankId);
    fName = DfDbField(entity: this, name: "name", primaryKey: false, joinKey: false, fieldType: DfFieldType.text(strValidator: DfStringValidator(isAllowEmpty: false, minLen: 5, maxLen: 100, isAlphaAndSpaceAndDigitOnly: true)), getObjectValue: () => name, setObjectValue: (v) => name = v, value: pName);
    fMarkOn = DfDbField(entity: this, name: "markon", primaryKey: false, joinKey: false, fieldType: DfFieldType.datetime(dateTimeValidator: DfDateTimeValidator(isLtNowCheck: true)), getObjectValue: () => markOn, setObjectValue: (v) => markOn = v, value: pMarkOn);
    fActive = DfDbField(entity: this, name: "active", primaryKey: false, joinKey: false, fieldType: DfFieldType.boolean(), getObjectValue: () => active, setObjectValue: (v) => active = v, value: pActive);
    //fTankName = DfDbField(entity: Tank(), name: "name", primaryKey: false, joinKey: false, fieldType: DfFieldType.text(), displayOnly: true);
  }

  //Define the sqilite table fields available.
  //Define the fields will map back to the sqlite query executed reuslt: Map<String,dynamic>
  @override
  List<DfDbField> fields() {
     return [fId,fTankId,fName,fMarkOn,fActive/*,fTankName*/];
  }

  @override
  DfBaseEntity<DfBaseField> newInstance() {
    return TankTimesheet();
  }

  @override
  DfBaseEntity<DfDbField> parseExecRawJsonResultToEntity(DfQueryPurpose purpose, Mapper map) {
    return map.toObject<TankTimesheet>();
  }

  @override
  void customFieldJsonMap(Mapper map, List<DfDbField> fields) {
    for (var field in fields) {
      // if (field == fTank) {
      //   map<Tank>(field.name, field.getObjectValue(), (v) => field.setObjectValue(v), field.getTransform());        
      // }
    }
  }
}

DfDbField Parameter

  • Parameters: | Name | Type | Nullable | Remark | | ---------- | --------------------- | -------- | --------------------------------------------------------------------- | | entity | DfBaseEntity | N | field entity | | name | String | N | Sql table field name | | primaryKey | bool | N | Is primary key field | | joinKey | bool | N | will be use to join with another table | | fieldType | FieldType | N | Field type like text, real, int | | getObjectValue | Function | N | field value map to json attribute in a json request | | setObjectValue | Function | N | field value map to json attribute in a json response (store value to entity field) |
    | value | dynamic | Y | Any value | | displayOnly | bool | N | True means the field not under current entity but from other entity with display puporse only. |

4. TankTimesheeWeb entity class for web repository. The class will extends DfWebEntity and TankTimesheetField. One extra method need to implement is "convertToWebReqParam(DfQuery query)" this will let the entity know how to define the HTTP request params, path variable, http headers and json body based on the DfQuery object.

super(name: "tanktimesheet") - indicate the web service end point is "tanktimesheet"

Programmer can specify the web http method via query.purpose variable.

  • Parameters: | Name | Http Method | | ---------- | --------------------- | | Fetch | GET | | ADD | POST | | UPDATE_WHOLE_ENTITY_ATTRS | PUT | | UPDATE_SOME_ENTITY_ATTRS | PATCH | | DELETE | DELETE |
import 'package:ingdao/dataflow/control/dfquery.dart';
import 'package:ingdao/dataflow/control/dfqueryorderby.dart';
import 'package:ingdao/dataflow/entity/dfbaseentity.dart';
import 'package:ingdao/dataflow/entity/dfwebentity.dart';
import 'package:ingdao/dataflow/field/dfbasefield.dart';
import 'package:ingdao/dataflow/field/dfstringvalidator.dart';
import 'package:ingdao/dataflow/field/dfwebfield.dart';
import '../../field/tanktimesheet/tanktimesheet_field.dart';
import 'package:ingdao/dataflow/field/dffieldtype.dart';
import 'package:object_mapper/object_mapper.dart';
import '../../../appsetting.dart';

class TankTimesheetWeb extends DfWebEntity with TankTimesheetField {

  String id;
  String tankId;
  String name;
  DateTime markOn;
  bool active;
  //Tank tank;
  //List<TsNote> notes;

  TankTimesheetWeb ({String pId, String pTankId, String pName, DateTime pMarkOn, bool pActive}) : super(contextPath: "tanktimesheet") {
    fId = DfWebField(entity: this, name: "id", fieldType: DfFieldType.text(), getObjectValue: () => id, setObjectValue: (v) => id = v, value: pId);
    fTankId = DfWebField(entity: this, name: "tankId", fieldType: DfFieldType.text(), getObjectValue: () => tankId, setObjectValue: (v) => tankId = v, value: pTankId);
    fName = DfWebField(entity: this, name: "name", fieldType: DfFieldType.text(strValidator: DfStringValidator(minLen: 100)), getObjectValue: () => name, setObjectValue: (v) => name = v, value: pName);
    fMarkOn = DfWebField(entity: this, name: "markOn", fieldType: DfFieldType.datetime(), getObjectValue: () => markOn, setObjectValue: (v) => markOn = v, value: pMarkOn);
    fActive = DfWebField(entity: this, name: "active", fieldType: DfFieldType.boolean(), getObjectValue: () => active, setObjectValue: (v) => active = v, value: pActive);
    //fTank = DfWebField(entity: this, name: "tank", fieldType: DfFieldType.custom(), value: null);
    //fNotes = DfWebField(entity: this, name: "notes", fieldType: DfFieldType.custom(), value: null);
  }

  //Define what are the fields will map back the result from http response Map<String,dynamic>
  @override
  List<DfWebField> fields() {
     return [fId,fTankId,fName,fMarkOn,fActive];
  }

  @override
  DfWebReqParam convertToWebReqParam(DfQuery query) {
    DfWebReqParam param = DfWebReqParam();
    param.setAuthToken(AppSetting.token);

    if (query.purpose == DfQueryPurpose.FETCH) {

      if (query.paging != null) {
        param.reqParams["pageNumber"] = "${query.paging.pageNumber}";
        param.reqParams["perPageCount"] = "${query.paging.perPageCount}";
      }
        
      if (query.orderBy != null) {
        int i = 0;
        for (var orderBy in query.orderBy) {
          ++i;
          param.reqParams["orderBy$i"] = orderBy.field.name;
          param.reqParams["asc$i"] = orderBy.orderByType ==OrderByType.asc ? "true" : "false";
        }
      }

        if (query.criteriaGroups != null) {
          query.criteriaGroups.queryGroups.forEach((grp) => grp.criterias.forEach((cri) => param.pathVariables.add(cri.values[0])) );
        }

        return param;
    }
    return null;
  }

  @override
  DfBaseEntity<DfBaseField> newInstance() {
    return TankTimesheetWeb();
  }

  @override
  DfBaseEntity<DfWebField> parseExecRawJsonResultToEntity(DfQueryPurpose purpose, Mapper map) {
    if (purpose == DfQueryPurpose.FETCH)
      return map.toObject<TankTimesheetWeb>();

    return null;
  }

  @override
  void customFieldJsonMap(Mapper map, List<DfWebField> fields) {
    for (var field in fields) {
      // if (field == fData) {
      //   map<UpdateDetailsWeb>(field.name, field.getObjectValue(), (v) => field.setObjectValue(v), field.getTransform());        
      // }
    }
  }
}

DfWebField Parameter

  • Parameters: | Name | Type | Nullable | Remark | | ---------- | --------------------- | -------- | --------------------------------------------------------------------- | | entity | DfBaseEntity | N | field entity | | name | String | N | json body field name | | value | dynamic | Y | Any value |

5. Create a factory class that contains all the available repository and defined what the the entities for the repository.


import '../appsetting.dart';
import 'package:ingdao/ingdao.dart';
import 'package:ingdao/dataflow/model/dfwebmodel.dart';
import 'package:ingdao/dataflow/repository/dfwebrepository.dart';
import 'package:ingdao/dataflow/repository/dfsyncrepository.dart';
import 'package:ingdao/dataflow/model/dfdbmodel.dart';
import 'package:ingdao/dataflow/model/dfsyncmodel.dart';
import 'package:ingdao/dataflow/repository/dfdbrepository.dart';
import 'entity/tanktimesheet/tanktimesheet.dart';
import 'entity/tanktimesheet/tanktimesheetweb.dart';

const REPO_NAME_SQLITEDB = "sqlitedb";
const REPO_NAME_WEB = "web";
const REPO_NAME_ONLINE_SYNC = "onlinesync";

enum RepositoryType {
  sqlitedb,
  web,
  onlinesync
}

extension RepositoryTypeExtension on RepositoryType {

  String get name {
    switch (this) {
      case RepositoryType.sqlitedb:
        return REPO_NAME_SQLITEDB;
      case RepositoryType.web:
        return REPO_NAME_WEB;
      case RepositoryType.onlinesync:
        return REPO_NAME_ONLINE_SYNC;                
      default:
        return null;
    }
  }

  String desp() {
    return this.name;
  }
}

class DataFactory {
  DataFactory._();
  static final instance = DataFactory._();

  Future<DfFactory> getManipulator(RepositoryType type) async {
    DfConverter.dtTimeFormat = 'yyyy-MM-ddTHH:mm:ss'; //Specify the datetime parse format between json/sqlitedb and flutter variable.
    DfFactory dfFactory = DfFactory();

      //Web
      SslPinningCri sslPinningCri;//SslPinningCri(shakeys: appSetting.sslPinningFingerPrints, shatype: SslPinningCriShaType.SHA256);
      DfWebModel modelWeb = DfWebModel(name: RepositoryType.web.name, 
                                      encrypt: AppSetting.dataEncrypt, 
                                      entities: [TankTimesheetWeb()], 
                                      serverUrl: AppSetting.serverUrl, 
                                      commonHeaders: DfWebModel.formJsonHeader(),
                                      internetIsCompulsory: true, //True means the Internet must be available, otherwise, exception will throw 
                                      sslPinningCri: sslPinningCri);

      DfWebRepository repWeb = DfWebRepository(model: modelWeb); 
      dfFactory.registerRepository(RepositoryType.web.name, repWeb);

      //Database
      DfDbModel modelDb = DfDbModel(name: RepositoryType.sqlitedb.name, 
                                    dbName: AppSetting.dbName, 
                                    encrypt: AppSetting.dataEncrypt, 
                                    entities: [TankTimesheet()]);
      DfDbRepository repDb = DfDbRepository(model: modelDb);
      dfFactory.registerRepository(RepositoryType.sqlitedb.name, repDb);

      //Online Sync
      DfSyncModel modelSync = DfSyncModel(name: RepositoryType.onlinesync.name, 
                                          retryIntervalInSecond: 15 * 60, 
                                          dbRepository: repDb, 
                                          webRepository: repWeb);
      DfSyncRepository repSync = DfSyncRepository(model: modelSync);                                          
      dfFactory.registerRepository(RepositoryType.onlinesync.name, repSync);

      //Activate
      await dfFactory.activateRepository(type.name);  
      return dfFactory;
  }
}

DfDbModel Parameter

  • Parameters: | Name | Type | Nullable | Remark | | ---------- | --------------------- | -------- | --------------------------------------------------------------------- | | name | String | N | repository name | | dbName | String | N | database name | | encrypt | bool | N | Database need to encrypt or not | | entities | List | N | list of database entities |

DfWebModel Parameter

  • Parameters: | Name | Type | Nullable | Remark | | ---------- | --------------------- | -------- | --------------------------------------------------------------------- | | name | String | N | repository name | | serverUrl | String | N | serverUrl e.g. https://www.myserver.com/rest/ | | encrypt | bool | N | Data transfer need to encrypt or not. AES will be used | | entities | List | N | list of database entities | | sslPinningCri | SslPinningCri | N | provide one or more fingerprints string for SSL pinning purpose |

5.1. Create a Service layer class. A class that should not contains any code related to UI. The class should extends from DfService. The class should contains business logic functions and utiltise the CRUD methods provided by the repositories.

import 'package:ingdao/ingdao.dart';
import 'package:ingdao/dataflow/field/dfbasefield.dart';
import 'package:ingdao/dataflow/control/dfquery.dart';
import 'package:ingdao/dataflow/control/dfquerypaging.dart';
import 'package:ingdao/dataflow/control/dfqueryorderby.dart';
import 'package:ingdao/dataflow/control/dfresponse.dart';
import 'package:ingdao/dataflow/control/dfquerycriteriagroup.dart';
import 'package:ingdao/dataflow/control/dfquerycriteria.dart';
import 'package:ingdao/dataflow/control/dfqueryentityjoin.dart';
import 'package:ingdao/dataflow/service/dfservice.dart';
import 'package:ingdao/dataflow/model/dfwebmodel.dart';
import 'package:ingdao/dataflow/repository/dfwebrepository.dart';
import 'package:ingdao/dataflow/model/dfdbmodel.dart';
import 'package:ingdao/dataflow/repository/dfdbrepository.dart';
import 'package:ingdao/dataflow/entity/dfbaseentity.dart';

class AppService extends DfService {
  DfFactory dfFactory = DfFactory();

  @override
  startInit() async {
  }
}

CRUD Sample. Add the following functions to the service class above. So this will be the data manipulation center.

DfQuery Parameter - define the request data/operation parameters for the repository generic method: fetch, delete, save and count. It defines the purpose, select fields, entities invole, criterias, order by, grouping and pagination.

  • Parameters: | Name | Type | Nullable | Remark | | ---------- | --------------------- | -------- | --------------------------------------------------------------------- | | purpose | DfQueryPurpose | N | Http Method | | queryFields | List or Map<DfBaseField, dynamic> | N | select query fields, or fields need to insert/save | | entityJoin | DfQueryEntityJoin | N | List of entities involves and the join type like inner join, left join | | criteriaGroups | DfQueryCriteriaGroups | Y | select criteria based on field value and operator like eq, neq, like all supported | | orderBy | List | Y | order by one or more field with asc/desc support | | groupBy | List | Y | Group by one or more field |
    | paging | Paging | Y | Pagination by Page number and limit |

CRUD - Save Record

Save the timesheet object. If the primary key exists, the record will be updated. Otherwise, insert.

Future<List<DfResponse>> saveTimesheet(List<TankTimesheet> entries) async {
    DfFactory dbFactory = await DataFactory.instance.getManipulator(RepositoryType.sqlitedb);
    DfBaseRepository repository = dbFactory.currentRepository;
    DfBaseEntity tankTimeSheetEntity = repository.model.entities[0];

    List<DfQuery> timesheetEntriesSaveQueries = List<DfQuery>();
    entries.forEach((entry) {
      DfQuery query = DfQuery(purpose: DfQueryPurpose.ADD, queryFields: entry.toMap(), entityJoin: DfQueryEntityJoin(entities: [tankTimeSheetEntity])  );
      timesheetEntriesSaveQueries.add(query);
    });
    return await dbFactory.save(timesheetEntriesSaveQueries);
}

CRUD - Fetch Record

First of all, use count function to see the local db got record or not. If not, fetch from web service and save to local db.

  Future<FetchEntityResult<TankTimesheet>> fetchTimesheet(String tankId, int pageNumber, int limit) async {
    DfFactory webFactory = await DataFactory.instance.getManipulator(RepositoryType.web);
    DfFactory dbFactory = await DataFactory.instance.getManipulator(RepositoryType.sqlitedb);

    DfDbRepository dbRepository = dbFactory.getRepositoryBasedOnName(REPO_NAME_SQLITEDB);

    FetchEntityResult<DfBaseEntity> webResult;

    //If you want, can truncate table first, so it always fetch from Internet and store to db.
    //dbFactory.truncate(TankTimesheet());

    //Check table got record, if no, then only fetch from web
    int recCount = await _fetchCount(dbFactory, tankId);
    //String snapShotVersion = await dbRepository.snapShot();
    if (recCount == 0) {
      //Fetch from web
      webResult = await _fetch(webFactory, tankId, pageNumber, limit);
      if (webResult.error == null) {
        //Save to db
        DfBaseRepository repository = dbFactory.currentRepository;
        DfBaseEntity tankTimeSheetEntity = repository.model.entities[0];

        List<DfQuery> timesheetEntriesSaveQueries = List<DfQuery>();
        webResult.entities.forEach((entry) {
          TankTimesheetWeb tankTimesheetWeb = entry;
          TankTimesheet saveObj = TankTimesheet();
          saveObj.copyFieldValueFromEntity(tankTimesheetWeb);
          // saveObj.id = tankTimesheetWeb.id;
          // saveObj.name = tankTimesheetWeb.name;
          // saveObj.active = tankTimesheetWeb.active;
          // saveObj.tankId = tankTimesheetWeb.tankId;
          // saveObj.markOn = tankTimesheetWeb.markOn;
          DfQuery query = DfQuery(purpose: DfQueryPurpose.ADD, queryFields: saveObj.toMap(), entityJoin: DfQueryEntityJoin(entities: [tankTimeSheetEntity])  );
          timesheetEntriesSaveQueries.add(query);
        });
        await dbFactory.save(timesheetEntriesSaveQueries);
      }
    }

    //await dbRepository.restoreToSnapShotVersion(snapShotVersion);

    //Demo how to fetch single record based on primary key value.
    DfResponse singleResp = await dbFactory.fetchSingleRecordWithPrimaryKeyValues(TankTimesheet(), ["TS 21"]);
    TankTimesheet singleEntity = singleResp.renderResultToEntity();
    if (singleEntity != null)
      print("Single Entity Name: ${singleEntity.name}");
    else
      print("Record not found for id: TS 21");    

    //Fetch latest from db
    FetchEntityResult<DfBaseEntity> dbResult = await _fetch(dbFactory, tankId, pageNumber, limit);
    if (webResult != null && webResult.error != null) {
      dbResult.error = webResult.error;
    }

    FetchEntityResult<TankTimesheet> finalResult = FetchEntityResult<TankTimesheet>();
    finalResult.error = dbResult.error;
    if (dbResult.entities != null) {
      finalResult.entities = List<TankTimesheet>();
      dbResult.entities.forEach((ent) {
        finalResult.entities.add(ent as TankTimesheet); 
      });
    }

    return finalResult;
  }

CRUD - Generic util method (entity fetch and count fetch) for function above.

  Future<int> _fetchCount(DfFactory factory, String tankId) async {
    DfBaseRepository repository = factory.currentRepository;
    DfBaseEntity tankTimeSheetEntity = repository.model.entities[0];
    TankTimesheetField tankTimesheetField = TankTimesheetField.createFieldBasedOnRepo(repository);

    List<DfQueryCriteria> criterias = [];
    if (tankId != null && tankId.length > 0) {
      criterias.add(DfQueryCriteria(field: tankTimesheetField.fTankId, oper: OperType.eq, values: [tankId] ));
    }

    DfQuery query = DfQuery(purpose: DfQueryPurpose.FETCH, 
                            queryFields: tankTimeSheetEntity.getSelectQueryCountField(), 
                            entityJoin: DfQueryEntityJoin(entities: [tankTimeSheetEntity], 
                            joins: []), //JoinType.innerJoin 
                            //queryStr can be something like "(g and g) or g"  - g represent a DfQueryCriteriaGroup object 
                            criteriaGroups: DfQueryCriteriaGroups(queryGroups: [DfQueryCriteriaGroup(criterias: criterias, andOrType: AndOrType.and)], queryStr: ""), 
                            orderBy: null, 
                            paging: null);

    DfResponse resp = await factory.fetch(query);
    return resp.renderFetchAggregateResultToInt();
  }

  Future<FetchEntityResult<DfBaseEntity>> _fetch(DfFactory factory, String tankId, int pageNumber, int limit) async {
    DfBaseRepository repository = factory.currentRepository;
    DfBaseEntity tankTimeSheetEntity = repository.model.entities[0];
    TankTimesheetField tankTimesheetField = TankTimesheetField.createFieldBasedOnRepo(repository);

    List<DfBaseField> selFields = tankTimeSheetEntity.fields();

    List<DfQueryCriteria> criterias = [];
    if (tankId != null && tankId.length > 0) {
      criterias.add(DfQueryCriteria(field: tankTimesheetField.fTankId, oper: OperType.eq, values: [tankId] ));
    }

    DfQueryOrderBy orderBy =DfQueryOrderBy(field: tankTimesheetField.fName, orderByType: OrderByType.asc);
    Paging paging = Paging(pageNumber: pageNumber, perPageCount: limit);

    DfQuery query = DfQuery(purpose: DfQueryPurpose.FETCH, 
                            queryFields: selFields, 
                            entityJoin: DfQueryEntityJoin(entities: [tankTimeSheetEntity], 
                            joins: []), //JoinType.innerJoin 
                            //queryStr can be something like "(g and g) or g"  - g represent a DfQueryCriteriaGroup object 
                            criteriaGroups: DfQueryCriteriaGroups(queryGroups: [DfQueryCriteriaGroup(criterias: criterias, andOrType: AndOrType.and)], queryStr: ""), 
                            orderBy: [orderBy], 
                            paging: paging);

    DfResponse resp = await factory.fetch(query);
    List<DfBaseEntity> finalList;
    if (resp.error == null) {
      finalList = resp.renderResultToEntityList();

      final validateMessages = resp.validate(false, DfValidatorListSize(min: 1,max: 10));
      validateMessages.forEach((key,arr) {
        print("Validating fail for $key");
        arr.forEach((err) {
          print("Validating error: ${err.code} ${err.message}");
        });
      });
    }

    return FetchEntityResult<DfBaseEntity>(entities: finalList, error: resp.error);
  }

Start initialize your service class

class SplashScr extends StatelessWidget {
  final AppService appService = AppService();

  _loadAppService(BuildContext context) async {
    await appService.startInit();
      Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) => UserListScr(appService: appService))
    );
  }

Error Handling

Error handling for below potential errors should be handle in service layer first and than propagate up to UI layer. DfResponse will contains an error object if any error below or generic HTTP error happen.

//Validation
class AllowEmptyValidateException extends DfError {
   AllowEmptyValidateException() : super(code: 10001, message: "Value empty is not allowed");
}

class ListSizeValidateException extends DfError {
   ListSizeValidateException(DfValidatorListSize size, int currentSize) : super(code: 10002, message: "List size must be ${size.toString()}, but is $currentSize");
}

class MinDateValidateException extends DfError {
   MinDateValidateException(DateTime dtTime) : super(code: 10013, message: "Input date/time is less than ${dtTime.toString()}");
}

class MaxDateValidateException extends DfError {
   MaxDateValidateException(DateTime dtTime) : super(code: 10014, message: "Input date/time is greater than ${dtTime.toString()}");
}

class LtNowValidateException extends DfError {
   LtNowValidateException() : super(code: 10015, message: "Input date/time must be less than now.");
}

class MinValueValidateException extends DfError {
   MinValueValidateException(double minValue) : super(code: 10011, message: "Input value is less than $minValue");
}

class MaxValueValidateException extends DfError {
   MaxValueValidateException(double maxValue) : super(code: 10012, message: "Input value is greater than $maxValue");
}

class MinLenValidateException extends DfError {
   MinLenValidateException(int minLen) : super(code: 10002, message: "Input length is less than min length: $minLen required.");
}

class MaxLenValidateException extends DfError {
   MaxLenValidateException(int maxLen) : super(code: 10003, message: "Input length is more than max length: $maxLen allowed.");
}

class EmailValidateException extends DfError {
   EmailValidateException() : super(code: 10004, message: "Invalid email address.");
}

class AlphaValidateException extends DfError {
   AlphaValidateException() : super(code: 10005, message: "Input must be alphabetic only.");
}

class AlphaSpaceValidateException extends DfError {
   AlphaSpaceValidateException() : super(code: 10006, message: "Input must be alphabetic or space only.");
}

class DigitValidateException extends DfError {
   DigitValidateException() : super(code: 10007, message: "Input must be digit character only.");
}

class NumericValidateException extends DfError {
   NumericValidateException() : super(code: 10008, message: "Input must be numeric character only.");
}

class AlphaDigitValidateException extends DfError {
   AlphaDigitValidateException() : super(code: 10009, message: "Input must be alphabetic or digit character only.");
}

class CustomInputValidateException extends DfError {
   CustomInputValidateException() : super(code: 10010, message: "Invalid input format.");
}

class AlphaSpaceDigitValidateException extends DfError {
   AlphaSpaceDigitValidateException() : super(code: 10011, message: "Input must be alphabetic, digit or space only.");
}

class AllowEmptyValidateException extends DfError {
   AllowEmptyValidateException() : super(code: 10001, message: "Value empty is not allowed");
}

//Db Operation
class OpenDbException extends DfError {
  OpenDbException() : super(code: 7100, message: "Fail to open db.");
}

class CloseDbException extends DfError {
  CloseDbException() : super(code: 7101, message: "Fail to close db.");
}

class UpdateDbStructException extends DfError {
  UpdateDbStructException() : super(code: 7102, message: "Fail to update db structure.");
}

class DeleteQueryException extends DfError {
  DeleteQueryException() : super(code: 7103, message: "Fail to execute delete query.");
}

class FetchQueryException extends DfError {
  FetchQueryException() : super(code: 7104, message: "Fail to execute fetch query.");
}

class SaveQueryException extends DfError {
  SaveQueryException() : super(code: 7105, message: "Fail to execute save query.");
}

//Web Operation
class InvalidWebReqException extends DfError {
  InvalidWebReqException() : super(code: 8100, message: "Invalid web request. Please specify web request in EntityJoin");
}

class SslPinningFailException extends DfError {
  SslPinningFailException() : super(code: 8101, message: "Fail to identify the backend server. SSL Pinning failure.");
}

class OfflineException extends DfError {
  OfflineException() : super(code: 8102, message: "Internet is not available.");
}

class NetworkException extends DfError {
  NetworkException() : super(code: 8103, message: "Timeout/fail to connect to server.");
}

//Synchronization
class LocalStoreException extends DfError {
  LocalStoreException() : super(code: 9101, message: "Fail to cache, so unable to proceed next step: online synchronization.");
}

Working Example

Please look at example/datasync folder. Key in the "Tank 1223" into search text field, then list of the records will show. Tap on the list row to edit the tank time sheet and save it.

Libraries

dfbaseentity
dfbasefield
dfbasemodel
dfbaserepository
dfconverter
dfdatetimevalidator
dfdbentity
dfdbfield
dfdbmodel
dfdbrepository
dfdbresponseentity
dffieldtype
dfnumericvalidator
dfquery
dfquerycriteria
dfquerycriteriagroup
dfquerycriteriagroups
dfqueryentityjoin
dfqueryorderby
dfquerypaging
dfresponse
dfservice
dfstringvalidator
dfsyncmodel
dfsyncquery
dfsyncrepository
dfvalidator
dfwebentity
dfwebfield
dfwebmodel
dfwebrepository
dfwebresponseentity
entwebreq
entwebreqexe
ingdao
just_animate
logsetting
util
webreqexefield
webreqfield