Painless android library for working with files and database in easiest way as possible.
Sqlite ORM + handy tools for saving and restoring any kind of data.
- No SQL knowledge needed. ✅
- Easy to setup and no bloody🩸 compile time code generations. ✅
- Supports nested objects and arrays. ✅
- Easy migration with no pain while app updates and schema changes. ✅
- Most complex queries are possible and easy to write with simple functions. ✅
- Async IO and Multi-Threading supported. ✅
- Kotlin programming language recommended. ✅
- Supports old android versions (min sdk 15) ✅
- Installation
- ORM
- Settings data save and load best practice (without ORM setup)
- SafeBox
- Working with files
- License
Add it in your root build.gradle at the end of repositories:
allprojects {
repositories {
// add this line
maven { url 'https://www.jitpack.io' }
}
}Step 2. Add the dependency (in your app build.gradle):
dependencies {
// add this line
implementation 'com.github.ali77gh:EasyDataAndroid:3.2.2'
}Step 1: all you need to do is implement "Model" interface
note: you need to have "id" field this will act like unique primary key
classUser(
overridevarid:String,
varhashPass:String,
varname:String,
varage:Int,
varrole:String,
varmoney:Int,
) : ModelStep 2: Make a Table class of your model that is extending EasyTable (you can also put in inside User.kt if you want)
classUserTable(context:Context) :
EasyTable<User>(context, User::class.java,autoSetId =false){
// your custom query here
}
// no need to put any code inside this class// anyway we will put some custom queries inside of this class laterautoSetId: this will generate random UUID while you insert a row (set it false if you have plan for id yourself or you are getting rows from server that already have id)
That's it 😉 now you are ready to go.
Make table class instance:
val users =UserTable(context)Insert:
val user =User(...)
users.insert(user)Read:
val user = users.getById("id")
// you can also loop on usersfor (user in users){
//do something with user
}
// note: its not loading all users on ram (check out under the hood part for more information)// see complex queries part for More reading samples Update:
user.name ="alireza"
users.update(user) // it find user row by id and apply changesDelete:
// delete one row by id
users.delete(user.id)
// delete many rows with where statement
users.deleteWhere { it.name=="ali" } I recommend you to write singleton (optional)
// repo singletoncompanionobject {
privatevar repo:UserTable?=nullfungetRepo(context:Context): UserTable {
if (repo ==null) repo =UserTable(context)
return repo!!
}
}
// ------// then you can get table class like this:var users =UserTable.getRepo(context)Important warning ☢ ️: don't forget to star ⭐ this repo
Now its time to come back to Table class and add some queries.
Note: You can write this queries right after users object too. but putting your queries here in Table class make your code cleaner.
Checkout samples:
classUserTable(context:Context) :
EasyTable<User>(context, User::class.java, autoSetId =false) {
// custom queries here.// you can access to all of standard high order functions on lists.fungetByName(name:String) = filter { it.name == name }
// you can use 'get' keyword when you have no input param for query.val admins get() = filter { it.role =="admin" }
// query can return any type (here is boolean).funisAdmin(id:String) = any { it.id == id && it.role =="admin" }
funisUnderAge(id:String) = any { it.id == id && it.age <18 }
// you can write almost every queries in one line.val top5Riches get() = sortedByDescending { it.money }.subList(0, 5)
// but you are free to write multi line query and use variables if you want. val top5Riches
get() {
val sorted = sortedByDescending { it.money }
val top5 = sorted.subList(0, 5)
return top5
}
// here is a check password and i have nothing to say ;)funcheckPassword(id:String, hashPass:String) = getById(id)!!.hashPass == hashPass
// lets delete some rows with where statementfunremoveUnderAges() = deleteWhere { it.age <18 }
// update all// 'it' object is before update and what you return will replace with old onefunincreaseAges1() = updateAll {
it.age++return@updateAll it
}
// same query in other stylefunincreaseAges2() = updateAll { it.age++;it }
// again same query in other style (you are free to write query in your way)funincreaseAges3() = updateAll { it.apply { age++ } }
// lets do some update with where statement // first function returns boolean and 'true' means this row should update// second function effects on that row (same in updateAll function)funincreaseRoleOfAlis() = updateWhere({ it.name =="ali" }, { it.role ="admin";it })
// IORun will run your code in other thread and pass result back to UI thread with callback// you can use it in other replaces too (not limited to writing queries)funasyncGetByName(name:String, cb: (user: User) ->Unit) =IORun({ filter { it.name == name }[0] }, cb)
}
This is a caching system and will speed up your read queries.
if you want to enable FastTable all you have to do is extending FastTable instead EasyTable:
classUserTable(context:Context) :
FastTable<User>(context, User::class.java,autoSetId =false)You have exact same functionality but faster read queries see performance part
Memory Usage: FastTable use 2MB more RAM with 10,000 of User object that we shows in this documentation.
You can have a field in type of list or custom class or list of custom class or even more nest levels.
checkout this:
classLoc(vallat:Double,vallng:Double)
classUser(
overridevarid:String,
varhashPass:String,
varname:String,
varage:Int,
varrole:String,
varmoney:Int,
varmarks:List<Float>,
varlocations:List<Loc>
) : Modelhandy? right?
No problem.
Just add field (nullable) and everything will work as well.
Warning: lets say you insert a rowA to table and then you change schema by adding fieldX. then fieldX of rowA will be null
Recommended way to handle this:
add init{} function in your model and set a default value for fieldX if is null.
Its easier way to run a code in other thread and pass result to Main thread (UI thread).
Checkout this:
IORun({
//do heavy process or IO operationreturn result
} , { result ->// this will run in ui thread// and you can do things with result object// object can have any type based on what you return in first function
})Its for 10,000 record on android virtualbox on my dual core laptop.
See running code of this test

