Moments of growth

[kotlin] ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ์‚ฌ์ง„ ๊ฐ€์ ธ์™€์„œ multipart๋กœ ์„œ๋ฒ„ ํ†ต์‹ ํ•˜๊ธฐ ๋ณธ๋ฌธ

Android [Kotlin] ๐Ÿ’ป๐Ÿค

[kotlin] ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ์‚ฌ์ง„ ๊ฐ€์ ธ์™€์„œ multipart๋กœ ์„œ๋ฒ„ ํ†ต์‹ ํ•˜๊ธฐ

๋ฎค๋ง์ด 2022. 7. 12. 19:37

๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ์‚ฌ์ง„์„ ๊ฐ€์ ธ์™€์„œ ํ”„๋กœํ•„ ์‚ฌ์ง„์„ ๋‹ค์‹œ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด๊ธฐ!

๋ฉฐ์น ๋™์•ˆ ์ˆ˜๋งŽ์€ ๊ตฌ๊ธ€๋ง๊ณผ ๋””๋ฒ„๊น… ๋์— ์„ฑ๊ณตํ–ˆ๋‹ค.

 

 

Postman API ๋ช…์„ธ์„œ

ํ—ค๋”๋กœ accesstoken์„ ๋ฐ›๊ณ , Body ๊ฐ’์œผ๋กœ File์„ ๋„ฃ์–ด์ค˜์•ผํ•˜๋Š” API์ด๋‹ค.

 

 


MyPageService.kt

 

interface MyPageService {
    @Multipart
    @POST("user/upload")
    fun postProfile(
        @Header("accesstoken") token: String,
        @Part file: MultipartBody.Part
    ): Call<ResponseMyPageProfile>
}

@Multipart ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ผญ ๋ถ™์—ฌ์ค˜์•ผํ•œ๋‹ค. 

 

Multipart๋ž€?

Multipart๋Š” HTTP๋ฅผ ํ†ตํ•ด File์„ Server๋กœ ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” Content-type ์ด๋‹ค. HTTP ํ”„๋กœํ† ์ฝœ์€ ํฌ๊ฒŒ Header์™€ Body๋กœ ๊ตฌ๋ถ„์ด ๋˜๊ณ  ๋ฐ์ดํ„ฐ๋Š” Body์— ๋“ค์–ด๊ฐ€์„œ ์ „์†ก์ด ๋˜๋Š”๋ฐ, Body์— ๋“ค์–ด๊ฐ€๋Š” ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ๋ช…์‹œํ•ด์ฃผ๋Š”๊ฒŒ Content-type์ด๋‹ค. ์ด๋•Œ ํƒ€์ž…์œผ๋กœ ์ง€์ •ํ•ด์ฃผ๋Š” ํ˜•ํƒœ๋ฅผ MIME ํƒ€์ž…์œผ๋กœ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋Š”๋ฐ, Multipart(=multipart/form-data)๋Š” MIME ํƒ€์ž… ์ค‘ ํ•˜๋‚˜์ด๋‹ค.

Multipart๋Š” ๋ง ๊ทธ๋Œ€๋กœ ๋ฉ”์‹œ์ง€(=ํŒŒ์ผ)๋ฅผ ์—ฌ๋Ÿฌ ํŒŒํŠธ๋กœ ๋‚˜๋ˆ„์–ด์„œ ๋ฉ”์„ธ์ง€๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

 

 

viewModel์—์„œ ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ์œ„์˜ ์ฝ”๋“œ ์ค‘ fun์„ suspend fun์œผ๋กœ ๊ณ ์ณ์คฌ๋”๋‹ˆ

<Registering an InstanceCreator with Gson for this type may fix this problem>

์ด๋Ÿฐ์‹์˜ ์˜ค๋ฅ˜๊ฐ€ ๋–ด๊ณ  ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์œผ๋กœ suspend๋ฅผ ์ง€์šฐ๋ผ๋Š” ๋‹ต๋ณ€ ๋•Œ๋ฌธ์— ๋ฉ€ํ‹ฐํŒŒํŠธ๋Š” suspend ํ•จ์ˆ˜๋กœ ๋ชป ์‚ฌ์šฉํ•˜๋‚˜?๋ผ๋Š” ์ƒ๊ฐ์„ ํ–ˆ์ง€๋งŒ

suspend fun์„ ์“ฐ๋ ค๋ฉด ๋ฆฌํ„ด ํƒ€์ž…๋„ Call<T> ์—์„œ T๋กœ ๊ฐ™์ด ๋ฐ”๊ฟ”์ค˜์•ผํ–ˆ์—ˆ๋˜ ์‹ค์ˆ˜์˜€๋‹ค.

 

 

