preface

Since LitePal fully supported Kotlin in version 2.0.0, I’ve been thinking about how to make LitePal better fit into the Kotlin language than just support.

Kotlin is really a great language, with many great features that you can’t implement in Java. So, after LitePal’s full support for Kotlin, I felt it would be a waste of me to dismiss these great features. So with the latest version of LitePal 3.0.0, I’m going to make LitePal take advantage of some of Kotlin’s language features to make our development easier.

In addition to introducing the upgrade to LitePal 3.0.0, this article also covers some advanced Kotlin.

First, let’s look at how to upgrade.

Way to upgrade

Why the jump from 2.0 to 3.0? Because this time LitePal has a qualitative change in structure.

To be more compatible with Kotlin, LitePal is now two libraries instead of just one, depending on the language you’re using. If you are using Java, add the following configuration to build.gradle:

Dependencies {implementation 'org. Litepal. Android: Java: 3.0.0'}Copy the code

If you are using Kotlin, add the following configuration to build.gradle:

Dependencies {implementation 'org. Litepal. Android: kotlin: 3.0.0'}Copy the code

So let’s take a look at what’s changed in LitePal 3.0.0.

I have to say that LitePal’s generic design has never been very friendly, especially when it comes to asynchronous queries. For example:

In the onFinish() callback to the asynchronous query, instead of the query object, we get a generic T object, which requires another forced transformation to get the object we really want to query.

If that’s not enough for you, consider this example:

As you can see, this query returns a List

, and we have to cast the entire List. Not only is there an extra line of code to write, but the key is that the development tool gives you an ugly warning.

Such a design is by no means friendly.

