A step-by-step guide to uploading images from a Worker instead of using a Service
.
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.
A
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() | |
} | |
} |
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 byregisterActivityForResult()
.
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) | |
} | |
} |
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
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 theWorkRequest
using the image URI received from the gallery and enqueued theWorkRequest
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() | |
} |
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 —
- Create a reference to the child node where you want to upload the image
- Retrieve the image URI from
input data
using thekey
you used to save it earlier.
Define the function to upload the image —
- Create a child node using the file’s
lastPathSegment, this later completes the file’s path in
Cloud Storage
. - 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 theUploadWorker
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() | |
} | |
} |
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