Home > other >  How can I detect when I'm done creating directories recursively with Kotlin Coroutines?
How can I detect when I'm done creating directories recursively with Kotlin Coroutines?

Time:09-22

I'm trying to download an entire directory from Google Drive and store it locally on my Android device. My process for doing this involves downloading all the directory info first, and then crawling through the hierarchy recursively (starting at the root directory), creating the subdirectories as I go. The below recursive method accomplishes this correctly.

The problem is this: I have no way of knowing when this method completes, since I don't know of any proper stop condition. I know that once the last coroutine completes it will be done, but I can't figure out how to track when they're all done.

Things I've tried:

  1. Incrementing/decrementing a counter for every coroutine I've started and checking when the counter reaches zero. This didn't work reliably.

  2. Tracking all the coroutines through their Jobs. This seemed promising but I couldn't figure out how to make it work.

  3. Not using coroutines at all for the recursive calls. This works but makes the operation take about twice as long.

Any help would be appreciated. And if I'm going about this the wrong way, I'd love to be set straight. Thanks!

My recursive method:

// directoryBeingBuilt starts as the root directory
private suspend fun createDirectoryStructure(directoryBeingBuiltId: String, directoryBeingBuiltUri: Uri) {

    // Create subdirectories for all children of directoryBeingBuilt.
    directoryMap[directoryBeingBuiltId]?.forEach { childDirectory ->

        // Create a directory for each childDirectory and set its Uri in directoryMap.
        val currentSubdirectoryUri = getOrCreateDirectory(directoryBeingBuiltUri, childDirectory.directoryName)!!
        childDirectory.directoryUri = currentSubdirectoryUri

        // Set the subdirectory we just built as directoryBeingBuilt and re-run this recursively.
        lifecycleScope.launch(Dispatchers.Default) {
            createDirectoryStructure(childDirectory.directoryId, currentSubdirectoryUri)
        }
    }
}

How I'm initially calling the recursive method:

withContext(Dispatchers.Default) {
    createDirectoryStructure(googleDriveRootDirectoryId.data, backupDirectoryUri)
    // I need to call some other method here only once the recursive method is done
}

directoryMap is a Map whose key = parent directory id and value = list of immediate subdirectories inside the parent directory:

private val directoryMap: MutableMap<String, List<DirectoryInfoContainer>> = mutableMapOf()

DirectoryInfoContainer:

data class DirectoryInfoContainer(val directoryId: String, val directoryName: String, var directoryUri: Uri?)

getOrCreateDirectory() method:

private suspend fun getOrCreateDirectory(parentUri: Uri, name: String): Uri? = withContext(Dispatchers.IO) {
    DocumentsContract.createDocument(
        requireActivity().contentResolver, parentUri, DocumentsContract.Document.MIME_TYPE_DIR, name
    )
}

CodePudding user response:

I didn't test this. Just an idea. Wrap your parallel work in coroutineScope so the suspend function won't return until all the inner jobs are finished.

Also, your suspend function is improper because it is marked suspend but calls blocking functions without wrapping them in withContext. This forces you to wrap calls to this function with an IO dispatcher, which defeats the purpose of marking it suspend and also breaks the convention for suspend functions.

But I could be wrong. Maybe getOrCreateDirectory() is a properly composed suspend function that internally delegates to an appropriate dispatcher. But if that's the case, you shouldn't have to specify the IO dispatcher when you launch coroutines that call this function. A proper suspend function can always be safely called from any dispatcher, because it's supposed to actually suspend and not block.

In the code below I'm assuming getOrCreateDirectory is a suspend function that wraps its blocking work in withContext(Dispatchers.IO).

private suspend fun createDirectoryStructure(directoryBeingBuiltId: String, directoryBeingBuiltUri: Uri) {
    coroutineScope {
        // Create subdirectories for all children of directoryBeingBuilt.
        directoryMap[directoryBeingBuiltId]?.forEach { childDirectory ->
    
            // Create a directory for each childDirectory and set its Uri in directoryMap.
            val currentSubdirectoryUri = getOrCreateDirectory(directoryBeingBuiltUri, childDirectory.directoryName)!!
            childDirectory.directoryUri = currentSubdirectoryUri
    
            // Set the subdirectory we just built as directoryBeingBuilt and re-run this recursively.
            launch {
                createDirectoryStructure(childDirectory.directoryId, currentSubdirectoryUri)
            }
        }
    }
}

So now if you call this function in a suspend function (without needing to wrap it in withContext!), it will suspend until all those directories are created or an IOException is thrown.

  • Related