Blog Infos
Author
Published
Topics
,
Published
The use-case

Uploading images to a remote server is an intensive operation. We would usually use a Service for this task but it becomes tricky when we want to upload the image in the background thread. We could use a CoroutineWorker for this purpose.

Service is an application component that can perform long-running operations in the background.

But that doesn’t mean background thread! According to the official documentation on Service:

A service runs in the main thread of its hosting process; the service does not create its thread and does not run in a separate process unless you specify otherwise.

If you want to learn how to upload images to Cloud storage using Service, check out this sample project:

WorkManager

Use WorkManager for immediate and persistent execution of tasks such as uploading an image.

According to the official documentation:

Even if you use a service, it still runs in your application’s main thread by default, so you should create a new thread within the service if it performs intensive or blocking operations.

TL;DR —

Use a CoroutineWorker with withContext(Dispatchers.IO) for intensive operations like upload and download.

This is the code you are looking for —

@HiltWorker
class UploadWorker @AssistedInject constructor(
@Assisted ctx: Context,
@Assisted workerParameters: WorkerParameters,
private val ioDispatcher: CoroutineDispatcher
) : CoroutineWorker(ctx, workerParameters) {
private val storageReference = Firebase.storage.reference.child("user_images")
override suspend fun doWork(): Result = withContext(ioDispatcher) {
val inputFileUri = inputData.getString(KEY_IMAGE_URI)
return@withContext try {
uploadImageFromUri(Uri.parse(inputFileUri))
} catch (exception: Exception) {
exception.printStackTrace()
Result.failure()
} catch (e: IOException) {
e.printStackTrace()
Result.failure()
}
}
private fun uploadImageFromUri(fileUri: Uri): Result {
fileUri.lastPathSegment?.let {
val photoRef = storageReference.child(it)
Log.d("Upload Worker", it)
photoRef.putFile(fileUri)
}
return Result.success()
}
}
view raw UploadWorker.kt hosted with ❤ by GitHub

Read ahead to discover how we reached there and follow the steps to implement this on your own.

The solution — use a Worker to upload images

Let’s approach the Result (pun intended) step-by-step —

Step 1. Get the image by launching an image picker

First, we create a Contract for handling image selection by registerActivityForResult().

import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContracts
class MyOpenDocumentContract : ActivityResultContracts.OpenDocument() {
override fun createIntent(context: Context, input: Array<String>): Intent {
val intent = super.createIntent(context, input)
intent.addCategory(Intent.CATEGORY_OPENABLE)
return intent
}
}

Then handle the given image URI in the Activity —

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var auth: FirebaseAuth
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
private val getContent = registerForActivityResult(MyOpenDocumentContract()) { uri: Uri? ->
// Handle the returned Uri
uri?.let {
onImageSelected(it)
}
Log.d(TAG, uri.toString())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
auth = Firebase.auth
auth.signInAnonymously()
val user = Firebase.auth.currentUser
binding.buttonSelectImage.setOnClickListener {
if (user != null){
getContent.launch(arrayOf("image/*"))
}
}
}
private fun onImageSelected(uri: Uri){
viewModel.uploadImageRequestBuilder(uri)
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Authorization is necessary for uploading to and downloading files from Cloud Storage. Set the sign-in method to Anonymous in the Firebase console.

set the sign-in method to ‘anonymous’.

Job Offers

Job Offers


    Senior Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Kotlin Multiplatform Mobile Developer

    Touchlab
    Remote
    • Full Time
    apply now

    Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

, ,

From Scoped Storage to Photo Picker: Everything to know about Storage

Persistence is a core element of every mobile app. Android provides different APIs to access or expose files with different tradeoffs.
Watch Video

From Scoped Storage to Photo Picker: Everything to know about Storage

Yacine Rezgui
Android developer advocate
Google

From Scoped Storage to Photo Picker: Everything to know about Storage

Yacine Rezgui
Android developer ad ...
Google

From Scoped Storage to Photo Picker: Everything to know about Storage

Yacine Rezgui
Android developer advocat ...
Google

Jobs

Step 2. Define WorkRequest based on input data in the ViewModel

Remember the uploadImageRequestBuilder() function we called from the MainActivity?

We just passed the returned URI from the gallery to the WorkRequest via uploadImageRequestBuilder() which in turn calls uriInputDataBuilder().

This method builds a Data object (inputData) for the UploadWorker class.

const val KEY_IMAGE_URI = "KEY_image_uri"
@HiltViewModel
class MainViewModel @Inject constructor(
application: Application
) : ViewModel() {
private val TAG = "MainViewModel"
private val workManager = WorkManager.getInstance(application)
fun uploadImageRequestBuilder(uri: Uri?) {
uri?.let {
val request =
OneTimeWorkRequestBuilder<UploadWorker>().setInputData(uriInputDataBuilder(uri)).build()
workManager.enqueue(request)
}
}
private fun uriInputDataBuilder(uri: Uri): Data {
return Data.Builder().putString(KEY_IMAGE_URI, uri.toString()).build()
}
}

Until now, we set input data for the WorkRequest using the image URI received from the gallery and enqueued the WorkRequest from the ViewModel.

Define the Work of uploading the image

Now, since you’ve worked your way this far in this article, let’s see how to define the function responsible for the actual task of uploading/downloading an image from the given file URI.

Hard-coding dispatchers directly in withContext() is a bad practice. Use constructor injection instead.

But how to do this in a worker injected by Hilt?

We just create a @HiltWorker annotated class using the following pattern

@HiltWorker
class ExampleWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
workerDependency: WorkerDependency // In our case it is CoroutineDispatcher
) : Worker(appContext, workerParams) { ... }