Thank you very much xiazunyang for proposing this Issue on GitHub (github.com/LitePalFram… The 3.0.0 generics optimizations are largely based on his recommendations.

Now let’s see how the same functionality can be written in LitePal 3.0.0:

LitePal.findAsync(Song.class, 1).listen(new FindCallback<Song>() {
    @Override
    public void onFinish(Song song) {

    }
});

Copy the code

As you can see, the generic type is declared as Song on the FindCallback interface, so the parameter in the onFinish() callback can be specified as Song directly, avoiding a cast.

Similarly, when querying multiple sets of data, we can write:

LitePal.where("duration > ?" . "100").findAsync(Song.class).listen(new FindMultiCallback<Song>() { @Override public void onFinish(List<Song> list) { } });Copy the code

In the onFinish() callback, we get a List

collection instead of the ugly warning.

The code would have been much simpler if it had been written using Kotlin:

LitePal.where("duration > ?" , "100").findAsync(Song::class.java).listen { list -> }Copy the code

Thanks to Kotlin’s excellent lambda mechanism, our code can be further streamlined. In the above code, the list argument at the end of the line is the queried list

collection.

So that’s all for generic optimization, and now let’s look at another topic, listening to database creation and upgrading.

Yes, LitePal 3.0.0 adds the ability to create and upgrade listening databases.

The feature was added because a friend of JakeHao made an Issue on GitHub (github.com/LitePalFram…

)

To do this, new interfaces must be added, and I am cautious about adding new interfaces because of their ease of use and impact on the overall framework.

Every interface in LitePal has been designed to be as simple and usable as possible, so as you can guess, listening for database creation and updates is easy to implement with just a few lines of code:

LitePal.registerDatabaseListener(new DatabaseListener() {
    @Override
    public void onCreate() {
    }

    @Override
    public void onUpgrade(int oldVersion, int newVersion) {
    }
});

Copy the code

Note that the registerDatabaseListener() method must be called before any other database operations, and then the onCreate() method will get a callback when the database is created, and the onUpgrade() method will get a callback when the database is upgraded, And tell the parameter tells you the old version number and the new version number after the upgrade.

Kotlin’s code is similar, but since the interface has two callback methods, it doesn’t use Kotlin’s single abstract method (SAM) syntactic sugar. Instead, it uses an anonymous object that implements the interface:

LitePal.registerDatabaseListener(object : DatabaseListener {
    override fun onCreate() {
    }

    override fun onUpgrade(oldVersion: Int, newVersion: Int) {
    }
})

Copy the code

With that in mind, we’ll quickly cover listening database creation and upgrade, and move on to the main story of this article.

As you can see from the above article, the Kotlin version of the code is generally more minimalist than Java code, and Google’s official statistics show that Kotlin can reduce code by approximately 25% or more.

But Kotlin, who is all about simplicity, has one usage that really bothers me. Select * from song where id = 1;

Song song = LitePal.find(Song.class, 1);

Copy the code

The same functionality is written in Kotlin:

val song = LitePal.find(Song::class.java, 1)

Copy the code

Since LitePal must know which table to query, it must pass a Class parameter to LitePal. In Java we just pass in song-class, but in Kotlin we write Song::class.java, which is longer than the Java code.

Of course, many people get used to writing and it’s not a big problem. But as I learned more about Kotlin, I realized that Kotlin provides a fairly powerful mechanism for optimizing this problem called generic realizations. I’ll explain the concept and usage of generic realizations in more detail.

To understand generic realizations, you first need to know the concept of generic erasure.

In any JVA-BASED language, whether Java or Kotlin, generics are basically implemented through type erasure. That is, generics’ constraints on types only exist at compile time; there is no way to directly check the types of generics at run time. For example, if we create a List

collection, while only String elements can be added to the collection at compile time, at run time the JVM does not know what type of elements it was intended to contain, only that it is a List.

Java’s generic erasure mechanism makes it impossible to use an if (an instanceof T) or t.class syntax.

Kotlin is also a JVM-based language, so Kotlin’s generics are erased at run time. However, Kotlin provides the concept of an inline function, in which the code in an inline function is automatically replaced at compile time to the place where it was called. This makes the parameter declaration and argument passing in the original method call directly become the variable call in the same method after compilation. This way, there is no generic erase problem, because Kotlin replaces the generic part of the inline method code with arguments directly after compilation.

To put it simply, Kotlin allows generics in inline methods to be implemented.

Generics firm

So how do you write that to actually implement generics? First, the method must be inline, that is, it is decorated with the inline keyword. Second, you must also add the reified keyword where you declare a generic to indicate that it is being implemented. The sample code looks like this:

inline fun <reified T> instanceOf(value: Any) {

}

Copy the code

The generic T in the above method is an implemented generic because it satisfies two prerequisites: the inline function and the reified keyword. So what can we achieve with generic realizations? As can be seen from the name of the method, here we use generics to implement an instanceOf effect, as shown below:

inline fun <reified T> instanceOf(value: Any) = value is T

Copy the code

It’s only one line of code, but it does something completely impossible in Java — determine if the type of a parameter is a generic type. That’s the magic of generic realizations.

So how do we use this method? In Kotlin you can write:

val result1 = instanceOf<String>("hello")
val result2 = instanceOf<String>(123)


Copy the code

As you can see, the first line of code specifies the generic type String and takes the String “hello”, so the final result is true. The second line of code specifies that the generic is String and the argument is the number 123, so the final result is false.

In addition to making type judgments, we can also get the Class type of a generic directly. Take a look at this code:

inline fun <reified T> genericClass() = T::class.java

Copy the code

The genericClass() method returns the class type of the currently specified generic. A syntax like t.class is not possible in Java, whereas in Kotlin you can use a syntax like T::class.java with the help of generic realizations.

We can then call:

val result1 = genericClass<String>()
val result2 = genericClass<Int>()


Copy the code

As you can see, if we specify a generic String, we end up with java.lang.String’s Class, and if we specify a generic Int, we end up with java.lang.Integer’s Class.

That’s the end of the Kotlin generics realizations section, and now let’s go back to LitePal. With all this talk about generic implementations, how exactly can LitePal be optimized to take advantage of this feature?

Select * from song where id = 1;

val song = LitePal.find(Song::class.java, 1)

Copy the code

Song::class.java is passed in because it tells LitePal to query the data in the Song table. As we saw in the generics implementation section, the syntax T::class.java is available in Kotlin, so I extended this feature in LitePal 3.0.0 to allow you to declare which table to query by specifying generics. The code can then be optimized to look like this:

val song = LitePal.find<Song>(1)

Copy the code

How’s that? Does the code feel cleaner all of a sudden? It looks even more minimalist than the Java version of the query.

Thanks to Kotlin’s excellent type derivation, we could also write the code as follows:

val song: Song? = LitePal.find(1)

Copy the code

It’s exactly the same because if I declare song after song, right? Type, then the find() method can automatically derive generic types, eliminating the need for manual generic specification of

.

In addition to the find() method, I’ve optimized almost all of the public apis in LitePal by adding an extension method that specifies generics instead of Class arguments to any interface that used to pass Class arguments. Note that I am using the extension method instead of modifying the original method, so that you can use both methods as you like. If you directly modify the original method, then the project may cause a large number of errors after upgrading, which is not what anyone wants to see.

Let me show you a few more examples of optimized CRUD operations. For example, when I want to use a WHERE conditional query, I can write:

val list = LitePal.where("duration > ?" , "100").find<Song>()Copy the code

Here the generic

is specified in the final find() method, which results in a List

collection.

Select * from ‘song’ where id = 1;

LitePal.delete<Song>(1)

Copy the code

To count the number of records in the song table, write:

val count = LitePal.count<Song>()

Copy the code

Some other methods of optimization are similar, I believe that we can draw inferences from one another, no longer one demonstration.

That brings us to the main features of the new version of LitePal. Of course, in addition to these new features, I’ve also fixed a few known bugs and improved the overall stability of the framework, so if that’s what you need, go ahead and upgrade.