์ฆ‰ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

๊ธฐ๋ณธ์ ์œผ๋กœ Call<ResponseMyPageProfile>๋ฅผ ์‚ฌ์šฉํ–ˆ๋˜ ๊ฒƒ๊ณผ ๋‹ฌ๋ฆฌ ์•„๋ž˜์™€ ๊ฐ™์ด CoroutineScope ์—์„œ ์‹คํ–‰๋˜๋Š” suspend ํ•จ์ˆ˜๋กœ ์ง€์ •ํ•˜๋ฉด Retrofit Package๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋Š” return type์— Retrofit2์—์„œ ์ œ๊ณตํ•˜๋Š” Call<T>๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์‚ฌ์šฉํ•œ๋‹ค.

@Multipart
@POST("user/upload")
suspend fun postProfile(
    @Header("accesstoken") token: String,
    @Part file: MultipartBody.Part
): ResponseMyPageProfile

 

 


 

AndroidManifest.xml

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

์ €์žฅ์†Œ ๊ด€๋ จ ๊ถŒํ•œ์„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

 

 


MyPageViewModel.kt

fun requestMypageProfile(body: MultipartBody.Part) {
    RetrofitBuilder.mypageService.postProfile(
        SeeMeetSharedPreference.getToken(),
        body
    ).enqueue(object : Callback<ResponseMyPageProfile> {
        override fun onFailure(call: Call<ResponseMyPageProfile>, t: Throwable) {
            _profileStatus.value = 0
        }

        override fun onResponse(
            call: Call<ResponseMyPageProfile>,
            response: Response<ResponseMyPageProfile>
        ) {
            _profileStatus.value = 1
        }
    })
}

 

์ฒ˜์Œ์— fun postProfile()๋กœ ํ–ˆ์„ ๊ฒฝ์šฐ ์ด๋ ‡๊ฒŒ ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์„œ๋ฒ„ํ†ต์‹ ์„ ํ–ˆ๋‹ค.

 

viewModelScope๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค.

fun requestMypageProfile(body: MultipartBody.Part) = viewModelScope.launch(exceptionHandler) {
    RetrofitBuilder.mypageService.postProfile(
        SeeMeetSharedPreference.getToken(),
        body
    )
    _profileStatus.value = true
}

 

viewModelScope.launch(exceptionHandler)์—์„œ exceptionHandler๋Š” ์ฝ”๋ฃจํ‹ด ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํ•ธ๋“ค๋Ÿฌ๋กœ ๊ฐ๊ฐ์˜ ์—๋Ÿฌ ์ƒํ™ฉ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. 

abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {

    // ์˜ˆ์™ธ ๋‚œ ๊ฑฐ ์ €์žฅํ•˜๋Š” ๋ณ€์ˆ˜
    private val _fetchState = MutableLiveData<Pair<Throwable, FetchState>>()
    val fetchState : LiveData<Pair<Throwable, FetchState>>
        get() = _fetchState

    //์ฝ”๋ฃจํ‹ด ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํ•ธ๋“ค๋Ÿฌ
    protected val exceptionHandler = CoroutineExceptionHandler{ _, throwable ->
        throwable.printStackTrace()
        //๊ฐ๊ฐ์˜ ์—๋Ÿฌ ์ƒํ™ฉ์—์„œ ๊ฐ’ ์„ค์ •. 400~500๋Œ€ ์—๋Ÿฌ๋Š” HttpException์œผ๋กœ ์ฒ˜๋ฆฌ ๋  ๊ฒƒ.
        when(throwable){
            is UnknownHostException -> _fetchState.postValue(Pair(throwable, FetchState.BAD_INTERNET))
            is HttpException -> _fetchState.postValue(Pair(throwable, FetchState.PARSE_ERROR))
            is SocketException -> _fetchState.postValue(Pair(throwable, FetchState.WRONG_CONNECTION))
            else -> _fetchState.postValue(Pair(throwable, FetchState.FAIL))
        }
    }

    enum class FetchState {
        BAD_INTERNET, PARSE_ERROR, WRONG_CONNECTION, FAIL
    }

}

 

 


MyPageActivity.kt

viewModel.profileStatus.observe(this) {
    if (it) {
        CustomToast.createToast(this, "ํ”„๋กœํ•„ ์‚ฌ์ง„์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค")!!.show()
        SeeMeetSharedPreference.setUserProfile(currentImageUrl)
        binding.btnProfileEditOrSave.text = "ํ”„๋กœํ•„ ์‚ฌ์ง„ ํŽธ์ง‘"
        binding.btnSelectImage.visibility = View.INVISIBLE
        binding.camera.visibility = View.INVISIBLE
        profile_position = DEFAULT
    }
}

