JetpackCompose から 共有ストレージのファイルにアクセスする

概要

Jetpack Compose から 共有ストレージのファイルにアクセスする方法を説明します。 具体的には以下を実装します。

  1. ユーザにフォルダーを選択させ、そこにファイルを作成する。
  2. ユーザにファイルを選択させ、その内容を読み取る。

なお、アプリがアクセスできるファイルやフォルダーの選択をユーザーが行うため、このメカニズムではシステム権限は必要ありません。

実装

UserDictionaryScreen.kt
package com.kyamada.listentonovels.androidApp.ui.compose.screens.dictionary
 
import android.net.Uri
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.screen.Screen
import com.kyamada.listentonovels.androidApp.extension.getText
import org.koin.core.component.KoinComponent
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
 
 
const val TAG = "UserDictionaryScreen"
 
class UserDictionaryScreen : Screen, KoinComponent {
 
    @Composable
    override fun Content() {
        val context = LocalContext.current
 
        val createDocumentLauncher =
            rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/tab-separated-values")) { uri: Uri? ->
                val fileUri = uri ?: return@rememberLauncherForActivityResult
                try {
                    context.contentResolver.openFileDescriptor(fileUri, "w")?.use { descriptor ->
                        FileOutputStream(descriptor.fileDescriptor).use { os ->
                            os.write("a,b,c".toByteArray())
                        }
                    }
                } catch (e: FileNotFoundException) {
                    e.printStackTrace()
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
 
        val openDocumentLauncher =
            rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
                if (uri == null) return@rememberLauncherForActivityResult
                val tsv = context.contentResolver.getText(uri, Charsets.UTF_8)
                Log.d(TAG, tsv)
            }
 
        LazyColumn {
            item {
                TextButton(
                    onClick = {
                        // 1. ユーザにフォルダーを選択させ、そこにファイルを作成する。
                        // 引数には作成するファイル名を指定します。
                        createDocumentLauncher.launch("fileName.tsv")
                    }
                ) {
                    Text("exportToFile")
                }
            }
            item {
                TextButton(
                    onClick = {
                        // 2. ユーザにファイルを選択させ、その内容を読み取る。
                        // 引数にはファイルのMIMEタイプを指定します。
                        openDocumentLauncher.launch(arrayOf("text/tab-separated-values"))
                    }
                ) {
                    Text("importFromFile")
                }
            }
        }
    }
}
ContentResolver+.kt
import android.content.ContentResolver
import android.net.Uri
import io.ktor.utils.io.charsets.Charset
import java.io.BufferedReader
import java.io.InputStreamReader
 
// テキストファイルの内容を取得する拡張メソッド
fun ContentResolver.getText(uri: Uri, charset: Charset): String {
    val stringBuilder = StringBuilder()
    this.openInputStream(uri)?.use { inputStream ->
        BufferedReader(InputStreamReader(inputStream, charset)).use { reader ->
            var line: String? = reader.readLine()
            while (line != null) {
                stringBuilder.appendLine(line)
                line = reader.readLine()
            }
        }
    }
    return stringBuilder.toString()
}

参考

© 品川アプリ.RSS