2022. 9. 21. 01:37ㆍ[Android APP] feat. Kotlin/Kotlin 공부
개요
사용자가 무언가 첨부하거나 사진을 선택할 때, 또는 파일을 서버에 업로드 할 경우
우리는 MultyPart/form-data를 사용해 API 서버에 전송하게 된다.
본문
자 우선 첨부 버튼을 클릭 했을 때 화면에 파일 이름을 띄어주고 가입버튼을 눌렀을 때 API서버로 파일을 전송해주는 로직을 짜보자.
1. xml
우선 간단하게 레이아웃을 짜보자
아주아주 간단하게 3개의 파일을 첨부한다고 가정하고 짜본다.
Tip. ScrollView를 사용하는데 에러가 뜨면서 실행이 안되는건 ScrollView 안에 여러 위젯이 들어있으면 안되고 하나의 레이아웃으로 묶어줘야한다. 예를 들면 ScrollView 안에 LinearLayout으로 묶어주면 제대로 실행된다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/logo_image"
android:layout_width="match_parent"
android:layout_height="100dp"
android:src="@drawable/join_logo"
android:padding="25sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</ImageView>
<ScrollView
android:id="@+id/join_Scrollview"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/logo_image">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="회원가입을 진행해주세요\n가입해주셔서 감사합니다."
android:textAlignment="textStart"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="14"
android:text="@string/attachtext"
android:textSize="20sp"
android:textColor="@color/dark_gray"
android:textStyle="bold" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:padding="10dp"
android:layout_marginTop="10sp"
android:background="@color/dark_gray">
</View>
</LinearLayout>
<LinearLayout
android:id="@+id/a"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="A파일"
android:textSize="20sp"
android:textColor="@color/black"
android:textStyle="bold" />
<TextView
android:id="@+id/a_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="@color/black"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/a_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/attachtext"
android:minHeight="48dp" />
<Button
android:id="@+id/a_delete_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/deletetext"
android:minHeight="48dp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/b"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="B파일"
android:textSize="20sp"
android:textColor="@color/black"
android:textStyle="bold" />
<TextView
android:id="@+id/b_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="@color/black"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/b_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/attachtext"
android:minHeight="48dp" />
<Button
android:id="@+id/b_delete_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/deletetext"
android:minHeight="48dp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/c"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="C파일"
android:textSize="20sp"
android:textColor="@color/black"
android:textStyle="bold" />
<TextView
android:id="@+id/c_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="@color/black"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/c_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/attachtext"
android:minHeight="48dp" />
<Button
android:id="@+id/c_cancle_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/deletetext"
android:minHeight="48dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/join_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="가입"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
</Button>
<Button
android:id="@+id/cancel_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="취소"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
</Button>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
이런 화면이다.
2. Interface
Retrofit으로 API서버에 파일을 전송하기 위한 인터페이스를 작성해준다.
interface JoinService {
@Multipart
@PUT("/userManage/joinUser")
fun requestJoin(
@Part("data") data : RequestBody,
@Part f_a_license : MultipartBody.Part?,
@Part f_b_license : MultipartBody.Part?,
@Part f_c_license : MultipartBody.Part?
): Call<JoinDTO>
}
여기서 데이터는 data라는 RequestBody로 묶어서 한번에 보내주고
파일은 따로 MultipartBody.Part 형식으로 전송해줘야 한다.
여기서 중요한게 있는데 위에 어노테이션으로 @Multipart를 꼭 붙여줘야 파일이 정상적으로 전송된다.
3. Activity
1) 파일 탐색기에 접근하기 위한 사용자 권한 체크 받기
이 부분은 다른 포스트에 있기 때문에 생략하겠다.
//사용자 저장소 권한체크
requestMultiplePermissions()
//저장소 사용자 권한 요청(안드로이드 정책)
private fun requestMultiplePermissions() {
Dexter.withActivity(this)
.withPermissions(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
// check if all permissions are granted
if (report.areAllPermissionsGranted()) {
Toast.makeText(
applicationContext,
"이미 권한체크를 완료했습니다",
Toast.LENGTH_SHORT
).show()
}
// check for permanent denial of any permission
if (report.isAnyPermissionPermanentlyDenied) {
// show alert dialog navigating to Settings
}
}
override fun onPermissionRationaleShouldBeShown(
permissions: List<PermissionRequest?>?,
token: PermissionToken
) {
token.continuePermissionRequest()
}
}).withErrorListener {
Toast.makeText(applicationContext, "Some Error! ", Toast.LENGTH_SHORT).show()
}
.onSameThread()
.check()
}
2) 버튼 클릭 시 파일 탐색기 열기
//파일 첨부 버튼 : 파일 탐색기 열기
binding.aBtn.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
startActivityForResult(intent, 10)
}
binding.bBtn.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
startActivityForResult(intent, 20)
}
binding.cBtn.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
startActivityForResult(intent, 30)
}
- Intent를 통해 파일 탐색기를 열어주고
- 타입은 모든 형식의 파일을 선택할 것이기 때문에 "*/*"로 설정해준다.
(img파일만 선택하게 하고 싶다면 이 형식을 바꿔주면 된다.)
- 그리고 startActivityForResult를 통해 파일 탐색기를 불러온다.
(10, 20, 30은 각 요청 코드이다.)
2-1) 파일 탐색기를 열었을 때 활동
//파일 호출 content:// uri로 받아온 후 내부 저장소에 파일 복사, 복사한 파일 return, 첨부한 파일 이름 사용자에게 보여주기
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
//URI 정보를 저장하기 위해 선언
val sharedPreference = getSharedPreferences("user_auto", MODE_PRIVATE)
val editor = sharedPreference.edit()
if (resultCode != Activity.RESULT_OK) {
return
}
when (requestCode) {
//코드가 10일 경우, 즉 a파일 첨부 버튼을 눌렀을 경우
10 -> {
data?:return
val uri = data.data as Uri
val filename = getName(uri)
val imgPath = usiPathUtil.getRealPathFromURI(this, uri)
if (filename != "" && imgPath != null) {
val `in` = contentResolver.openInputStream(uri) //src
val file = File(applicationContext.filesDir, filename)
if (`in` != null) {
try {
val out: OutputStream = FileOutputStream(file) //dst
try {
// Transfer bytes from in to out
val buf = ByteArray(4096)
var len: Int
while (`in`.read(buf).also { len = it } > 0) {
out.write(buf, 0, len)
}
editor.putString("a", file.toString())
editor.apply()
} finally {
out.close()
}
} finally {
`in`.close()
}
}
//텍스트에 파일명을 띄어준다
binding.aTxt.text = filename
}
}
20 ->{
data?:return
val uri = data.data as Uri
val filename = getName(uri)
val imgPath = usiPathUtil.getRealPathFromURI(this, uri)
if (filename != "" && imgPath != null) {
val `in` = contentResolver.openInputStream(uri) //src
val file = File(applicationContext.filesDir, filename)
if (`in` != null) {
try {
val out: OutputStream = FileOutputStream(file) //dst
try {
// Transfer bytes from in to out
val buf = ByteArray(4096)
var len: Int
while (`in`.read(buf).also { len = it } > 0) {
out.write(buf, 0, len)
}
editor.putString("b", file.toString())
editor.apply()
} finally {
out.close()
}
} finally {
`in`.close()
}
}
//텍스트에 파일명을 띄어준다
binding.bTxt.text = filename
}
}
30 ->{
data?:return
val uri = data.data as Uri
val filename = getName(uri)
val imgPath = usiPathUtil.getRealPathFromURI(this, uri)
if (filename != "" && imgPath != null) {
val `in` = contentResolver.openInputStream(uri) //src
val file = File(applicationContext.filesDir, filename)
if (`in` != null) {
try {
val out: OutputStream = FileOutputStream(file) //dst
try {
// Transfer bytes from in to out
val buf = ByteArray(4096)
var len: Int
while (`in`.read(buf).also { len = it } > 0) {
out.write(buf, 0, len)
}
editor.putString("c", file.toString())
editor.apply()
} finally {
out.close()
}
} finally {
`in`.close()
}
}
//텍스트에 파일명을 띄어준다
binding.cTxt.text = filename
}
}
}
}
안드로이드 보안 정책 변경으로 인해 직접적으로 파일 탐색기에서 파일을 가져와 다른 앱이나 서버에 통신할 수 없도록 바뀌었다고 한다.
따라서 inputStream을 이용해 내부 저장소에 가져온 파일을 복사한 후 그 파일을 가져와 서버에 보내줄 수 있도록 해야한다.
3) 가져온 파일 URI에서 파일명만 추출하는 함수
//Uri to 파일명 추출 함수
private fun getName(uri: Uri?): String {
val projection = arrayOf(MediaStore.Files.FileColumns.DISPLAY_NAME)
val cursor = managedQuery(uri, projection, null, null, null)
val column_index = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
cursor.moveToFirst()
return cursor.getString(column_index)
}
4) 파일 URI 가져오는 클래스
class UriPathUtils
{
fun getRealPathFromURI(context: Context, uri: Uri): String? {
when {
// DocumentProvider
DocumentsContract.isDocumentUri(context, uri) -> {
when {
// ExternalStorageProvider
isExternalStorageDocument(uri) -> {
//Toast.makeText(context, "From Internal & External Storage dir", Toast.LENGTH_SHORT).show()
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
// This is for checking Main Memory
return if ("primary".equals(type, ignoreCase = true)) {
if (split.size > 1) {
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} else {
Environment.getExternalStorageDirectory().toString() + "/"
}
// This is for checking SD Card
} else {
"storage" + "/" + docId.replace(":", "/")
}
}
isDownloadsDocument(uri) -> {
//Toast.makeText(context, "From Downloads dir", Toast.LENGTH_SHORT).show()
val fileName = getFilePath(context, uri)
if (fileName != null) {
return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
}
var id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
id = id.replaceFirst("raw:".toRegex(), "")
val file = File(id)
if (file.exists()) return id
}
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
}
isMediaDocument(uri) -> {
//Toast.makeText(context, "From Documents dir", Toast.LENGTH_SHORT).show()
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
"document" -> {
val fileName = getFilePath(context, uri)
if (fileName != null) {
return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
}
var id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
id = id.replaceFirst("raw:".toRegex(), "")
val file = File(id)
if (file.exists()) return id
}
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs)
}
isGoogleDriveUri(uri) -> {
Toast.makeText(context, "From Google Drive", Toast.LENGTH_SHORT).show()
return getDriveFilePath(uri, context)
}
}
}
"content".equals(uri.scheme, ignoreCase = true) -> {
//Toast.makeText(context, "From Content Uri", Toast.LENGTH_SHORT).show()
// Return the remote address
return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null)
}
"file".equals(uri.scheme, ignoreCase = true) -> {
//Toast.makeText(context, "From File Uri", Toast.LENGTH_SHORT).show()
return uri.path
}
}
return null
}
private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
//여기서부터 갈림
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
//파일 확장자 함수수
fun getFileExtension(context : Context, uri: Uri): String? {
val fileType: String? = context.contentResolver.getType(uri)
return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType)
}
private fun getMediaDocumentPath(context: Context, uri: Uri?,selection: String?, selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
private fun getFilePath(context: Context, uri: Uri?): String? {
var cursor: Cursor? = null
val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, null, null,
null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
private fun getDriveFilePath(uri: Uri, context: Context): String? {
val returnCursor = context.contentResolver.query(uri, null, null, null, null)
val nameIndex = returnCursor!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE)
returnCursor.moveToFirst()
val name = returnCursor.getString(nameIndex)
val size = returnCursor.getLong(sizeIndex).toString()
val file = File(context.cacheDir, name)
try {
val inputStream = context.contentResolver.openInputStream(uri)
val outputStream = FileOutputStream(file)
var read = 0
val maxBufferSize = 1 * 1024 * 1024
val bytesAvailable = inputStream!!.available()
//int bufferSize = 1024;
val bufferSize = Math.min(bytesAvailable, maxBufferSize)
val buffers = ByteArray(bufferSize)
while (inputStream.read(buffers).also { read = it } != -1) {
outputStream.write(buffers, 0, read)
}
Log.e("File Size", "Size " + file.length())
inputStream.close()
outputStream.close()
Log.e("File Path", "Path " + file.path)
Log.e("File Size", "Size " + file.length())
} catch (e: Exception) {
Log.e("Exception", e.message!!)
}
return file.path
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGoogleDriveUri(uri: Uri): Boolean {
return "com.google.android.apps.docs.storage" == uri.authority || "com.google.android.apps.docs.storage.legacy" == uri.authority
}
}
5) 이렇게 가져온 URI 정보를 가지고 MultiPart형태로 변환시켜준다.
//파일 API서버에 첨부
val a_path = sharedPreference.getString("a", "")
val a_file = a_path?.let { it1 -> File(it1) }
val requestBody = a_file?.let { it1 -> RequestBody.create(MediaType.parse("multipart/form-data"), it1) }
var a_body : MultipartBody.Part?= null
//파일명이 없으면 null값 있으면 파일Body
if(a_name != "") {
a_body = requestBody?.let { it1 -> MultipartBody.Part.createFormData("f_a_license", a_file.name, it1) }
}else if(a_name == ""){
a_body = null
}
val b_path = sharedPreference.getString("b", "")
val b_file = b_path?.let { it1 -> File(it1) }
val requestFile1 = b_file?.let { it1 -> RequestBody.create(MediaType.parse("multipart/form-data"), it1) }
var b_body :MultipartBody.Part?= null
if(b_license_name != ""){
b_body = requestFile1?.let { it1 -> MultipartBody.Part.createFormData("f_b_license", b_file.name, it1) }
}else if(b_license_name == ""){ b_body = null }
val c_path = sharedPreference.getString("c", "")
val c_file = c_path?.let { it1 -> File(it1) }
val requestFile2 = c_file?.let { it1 -> RequestBody.create(MediaType.parse("multipart/form-data"), it1) }
var c_body : MultipartBody.Part? = null
if(c_license_name != ""){
c_body = requestFile1?.let { it1 -> MultipartBody.Part.createFormData("f_c_license", c_file.name, it1) }
}else if(c_license_name == ""){ b_body = null }
//editor저장소 비워주기
editor.clear()
editor.apply()
6) Retrofit을 사용해 파일을 전송해준다.
joinService.requestJoin(data, a_body ,b_body, c_body ).enqueue(object : Callback<JoinDTO> {
override fun onFailure(call: Call<JoinDTO>, t: Throwable) {
Log.d("retrofit", t.toString())
}
override fun onResponse(call: Call<JoinDTO>, response: Response<JoinDTO>) {
join = response.body()
}
})
data같은 경우에는 본인같은 경우 Json으로 묶어서 보내줬기 때문에 JsonObject 형태로 묶어서 보내줬다.
Map 형태로 보내줘도 무난하다.
7) 파일 삭제 버튼 클릭 시 sharedpreference에 저장했던 정보를 삭제하고 파일명도 삭제한다.
//파일 삭제 버튼 : 저장된 Uri정보 없애고, 사용자에게 빈 파일명 보여주기
binding.aDeleteBtn.setOnClickListener {
binding.aTxt.text = ""
editor.remove("a")
editor.apply()
}
binding.bDeleteBtn.setOnClickListener {
binding.bTxt.text = ""
editor.remove("b")
editor.apply()
}
binding.cDeleteBtn.setOnClickListener {
binding.cTxt.text = ""
editor.remove("c")
editor.apply()
}
요약하자면
1. Retrofit 통신을 위해 interface, DTO등을 만들어준다.
2. 파일 탐색기를 연다.
3. 파일을 선택하고 URI 정보를 얻어온다.
4. 외부저장소에서 직접 가져와 통신하는 것이 보안정책으로 인해 접근이 불가하기 때문에
내부저장소에 Inputstream, outputstream을 이용해 복사해준다.
5. 내부저장소에 복사한 파일을 가져와 multipart/form-data로 변환시켜준다.
6. Retrofit을 이용해 API서버로 파일을 전송한다.
정말 기초 정보만 알려준 거기 때문에 나머지 세부 코드들은 알아서 각자 맞게 사용하면 될 듯 하다.
'[Android APP] feat. Kotlin > Kotlin 공부' 카테고리의 다른 글
Android Kotlin : 안드로이드 Activity 생명 주기 (0) | 2022.10.22 |
---|---|
Android Kotlin : GlobalScope와 CoroutineScope (0) | 2022.10.22 |
Android Kotlin : MVVM, MVP, MVC 디자인 패턴이란 무엇일까? (0) | 2022.09.20 |
Android Kotlin : Coroutine과 Thread (0) | 2022.09.15 |
Android Koltin : 비밀번호, 이메일 등 정규식 (0) | 2022.09.12 |