profileStatus๋กœ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ๋Š”์ง€ ์„ฑ๊ณตํ–ˆ๋Š”์ง€๋ฅผ ๋ฐ›์•„์˜จ๋‹ค.

 

//ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋ณ€๊ฒฝํ•˜๋Š” ํ•จ์ˆ˜
private fun changeProfile() {
    // ์‚ฌ์ง„์ด ๋˜‘๊ฐ™์€ ๊ฒฝ์šฐ ์„œ๋ฒ„ ํ†ต์‹  ์•ˆํ•จ
    if (SeeMeetSharedPreference.getUserProfile() == currentImageUrl) {
        binding.btnProfileEditOrSave.text = "ํ”„๋กœํ•„ ์‚ฌ์ง„ ํŽธ์ง‘"
        binding.btnSelectImage.visibility = View.INVISIBLE
        binding.camera.visibility = View.INVISIBLE
        profile_position = DEFAULT
    }
    // ์‚ฌ์ง„์ด ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ ์„œ๋ฒ„ ํ†ต์‹ 
    else {
        val url = currentImageUrl?.toUri()

        // File ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.
        val file = File(getPath(this, url!!))
        
        // File ๊ฐ์ฒด๋ฅผ RequestBody ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
        val requestFile =
            RequestBody.create("multipart/form-data".toMediaTypeOrNull(), file)
            
        // MultipartBody.Part๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค.
        val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
        
        // body๋Š” MultipartBody.Part, ์„œ๋ฒ„ํ†ต์‹ 
        viewModel.requestMypageProfile(body = body)
    }
}

// ์ ˆ๋Œ€ ๊ฒฝ๋กœ ๋ฐ›์•„์˜ค๊ธฐ
fun getPath(context: Context, uri: Uri): String? {
    val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT

    // DocumentProvider
    if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
        // ExternalStorageProvider
        if (isExternalStorageDocument(uri)) {
            val docId = DocumentsContract.getDocumentId(uri)
            val split = docId.split(":").toTypedArray()
            val type = split[0]
            if ("primary".equals(type, ignoreCase = true)) {
                return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
            }
        } else if (isDownloadsDocument(uri)) {
            val id = DocumentsContract.getDocumentId(uri)
            val contentUri = ContentUris.withAppendedId(
                Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)
            )
            return getDataColumn(context, contentUri, null, null)
        } else if (isMediaDocument(uri)) {
            val docId = DocumentsContract.getDocumentId(uri)
            val split = docId.split(":").toTypedArray()
            val type = split[0]
            var contentUri: Uri? = null
            if ("image" == type) {
                contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            } else if ("video" == type) {
                contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
            } else if ("audio" == type) {
                contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
            }
            val selection = "_id=?"
            val selectionArgs = arrayOf(
                split[1]
            )
            return getDataColumn(context, contentUri, selection, selectionArgs)
        }
    } else if ("content".equals(uri.scheme, ignoreCase = true)) {

        // Return the remote address
        return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
            context,
            uri,
            null,
            null
        )
    } else if ("file".equals(uri.scheme, ignoreCase = true)) {
        return uri.path
    }
    return null
}

/**
 * Get the value of the data column for this Uri. This is useful for
 * MediaStore Uris, and other file-based ContentProviders.
 *
 * @param context The context.
 * @param uri The Uri to query.
 * @param selection (Optional) Filter used in the query.
 * @param selectionArgs (Optional) Selection arguments used in the query.
 * @return The value of the _data column, which is typically a file path.
 */
fun getDataColumn(
    context: Context, uri: Uri?, selection: String?,
    selectionArgs: Array<String>?
): String? {
    var cursor: Cursor? = null
    val column = "_data"
    val projection = arrayOf(
        column
    )
    try {
        cursor = context.contentResolver.query(
            uri!!, projection, selection, selectionArgs,
            null
        )
        if (cursor != null && cursor.moveToFirst()) {
            val index: Int = cursor.getColumnIndexOrThrow(column)
            return cursor.getString(index)
        }
    } finally {
        if (cursor != null) cursor.close()
    }
    return null
}

/**
 * @param uri The Uri to check.
 * @return Whether the Uri authority is ExternalStorageProvider.
 */
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.
 */
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.
 */
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.
 */
