I am using room persistence library for my android application, Now I have to insert image in my db. I successfully define #Entity for the primitive data type. and also through converter class, i stored all object, date, time. Now I have to store Image. I am not able to understand how we define Column info and entity and how we insert that data as well as read data from the table.
What is the maximum size of data which inserted into the single row? What is max and min size of data in one field in Android SQLite?
It is usually not recommended to store image data into the database.
But however if it is required for your project then you can do so.
Image data are usually stored into db using BLOB data type, Room also provide support for BLOB data type Documentation
You can declare your entity class as mentioned below to store Image data.
#Entity(tableName = "test")
public class Test{
#PrimaryKey
#ColumnInfo(name = "_id")
private int id;
#ColumnInfo(typeAffinity = ColumnInfo.BLOB)
private byte[] image;
}
As Pinakin mentioned, it is not recommended to store an image into database and file path would be better but if it is required to store image I would suggest compress the image to below 2 MB (here is an example) to avoid breaking app. Room supports BLOB for image.
Entity class in kotlin:
ImageTest.kt
#Entity
class ImageTest {
#PrimaryKey(autoGenerate = true)
var id: Int = 1
#ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var data: ByteArray? = null
}
ImageDao.kt
#Dao
interface ImageTestDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsertByReplacement(image: List<ImageTest>)
#Query("SELECT * FROM image")
fun getAll(): List<ImageTest>
#Query("SELECT * FROM image WHERE id IN (:arg0)")
fun findByIds(imageTestIds: List<Int>): List<ImageTest>
#Delete
fun delete(imageTest: ImageTest)
}
Databse.kt
import android.arch.persistence.room.Database
import android.arch.persistence.room.RoomDatabase
import android.arch.persistence.room.TypeConverters
#Database(entities = arrayOf(ImageTest::class), version = 1)
#TypeConverters(DataConverters::class)
abstract class Database : RoomDatabase() {
abstract fun getImageTestDao(): ImageTestDao
}
In DatabaseHelper something like
class DatabaseHelper(context: Context) {
init {
DatabaseHelper.context = WeakReference(context)
}
companion object {
private var context: WeakReference<Context>? = null
private const val DATABASE_NAME: String = "image_test_db"
private var singleton: Database? = null
private fun createDatabase(): Database {
return Room.databaseBuilder(context?.get() ?:
throw IllegalStateException("initialize by calling
constructor before calling DatabaseHelper.instance"),
Database::class.java,
DATABASE_NAME)
.build()
}
val instance: Database
#Synchronized get() {
if (null == singleton)
singleton = createDatabase()
return singleton as Database
}
fun setImage(img: Bitmap){
val dao = DatabaseHelper.instance.getImageTestDao()
val imageTest = ImageTest()
imageTest.data = getBytesFromImageMethod(image)//TODO
dao.updsertByReplacement(imageTest)
fun getImage():Bitmap?{
val dao = DatabaseHelper.instance.getImageTestDao()
val imageByteArray = dao.getAll()
return loadImageFromBytes(imageByteArray[0].data)
//change accordingly
}
Correct me if I am wrong. Hope this helps someone out there
Save the image as a file and save the file path Uri to Room
As seen in CameraX's image capture use case, when a photo is successfully taken, the File path reference Uri, savedUri, can be retrieved safely.
Then, the Uri can be converted to a string with savedUri.toString(), and saved to Room.
It's important to ensure the Room file reference is also updated if the file is moved or deleted.
The image String saved in Room may need to be converted back into a Uri to be displayed with an image library such as Glide with Uri.parse(someString).
In the CameraX sample, an image path's Uri can safely be obtained in onImageSaved.
It would then be saved into Room off of the main thread using Kotlin Coroutines or RxJava, preferably in a ViewModel or somewhere that handles the business logic separate from the view logic.
Getting Started with CameraX > 5. Implement ImageCapture use case
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// Create time-stamped output file to hold the image
val photoFile = File(
outputDirectory,
SimpleDateFormat(FILENAME_FORMAT, Locale.US
).format(System.currentTimeMillis()) + ".jpg")
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
// Set up image capture listener, which is triggered after photo has
// been taken
imageCapture.takePicture(
outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
})
}
This strategy is outlined in Saving image in Room database on Reddit.
Cloud Storage
Creating a file for the image and saving the file path in Room covers local storage. In order to ensure the images are saved across multiple devices or when if data cache and data are cleared, a form of Cloud Storage is needed to upload the files to and to download and sync with the local storage.
Related
So the return function of my view model gives no return where in it is expected to give a return of bitmap so it can be used in UI to set the image.
Code of View Model :
val bitmap : MutableLiveData<Bitmap> by lazy { MutableLiveData<Bitmap>() }
fun retrive(doc_name : String,uid: String){
viewModelScope.launch(Dispatchers.IO){
bitmap.postValue(repository.retrive(doc_name,uid))
}
}
Code of Repository:
var localfile = createTempFile("tempImage", null)
var bitmap :Bitmap? = null
override suspend fun retrive(doc_name:String,uid: String) : Bitmap?{
val storageRef = FirebaseStorage.getInstance().reference?.child("/image/8WEFQnomCEMtlaSkCIkrBgT7XeO2/download")
storageRef.getFile(localfile).addOnSuccessListener {
bitmap = BitmapFactory.decodeFile(localfile.absolutePath)
}
return bitmap
}
Code in Fragment inside on View Created part:
val obsover = Observer<Bitmap>{
image.setImageBitmap(it)
}
admin_viewmodel.bitmap.observe(viewLifecycleOwner,obsover)
So because I kept in my Repository function that bitmap can be null it opens the fragment with no image in image view
But if I keep the Bitmap to not be null(!!) the app crashes and gives the Null Pointer Exception error in the lines below:
Inside the repository code I shared above:
return bitmap!!
2.Inside the View Model code I shared above:
bitmap.postValue(repository.retrive(doc_name,uid))
What I think is its Unable to return because things are working on different threads.
Kindy help me solve this, Thanks.
Edit after Broot Reply code changes:
override suspend fun retrive(doc_name:String,uid: String) : Bitmap {
return suspendCoroutine { cont ->
val storageRef =
FirebaseStorage.getInstance().reference?.child("/image/0XhL4jD4XCemk38rcRkIEjJMgjh2/Aadhar")
val localfile = createTempFile("tempImage", null)
storageRef.getFile(localfile).addOnSuccessListener {
val x = cont.resume(bitmap!!)
Log.d("checck", "$x")
}
}
}
Your original code was incorrect because it fires off an asynchronous function to the API and then returns immediately, before that asynchronous work is done and has fired its callback to update the bitmap property.
Your second code is wrong, because it tries to resume the continuation with the value of the property bitmap, which you have not updated with the value that was returned in the callback. Also, since you're just wanting a Bitmap from the cloud file, there's no reason to download it to a temporary file. You can work directly with the bytes. And there's no reason to use a property that I can see. bitmap can be a local variable.
Also, since you don't do anything in case of failure, your function would hang if there is a problem retrieving the data from Firebase. Below, I just throw the error, but you could do something different like returning null if you want.
I don't know what you're doing with those two parameters, but I left them. I leave it up to you to decide what your byte limit should be (I just used 5 million bytes). I don't remember the guaranteed minimum amount of available memory is for an Android app, and you might know that the file you're retrieving is below that value anyway.
override suspend fun retrive(doc_name: String, uid: String): Bitmap = suspendCoroutine { cont ->
val storageRef =
FirebaseStorage.getInstance().reference.child("/image/0XhL4jD4XCemk38rcRkIEjJMgjh2/Aadhar")
storageRef.getBytes(5_000_000L).addOnSuccessListener { byteArray ->
val bitmap = BitmapFactory.decodeByteArray(byteArray)
cont.resume(bitmap)
}.addOnFailureListener {
cont.resumeWithException(it)
}
}
However: Firebase already comes with the await() extension suspend function so you don't have to use suspendCoroutine.
override suspend fun retrive(doc_name: String, uid: String): Bitmap {
val storageRef =
FirebaseStorage.getInstance().reference.child("/image/0XhL4jD4XCemk38rcRkIEjJMgjh2/Aadhar")
val byteArray = storageRef.getBytes(5_000_000L).await()
return BitmapFactory.decodeByteArray(byteArray)
}
Since decoding a bitmap is kind of a heavy operation, I would do this in Dispatchers.Default:
override suspend fun retrive(doc_name: String, uid: String): Bitmap = withContext(Dispatchers.Default) {
val storageRef =
FirebaseStorage.getInstance().reference.child("/image/0XhL4jD4XCemk38rcRkIEjJMgjh2/Aadhar")
val byteArray = storageRef.getBytes(5_000_000L).await()
return BitmapFactory.decodeByteArray(byteArray)
}
I am currently retrieving data from firestore and saving the retrieved data to a list, the list is used to create a cache file in the internal storage. When the app is started I check for network, and if there is no network I use the data from the cache to populate a recyclerview. This works fine, however I would like to perform CRUD operations while offline and I struggle to find a way to accomplish this.
This is the code when retrieving data from firestore and calling to create a cache file.
override fun getFromFirestore(context: Context, callback: (MutableList<PostFirestore>) -> Unit) {
db.firestoreSettings = settings
val notesList = mutableListOf<PostFirestore>()
try {
db.collection("DiaryInputs")
.addSnapshotListener { snapshot, e ->
notesList.clear()
if (snapshot != null && !snapshot.isEmpty) {
for (doc in snapshot.documents) {
val note = doc.toObject(PostFirestore::class.java)
notesList.add(note!!)
}
cacheHelper.createCachedFile(context, notesList)
callback(notesList)
} else {
//Refreshing the RV and cache if firestore is empty.
cacheHelper.deleteCachedFile(context)
callback(notesList)
}
}
} catch (e: Exception){
Log.d(TAG, "Failure", e)
}
}
This is how the creation of the cache file is made:
#Throws(IOException::class)
override fun createCachedFile(context: Context, notesList: MutableList<PostFirestore>) {
val outputFile = File(context.cacheDir, "cache").toString() + ".tmp"
val out: ObjectOutput = ObjectOutputStream(
FileOutputStream(
File(outputFile)
)
)
out.writeObject(notesList)
out.close()
}
This is the dataclass PostFirestore
class PostFirestore: Serializable {
var id: String? = null
var diaryInput: String? = null
var temp: String? = null
var creationDate: String? = null
constructor() {}
constructor(id: String, diaryInput: String, temp: String, creationDate: String) {
this.id = id
this.diaryInput = diaryInput
this.temp = temp
this.creationDate = creationDate
}
}
What should I do to be able to add something to the cached file "cache.tmp" instead of overwriting it ? And possibly how to edit/delete something in the list that is stored in the cached file?
Firestore handles offline read and writes automatically. See its documentation on handling read and writes while offline.
If you don't want to depend on that, you'll have to replicate it in your own persistence layer. This includes (but may not be limited to):
Keeping a list of pending writes, that you send to the server-side database when the connection is restored.
Return the up-to-date data for local reads, meaning you merge the data from the local cache with any pending writes of that same data.
Resolve any conflicts that may happen when the connection is restored. So you'll either update your local cache at this point, or not, and remove the item from the list of pending writes.
There are probably many more steps, depending on your scenario. If you get stuck on a specific step, I recommend posting back with a new question with concrete details about that step.
The documentation describes that Firestore supports Unicode. You just need to insert already formatted text into Firestore. But when unloading, the following are not taken into account:
Line break;
Unicode characters inserted directly into the text (eg \u000a).
The code is below.
Repository
suspend fun getData(): Response<List<Model>> =
suspendCoroutine { cont ->
val collection =
firestore
.collection(COLLECTION_NAME)
.whereEqualTo(DEFAULT_CONDITION_FIELD, DEFAULT_CONDITION_VALUE)
.orderBy(SORT_FIELD, SORT_DIRECTION)
.get()
collection
.addOnSuccessListener { query ->
val data = arrayListOf<Model>()
query.toObjects(ModelDomain::class.java).forEach { data.add(it.toModel()) }
cont.resume(Response.Success(data))
}
.addOnFailureListener { cont.resume(Response.Error(it)) }
}
ViewModel
private val _data: LiveData<Response<List<Model>>> = loadData()
val data get() = _data
private fun loadData(): LiveData<Response<List<Model>>> =
liveData(Dispatchers.IO) {
emit(Response.Loading)
try {
emit(repository.getData())
} catch (e: Exception) {
emit(Response.Error(e))
}
}
Model
data class ModelDomain(
var description: String = ""
) : KoinComponent {
fun toModel() =
Model(
description = description
)
}
data class Model(
val description: String
)
Part of the code has been omitted.
UPDATE
Just wrote in Notepad ++:
Copied this to Firestore:
Result:
Firestore does not, in any way, modify data that you write to it. If you write something to a document, then read the document, you will get exactly the same data that you put into it.
If you're looking at the document in the Firebase console, you will not see all carriage returns and whitespace. Those are collapsed to save space on screen when rendering large amounts of data. But if you read the data programmatically, it will definitely be exactly as you wrote it.
I have implemented application using codelabs tutorial for new Paging 3 library, which was release week ago.
The problem is application is not working in offline mode. It does not retrieve data from Room database.
Tutorial Repo link :- https://github.com/googlecodelabs/android-paging
Code:-
RepoDao.kt
#Dao
interface RepoDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(repos: List<Repo>)
#Query("SELECT * FROM repos WHERE " +
"name LIKE :queryString OR description LIKE :queryString " +
"ORDER BY stars DESC, name ASC")
fun reposByName(queryString: String): PagingSource<Int, Repo>
#Query("DELETE FROM repos")
suspend fun clearRepos()
}
GithubRepository.kt
class GithubRepository(
private val service: GithubService,
private val database: RepoDatabase
) {
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
val dbQuery = "%${query.replace(' ', '%')}%"
val pagingSourceFactory = { database.reposDao().reposByName(dbQuery) }
return Pager(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
remoteMediator = GithubRemoteMediator(
query,
service,
database
),
pagingSourceFactory = pagingSourceFactory
).flow
}
companion object {
private const val NETWORK_PAGE_SIZE = 50
}
}
SearchRepositoriesViewModel.kt
#ExperimentalCoroutinesApi
class SearchRepositoriesViewModel(private val repository: GithubRepository) : ViewModel() {
private var currentQueryValue: String? = null
private var currentSearchResult: Flow<PagingData<Repo>>? = null
fun searchRepo(queryString: String): Flow<PagingData<Repo>> {
val lastResult = currentSearchResult
if (queryString == currentQueryValue && lastResult != null) {
return lastResult
}
currentQueryValue = queryString
val newResult: Flow<PagingData<Repo>> = repository.getSearchResultStream(queryString).cachedIn(viewModelScope)
currentSearchResult = newResult
return newResult
}
}
SearchRepositoriesActivity.kt
#ExperimentalCoroutinesApi
class SearchRepositoriesActivity : AppCompatActivity() {
.....
private lateinit var viewModel: SearchRepositoriesViewModel
private val adapter = ReposAdapter()
private var searchJob: Job? = null
// this is where adapter get flow data from viewModel
// initially this is called with **Android** as a query
private fun search(query: String) {
searchJob?.cancel()
searchJob = lifecycleScope.launch {
viewModel.searchRepo(query).collectLatest {
adapter.submitData(it)
}
}
}
.....
}
Output:- It is just showing the empty recyclerview when application is open in offline mode.
If you're able to share your code or how you reached that conclusion I could probably help pinpoint the problem a bit better, but the codelab does load data from Room on the branch: step13-19_network_and_database
There are two components here:
PagingSource: Provided by Room by declaring a #Query with a PagingSource return type, will create a PagingSource that loads from Room. This function is called in the pagingSourceFactory lambda in Pager which expects a new instance each call.
RemoteMediator: load() called on boundary conditions where the local cache is out of data, this will fetch from network and store in the Room db, which automatically propagates updates to PagingSource implementation generated by Room.
One other issue you might be seeing could be related to loadStateListener/Flow, essentially the codelab shows an error state by checking for CombinedLoadStates.refresh, but this always defers to the RemoteMediator's load state when available and if you want to show the locally cached data, even when RemoteMediator errors out, you'll need to disable hiding of the list in that case.
Note that you can access individual LoadState with CombinedLoadStates.source or CombinedLoadStates.mediator.
Hopefully this is enough to help you, but it's hard to guess your issue without some more concrete example / information about what you're seeing.
Edit: While the above are still good things to check for, it looks like there's an underlying issue with the library that I'm chasing down here: https://android-review.googlesource.com/c/platform/frameworks/support/+/1341068
Edit2: This is fixed now and will be released with alpha02.
At the moment as a sample I'm recreating instagram-like post, where user can post a description and image (along with username since I'm still not configuring authentication). So far as follows I can only post username and description. Along side of that I can load new image as a source into the imageview on the screen.
My data class is:
data class Post(val username: String,
val timestamp: Date,
val postTxt: String,
val numLikes: Int,
val numComments: Int,
val documentId: String)
I put data in a hashmap as follows:
val data = hashMapOf(
NUM_LIKES to 0,
NUM_COMMENTS to 0,
POST_TXT to addPostTxt.text.toString().trim(),
TIMESTAMP to FieldValue.serverTimestamp(),
USERNAME to addUsernameTxt.text.toString().trim()
)
and then pass it to upload to firestore:
FirebaseFirestore.getInstance().collection(POST_REF)
.add(data)
.addOnSuccessListener { finish() }
.addOnFailureListener {
Log.e("Exception", "Could not add post: $it")
toast("Could not add post")
}
And to get the data I created a function which takes snapshot of type QuerySnapshot like so:
// Clear all the posts before relaod
posts.clear()
snapshot.documents.forEach { document ->
val data = document.data
val name = data?.get(USERNAME) as String
val timestamp = data[TIMESTAMP] as Date
val postTxt = data[POST_TXT] as String
val numLikes = data[NUM_LIKES] as Long
val numComments = data[NUM_COMMENTS] as Long
val documentId = document.id
val newPost = Post(name, timestamp, postTxt, numLikes.toInt(), numComments.toInt(), documentId)
posts.add(newPost)
}
postsAdapter.notifyDataSetChanged()
So far uploading text, current time of upload works fine for both upload and reading it from the database; however my question here is:
- What is the best way (following current codebase) to upload image to firestore and storage and display it accordingly in the main window (preferably using Glide or picasso)?
It's generally not a good idea to store binary data like images in Firestore. You could very easily exceed the limit for how much data you can store in a document, which is 1MB. Large documents also take more time to load on the client, which is even worse if you don't even need to use the image at the time of the read.
Instead, consider uploading the image to Cloud Storage and saving a path to that location in a field in your document. Then, when it's time to display the image, load it from there.