FastTable use 2MB more RAM with 10,000 of User object that we shows in this documentation
This library use sqlite and json to saving and restoring records.
KeyValDb class handle all SQL stuff.
EasyTable class is extending KeyValDb and have json serialize and deserialize and generic things and also iteration stuff that provides high order functions on sqlite cursor.
FastTable class is extending EasyTable and have some caching stuff for better read speed. see performance
This class used for saving password and other sensitive data.
This class actually encrypt and saves data in app local storage.
Checkout this:
// This functions generates key with device unique id// this line is optional and you can use any string as a key val key =DeviceKeyGenerator.Generate(this)
// Safe box instanceval safeBox =SafeBox(this, key)
safeBox.save("password", "secret")
var readed = safeBox.load("password")This tools provide Read , Write , Delete for you with Sync or Async mode on External , Local , Cache Storage
RootModes : LOCAL , EXTERNAL , CACHE
LOCAL: app private storage (user and other apps can not access)
EXTERNAL: root of public storage (user and other apps can access)
CACHE: files in this mode will be removed by cleaner apps
// instanceval bitmapDAO =BitmapDAO(this, RootMode.LOCAL) // see RootMode in Working with files section// simple save
bitmapDAO.save("testBitmap.bmp",bitmap)
// save with 50% of quality
bitmapDAO.save("testBitmap50.bmp",bitmapForTest,50)
// resize and save with given width and height
bitmapDAO.save("testBitmap10px.bmp",bitmapForTest, width=10, height=10)
// async save
bitmapDAO.saveAsync("testBitmapAsync.bmp", bitmapForTest, callback = {
// done!
})
// simple loadval bitmap = bitmapDAO.load("testBitmap.bmp")
// async load
bitmapDAO.load("testBitmap.bmp",{ bitmap->
})// instanceval bytesDAO =BytesDAO(this, RootMode.LOCAL) // see RootMode in Working with files section// simple save
bytesDAO.save("test", byteArray)
// async save
bytesDAO.saveAsync("test", byteArray,callback = {
// done!
})
// simple loadval byteArray = bytesDAO.load("test")
// async load
bytesDAO.load("test", { bytes ->
})// instanceval stringDAO =StringDAO(this, RootMode.LOCAL) // see RootMode in Working with files section// simple save
stringDAO.save("test", "your string here")
// async save
stringDAO.saveAsync("test", yourString, callback = {
// done!
})
// simple loadval yourString = stringDAO.load("test")
// async load
stringDAO.load("test", { bytes ->
})// instanceval objectDAO =ObjectDAO(this, RootMode.LOCAL) // see RootMode in Working with files section// simple save
objectDAO.save("test", obj)
// async save
objectDAO.saveAsync("test", obj, callback = {
// done!
})
// simple loadval user = objectDAO.load("test",User::class.java) asUser// async load
objectDAO.load("test", { obj ->
})