fun isGooglePhotosUri(uri: Uri): Boolean {
    return "com.google.android.apps.photos.content" == uri.authority
}

// ๊ฐค๋Ÿฌ๋ฆฌ ๋“ค์–ด๊ฐ€๋Š” ํ•จ์ˆ˜
private val OPEN_GALLERY = 1
private fun openGallery() {
    /* ๊ถŒํ•œ ๋ฐ›๊ธฐ */
    if (ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.READ_EXTERNAL_STORAGE
        ) == PackageManager.PERMISSION_GRANTED
    ) {
        val intent = Intent(Intent.ACTION_GET_CONTENT)
        intent.setData(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
        intent.setType("image/*")
        startActivityForResult(intent, OPEN_GALLERY)
    } else {
        val permissions: Array<String> = arrayOf(
            Manifest.permission.READ_EXTERNAL_STORAGE
        )

        ActivityCompat.requestPermissions(this, permissions, 0)
    }
}

// ๊ถŒํ•œ ๊ด€๋ จ ํ•จ์ˆ˜
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)

    when (requestCode) {
        0 -> {
            if (grantResults.isNotEmpty()) {
                var isAllGranted = true
                // ์š”์ฒญํ•œ ๊ถŒํ•œ ํ—ˆ์šฉ/๊ฑฐ๋ถ€ ์ƒํƒœ ํ•œ๋ฒˆ์— ์ฒดํฌ
                for (grant in grantResults) {
                    if (grant != PackageManager.PERMISSION_GRANTED) {
                        isAllGranted = false
                        break;
                    }
                }
                // ์š”์ฒญํ•œ ๊ถŒํ•œ์„ ๋ชจ๋‘ ํ—ˆ์šฉํ–ˆ์Œ.
                if (isAllGranted) {
                    // ๋‹ค์Œ step์œผ๋กœ ~
                    val intent = Intent(Intent.ACTION_GET_CONTENT)
                    intent.setType("image/*")
                    startActivityForResult(intent, OPEN_GALLERY)
                }
                // ํ—ˆ์šฉํ•˜์ง€ ์•Š์€ ๊ถŒํ•œ์ด ์žˆ์Œ. ํ•„์ˆ˜๊ถŒํ•œ/์„ ํƒ๊ถŒํ•œ ์—ฌ๋ถ€์— ๋”ฐ๋ผ์„œ ๋ณ„๋„ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ•จ.
                else {
                    if (!ActivityCompat.shouldShowRequestPermissionRationale(
                            this,
                            Manifest.permission.READ_EXTERNAL_STORAGE
                        )
                    ) {
                        // ๋‹ค์‹œ ๋ฌป์ง€ ์•Š๊ธฐ ์ฒดํฌํ•˜๋ฉด์„œ ๊ถŒํ•œ ๊ฑฐ๋ถ€ ๋˜์—ˆ์Œ.
                        CustomToast.createToast(this@MyPageActivity, "์Šคํ† ๋ฆฌ์ง€์— ์ ‘๊ทผ ๊ถŒํ•œ์„ ํ—ˆ๊ฐ€ํ•ด์ฃผ์„ธ์š”")
                            ?.show()
                    } else {
                        // ์ ‘๊ทผ ๊ถŒํ•œ ๊ฑฐ๋ถ€ํ•˜์˜€์Œ.
                        CustomToast.createToast(this@MyPageActivity, "์Šคํ† ๋ฆฌ์ง€์— ์ ‘๊ทผ ๊ถŒํ•œ์„ ํ—ˆ๊ฐ€ํ•ด์ฃผ์„ธ์š”")
                            ?.show()
                    }
                }
            }
        }
    }
}

