Skip to content

DevBricks provides several classes which will be usually used in daily Android development.

License

Notifications You must be signed in to change notification settings

dailystudio/devbricks

Repository files navigation

DevBricks

License API Maven Central Android Arsenal

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.6</version>
</dependency>

or Gradle dependency:

compile 'com.github.dailystudio:devbricks:1.1.6'

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 from DevBricksApplication.

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() returns true, all of the debug output will be printed. Otherwise no any outputs.
  • debugSecure() will be only enabled when isDebugBuild() returnes true. It will not be affected by any dslog_force or dslog_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 own BuildConfig.DEBUG is false. There is no way for DevBricks to get correct BuildConfig.DEBUG value of host application. So DevBricks provides an interface isDebugBuild() 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 of Column which is usually used to describe how to convert a DatabaseObject 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).andbmiToken.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-2018 by Daily Studio.

About

DevBricks provides several classes which will be usually used in daily Android development.

Resources

License

Stars

Watchers

Forks

Packages

No packages published