In our case —

@HiltWorker
class UploadWorker @AssistedInject constructor(
@Assisted ctx: Context,
@Assisted workerParameters: WorkerParameters,
private val ioDispatcher: CoroutineDispatcher
) : CoroutineWorker(ctx, workerParameters) {
// TODO implement uploadImageFromUri() and doWork()
}
view raw UploadWorker.kt hosted with ❤ by GitHub

Provide CororutineDispatcher as a dependency —

@Module
@InstallIn(SingletonComponent::class)
object CororutineDispatchersModule{
@Provides
@Singleton
fun provideIoDispatcher(): CororutineDispatcher = Dispatchers.IO
}

Then set the workerFactory in the application class —

@HiltAndroidApp
class ExampleApplication : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}

Since you’d be using WorkManager version higher than 2.6 which uses App Startup internally to initialize work-manager, add this to the AndroidManifest.xml file —

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
Now the part where we define the CoroutineWorker class for upload/download operations —
  1. Create a reference to the child node where you want to upload the image
  2. Retrieve the image URI from input data using the key you used to save it earlier.
Define the function to upload the image —
  1. Create a child node using the file’s lastPathSegment, this later completes the file’s path in Cloud Storage.
  2. Now begin upload using the putFile() method which accepts the image’s URI as a parameter.

Here’s the complete implementation of the doWork() method and the UploadWorker class —

@HiltWorker
class UploadWorker @AssistedInject constructor(
@Assisted ctx: Context,
@Assisted workerParameters: WorkerParameters,
private val ioDispatcher: CoroutineDispatcher
) : CoroutineWorker(ctx, workerParameters) {
private val storageReference = Firebase.storage.reference.child("user_images")
override suspend fun doWork(): Result = withContext(ioDispatcher) {
val inputFileUri = inputData.getString(KEY_IMAGE_URI)
return@withContext try {
uploadImageFromUri(Uri.parse(inputFileUri))
} catch (exception: Exception) {
exception.printStackTrace()
Result.failure()
} catch (e: IOException) {
e.printStackTrace()
Result.failure()
}
}
private fun uploadImageFromUri(fileUri: Uri): Result {
fileUri.lastPathSegment?.let {
val photoRef = storageReference.child(it)
Log.d("Upload Worker", it)
photoRef.putFile(fileUri)
}
return Result.success()
}
}
view raw UploadWorker.kt hosted with ❤ by GitHub
Conclusion

Many open-source samples demonstrate the use of Firebase features like Cloud Storage, but only a few explain its usage with Jetpack libraries.

Another use-case of WorkManager is in downloading Machine Learning models. Check out this code lab and let me know in the comments how you would implement step no. 12 using WorkManager.

 

This article was originally published on proandroiddev.com on August 29, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Nowadays authentication has become common in almost all apps. And many of us know…
READ MORE
blog
Yes! You heard it right. We’ll try to understand the complete OTP (one time…
READ MORE
blog
Firebase Remote Config is a cloud service that lets you change the behavior and…
READ MORE
blog
This series of articles will explain how to generate a code coverage report for…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu