category
让我们来看看一些很棒的库,它们可以帮助我们创建更好的Android应用程序。
TL;DR:在这篇文章中,我们将简要介绍15个库,它们可以帮助我们进行日常的Android开发。有了它们,我们将能够创建性能更好、提供更好用户体验的应用程序。
“流行的Android库可以帮助我们创建具有更好性能和用户体验的应用程序。”
Popular Android Libraries
Libraries are major game changers in software development irrespective of platform or stack. With libraries, we leverage the efforts of other developers to perform actions/functions faster, more effective, and with lesser boilerplate codes. In this article, we will look at various categories in Android development and the common libraries used in them.
Android Libraries—Image Loading
Image loading libraries come in very handy to avoid high memory consumption caused by loading multiple images at the same time. Images are the greatest source of Out of Memory errors in Android development. These libraries, therefore, reduce the hassle of loading and caching images together with minimizing memory usage to provide a seamless user experience. Let's take a look at two of the commonly used image loading libraries: Glide and Picasso.
Glide
Glide is an image loading library focused on smooth scrolling. Glide ensures image loading is both as fast and as smooth as possible by applying smart automatic down-sampling and caching to minimize storage overhead and decode times. It also re-uses resources like byte arrays and automatically releases application resources where necessary. At the time of writing, Glide's latest version requires a minimum SDK of API 14 (Android 4.0) and requires a compile SDK of API 26 (Android 8.0) or later.
Using Glide
We first need to make sure we have the Maven and Google repositories in our project build.gradle
file:
repositories {
mavenCentral()
google()
}
Then, we add the library dependency in our app-module build.gradle
file and sync it to make the library available for use:
implementation 'com.github.bumptech.glide:glide:4.4.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.4.0'
We can then load an image from a remote URL with a single line of code:
GlideApp
.with(this)
.load("https://res.cloudinary.com/demo/video/upload/dog.png")
.into(imageView);
The with
method can take a Context
, Activity
, Fragment
or View
object. The load
method takes a remote URL or a drawable file (e.g R.drawable.image
). The imageView
instance, passed as an argument to the into
method, should be of type ImageView
.
Note that, if your
gradle
version is below 3.0, you should use thecompile
keyword instead ofimplementation
to add dependencies.
Picasso
Picasso is another great image library for Android. It's created and maintained by Square, a company that is heavily dependent and contributor to the open source world, that caters to image loading and processing. By using Picasso, the process of displaying images from external locations is simplified. Picasso supports complex image transformations, automatic caching to disk, ImageView
recycling, and download cancellation in an adapter.
The library handles every stage of the process. It starts by handling HTTP requests and also handles the caching of the image. Just as Glide does.
Using Picasso
The first thing we need to do is to add the Picasso dependency in our app-module build.gradle
file:
implementation 'com.squareup.picasso:picasso:2.5.2'
After that, we sync our gradle
file and load an image resource with a single line of code:
Picasso
.with(this)
.load("https://res.cloudinary.com/demo/video/upload/dog.png")
.into(imageView);
As we can see, the API provided by Picasso is very similar to the one provided by Glide.
Over time, there has been series of arguments and controversies as to which library performs better. Below is a table showing the strengths and flaws of them both.
Android Libraries—Videos
Displaying videos poses to be another difficult task during development. The process and details to take care of can be too numerous to handle. In this category, there are a few available options. However, as the most popular and powerful one is ExoPlayer, we will focus this section on it.
ExoPlayer
ExoPlayer is an Android media player library developed by Google. It provides an alternative to Android’s MediaPlayer API for playing audio and video (both locally and over the internet) with some added advantages. ExoPlayer supports features not currently supported by Android’s MediaPlayer API, like DASH and SmoothStreaming adaptive playbacks. One of ExoPlayer’s biggest advantage is its easy customization.
Using ExoPlayer
The first step is to make sure we have the JCenter and Google repositories included in the project build.gradle
configuration file:
repositories {
jcenter()
google()
}
Next, we need to add a Gradle compile dependency to the same file:
implementation 'com.google.android.exoplayer:exoplayer:2.6.0'
Then, in our layout resource file, we add the SimpleExoPlayerView
component:
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
android:id="@+id/simple_exoplayer_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
After that, in the corresponding Activity class, we instantiate ExoPlayer's classes:
SimpleExoPlayerView simpleExoPlayerView;
SimpleExoPlayer player;
We then initialize our simpleExoPlayerView
in the onCreate
method of our Activity:
simpleExoPlayerView = findViewById(R.id.simple_exoplayer_view);
And, in the onStart
method, we call the setupPlayer
method:
@Override
protected void onStart() {
super.onStart();
setupPlayer();
}
And the setupPlayer
method:
void setupPlayer(){
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(bandwidthMeter);
TrackSelector trackSelector =
new DefaultTrackSelector(videoTrackSelectionFactory);
//initialize the player with default configurations
player = ExoPlayerFactory.newSimpleInstance(this, trackSelector);
//Assign simpleExoPlayerView
simpleExoPlayerView.setPlayer(player);
// Produces DataSource instances through which media data is loaded.
DataSource.Factory dataSourceFactory =
new DefaultDataSourceFactory(this, Util.getUserAgent(this, "CloudinaryExoplayer"));
// Produces Extractor instances for parsing the media data.
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
// This is the MediaSource representing the media to be played.
MediaSource videoSource = new ExtractorMediaSource(videoUri,
dataSourceFactory, extractorsFactory, null, null);
// Prepare the player with the source.
player.prepare(videoSource);
}
Here, we initialized the player
with some default configurations and then assigned it to the SimpleExoPlayerView
instance. The videoUri
is of type Uri
. Every file stored our device has a Uri
turning it into a unique address to that file. If we intend to display a video from a remote URL
, we have to parse it this way:
Uri videoUri = Uri.parse("any_remote_url");
With this, we have a basic implementation of ExoPlayer
. Google provides a great tutorial with more information on how to get started with this library.
Android Libraries—Networking
Nowadays, virtually every mobile app needs some sort of network communication to perform one function or the other. Previously, if we wanted to make a network request, we would have to execute an Async
task class and use HttpsUrlConnection
to fetch data. However, this is not very effective especially when we are dealing with APIs that return large data.
Luckily for us, there are great networking libraries available to help us optimize this process while managing threads and the device’s resources properly. Among the alternatives, there are two that stand out: Retrofit and Volley. As Retrofit is the most popular between the two, let's take a look at it.
Retrofit
Retrofit is a type-safe HTTP client for Android and Java developed and maintained by Square (the same company that supports Picasso). Retrofit is the most used networking library in Android development. In Retrofit, with just annotations, you can easily add a request body, manipulate endpoints, manipulate headers, add query parameters, and choose request methods. Retrofit also handles parsing to POJOs very well by using converters.
Using Retrofit
To use Retrofit, first, we need to add the dependency to our app build.gradle
file:
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
We would also need to insert dependency for converters that we intend to use. Converters handle the mapping of Java objects to the response body. You can read more about converters here.
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.3.0'
The first converter in the list above (converter-gson
), as the name states, maps to and from the JSON format. The second is used when we want to deal with primitive data types like String
. After importing the desired converters, we then create an interface to configure the endpoints that will be accessed :
public interface ApiService {
@GET("/data")
Call<ResponseClass> fetchData(@Body JsonObject jsonObject);
}
From the snippet, we have an endpoint /data
that requires a JsonObject
@Body
for the request. We also have a ResponseClass
which will be mapped to the expected response body of our request. However, the class is omitted here for brevity. To map JSON objects to POJOs, we can use the JsonSchema2Pojo library.
After defining the endpoints available, we then create a Retrofit client:
public class RetrofitClient {
static ApiService getService(){
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl("http://127.0.0.1:5000/")
.addConverterFactory(GsonConverterFactory.create());
Retrofit retrofit = builder
.client(httpClient.build())
.build();
return retrofit.create(ApiService.class);
}
}
When building our Retrofit object, we can add as many converters as we want. This provides us a variety of options to parse our data. Thereafter, we make a network request by calling:
RetrofitClient.getService().fetchData(jsonObject).enqueue(new Callback<ResponseClass>() {
@Override
public void onResponse(Call<ResponseClass> call, Response<ResponseClass> response) {
}
@Override
public void onFailure(Call<ResponseClass> call, Throwable t) {
}
});
Where jsonObject
contains the request parameters. This request will be made to the http://127.0.0.1:5000/data
endpoint, as defined in the previous code snippet. As we can see in this last snippet, Retrofit also provides us callback methods to give us the status of the request.
Android Libraries—Dependency Injection
Dependency injection is a concept where an object does not need to configure its dependencies. Instead, dependencies are passed in by another object. This principle helps us to decouple our classes from their implementation. It is worth noting that this is a good software engineering practice because it makes our code loosely-coupled, which makes it easier to maintain and test. There are a number of dependency injection libraries but Dagger2 seems to be the lord of them.
Dagger2
Dagger2 is a fully static, compile-time dependency injection framework for both Java and Android. It is an upgrade to the earlier version (Dagger1) created by Square that is now maintained by Google. The recent Dagger version includes Android specific helpers for Android. Specifically, the auto-generation of subcomponents using a new code generator. Dagger2 is very deep and may require just more than the brief sample usage for adequate understanding, but let's take a look at it anyway.
Using Dagger2
As always, first, we need to add the dependencies to our app-module build.gradle
file:
implementation 'com.google.dagger:dagger:2.14.1'
annotationProcessor 'com.google.dagger:dagger-compiler:2.14.1'
// we add this so we can use the android support libraries
implementation 'com.google.dagger:dagger-android-support:2.14.1'
annotationProcessor 'com.google.dagger:dagger-android-processor:2.14.1'
After that, we can create an activity builder module class to enable Dagger create sub-components for the activity that will need dependencies.
@Module
public abstract class ActivityBindingModule {
@ContributesAndroidInjector()
abstract MainActivity mainActivity();
}
We can optionally create a module class with specific dependencies to an activity. We then have to add the modules to the constructor of the @ContributesAndroidInjector
annotation for that activity, for instance:
@ContributesAndroidInjector(modules = {MainActivityModule.class} )
abstract MainActivity mainActivity();
We can also create another module class to provide dependencies to be used beyond just one activity class.
A module
class is annotated with @Module
and is responsible for providing objects. Objects are provided by creating methods (usually annotated with @Provides
or @Binds
) that have the same return type as what is to be provided. The sample module class below provides a String
for our app:
@Module
public abstract class AppModule {
@Provides
static String providesString(){
return "I love Auth0";
}
}
With @Module
in place, we then create an abstract class or an interface. It will be named AppComponent
in our case. The AppComponent
is annotated with @Component
. The annotation takes in the module classes created earlier including the AndroidSupportInjection
class which is from the support library. Dagger generates a class which then implements this interface. The class provides the injected instances from the modules passed. The Component
interface looks like this :
@Singleton
@Component(modules = {AndroidSupportInjectionModule.class, AppModule.class, ActivityBindingModule.class})
public interface AppComponent extends AndroidInjector<AppController> {
@Override
void inject(App instance);
@Component.Builder
interface Builder {
@BindsInstance
Builder application(Application application);
AppComponent build();
}
}
AppController
is our application class for the app where objects are initialized once throughout the app life-cycle. Our AppController
looks this way:
public class App extends Application implements HasActivityInjector {
@Inject
DispatchingAndroidInjector<Activity> activityDispatchingAndroidInjector;
@Override
public void onCreate() {
super.onCreate();
DaggerAppComponent.builder().application(this)
.build().inject(this);
}
@Override
public DispatchingAndroidInjector<Activity> activityInjector() {
return activityDispatchingAndroidInjector;
}
}
We had to create an instance of DispatchingAndroidInjector<Activity>
and return it in the implemented method. The reason for this is to perform members-injection on activities. Then, we built our Component
and injected the Application
class into it.
Finally, in the activity where we intend to use dependencies, we implement HasSupportFragmentInjector
and access our dependencies by just using the @Inject
annotation:
public class MainActivity extends AppCompatActivity implements HasSupportFragmentInjector {
@Inject
DispatchingAndroidInjector<Fragment> fragmentDispatchingAndroidInjector;
@Inject
String string;
@Override
protected void onCreate(Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
Log.d("TAG",string);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return fragmentDispatchingAndroidInjector;
}
}
We did something similar to what we did in the App
class. The only difference is that we are implementing HasSupportFragmentInjector
so the DispatchingAndroidInjector<T>
has Fragment
in its type now. The logic is this: the Application
will contain Activities
, and these will house Fragments
. Thereafter, we called AndroidInjection.inject(this)
in our onCreate
method.
From our brief example, if we run the app, we should see I love Auth0
printed on the log. We were able to inject the dependencies instead of initializing it in the class. This is just a bit of what Dagger2 has to offer. For more information, there are two great articles on Medium that we can reference:
Android Libraries—View Binding
View binding libraries emerged when there was a need to reduce the boilerplate code when assigning views to variables and having access to them in the activity class. Libraries in this area are limited. Basically, there are two that worth mentioning: ButterKnife and Android Databinding.
Butterknife
ButterKnife is a view binding library developed by Jake Wharton. Butterknife is a library that helps us assign ids
to views easily thereby avoiding the excess findViewById
. According to the documentation, “Butterknife is like Dagger only infinitely less sharp”. This means that view binding can be seen as a form of dependency injection. In ButterKnife, annotations are used to generate boilerplate code for us instead.
Using Butterknife
to use Butterknife, we need to add the dependencies in our app-module build.gradle
file as follows:
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
Then, in our activity, we use the @BindView
annotation to assign an id
to its view:
class MainActivity extends AppCompatActivity {
@BindView(R.id.firstname) EditText firstName;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
ButterKnife.bind(this);
// TODO ...
}
}
Note that, in the code snippet above, we initialized ButterKnife in our onCreate
method by using its bind
method. Alternatively, if we want to use ButterKnife
in a Fragment
we initialize it in the onCreateView
method this way:
View view = inflater.inflate(R.layout.sample_fragment, container, false);
ButterKnife.bind(this,view);
With ButterKnife, we also avoid creating OnClickListeners
. For instance, we can use an @OnClick
annotation together with the view to add a click listener to a view:
@OnClick(R.id.button)
void buttonClicked() {
// TODO ...
}
We can access the full documentation of ButterKnife here to explore more functionalities of the library.
Android Databinding Library
The Android Databinding Library is inbuilt to the Android Support Library. It requires at least Android studio version 1.3
to work. This library, unlike ButterKnife, does not make use annotations.
Using Databinding Library
Enable data binding in the app-module build.gradle
file and sync:
android {
....
dataBinding {
enabled = true
}
}
Thereafter, we set the root tag of our layout file to layout
:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>
</layout>
An activity binding class is then generated for us (ActivityMainBinding
) based on the naming of the layout: activity.main.xml
. We will use an instance of this class to access our views. We also have another class, DataBindingUtil
, generated to handle other utilities.
Then in our activity class:
public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.textview.setText("Hello world!");
}
}
The Android Databinding Library offers a replacement for onClick
listeners together with a whole lot of other features. These features can be found in the official docs.
These two libraries, ButterKnife and Android Databinding Library, significantly reduce the amount of code written to access views. However, the Android DataBinding library looks like a winner here because it is easier to setup, it achieves the result with fewer code as compared to ButterKnife and offers more functionalities.
Android Libraries—Reactive Programming
Reactive programming is a paradigm where data is emitted from a component (a source) to another component (a subscriber). This helps us to handle asynchronous tasks effectively. Reactive programming libraries are, therefore, libraries that help in passing the data from sources to subscribers. The most popular alternative available for Android developers is RxJava2 and RxAndroid. As such, let's take a look at them.
RxJava2 & RxAndroid
RxJava is a library that lets us implement reactive programming and, as such, create reactive applications. RxJava2 is an update to the earlier version of RxJava. In RxJava2, we have Observables
, Observers
, and Schedulers
.
Observables
are the data sources and they exist in various types: Observer
, Single
, Flowable
, Maybe
, and Completable
. Each of these types has a unique use case which we can read more about here.
Notably, the Flowable
comes with a backpressure support. Backpressure is when an Observer
can signal to the Observable
that the latter is emitting values too fast. Observers
are the data receivers (or consumers) while Schedulers
help to manage threads.
RxAndroid, on the other hand, is an extension of RxJava2. It offers functionalities just peculiar to the Android platform, like the provision of a Scheduler
that schedules on the main thread or any given Looper
.
Using RxAndroid
To use RxAndroid, we need to add the dependency to our app-module build.gradle
file:
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
// Because RxAndroid releases are few and far between, it is recommended you also
// explicitly depend on RxJava's latest version for bug fixes and new features.
compile 'io.reactivex.rxjava2:rxjava:2.1.7'
Then, we create an instance of a CompositeDisposable
in our class. The CompositeDisposable
is simply a container that can hold multiple disposables:
disposables.add(
Observable.just("Hello world!")
// Run on a background thread
.subscribeOn(Schedulers.io())
// Be notified on the main thread
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new DisposableObserver<String>() {
@Override
public void onComplete() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(String value) {
}
})
);
In the code snippet above, what we added to our disposables
variable consists of the data source which will emit one string, the thread where the process will take place, the thread where our observer will be notified of the result, and our subscriber/observer. The DisposableObserver<String>
is our observer, which provides us with three implemented methods. First is onNext
which is called whenever data is emitted, next is onError
to show that an error occurred, and finally onCompleted
to show that the Observable
has finished emitting data and won't call onNext
anymore.
We then clear resources when our activity is in the background to avoid memory leaks:
@Override
protected void onPause() {
super.onPause();
disposables.clear();
}
There is a basic, but interesting, tutorial by Mindorks to help you get started with RxJava2 here.
Android Libraries—Testing
Testing is simply executing a software with the aim of finding bugs. Testing has evolved over time and has gone beyond just being one of the final stages of a software development process. In fact, testing could be adopted as an early part of the coding stage where tests are written first, then the logic of the software is then implemented to only pass what the test expects. This is usually referred to as Test Driven Development. It is good practice to write tests for our applications as it helps us to spot bugs quickly and enhance our application. There are a number of test libraries with different strengths available for Android development. Let's take a look at four of them: JUnit, Mockito, Robolectric, and Espresso.
JUnit
JUnit is a framework used for unit testing. Unit testing is a type of testing where individual units of the source code are tested. The framework contains a set of assert
methods to check an expected result against the actual result. JUnit makes heavy use of annotations. Just to name a few, we have @Test
(to identify a test method), @Before
(to declare a method that should be called before a test is called), and @After
(to declare a method that should be called after a test).
Using JUnit
First, we add the dependency in our app-module build.gradle
file:
testImplementation 'junit:junit:4.12'
Then our sample test class looks like this:
public class ExampleUnitTest {
@Test
public void additionIsCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
Here, we checked to affirm that the addition of two and two equals four. JUnit tests are usually very fast because they run on the JVM and don’t require the device or an emulator. We can learn more about JUnit testing here.
Mockito
Most times, the classes we intend to write tests for depend on other classes. Configuring these classes just for this purpose can be hectic. This is where Mockito comes in. It is a mocking framework that helps us create and configure mock (fake) objects. It is usually used with together with JUnit.
Using Mockito
First, we need to make sure the JCenter repository, jcenter()
is in our project build.gradle
file. Next, we add the dependencies to the app-module build.gradle
file and sync:
testCompile 'junit:junit:4.12'
// required if we want to use Mockito for unit tests
testCompile 'org.mockito:mockito-core:2.13.0'
// required if we want to use Mockito for Android tests
androidTestCompile 'org.mockito:mockito-android:2.13.0'
Then we use Mockito like that:
@Test
public void mockitoTest throws Exception {
List mockedList = mock(List.class);
//using mock object
mockedList.add("one");
mockedList.clear();
//verification
verify(mockedList).add("one");
verify(mockedList).clear();
}
Here, we mocked a list, added data to it, and cleared it. We then verified that these actions were performed. We can learn more testing with Mockito in the official documentation.
Robolectric
Robolectric is another unit testing library. The difference to JUnit is that this Robolectric was created aiming to help Android developers. Robolectric handles inflation of views, resource loading, and lots of other stuff. This allows tests to do most things we could do on a real device that have Android framework dependencies without launching a device as in Espresso. In a way, Robolectric simulates the Android SDK for our tests. With Robolectric, we do not need additional mocking frameworks such as Mockito.
Using Robolectric
To use Robolectric, we add the dependency in our app build.gradle
file:
testCompile "org.robolectric:robolectric:3.6.1"
Then we create a sample test class:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class MyActivityTest {
@Test
public void checkActivityNotNull() throws Exception {
Activity activity = Robolectric.setupActivity(MyActivity.class);
assertNotNull( activity );
}
}
This is a very basic test that sets up our activity and just checks that it is not null. We can learn more about testing with Robolectric right here!
Espresso
Espresso is a test framework which is part of the Android Testing Support Library. This test framework allows us to create user interface tests for our Android apps. This means that, with Espresso, we can write tests that can check if the text of a TextView
matches another text. Espresso tests run on both actual devices and emulators and behave as if an actual user was using the app.
Using Espresso
First, we add these dependencies to our app-module build.gradle
file:
androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.1'
androidTestCompile 'com.android.support.test:runner:1.0.1'
Then, still in the same gradle
file, we set the instrumentation runner. Let's not forget to sync our Gradle files after that:
defaultConfig {
applicationId "com.my.awesome.app"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
Then, we can create a test file (e.g. EspressoSampleTest
) that will look like this:
@RunWith(AndroidJUnit4.class)
public class EspressoSampleTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule =
new ActivityTestRule(MainActivity.class);
@Test
public void isHelloWorldDisplayed() {
onView(withText("Hello world!"))
.check(matches(isDisplayed()));
}
}
This test class checks to see if "Hello World!" is displayed when MainActivity
is opened.
From our study of the various test libraries, we deduce that JUnit competes with no one as it works together with other libraries. Espresso gives us a good platform for our user interface tests. Both Mockito and Robolectric have similar capabilities, but Robolectric achieves testing with fewer code. Robolectric also offers more functionalities like being able to test views. Robolectric also has the advantage of being supported by Google engineers.
Android Libraries—Database Helpers
Offline data persistence is very important to enhance user experience. We usually need our applications to store important information that will be required on a next startup of the app or to make data available when no internet connection is available. As storing data is more complex than just combining key and value pairs, numerous libraries have been created to make storing this task easier in Android. In this section, we will look at one great persistence library: Room.
Room
Room is a persistence library which is part of the Android Architecture Components. Room provides local data persistence with minimal boilerplate code. It provides an abstraction layer over SQLite, thereby making it easier to work with databases in our app. This library comes with a lot of advantages such as verifying SQL queries at compile time, rejecting database queries on the main thread (except when explicitly stated while initializing the database), providing implementation best practices, etc.
Room is composed of three main components: the Database, the DAO (Data Access Objects), and Entities. Each of them is co-related in order to make the library functional. The Entity
class represents a database table and has to be annotated with @Entity
. The variables in the class represent the columns the table will have. The DAO is an interface that contains the methods used for accessing the database. Room uses the interface to generate an implementation class for us.
There are four specific annotations for the basic DAO operations: @Insert
, @Update
, @Delete
, and @Query
. Then, we have the Database class. This is an abstract class annotated with @Database
and that extends RoomDatabase
. This class defines the list of entities and their DAOs to be used.
Using Room
To use Room, we first need to add the Google Maven repository in our project build.gradle
file. Note that recent Android projects are pre-configured with this though:
allprojects {
repositories {
// ... other repositories
google()
}
}
Then, we add the Room dependencies in our app-module build.gradle
file:
implementation "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
Next, we create our Entity
class. As usual, we will go for something pretty simple. We will have two columns: id
and name
. The id
column will be our primary key and it will be autogenerated to make every row added unique.
@Entity(tableName = Person.TABLE_NAME)
public class Person {
public static final String TABLE_NAME = "person";
String name;
@PrimaryKey(autoGenerate = true)
public int id;
}
It's a best practice to name our tables with lowercase letters all through. As this is not the same convention when naming Java classes, we can use a custom name for our tables. Ordinarily, Room uses the class name as the table name. Thereafter, we create our DAO. The DAO is going to give an interface into the database itself and will take care of querying and storing our data. Next, we create our DAO to contain the methods to be used for the database manipulation:
@Dao
public interface PersonDao {
// Adds a person to the database
@Insert
void insert(Person person);
// Removes a person from the database
@Delete
void delete(Person person);
// Gets all people in the database
@Query("SELECT * FROM "+Person.TABLE_NAME)
List<Person> getAllPeople();
}
The interface is self explanatory with the annotations placed on its various methods. Thereafter, we can create the database class. This is an abstract class responsible for maintaining the database and providing instances to our DAOs :
@Database(entities = {Person.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract PersonDao getPersonDao();
}
With these classes in place, we can get an instance of our database and start making queries. It's recommended to make just one instance of our database throughout the app life-cycle. We can do this in our Application
class:
public class App extends Application {
private static AppDatabase appDatabase;
@Override
public void onCreate() {
super.onCreate();
// initialize the db once during the app lifecycle
appDatabase = Room.databaseBuilder(
getApplicationContext(),
AppDatabase.class,
"person.db"
).build();
}
public static AppDatabase provideDb(){
return appDatabase;
}
}
Then we can insert data into our database by accessing the database instance. This is done by calling the insert
method of our DAO class:
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
// insert into the database
Person person = new Person();
person.name= "Idorenyin Obong"
App.provideDb().getPersonDao().insert(person);
}
});
Here, we created an instance of Person
and inserted into the database. We only assigned the name because the id
, as mentioned before, will be automatically generated for us. Notice that we also created a new thread to execute the query in a background. We can equally adopt the above pattern to perform other manipulating operations on the database.
Google offers a nice tutorial to help us get started with this library.
Android Libraries-Custom Fonts
Almost every Android developer is passionate about the look and feel of their app. Sometimes we might need to go the extra mile into choosing a unique font for the app to give it the same feel across all devices. In situations like this, there are some libraries that can help us to use a custom font for all our texts in the app.
Calligraphy
Calligraphy is one of the most popular custom font libraries available and it is quite easy to get along with. With this library, we can easily declare a single font across our whole application or define fonts individually to a text.
Using Calligraphy
As usual, we add the dependency in our app-module build.gradle
file and sync it:
implementation 'uk.co.chrisjenx:calligraphy:2.3.0'
Then, we create an assets
folder and insert our custom font there. We can do that by right-clicking the app root folder in our project directory in Android Studio, select "New", choose "Folder", and then "Assets Folder". This will generate an assets folder. Thereafter, we initialize the library and set our default font in the Application
class:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
CalligraphyConfig.initDefault(new CalligraphyConfig.Builder()
.setDefaultFontPath("red-velvet.ttf")
.setFontAttrId(R.attr.fontPath)
.build()
);
}
}
Finally, we override the attachBaseContext
method in each of our activities:
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
}
And we are good to go! This gives us the Red Velvet font as our app’s default font. We can also decide to apply a particular font to a single text like this:
<TextView
android:text="@string/hello_world"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
fontPath="fonts/red-velvet.ttf"/>
Custom Fonts with Support Library
Thanks to the Android Support Library, from version 26
upward, we can make use of custom fonts without having to increase our app dependencies. This is so because in one way or the other we find the appcompat
dependency in our build.gradle
file. It is automatically added when creating a new Android project.
implementation 'com.android.support:appcompat-v7:26.1.0'
Using Custom Fonts with Support Library
First, we create a fonts
resource folder. We can do this by right-clicking the res
folder and then choose "New → Android resource" directory. After that, we have to choose font as the resource type and select OK. We then add the desired font files in the font
resource directory (e.g. redvelvet
).
We can apply the custom fonts directly in our XML layouts:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/redvelvet"/>
Additionally, we can create a new font family. A font family is a set of font files along with their style and weight details. Right click on the font source folder, select "New" and then "New Font Resource File", insert a name and select OK. A sample font family looks like this:
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
<font
app:font="@font/redvelvet"
app:fontStyle="normal"
app:fontWeight="400" />
</font-family>
When we apply a font to our XML
layout, the system picks the correct font based on the text style we used. Apart from applying the fonts in the XML
layouts, we can also apply them programmatically and in multiple forms as we can see in the official docs. Another great resource about using custom fonts with Support Library can be found here.
Job Scheduling
More often than not, our Android applications might need to perform operations out of the user's interaction. This requires handling tasks asynchronously and intelligently to optimize the app’s performance and the device in general. This can equally be called handling tasks in the background.
Android has its own API for scheduling background tasks, JobScheduler, which comes with a drawback: it can only be used when supporting API 21 (Android 5.0) or later.
Two other common options in this category are Android-job library by Evernote and Firebase Jobdispatcher by Firebase.
Firebase Jobdispatcher comes at another cost which is the need for Google Play Services. However, it is compatible all the way back to API 9 (Android 2.3). Android-job combines the effort of Android's JobScheduler and Firebase Jobdispatcher to provide a firm job scheduling library. Little wonder, it has been widely adopted.
Android-job
Android-job is an Android library used to handle jobs in the background. Depending on the Android version either the JobScheduler
, GcmNetworkManager
or AlarmManager
will be used. This is why this library wins the heart of all. Instead of using separate APIs within one codebase and checking for API versions to know which scheduling API to use, Android-job reduces our code size together with the stress and does this for us. This library requires API 14 (Android 4.0) or later.
Using Android-job
To use Android-job, we add the dependency in our app-module build.gradle
file and sync it:
compile 'com.evernote:android-job:1.2.1'
Then, we initialize our JobManager
in the Application
class:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
JobManager.create(this).addJobCreator(new SampleJobCreator());
}
}
The Application
class is used to initialize objects just once throughout the app lifecycle. The SampleJobCreator
is a class which returns instances of a Job
based on the job’s unique tag. The SampleJobCreator
can look like this:
public class SampleJobCreator implements JobCreator {
@Override
@Nullable
public Job create(@NonNull String tag) {
switch (tag) {
case FirstJob.TAG:
return new FirstJob();
default:
return null;
}
}
}
Finally, our Job
class named FirstJob
looks like this:
public class FirstJob extends Job {
public static final String TAG = "first_job_tag";
@Override
@NonNull
protected Result onRunJob(@NonNull Params params) {
// run your job here
return Result.SUCCESS;
}
public static void scheduleJob(long timeJobShouldStart) {
new JobRequest.Builder(ReviewStayJob.TAG)
.setExact(timeJobShouldStart)
.build()
.schedule();
}
}
In the code snippet above, we tell the Job
what to do in the onRunJob
method. Then, we schedule a job by just calling FirstJob.scheduleJob(timeinMills)
(where timeInMillis
is of datatype long
). The GitHub repo gives us more complex scheduling options like telling our job just to run when the device is charging, among others.
"I just read an amazing article that talks about great Android libraries."
Tweet This
Aside: Securing Android Apps with Auth0
Securing applications with Auth0 is very easy and brings a lot of great features to the table. With Auth0, we only have to write a few lines of code to get solid identity management solution, single sign-on, support for social identity providers (like Facebook, GitHub, Twitter, etc.), and support for enterprise identity providers (Active Directory, LDAP, SAML, custom, etc.).
In the following sections, we are going to learn how to use Auth0 to secure Android apps. As we will see, the process is simple and fast.
Dependencies
To secure Android apps with Auth0, we just need to import the Auth0.Android library. This library is a toolkit that let us communicate with many of the basic Auth0 API functions in a neat way.
To import this library, we have to include the following dependency in our build.gradle
file:
dependencies {
compile 'com.auth0.android:auth0:1.12.0'
}
After that, we need to open our app's AndroidManifest.xml
file and add the following permission:
<uses-permission android:name="android.permission.INTERNET" />
Create an Auth0 Application
After importing the library and adding the permission, we need to register the application in our Auth0 dashboard. By the way, if we don't have an Auth0 account, this is a great time to create a free one .
In the Auth0 dashboard, we have to go to Applications and then click on the Create Application button. In the form that is shown, we have to define a name for the application and select the Native type for it. After that, we can hit the Create button. This will lead us to a screen similar to the following one:
On this screen, we have to configure a callback URL. This is a URL in our Android app where Auth0 redirects the user after they have authenticated.
We need to whitelist the callback URL for our Android app in the Allowed Callback URLs field in the Settings page of our Auth0 application. If we do not set any callback URL, our users will see a mismatch error when they log in.
demo://bkrebs.auth0.com/android/OUR_APP_PACKAGE_NAME/callback
Let's not forget to replace OURAPPPACKAGE_NAME with our Android application's package name. We can find this name in the applicationId
attribute of the app/build.gradle
file.
Set Credentials
Our Android application needs some details from Auth0 to communicate with it. We can get these details from the Settings section for our Auth0 application in the Auth0 dashboard.
We need the following information:
- Client ID
- Domain
It's suggested that we do not hardcode these values as we may need to change them in the future. Instead, let's use String Resources, such as @string/com_auth0_domain
, to define the values.
Let's edit our res/values/strings.xml
file as follows:
<resources>
<string name="com_auth0_client_id">2qu4Cxt4h2x9In7Cj0s7Zg5FxhKpjooK</string>
<string name="com_auth0_domain">bkrebs.auth0.com</string>
</resources>
These values have to be replaced by those found in the Settings section of our Auth0 application.
Android Login
To implement the login functionality in our Android app, we need to add manifest placeholders required by the SDK. These placeholders are used internally to define an intent-filter
that captures the authentication callback URL configured previously.
To add the manifest placeholders, let's add the next line:
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "com.auth0.samples"
minSdkVersion 15
targetSdkVersion 25
//...
//---> Add the next line
manifestPlaceholders = [auth0Domain: "@string/com_auth0_domain", auth0Scheme: "demo"]
//<---
}
}
After that, we have to run Sync Project with Gradle Files inside Android Studio or execute ./gradlew clean assembleDebug
from the command line.
Start the Authentication Process
The Auth0 login page is the easiest way to set up authentication in our application. It's recommended using the Auth0 login page for the best experience, best security, and the fullest array of features.
Now we have to implement a method to start the authentication process. Let's call this method login
and add it to our MainActivity
class.
private void login() {
Auth0 auth0 = new Auth0(this);
auth0.setOIDCConformant(true);
WebAuthProvider.init(auth0)
.withScheme("demo")
.withAudience(String.format("https://%s/userinfo", getString(R.string.com_auth0_domain)))
.start(MainActivity.this, new AuthCallback() {
@Override
public void onFailure(@NonNull Dialog dialog) {
// Show error Dialog to user
}
@Override
public void onFailure(AuthenticationException exception) {
// Show error to user
}
@Override
public void onSuccess(@NonNull Credentials credentials) {
// Store credentials
// Navigate to your main activity
}
});
}
As we can see, we had to create a new instance of the Auth0 class to hold user credentials. We can use a constructor that receives an Android Context if we have added the following String resources:
R.string.com_auth0_client_id
R.string.com_auth0_domain
If we prefer to hardcode the resources, we can use the constructor that receives both strings. Then, we can use the WebAuthProvider
class to authenticate with any connection enabled on our application in the Auth0 dashboard.
After we call the WebAuthProvider#start
function, the browser launches and shows the Auth0 login page. Once the user authenticates, the callback URL is called. The callback URL contains the final result of the authentication process.
Capture the Result
After authentication, the browser redirects the user to our application with the authentication result. The SDK captures the result and parses it.
We do not need to declare a specific
intent-filter
for our activity because we have defined the manifest placeholders with we Auth0 Domain and Scheme values.
The AndroidManifest.xml
file should look like this:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.auth0.samples">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity android:name="com.auth0.samples.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
That's it, we now have an Android application secured with Auth0. To learn more about this, we can check the official documentation. There, we will find more topics like Session Handling and Fetching User Profile.
Conclusion
We have seen how libraries significantly reduce the amount of boilerplate code written to perform various functions while developing for Android. We also able took a quick look on how to use these libraries in most cases. Knowing the best libraries available out there can help us improve the quality of our apps and make us produce more in less time. So, no doubt, knowing and using these libraries leads to a win-win situation.
- 登录 发表评论