Leveraging Enterlib Android Features – DEVELOPPARADISE
27/05/2018

Leveraging Enterlib Android Features


Introduction

Enterlib for Android is a framework which helps to decouple the application’s components into separated logical layers where the communications between them is through well-defined interfaces or also called code contracts. The framework’s components help to write reusable, robust and testable pieces of code that can scale with little impact in the rest of the application. In addition, the library provides utilities for:

  • Data binding
  • Asynchronous operations
  • Data validations and conversion
  • JSON serialization
  • Consuming RESTful HTTP Services
  • SQLite ORM
  • Messaging
  • Extended Views
  • ViewModels

Maven Repository

The library is hosted on a maven repository. Therefore, in order to use the library in your projects, you must include an additional repository to your main gradle configuration file as shown below:

buildscript {     repositories {         jcenter()         maven { url "https://dl.bintray.com/ansel86castro/enterlib" }     }     dependencies {         classpath 'com.android.tools.build:gradle:2.3.3'          // NOTE: Do not place your application dependencies here; they belong         // in the individual module build.gradle files     } }

Once the maven repository is added, you can reference the library in your modules as shown below:

dependencies {  ...... other dependencies      compile 'com.cybtans:enterlib:2.0.0@aar'    ....... }

Using the Code

Dependency Injection

Enterlib provides a dependency injection engine through the DependencyContext class which implements the IDependencyContext interface. You can use this class for registering objects like singletons or factories for creating object instances. The caching mechanism is controlled by the LifeType value specified when the dependency is registered. You can have several IDependencyContext linked together creating a context tree where the children of a IDependencyContext object are called scopes. Instances are cached inside the scope from where they were requested, following its LifeType specification. You should call the IDependencyContext.dispose()method when you are done with your scope. The dispose operation will release the cache and dispose all the objects which implement the com.enterlib.IClosable or java.io.Closable interfaces.

The following example will show the best place for creating the root IDependencyContext and registering your dependencies:

class MainApp  extends Application {        IDependencyContext dependencyContext;   @Override   public void onCreate() {         super.onCreate();          dependencyContext = new DependencyContext();         DependencyContext.setContext(dependencyContext);          //Register a  factory gives flexibility for creating instances.          //Other dependencies can be requested using the IServiceProvider parameter                         dependencyContext.registerFactory(IEntityContext.class, new IDependencyFactory(){                     @Override                     public Object createInstance                         (IServiceProvider serviceProvider, Class<?> requestType) {                         EntityMapContext dbContext = new EntityMapContext(MainApp.this, "db.db3");                         return dbContext;                     }                 }, LifeType.Default);           dependencyContext.registerType(SomeService.class, LifeType.Scope);          dependencyContext.registerSingleton(Context.class, this);          dependencyContext.registerTypes(IDomainService.class, MyDomainService.class, LifeType.Scope);                    // Creating a scope           IDependencyContext scope = dependencyContext.createScope();                    //Objects resolved from the scope that has LifeType.Scope           //when registered in the scope or in any of its parent's scopes are cached           //so the same instance is reused per scope request.                                  IDomainService service = scope.getService(IDomainService.class);                    //Registering types for using in a scope only          scope.registerTypes(IScopedDomainService.class, MyScopedDomainService.class, LifeType.Scope);                    //disposing a scope frees from the scope cache all objects with LifeType.Scope           scope.dispose();    }     }

The LifeType enumeration indicates how the dependency injection engine will create the instances. It has the following options:

  • Default: The object is created every time it’s requested.
  • Scope: The object is created only one time per scope request, following requests will return the cached instance on the scope.
  • Singleton: The same instance is returned for every request. If the singleton was registered using a Factory, then it will be created only one time.

Data Access with Object Relational Mapping (ORM)

Enterlib provides a powerful Object Relational Mapper (ORM) for SQLite Databases on Android. The mapping is driven using specific Java annotations on the model’s class declaration as shown below:

@TableMap(name = "Accounts")  public class Account{       @ColumnMap(key = true)      public int Id;       @ColumnMap      public String Name;       @ColumnMap      @ForeingKey(model = Currency.class)      public int CurrencyId;       @ExpressionColumn(expr = "CurrencyId.Name")      public String CurrencyName;       @ExpressionColumn(expr = "CurrencyId.CreateDate")      public Date CurrencyCreateDate;       @ColumnMap(column = "Id")      @ForeingKey(model = Transaction.class, field = "AccountId")      @ExpressionColumn(expr = "SUM(Transactions.Amount)")      public double Balance;       @ColumnMap(column = "Id", nonMapped = true)      @ForeingKey(model = Transaction.class, field = "AccountId")      @ExpressionColumn(expr = "Transactions.Description")      public String Description;       /// Definitions for navigation properties       private Currency currency;      public Currency getCurrency(){          return currency;      }      public void setCurrency(Currency value){          this.currency = value;      }  } 

The annotations are used to specify columns mapping, relationships or computed columns. The most important annotations are:

  • TableMap: Optional and can be used to identify the table for the mapping
  • ColumnMap: Required to identify the column-field mapping and some argument can be use to add additional information like whether it’s writable, a primary key, the column name or the primary key order.
  • ForeignKey: Specify foreing key relationships where you must set the target class and optionally the field referenced.
  • ExpressionColumn: Powerful mechanism by which fields of foreing key relationships can be included in the current class declaration or using computed columns like aggregations. You can use this annotation to define data views the same way you can do using SQL views.

The remaining models for the example are shown below:

@TableMap(name = "Transactions")  public class Transaction{       @ColumnMap(key = true, writable = false)      public int Id;       @ColumnMap      public String Description;       @ColumnMap      public double Amount;       @ColumnMap      @ForeingKey(model = Account.class)      public int AccountId;       @ExpressionColumn(expr = "AccountId.Name")      public String AccountName;       @ExpressionColumn(expr = "AccountId.CurrencyId.Name")      public String CurrencyName;       private Account account;       public Account getAccount(){          return account;      }       public void setAccount(Account value){          this.account = value;      }  }   @TableMap(name = "Currencies")  public class Currency{       @ColumnMap(key = true)      public int Id;       @ColumnMap(column = "Code")      public String Name;       @ColumnMap      public Date CreateDate;  }   public class Category{       @ColumnMap(key = true)      public int Id;       @ColumnMap      public String Name;       @ColumnMap(column = "Id", nonMapped = true)      @ForeingKey(model = AccountCategory.class, field = "CategoryId")      public ArrayList<AccountCategory> Accounts;  }   public class AccountCategory{       @ColumnMap(key = true, order = 0)      @ForeingKey(model = Account.class, field = "Id")      public int AccountId;       @ColumnMap(key = true, order = 1)      @ForeingKey(model = Category.class, field = "Id")      public int CategoryId;  } 

Enterlib Android also helps for deploying sqlite databases. If you have the SQLite database on a file stored in the app’s asset directory like for example named db.db3, you can easily deploy it using the EntityMapContext class as shown below:

EntityMapContext.deploy(this, "db.db3"); 

Then after the database is deployed, the next step is creating an instance of an IEntityContext. This object represents the database connection and can be used to retrieve IRepository<T> instances.

IEntityContext context = new EntityMapContext(MainApp.this, "db.db3");

On the other hand, IRepository<T> instances are used to query or modified the database.

IRepository<Transaction> map = context.getRepository(Transaction.class); ArrayList<Transaction> list = map.query().toList();

In the example above, the query will generate the following SQL statement when it’s evaluated into a List.

SELECT t0.Description as "Description" ,t0.AccountId as "AccountId" ,t0.Amount as "Amount" ,t0.Id as "Id" ,t1.Name as "AccountName" ,t2.Code as "CurrencyName" FROM "Transactions" t0 INNER JOIN "Accounts" t1 on t1.Id = t0.AccountId  INNER JOIN "Currencies" t2 on t2.Id = t1.CurrencyId 

The IRepository<T> also provides methods for creating, updating, deleting entities as shown below:

 //Creates a new entity in the persisting store  transaction = new Transaction();  map.create(transaction);   //update the entity in the persisting store with new values  map.update(transaction);  //delete the entity from the persisting store  map.delete(transaction);   //delete all entities satisfying the condition  map.delete("Description = 'Abc'");  //returns the total count of entities  map.query().count();   //return the first element in the query  transaction = map.query().first(); 

Lazy Evaluation

Enterlib Android was designed having performance as a priority in mind, so for that reason, the IEntityCursor<T> was introduced . The IEntityCursor<T> is a mechanism for iterating through the query in a more efficient way. Meaning entities are loaded on demand, optimizing memory usage and therefore it’s the recommended usage for iterating large result sets from queries.

IRepository<Transaction> map = context.getRepository(Transaction.class);  IEntityCursor<Transaction> cursor = map.query().toCursor(); for (Transaction t: cursor ) {         //do something with t } cursor.close();

Another example using cursors where the IEntityCursor<T> is implicitly closed when there are no more elements to iterate.

IRepository<Transaction> map = context.getRepository(Transaction.class);   for (Transaction t: map.query() ) {         //do something with t }

The IEntityCursor<T> has the following interface definition:

public interface IEntityCursor<T> extends IClosable, Iterable<T> {     //return the total of elements in the query     int getCount();          //return an element if at the specified position    T getItem(int position);     }

Filters Expressions and Functions

Enterlib’s ORM for Android supports the following functions in string expressions for filtering or as arguments in the ExpressionColumn annotations:

  • sum(expression)
  • avg(expression)
  • count(expression)
  • max(expression)
  • min(expression)
  • concat(expression): for string fields, returns the concatenations of the values
  • ifnull(exp1, exp2): returns exp2 if exp1 is null
  • contains(expression)
  • exclude(expression)

Example:

 IRepository<Account> map = context.getRepository(Account.class);  IQuerable<Account> querable  = map.query()                 .include("Currency")                 .where("CurrencyId.Name = 'USD'")                 .where("AVG(Transactions.Amount) > 5")                 .orderBy("Name desc")                 .skip(5)                 .take(10);                  ArrayList<Transaction> list = querable.toList();

The instruction querable.toList() compiles the query and generates the following SQL:

SELECT t1.CreateDate as "CurrencyCreateDate" ,t0.Id as "Id" ,total(t2.Amount) as "Balance" ,t0.CurrencyId as "CurrencyId" ,t0.Name as "Name" ,t1.Code as "CurrencyName" ,t1.Id as ".Currency.Id" ,t1.CreateDate as ".Currency.CreateDate" ,t1.Code as ".Currency.Name" FROM "Accounts" t0 INNER JOIN "Currencies" t1 on t1.Id = t0.CurrencyId  LEFT OUTER JOIN "Transactions" t2 on t2.AccountId = t0.Id  WHERE t1.Code = 'USD' GROUP BY t0.Id,t1.CreateDate,t0.Name,t1.Id,t0.CurrencyId,t1.Code HAVING avg(t2.Amount) > 5 ORDER BY t0.Name DESC LIMIT 10 OFFSET 5

Here is another example using the include method. The include method will add the related entities into the result set:

IRepository<Transaction> map = context.getRepository(Transaction.class);  IQuerable<Transaction> query  = map.query()          .include("Account.Currency")          .where("Account.Currency.Name = 'USD'");  System.out.println(query.toString()); 

As a result, it will print the following SQL statement:

SELECT t0.Id as "Id" ,t0.Description as "Description" ,t0.Amount as "Amount" ,t0.AccountId as "AccountId" ,t1.Name as "AccountName" ,t2.Code as "CurrencyName" ,t1.Id as ".Account.Id" ,t1.Name as ".Account.Name" ,t1.CurrencyId as ".Account.CurrencyId" ,t2.Code as ".Account.CurrencyName" ,t2.CreateDate as ".Account.CurrencyCreateDate" ,total(t3.Amount) as ".Account.Balance" ,t2.Id as ".Account.Currency.Id" ,t2.Code as ".Account.Currency.Name" ,t2.CreateDate as ".Account.Currency.CreateDate" FROM "Transactions" t0 INNER JOIN "Accounts" t1 on t1.Id = t0.AccountId  INNER JOIN "Currencies" t2 on t2.Id = t1.CurrencyId  LEFT OUTER JOIN "Transactions" t3 on t3.AccountId = t1.Id  WHERE t2.Code = 'USD' GROUP BY t2.Code,t0.Description,t1.Name,t2.Id,t0.Amount,t0.Id,          t1.CurrencyId,t1.Id,t0.AccountId,t2.CreateDate

The expressions Account.Currency.Name and AccountId.CurrencyId.Name passed in the where method are equivalent – they produce the same result. Enterlib follows a set of conventions in order to find the foreign keys for a given navigation property.

On the other hand, for using the include method, you must declare the navigation property to be injected with the related object. For example, below is defined the navigation property for the AccountId field in the Transaction class. By convention, it will look for the options AccountId, Accountid, Account_id when using include(Account) in order to find the foreign key for the navigation property Account.

public class Transaction{    // other code ....     private Account account;     public Account getAccount(){        return account;    }     public void setAccount(Account value){        this.account = value;    }    // other code .... } 

Using Aliases for Fields in Associated Models

The expressions supported by the ORM can include aliases for fields of the related models. An alias is nothing more that a short form for referencing those fields that belong to associated models. For example:

IRepository<Account> map = context.getRepository(Account.class); IQuerable<Account> querable  = map.query()                 .where("CurrencyName = 'EUR'");

In the previous example, the field CurrencyName is an alias for CurrencyId.Name. You could observe the CurrencyName field is defined using the ExpressionColumn annotation as follows:

@TableMap(name = "Accounts") public class Account{     // other code ....      @ExpressionColumn(expr = "CurrencyId.Name")     public String CurrencyName;     // other code .... } 

The resulting query will be compiled into the following SQL statements:

SELECT t1.CreateDate as "CurrencyCreateDate" ,t0.Id as "Id" ,total(t2.Amount) as "Balance" ,t0.CurrencyId as "CurrencyId" ,t0.Name as "Name" ,t1.Code as "CurrencyName" FROM "Accounts" t0 INNER JOIN "Currencies" t1 on t1.Id = t0.CurrencyId  LEFT OUTER JOIN "Transactions" t2 on t2.AccountId = t0.Id  WHERE t1.Code = 'EUR' GROUP BY t0.Id,t1.CreateDate,t0.Name,t0.CurrencyId,t1.Code

Advance Filtering

Enterlib provides extended filtering functions like Contains or Exclude, note that they are case insensitive. For example, the following query will retrieve all the Category objects associated with at least five Account entities:

IRepository<Category> map = context.getRepository(Category.class); IQuerable<Category> querable  = map.query()                 .where("CONTAINS(COUNT(Accounts.AccountId) > 5)");

When the query evaluates, it will produce the following SQL statement:

SELECT t0.Id as "Id" ,t0.Name as "Name" FROM "Category" t0 WHERE t0.Id IN (SELECT t0.CategoryId as "CategoryId" FROM "AccountCategory" t0 GROUP BY t0.CategoryId HAVING count(t0.AccountId) > 5)

The same way using the exclude function you can query for example all the Category objects which are not associated to any Account entity with Currency 'UYU'.

IRepository<category> map = context.getRepository(Category.class); IQuerable<category> querable  = map.query()                 .where("EXCLUDE(Accounts.AccountId.CurrencyId.Name = 'UYU')");
 SELECT t0.Id as "Id" ,t0.Name as "Name" FROM "Category" t0 WHERE t0.Id NOT IN (SELECT t0.CategoryId as "CategoryId" FROM "AccountCategory" t0 INNER JOIN "Accounts" t1 on t1.Id = t0.AccountId  INNER JOIN "Currencies" t2 on t2.Id = t1.CurrencyId  WHERE t2.Code = 'UYU') 

Points of Interest

In this article, we learned how we can leverage the Dependency Injection engine provided by Enterlib in order to create more modular and decoupled architecture for our apps. The Object Relational Mapping Engine shows a good potential for saving time when working with offline data. As shown before, it supports very expressive string expressions for filtering and mapping configurations.