#DevBricks
DevBricks provides several classes which will be usually used in daily Android development. With these "bricks", your development will become:
- Efficient : The classes provided by DevBricks almost cover all of the aspect in daily development, from low-end database to user interface. You do not need to waste your time on those repeating work.
- Reliable : More than 60% code has related Unit test. Your work will stand on stable foundation.
- Consistent : DevBricks includes unified logging system, database accessing, UI elements and styles. This make all of your applications has consistency at primary impression.
Quick Setup
To use DevBricks Library, follow these steps.
Step 1: Include the Library
Maven dependency:
<dependency> <groupId>com.github.dailystudio</groupId> <artifactId>devbricks</artifactId> <version>1.1.3</version> </dependency>
or Gradle dependency:
compile 'com.github.dailystudio:devbricks:1.1.3'
Step 2: Application initialization
Extends your main Application from DevBricks Application:
public class MyApplication extends DevBricksApplication {
/* your own code about application */
}
Then declare it in your AndroidMenifest.xml
:
<manifest> ... <application
android:name=".MyApplication">
... </application> ... </manifest>
DevBricksApplication
does two things for you:
- Bind and unbind a global context with your Application Context.
- Disable or Enable Logging accroding to your build types and runtime environment.
You will know more about these two concepts in following chapters. After you understand well with them, you can do it by yourself without derving your
Application
fromDevBricksApplication
.
DevBricksApplication
will enable logging mechanism according to the following three factors by priority:
- If there is a file name in
dslog_force.your-package-name
under root directory of your external storage (e.g./sdcard
), all of debug output of your applications will be printed even the build type of your application is release. - If there is a file name in
dslog_suppress.your-package-name
under root directory of your external storage (e.g./sdcard
), there will be no any debug outputs for your applications. - If
isDebugBuild()
returnstrue
, all of the debug output will be printed. Otherwise no any outputs. debugSecure()
will be only enabled whenisDebugBuild()
returnestrue
. It will not be affected by anydslog_force
ordslog_suppress
file on your external storage.
On Android 6.0, new permission framework will affect this feature is your application does not grant the permission
android.permission.READ_EXTERNAL_STORAGE
. To grant the permission is not DevBricks responsibility. If you want to have this feature, you need to handle it on your application side.
Usually, there is no any special files (prefix with dslog_
) exist on your external storage. The last factor will be used. If you want to correctly configure your debug output, you need to override isDebugBuild()
, the easiest way to directly return BuildConfig.DEBUG
:
public class MyApplication extends DevBricksApplication {
...
@Override protected boolean isDebugBuild() {
return BuildConfig.DEBUG;
}
}
Due to DevBricks is a library project released in
.aar
format on Maven repository, its ownBuildConfig.DEBUG
is false. There is no way for DevBricks to get correct BuildConfig.DEBUG value of host application. So DevBricks provides an interfaceisDebugBuild()
for application to override the correct value of build type.
Global Context
As you know Context
is an important thing in Android application. Your code can do few things without Context
. DevBricks provides an interfaces to bind a global application context - GlobalContextWrapper
. You can retrieve it at anywhere in your application. To bind the context, you can call bindContext()
:
public class MyApplication extends Application {
@Override public void onCreate() {
super.onCreate();
GlobalContextWrapper.bindContext(getApplicationContext());
}
}
Once, you have bound the application context. You can call getContext()
when you need a Context
instance. Here is an example:
Context context = GlobalContextWrapper.getContext();
if (context != null) {
context.startActivity(launchIntent);
}
GlobalContextWrapper
will bind an application context rather than an activity context. Even you pass an Activity
object as second parameter to bindContext()
, it will call getApplicationContext()
of Activity
to retrieve correct application context for further operation. That means you needn't to worry about memory leak of this global context holder. Each application only has one application context instance and will not hold any information about view root. Anyway, to make the usage rigorous, you can call unbindContext()
before your application is terminated.
public class MyApplication extends Application {
@Override public void onTerminate() {
GlobalContextWrapper.unbindContext(getApplicationContext());
super.onTerminate();
}
}
Logging
DevBricks bases and enhance the default Android logging mechanism. Same as default logging mechanism, it separates the log in four different priorities:
DevBricks Logger | Android Log |
---|---|
.debug() | Log.d() |
.debugSecure() | Log.d() |
.info() | Log.i() |
.warn() | Log.w() |
.error() | Log.e() |
Different with default logging utils, Logger
do not need you to provide a TAG when you print the log. It will automatically provides a TAG according the class and method which is currently calling Logger
to print the logs. For example,
public class MyApplication extends DevBricksApplication {
@Override public void onCreate() {
super.onCreate();
Logger.debug("Hello app: %s", getString(R.string.app_name))
}
}
The first parameter is the output format of log, while rest parameters provide the content of the arguments describe in the first parameter. It is exactly same as String.format()
. The TAG will be generated as MyApplication: onCreate()
and the log output will be like this:
... 02-22 17:09:06.888 8476-8476/? D/MyApplication: onCreate(): Hello app: MyApplication ...
There is another important interfaces in Logger
class is setDebugEnabled()
and setSecureDebugEnabled()
. As you seen in the last chapter, DevBricksApplication
will automatically enable or disable debug outputs according to some case. But you can use setDebugEnabled()
and ** setSecureDebugEnabled()
**to force enable or disable debug logging.
Database
Database facilities in DevBricks provides a efficient way to convert between In-Memory Data Structures and SQLite Database Records?
DatabaseObject
represents object in memory which could be easily store in permanent database through Database read/write facility classes.Column
describe how to map a field of a In-Memory class to a column of database record.Template
contains a set ofColumn
which is usually used to describe how to convert aDatabaseObject
to database record.Query
is used to describe query parameters when loading objects from databases. It converts most parts of common SQL select statement into Java language.DatabaseReader
is a shortcut class to reading obejcts from database.DatabaseWriter
is a shortcut class to saving objects into database.
With these classes, even you do not have any knowledge about SQL or Androiud Content Provider, you can easily bind data in your application with permanent database storage.
Define an Object
For example, if you have a class named People
, which represent a people data structure in memory. It is defined as below:
public class People {
private String mName; private int mAge; private float mWeight; private int mHeight; private boolean mMarried;
}
You want each people will be stored as one record in database, like this:
ID | Name | Age | Weight | Height | Married |
---|---|---|---|---|---|
1 | David | 34 | 69 | 175 | 1 |
2 | Lucy | 33 | 48.5 | 165 | 0 |
... | ... | .. | .. | ... | . |
To map a People
to a database record, you need to derive People
from DatabaseObject
firstly, then define a template and bind them together:
public class People extends DatabaseObject {
public static final Column COLUMN_ID = new IntegerColumn("_id", false, true);
public static final Column COLUMN_NAME = new StringColumn("name");
public static final Column COLUMN_AGE = new IntegerColumn("age");
public static final Column COLUMN_WEIGHT = new DoubleColumn("weight");
public static final Column COLUMN_HEIGHT = new IntegerColumn("height");
public static final Column COLUMN_MARRIED = new IntegerColumn("married");
private final static Column[] COLUMNS = {
COLUMN_ID,
COLUMN_AGE,
COLUMN_NAME,
COLUMN_WEIGHT,
COLUMN_HEIGHT,
COLUMN_MARRIED,
}
;
public People(Context context) {
super(context);
final Template templ = getTemplate();
templ.addColumns(COLUMNS);
}
pubic void setId(int id) {
setValue(COLUMN_ID, id)
}
public int getId() {
return getIntegerValue(COLUMN_ID);
}
...
pubic void setMarried(boolean married) {
setValue(COLUMN_MARRIED, (married ? 1 : 0))
}
public boolean isMarried() {
return (getIntegerValue(COLUMN_MARRIED) == 1);
}
}
###Saving or loading objects Before moving forward, you need to understand a little more implementation behind the interface. Database manipulation in DevBricks is basing on ContentProvider
, which is an important component on Android platform. Even you do not need to know more about this concept, you have to declare things in your AndroidManifest.xml
before you start to use these interfaces. Firstly, you need to declare a ContentProvider
in the AndroidManifest.xml
of your project:
<application android:icon="@drawable/ic_app"
android:label="@string/app_name">
... <provider
android:name=".AppConnectivityProvider"
android:authorities="com.yourdomain.youapp" />
... </application>
Class AppConnectivityProvider
is derived from DatabaseConnectivityProvider
. Keep it implementation empty is enough.
public class AppConnectivityProvider extends DatabaseConnectivityProvider {
}
Usually, you only need one provider like this to handle all the database operations in your application. Defining the authority of this provider same as your package name will make everything easy. When you create a DatabaseReader
or DatabaseWriter
, you can use a shortcut creator, like this:
DatabaseReader<People> reader = new DatabaseReader(context, People.class);
DatabaseWriter<People> writer = new DatabaseWriter(context, People.class);
...
But sometimes, you need to handle more complicated cases. You may need to define two providers. One is using inside application, while the other one is using to share data with other applications. In this case, you need to declare another provider with a different authority:
<application android:icon="@drawable/ic_app"
android:label="@string/app_name">
... <provider
android:name=".ExternConnectivityProvider"
android:authorities="com.yourdomain.external" />
... </application>
At the same time, when you want to use DatabaseReader
or DatabaseWriter
on this provider, you need to pass the authority as second parameter in creator:
DatabaseReader<People> reader = new DatabaseReader(context, "com.yourdomain.external", People.class);
DatabaseWriter<People> writer = new DatabaseWriter(context, "com.yourdomain.external", People.class);
...
Now, when you finish these steps above, you can easily use database read/write facilites to save or load People
objects between memory and database.
DatabaseWriter
is a shortcut class to save im-memory obejcts to database. For example, add a People
to database:
DatabaseWriter<People> writer = new DatabaseWriter(context, People.class);
People p = new People();
p.setName("David");
p.setAge(33);
p.setWeight(69);
p.setHeight(175);
p.setMarried(true);
writer.insert(p);
DatabaseReader
is a shortcut class to load database records into memory. For example, query all People
from database:
DatabaseReader<People> reader = new DatabaseReader(context, People.class);
List<People> people = reader.query(new Query(People.class));
for (People p: people) {
/* process each people */
}
Sometimes, you may not want to retrieve all the columns from the database or you want to retrieve some calculated columns, like count(), sum() in SQLite. Another query interface will help you on this case. Before using the interface, you need to defined an projection class.
Here is example, which includes basic information about people and related BMI.
BMI stands for Body Mass Index. BMI is used as one measure to gauge risk for overall health problem. The standard range of BMI is from 18.5 to 24. The formula of BMI calculation is:
BMI = weight (kg) / height ^ 2 (m)
The class PeopleBmi
is defined as:
public class PeopleBmi extends DatabaseObject {
public static final Column COLUMN_AGE = new IntegerColumn("age");
public static final Column COLUMN_BMI = new DoubleColumn(People.COLUMN_WEIGHT.divide(People.COLUMN_HEIGHT.multiple(People.COLUMN_HEIGHT)).toString());
private final static Column[] COLUMNS = {
People.COLUMN_ID,
People.COLUMN_NAME,
COLUMN_BMI,
}
;
public PeopleBmi(Context context) {
super(context);
final Template templ = getTemplate();
templ.addColumns(COLUMNS);
}
public int getId() {
return getIntegerValue(People.COLUMN_ID);
}
public String getName() {
return getTextValue(People.COLUMN_NAME);
}
public double getBMI() {
return getDoubleValue(COLUMN_BMI);
}
}
Then you pass this class as second parameters of query interfaces and cast returned result to PeopleBmi
objects:
DatabaseReader<People> reader = new DatabaseReader(context, People.class);
List<DatabaseObject> bmiList = reader.query(new Query(People.class), PeopleBmi.class);
PeopleBmi bmi; for (DatabaseObject obj: bmiList) {
if (object instanceof PeopleBmi == false) {
/* usually, this will not happen */
continue;
}
bmi = (PeopleBmi)obj; /* process each people BMI */
}
When you are using the DatabaseReader
, the Query
will become a much more important helper class. You need to rely on this helper class to describe all of your query on the database. A Query
object combines the following ExpressionToken
together to define a query. Each kind of these ExpressionToken
correspond to a related SQLite statement:
Expression Token | SQLite Statement |
---|---|
Selection Token | where |
GroupBy Token | group by |
OrderBy Token | order by |
Having Token | having |
Limit Token | limit |
Well known binary operators can be performed on a ExpressionToken
, including:
Op function | SQLite Equivalent | Explanation |
---|---|---|
.plus() | + | a + b |
.minus() | - | a - b |
.multiple() | * | a * b |
.divide() | / | a / b |
.modulo() | % | a % b |
Besides, logical operations can between combine two ExpressionToken
together:
Op function | SQLite Equivalent | Explanation |
---|---|---|
.and() | && | a && b |
.or() | || | a || b |
.eq() | == | a == b |
.neq() | <> | a <> b |
.gt() | > | a > b |
.gte() | >= | a >= b |
.lt() | < | a < b |
.lte() | <= | a <= b |
.in() | >= and <= | a >= b && a <= c |
.out() | < or > | a < b || a > c |
Here is a real case to demonstrate how to convert a SQLite query statement into a Query object. Taking People as example, we want to find out a set of people who is older than 30 and their BMI is not in standard range:
SELECT * FROM People WHERE (age > 30) AND (weight / (height * height) > 24) OR (weight / (height * height) < 18.5);
To describe this query with Query object, here is the snippet:
Query query = new Query(People.class);
ExpressionToken bmiToken = People.COLUMN_WEIGHT.divide(People.COLUMN_HEIGHT.multiple(People.COLUMN_HEIGHT));
ExpressionToken selToken = People.COLUMN_AGE.gt(30).and?bmiToken.outOf(18.5, 24)? query.setSelection(selToken);
Last but not the least, accessing the database may be high latency operations. It is better to move these kind of operations out of main UI thread. To handle this, you can move forward to the next chapter - Loaders and AsyncTasks.
Loaders and AsyncTasks
Loader
and AsyncTask
are both designed to be helper classes around Thread and Handler in Android framework. Loader
is better integrated with Activity
and Fragment
. As mentioned in the last chapter, accessing the database should not be frequently used in main UI thread. To easily use database classes and facilities in your applications, DevBricks also provides you a set of helper classes to combine Loader
and AsyncTask
with DatabaseObject
.
###Loaders Breifly, DevBricks provides two helper classes for you to access database asynchronously, DatabaseObjectsLoader
and DatabaseCursorLoader
. The main difference between these two classes is the returned value. DatabaseObjectsLoader
will return a list of DatabaseObject
, while DatabaseCursorLoader
will directly return the Cursor
. The advantage of returning a list of objects is you can add more properties to the objects in memory. For example, the portrait of a person. You could not save the entire image of the portrait in database. Usually, you only save the URI in database and save the resolved image in the same data structure in memory. After you load a list of objects from database, you will traverse the list and resolve each URI of portrait and then attach to the related object. In this case, using DatabaseCursorLoader
will be more complicated. Because you could not attach anything on the return cursor. The solutions is creating an extra map to holds the relationship between images and database objects. Obviously, the DatabaseCursorLoader
has its own applications, saving the memory. If you have thousands records in the database, loading them all to the memory may not be good choice. DatabaseCursorLoader
will only return a cursor. You can use the cursor to traverse the entire database, but there is only a small piece of memory used to keep active content of database. DatabaseObjectsLoader
and DatabaseCursorLoader
are abstract classes. You need to implement the only abstract interface getObjectClass()
before using them. Here is an example:
public class PeopleObjectsLoader extends DatabaseObjectsLoader<People> {
public PeopleObjectsLoader(Context context) {
super(context);
}
protected Class<People> getObjectClass() {
return People.class;
}
}
public class PeopleCursorLoader extends DatabaseCursorLoader {
public PeopleCursorLoader(Context context) {
super(context);
}
protected Class<People> getObjectClass() {
return People.class;
}
}
The retrieved data will be return in onLoaderFinished() callback. For PeopleObjectsLoader
, a list of People objects will be passed as second parameter. For PeopleCursorLoader
, a Cursor
will be passed and you can use fillValuesFromCursor()
of DatabaseObject
to convert Cursor
to a DatabaseObject
.
DatabaseObjectsLoader
has an advanced classe: ProjectedDatabaseObjectsLoader
. ProjectedDatabaseObjectsLoader
is used to handle cases that the returned data are projections of original database. Taking the class PeopleBmi
shown in last chapter as example, you need to override on more interface of ProjectedDatabaseObjectsLoader
:
public class PeopleBmisLoader extends ProjectedDatabaseObjectsLoader<People, PeopleBmi> {
public PeopleBmisLoader(Context context) {
super(context);
}
protected Class<People> getObjectClass() {
return People.class;
}
@Override protected Class<PeopleBmi> getProjectionClass() {
return PeopleBmi.class;
}
}
If you want a more complicated customized query, you can also override the protected function getQuery()
for all the loaders above. For example, we only need people who is older than 30:
...
@Override protected Query getQuery(Class<People> klass) {
Query query = super.getQuery(klass);
ExpressionToken selToken = People.COLUMN_AGE.gt(30);
query.setSelection(selToken);
return query;
}
...
Don't forget that if your authority of ContentProvider
is not same as the package name of your application, you may need to override getDatabaseConnectivity()
to define a special DatabaseConnectivity
:
...
@Override protected DatabaseConnectivity getDatabaseConnectivity(
Class<? extends DatabaseObject> objectClass) {
return new DatabaseConnectivity(getContext(), "com.yourdomain.external", objectClass);
}
...
###AsyncTask AsyncTask is quite same as Loader, DatabaseObjectsAsyncTask
is the most used class to retrieve data from database , while ProjectedDatabaseObjectsAsyncTask
is its advance version which support projection of database content. Most interfaces mentioned above are also available in AsyncTask. Here is an example about ProjectedDatabaseObjectsAsyncTask
which is calculate the BMI of people who is older than 30:
public class PeopleBmisAsyncTask extends ProjectedDatabaseObjectsAsyncTask<People, PeopleBmi> {
public PeopleBmisAsyncTask(Context context) {
super(context);
}
protected Class<People> getObjectClass() {
return People.class;
}
@Override protected Class<PeopleBmi> getProjectionClass() {
return PeopleBmi.class;
}
@Override protected Query getQuery(Class<People> klass) {
Query query = super.getQuery(klass);
ExpressionToken selToken = People.COLUMN_AGE.gt(30);
query.setSelection(selToken);
return query;
}
@Override protected DatabaseConnectivity getDatabaseConnectivity(
Class<? extends DatabaseObject> objectClass) {
return new DatabaseConnectivity(getContext(), "com.yourdomain.external", objectClass);
}
}
All the Loader
in DevBricks are derived from android.support.v4.content.Loader
, while the AsyncTask
are drived from android.os.AsyncTask
. How to use a Loader
or AsyncTask
is not covered in this document, you can refer to detailed guides on offical Android Devloper website. But if you want to save your energy to save the world, please move on to read the following chapter - Fragments.
Fragments
A Fragment
is a piece of an application's user interface or behavior that can be placed in an Activity
. DevBricks provide you some classes derived from Fragment
and well integrated the concept mentioned in previous chapters. With these pre-defined classes, you can easily use DatabaseObject
and Loader
in your own application.
The first class you should know is BaseIntentFragment
, this class provides an interface bindIntent()
which will be call on the host Activity
is created or the host Activity
receives New Intent event, when onNewIntent()
of host Activity
is called. This class the base of the classes you will in following paragraphs.
The next class will be AbsLoaderFragment
, which defines four Loader
related interfaces. Three of them are abstracted, you need to implement them before using the loader. Taking the PeopleBmisLoader
as an example, here is a simple exmaple how to implement AbsLoaderFragment
:
public class PeopleBmisFragment extends AbsLoaderFragment<List<PeopleBmi>> {
private final static int LOADER_PEOPLE_BMI_ID = 0x100;
@Override
public void onLoadFinished(Loader<List<PeopleBmi>> loader, List<PeopleBmi> data) {
/* bind your data with UI */
}
@Override
public void onLoaderReset(Loader<List<PeopleBmi>> loader) {
/* reset your UI */
}
@Override
public Loader<List<PeopleBmi>> onCreateLoader(int id, Bundle args) {
return new PeopleBmisLoader(getActivity());
}
@Override protected int getLoaderId() {
return LOADER_PEOPLE_BMI_ID;
}
@Override protected Bundle createLoaderArguments() {
return new Bundle();
}
}
In onCreateLoader()
, you need to create the loader which will load data used in this Fragment
asynchronously. AbsLoaderFragment
is defined as a template class. Template T is a type abstraction of data passing through from Loader
to Fragment
. In the code above, PeopleBmisLoader
will passing a list of retrieved PeopleBmi
objects to the callback, so you need to declare T as List<PeopleBmi>
. In getLoaderId()
, you need to return an unique integer in your application scope as an identifier of the loader. In createLoaderArguments()
, each time you call restartLoader()
, this function will be called. You can create different arguments Bundle
according to your needs. Besides these abstract methods, AbsLoaderFragment
also provides a method named restartLoader()
, which can restart the loader at anytime you want. Same as bindIntent()
in its parents class, the restartLoader()
is also automatically called when the host Activity
is created or the host Activity
receives an New Intent event. Due to restartLoader()
is called after bindIntent()
, you are at ease about creating your loader and its argments according the Intent
which is passed to the fragment.
As two successor of AbsLoaderFragment
, AbsArrayAdapterFragment
and AbsCursorAdapterFragment
add perfect integration with ListView
and GridView
. AbsArrayAdapterFragment
is used for the loader which is dervied from DatabaseObjectsLoader
, while AbsCursortAdapterFragment
is used for the loader which is dervied from DatabaseCursorLoader
. Due to deriving from AbsLoaderFragment
, onCreateLoader()
, createLoaderArguments()
and getLoderId()
must be implemented before using. But one more interface you must implement onCreateAdapter()
. In this funtion, you need to create an ArrayAdapter
or CursorAdapter
according to which Fragment
you want to use. The created adapter will bind with the ListView
or GridView
in the Fragment
. How does Loader
, Adapter
and Fragment
well bound together is the responsiblity of DevBricks, you do not need to care about. One important difference between these two successors and AbsLoaderFragment
is the template declaration. As we talked about above, the type declaration in AbsLoaderFragment
is the type of data passed between Loader
and Fragment
. But in these two successor, the type declaration is the type of the item used in ListView
or GridView
. For the AbsCursorAdapterFragment
, it has already declared the template as Cursor
and you needn't to anything else. For AbsArrayAdapterFragment
, here is an example:
public class PeopleBmisAdapterFragment extends AbsArrayFragment<PeopleBmi> {
private final static int LOADER_PEOPLE_BMI_ID = 0x100;
@Override
public Loader<List<PeopleBmi>> onCreateLoader(int id, Bundle args) {
return new PeopleBmisLoader(getActivity());
}
@Override protected int getLoaderId() {
return LOADER_PEOPLE_BMI_ID;
}
@Override protected Bundle createLoaderArguments() {
return new Bundle();
}
@Override protected BaseAdapter onCreateAdapter() {
return new PeopleBmisAdapter(getActivity());
}
}
Copyright 2010-2017 by Daily Studio.