@Override
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode == Activity.RESULT_OK) {
        if (requestCode == OPEN_GALLERY) {
            // ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜จ ๊ฒฝ์šฐ
            currentImageUrl = data?.data.toString()
            try {
                Glide.with(this).load(data?.data).circleCrop()
                    .into(binding.ivMypageProfile)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
}

 

ViewModel์—์„œ ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” androidx-lifecycle์—์„œ ์ œ๊ณตํ•˜๋Š” viewModelScope๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค.

viewModelScope๋Š” ViewModel์˜ extension property๋กœ ViewModel์ด destroy ๋  ๋•Œ ์ž์‹ ์ฝ”๋ฃจํ‹ด๋“ค์„ ์ž๋™์œผ๋กœ ์ทจ์†Œํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

 

Lifecycle-aware components define the following built-in scopes that you can use in your app.

A ViewModelScope is defined for each ViewModel in your app. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared. Coroutines are useful here for when you have work that needs to be done only if the ViewModel is active. For example, if you are computing some data for a layout, you should scope the work to the ViewModel so that if the ViewModel is cleared, the work is canceled automatically to avoid consuming resources.

You can access the CoroutineScope of a ViewModel through the viewModelScope property of the ViewModel, as shown in the following example:

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

 

 

 


Reference)

 

Retrofit ์œผ๋กœ ํŒŒ์ผ ์—…๋กœ๋“œ ํ•˜๊ธฐ | Jungwoon Blog

Retrofit์„ ์ด์šฉํ•ด์„œ ํŒŒ์ผ ์—…๋กœ๋“œ ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด๋ก  Retrofit์„ ํ†ตํ•ด์„œ ํŒŒ์ผ์„ ์—…๋กœ๋“œ ํ• ๋•Œ Multipart๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ Multipart์— ๋Œ€ํ•ด์„œ ๊ฐ„๋‹จํžˆ ์•Œ์•„๋ณด๊ณ  ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด

jungwoon.github.io

 

[์•ˆ๋“œ๋กœ์ด๋“œ] ์„œ๋ฒ„์— form data ๋กœ ๋ฐ์ดํ„ฐ ์ „์†กํ•˜๊ธฐ.(MultiPart)

ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ pdf, image ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ form-data๋กœ ๋„˜๊ฒจ์ค˜์•ผ ํ•˜๋Š” ์ƒํ™ฉ์— ๋งž๋‹ฅ๋œจ๋ ธ๋‹ค.. ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ฒจ์ฃผ๋Š” ๊ฒƒ์€ ํ•ด๋ณด์ง€ ์•Š์•„์„œ ์•ฝ๊ฐ„ ๊ฒ๋จน์—ˆ์—ˆ๋Š”๋ฐ, ํ•ด๋ณด๊ณ  ๋‚˜๋‹ˆ ๋ชจ๋“  ํƒ€์ž…์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ „

gaybee.tistory.com

 

[Android] viewModelScope.launch() ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฐ”๊ฟ”๋ณด๊ธฐ

ViewModel์—์„œ ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” androidx-lifecycle์—์„œ ์ œ๊ณตํ•˜๋Š” viewModelScope๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. viewModelScope๋Š” ViewModel์˜ extension property๋กœ ViewModel์ด destroy ๋  ๋•Œ ์ž์‹ ์ฝ”๋ฃจํ‹ด๋“ค์„ ์ž๋™..

leveloper.tistory.com

 

์ˆ˜๋ช… ์ฃผ๊ธฐ ์ธ์‹ ๊ตฌ์„ฑ์š”์†Œ์™€ ํ•จ๊ป˜ Kotlin ์ฝ”๋ฃจํ‹ด ์‚ฌ์šฉ  |  Android ๊ฐœ๋ฐœ์ž  |  Android Developers

์ˆ˜๋ช… ์ฃผ๊ธฐ ์ธ์‹ ๊ตฌ์„ฑ์š”์†Œ์™€ ํ•จ๊ป˜ Kotlin ์ฝ”๋ฃจํ‹ด ์‚ฌ์šฉ Kotlin ์ฝ”๋ฃจํ‹ด์€ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Kotlin ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋ฃจํ‹ด์ด ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ์‹œ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ

developer.android.com

 

์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ (30) viewModelScope

Android ๋Š” ํ˜„์žฌ ์ง‘์ค‘์ ์œผ๋กœ Coroutine ์„ ๋ฐ€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Android์—์„œ Coroutine์„ ์‘์šฉํ•œ api์™€ Coroutine ๊ด€๋ จ๋œ ์ฝ”๋“œ ์Šค๋‹ˆํŽซ ๋“ฑ๋“ฑ์ด ๋“ฑ์žฅํ•˜๋ฉด์„œ ์•ž์œผ๋กœ Android์—์„œ Coroutine์„ ํ™œ์šฉํ• ์ผ์ด ๋งŽ์•„์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

gift123.tistory.com

 

ViewModel์—์„œ Coroutine์„ ์ด์šฉํ•˜๋Š” ๋ฐฉ๋ฒ• #ViewModel Scope

Coroutine์„ ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ํ–ˆ์„ ๋•Œ, ViewModel์—์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋งค์šฐ ํšจ์œจ์ ์ธ๋ฐ์š”. ์˜ค๋Š˜์€ Coroutine์„ ViewModel์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•ด ๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. Coroutine์— ๊ด€ํ•ด ๊ธฐ๋ณธ

developer88.tistory.com

Comments