그중 개인적으로 많이 사용하는 Coil을 까볼 것이다.
Coil에서도 가장 기본적인 AsyncImage를 분석해보려 한다.
AsyncImage는 다음과 같이 작성되어 있다.
@Composable
@NonRestartableComposable
fun AsyncImage(
model: Any?,
contentDescription: String?,
imageLoader: ImageLoader,
modifier: Modifier = Modifier,
placeholder: Painter? = null,
error: Painter? = null,
fallback: Painter? = error,
onLoading: ((State.Loading) -> Unit)? = null,
onSuccess: ((State.Success) -> Unit)? = null,
onError: ((State.Error) -> Unit)? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DefaultFilterQuality,
clipToBounds: Boolean = true,
) = AsyncImage(
state = AsyncImageState(model, imageLoader),
contentDescription = contentDescription,
modifier = modifier,
transform = transformOf(placeholder, error, fallback),
onState = onStateOf(onLoading, onSuccess, onError),
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality,
clipToBounds = clipToBounds,
)
으로 되어있고 이걸 한번 더 들어가면
@Composable
private fun AsyncImage(
state: AsyncImageState,
contentDescription: String?,
modifier: Modifier,
transform: (State) -> State,
onState: ((State) -> Unit)?,
alignment: Alignment,
contentScale: ContentScale,
alpha: Float,
colorFilter: ColorFilter?,
filterQuality: FilterQuality,
clipToBounds: Boolean,
) {
val request = requestOfWithSizeResolver(
model = state.model,
contentScale = contentScale,
)
validateRequest(request)
Layout(
modifier = modifier.then(
ContentPainterElement(
request = request,
imageLoader = state.imageLoader,
modelEqualityDelegate = state.modelEqualityDelegate,
transform = transform,
onState = onState,
contentScale = contentScale,
filterQuality = filterQuality,
alignment = alignment,
alpha = alpha,
colorFilter = colorFilter,
clipToBounds = clipToBounds,
previewHandler = previewHandler(),
contentDescription = contentDescription,
),
),
measurePolicy = UseMinConstraintsMeasurePolicy,
)
}
위와 같이 되어있다.
그중 url주소를 넣는 model은 requestOfWithSizeResolver의 파라미터로 사용된다.
그럼 이 requestOfWithSizeResolver를 살펴보자
/** Create an [ImageRequest] with a not-null [SizeResolver] from the [model]. */
@Composable
@NonRestartableComposable
internal fun requestOfWithSizeResolver(
model: Any?,
contentScale: ContentScale,
): ImageRequest {
if (model is ImageRequest) {
if (model.defined.sizeResolver != null) {
return model
} else {
val sizeResolver = rememberSizeResolver(contentScale)
return remember(model, sizeResolver) {
model.newBuilder()
.size(sizeResolver)
.build()
}
}
} else {
val context = LocalPlatformContext.current
val sizeResolver = rememberSizeResolver(contentScale)
return remember(context, model, sizeResolver) {
ImageRequest.Builder(context)
.data(model)
.size(sizeResolver)
.build()
}
}
}
주석은 이 함수를 다음과 같이 설명한다.
Create an ImageRequest with a not-null SizeResolver from the model.
-> model에서 null이 아닌 SizeResolver를 사용하여 ImageRequest를 생성한다.
코드를 살펴보면
model의 타입이 ImageRequest일때 sizeResolver가 null이 아니면 바로 model을 null이면 contentScale을 파라미터로 받는 sizeResolver를 만들어 다시 ImageRequest를 빌드한다.
model의 타입이 ImageRequest이 아니라면 data를 model로 갖는 새로운 ImageRequest를 생성한다.
rememberSizeResolver는 내부적으로 아래와 같이 생겼는데 contentScale이 None인지 아닌지 여부를 판별하여 SizeResolver를 생성하는 함수이다.
@Composable
private fun rememberSizeResolver(contentScale: ContentScale): SizeResolver {
val isNone = contentScale == ContentScale.None
return remember(isNone) {
if (isNone) {
SizeResolver.ORIGINAL
} else {
ConstraintsSizeResolver()
}
}
}
이렇게 생성된 ImgaeRequest를 validateRequest(request)로 유효성 검사를 해준다.
internal fun validateRequest(request: ImageRequest) {
when (request.data) {
is ImageRequest.Builder -> unsupportedData(
name = "ImageRequest.Builder",
description = "Did you forget to call ImageRequest.Builder.build()?",
)
is ImageBitmap -> unsupportedData("ImageBitmap")
is ImageVector -> unsupportedData("ImageVector")
is Painter -> unsupportedData("Painter")
}
validateRequestProperties(request)
}
private fun unsupportedData(
name: String,
description: String = "If you wish to display this $name, use androidx.compose.foundation.Image.",
): Nothing = throw IllegalArgumentException("Unsupported type: $name. $description")
/** Validate platform-specific properties of an [ImageRequest]. */
internal expect fun validateRequestProperties(request: ImageRequest)
validateRequestProperties는 android기준 다음과 같이 구현되어 있다.
internal actual fun validateRequestProperties(request: ImageRequest) {
require(request.target == null) { "request.target must be null." }
require(request.lifecycle == null) { "request.lifecycle must be null." }
}
이렇게 유효성 검증까지 끝난 ImageRequest를 사용해서 ContentPainterElement라는 Coil의 커스텀 Modifier로 이미지를 그리게 된다.
/**
* A custom [paint] modifier used by [AsyncImage].
*/
internal data class ContentPainterElement(
private val request: ImageRequest,
private val imageLoader: ImageLoader,
private val modelEqualityDelegate: AsyncImageModelEqualityDelegate,
private val transform: (State) -> State,
private val onState: ((State) -> Unit)?,
private val filterQuality: FilterQuality,
private val alignment: Alignment,
private val contentScale: ContentScale,
private val alpha: Float,
private val colorFilter: ColorFilter?,
private val clipToBounds: Boolean,
private val previewHandler: AsyncImagePreviewHandler?,
private val contentDescription: String?,
) : ModifierNodeElement<ContentPainterNode>() {
override fun create(): ContentPainterNode {
val input = Input(imageLoader, request, modelEqualityDelegate)
// Create the painter during modifier creation so we reuse the same painter object when the
// modifier is being reused as part of the lazy layouts reuse flow.
val painter = AsyncImagePainter(input)
painter.transform = transform
painter.onState = onState
painter.contentScale = contentScale
painter.filterQuality = filterQuality
painter.previewHandler = previewHandler
painter._input = input
return ContentPainterNode(
painter = painter,
constraintSizeResolver = request.sizeResolver as? ConstraintsSizeResolver,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
clipToBounds = clipToBounds,
contentDescription = contentDescription,
)
}
override fun update(node: ContentPainterNode) {
val previousIntrinsics = node.painter.intrinsicSize
val previousConstraintSizeResolver = node.constraintSizeResolver
val input = Input(imageLoader, request, modelEqualityDelegate)
val painter = node.painter
painter.transform = transform
painter.onState = onState
painter.contentScale = contentScale
painter.filterQuality = filterQuality
painter.previewHandler = previewHandler
painter._input = input
val intrinsicsChanged = previousIntrinsics != painter.intrinsicSize
node.alignment = alignment
node.constraintSizeResolver = request.sizeResolver as? ConstraintsSizeResolver
node.contentScale = contentScale
node.alpha = alpha
node.colorFilter = colorFilter
node.clipToBounds = clipToBounds
if (node.contentDescription != contentDescription) {
node.contentDescription = contentDescription
node.invalidateSemantics()
}
val constraintSizeResolverChanged =
previousConstraintSizeResolver != node.constraintSizeResolver
// Only remeasure if intrinsics have changed.
if (intrinsicsChanged || constraintSizeResolverChanged) {
node.invalidateMeasurement()
}
// Redraw because one of the node properties has changed.
node.invalidateDraw()
}
override fun InspectorInfo.inspectableProperties() {
name = "content"
properties["request"] = request
properties["imageLoader"] = imageLoader
properties["modelEqualityDelegate"] = modelEqualityDelegate
properties["transform"] = transform
properties["onState"] = onState
properties["filterQuality"] = filterQuality
properties["alignment"] = alignment
properties["contentScale"] = contentScale
properties["alpha"] = alpha
properties["colorFilter"] = colorFilter
properties["clipToBounds"] = clipToBounds
properties["previewHandler"] = previewHandler
properties["contentDescription"] = contentDescription
}
}
여기서 Compose의 UI 트리를 조금 알아보자
Compose는 다음과 같은 단계로 UI를 그린다.

Composition 단계에서 Compose 런타임은 컴포저블 함수를 실행하고 UI를 나타내는 트리 구조를 구성하는데 이때의 노드가 Layout 단계에서 사용되는 정보를 담고 있는 Layout 노드이다.
androidx.compose.ui.node.NodeChain.kt 를 살펴보면 아래와 같은 코드가 있다.
...
else if (layoutNode.applyingModifierOnAttach && beforeSize == 0) {
// common case where we are initializing the chain and the previous size is zero. In
// this case we just do all inserts. Since this is so common, we add a fast path here
// for this condition. Since the layout node is currently attaching, the inserted nodes
// will not get eagerly attached, which means we can avoid dealing with the coordinator
// sync until the end, which keeps this code path much simpler.
coordinatorSyncNeeded = true
var node = paddedHead
while (i < after.size) {
val next = after[i]
val parent = node
node = createAndInsertNodeAsChild(next, parent)
logger?.nodeInserted(0, i, next, parent, node)
i++
}
syncAggregateChildKindSet()
...
모든 Modifier는 이 NodeChain에 등록되는데 이 과정에서 Layout 노드가 처음 트리에 attach 될때 createAndInsertNodeAsChild() 라는 함수를 호출한다.
이 함수는 다음과 같다.
private fun createAndInsertNodeAsChild(
element: Modifier.Element,
parent: Modifier.Node,
): Modifier.Node {
val node =
when (element) {
is ModifierNodeElement<*> ->
element.create().also {
it.kindSet = calculateNodeKindSetFromIncludingDelegates(it)
}
else -> BackwardsCompatNode(element)
}
checkPrecondition(!node.isAttached) {
"A ModifierNodeElement cannot return an already attached node from create() "
}
node.insertedNodeAwaitingAttachForInvalidation = true
return insertChild(node, parent)
}
여기서 바로 ModifierNodeElement의 create()함수를 호출한다.
이제 다시 ContentPainterElement로 돌아오면 create 함수가 다음과 같이 되어있고 여기서 painter가 실제 이미지를 그리는 객체이다.
override fun create(): ContentPainterNode {
val input = Input(imageLoader, request, modelEqualityDelegate)
// Create the painter during modifier creation so we reuse the same painter object when the
// modifier is being reused as part of the lazy layouts reuse flow.
val painter = AsyncImagePainter(input)
painter.transform = transform
painter.onState = onState
painter.contentScale = contentScale
painter.filterQuality = filterQuality
painter.previewHandler = previewHandler
painter._input = input
return ContentPainterNode(
painter = painter,
constraintSizeResolver = request.sizeResolver as? ConstraintsSizeResolver,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
clipToBounds = clipToBounds,
contentDescription = contentDescription,
)
}
그럼 이 AsyncImagePainter가 이미지를 어떻게 그리는지 살펴보자
/**
* A [Painter] that that executes an [ImageRequest] asynchronously and renders the [ImageResult].
*/
@Stable
class AsyncImagePainter internal constructor(
input: Input,
) : Painter(), RememberObserver {
private var painter: Painter? by mutableStateOf(null)
private var alpha: Float = DefaultAlpha
private var colorFilter: ColorFilter? = null
private var isRemembered = false
private var rememberJob: Job? = null
set(value) {
field?.cancel()
field = value
}
private var drawSizeFlow: MutableSharedFlow<Size>? = null
private var drawSize = Size.Unspecified
set(value) {
if (field != value) {
field = value
drawSizeFlow?.tryEmit(value)
}
}
internal lateinit var scope: CoroutineScope
internal var transform = DefaultTransform
internal var onState: ((State) -> Unit)? = null
internal var contentScale = ContentScale.Fit
internal var filterQuality = DefaultFilterQuality
internal var previewHandler: AsyncImagePreviewHandler? = null
internal var _input: Input? = input
set(value) {
if (field != value) {
field = value
restart()
if (value != null) {
inputFlow.value = value
}
}
}
private val inputFlow: MutableStateFlow<Input> = MutableStateFlow(input)
val input: StateFlow<Input> = inputFlow.asStateFlow()
private val stateFlow: MutableStateFlow<State> = MutableStateFlow(State.Empty)
val state: StateFlow<State> = stateFlow.asStateFlow()
override val intrinsicSize: Size
get() = painter?.intrinsicSize ?: Size.Unspecified
override fun DrawScope.onDraw() {
drawSize = size
painter?.apply { draw(size, alpha, colorFilter) }
}
override fun applyAlpha(alpha: Float): Boolean {
this.alpha = alpha
return true
}
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
this.colorFilter = colorFilter
return true
}
override fun onRemembered() = trace("AsyncImagePainter.onRemembered") {
(painter as? RememberObserver)?.onRemembered()
launchJob()
isRemembered = true
}
private fun launchJob() {
val input = _input ?: return
rememberJob = scope.launchWithDeferredDispatch {
val previewHandler = previewHandler
val state = if (previewHandler != null) {
// If we're in inspection mode use the preview renderer.
val request = updateRequest(input.request, isPreview = true)
previewHandler.handle(input.imageLoader, request)
} else {
// Else, execute the request as normal.
val request = updateRequest(input.request, isPreview = false)
input.imageLoader.execute(request).toState()
}
updateState(state)
}
}
override fun onForgotten() {
rememberJob = null
(painter as? RememberObserver)?.onForgotten()
isRemembered = false
}
override fun onAbandoned() {
rememberJob = null
(painter as? RememberObserver)?.onAbandoned()
isRemembered = false
}
/**
* Launch a new image request with the current [Input]s.
*/
fun restart() {
if (_input == null) {
rememberJob = null
} else if (isRemembered) {
launchJob()
}
}
/**
* Update the [request] to work with [AsyncImagePainter].
*/
private fun updateRequest(request: ImageRequest, isPreview: Boolean): ImageRequest {
// Connect the size resolver to the draw scope if necessary.
val sizeResolver = request.sizeResolver
if (sizeResolver is DrawScopeSizeResolver) {
sizeResolver.connect(lazyDrawSizeFlow())
}
return request.newBuilder()
.target(
onStart = { placeholder ->
val painter = placeholder?.asPainter(request.context, filterQuality)
updateState(State.Loading(painter))
},
)
.apply {
if (request.defined.sizeResolver == null) {
// If the size resolver isn't set, use the original size.
size(SizeResolver.ORIGINAL)
}
if (request.defined.scale == null) {
// If the scale isn't set, use the content scale.
scale(contentScale.toScale())
}
if (request.defined.precision == null) {
// AsyncImagePainter scales the image to fit the canvas size at draw time.
precision(Precision.INEXACT)
}
if (isPreview) {
// The request must be executed synchronously in the preview environment.
coroutineContext(EmptyCoroutineContext)
}
}
.build()
}
private fun updateState(state: State) {
val previous = stateFlow.value
val current = transform(state)
stateFlow.value = current
painter = maybeNewCrossfadePainter(previous, current, contentScale) ?: current.painter
// Manually forget and remember the old/new painters.
if (previous.painter !== current.painter) {
(previous.painter as? RememberObserver)?.onForgotten()
(current.painter as? RememberObserver)?.onRemembered()
}
// Notify the state listener.
onState?.invoke(current)
}
private fun ImageResult.toState() = when (this) {
is SuccessResult -> State.Success(
painter = image.asPainter(request.context, filterQuality),
result = this,
)
is ErrorResult -> State.Error(
painter = image?.asPainter(request.context, filterQuality),
result = this,
)
}
private fun lazyDrawSizeFlow(): Flow<Size> {
var drawSizeFlow = drawSizeFlow
if (drawSizeFlow == null) {
drawSizeFlow = MutableSharedFlow(
replay = 1,
onBufferOverflow = DROP_OLDEST,
)
val drawSize = drawSize
if (drawSize.isSpecified) {
drawSizeFlow.tryEmit(drawSize)
}
this.drawSizeFlow = drawSizeFlow
}
return drawSizeFlow
}
/**
* The latest arguments passed to [AsyncImagePainter].
*/
@Poko
class Input(
val imageLoader: ImageLoader,
val request: ImageRequest,
val modelEqualityDelegate: AsyncImageModelEqualityDelegate,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
return other is Input &&
imageLoader == other.imageLoader &&
modelEqualityDelegate == other.modelEqualityDelegate &&
modelEqualityDelegate.equals(request, other.request)
}
override fun hashCode(): Int {
var result = imageLoader.hashCode()
result = 31 * result + modelEqualityDelegate.hashCode()
result = 31 * result + modelEqualityDelegate.hashCode(request)
return result
}
}
/**
* The current state of the [AsyncImagePainter].
*/
sealed interface State {
/** The current painter being drawn by [AsyncImagePainter]. */
val painter: Painter?
/** The request has not been started. */
data object Empty : State {
override val painter: Painter? get() = null
}
/** The request is in-progress. */
data class Loading(
override val painter: Painter?,
) : State
/** The request was successful. */
data class Success(
override val painter: Painter,
val result: SuccessResult,
) : State
/** The request failed due to [ErrorResult.throwable]. */
data class Error(
override val painter: Painter?,
val result: ErrorResult,
) : State
}
companion object {
/**
* A state transform that does not modify the state.
*/
val DefaultTransform: (State) -> State = { it }
}
}
이 글에서 알아볼 것은 어떻게 네트워크에서 가져오고 그리는가이다.
다른 부분은 배제하고 launchedJob() 함수를 살펴보자
private fun launchJob() {
val input = _input ?: return
rememberJob = scope.launchWithDeferredDispatch {
val previewHandler = previewHandler
val state = if (previewHandler != null) {
// If we're in inspection mode use the preview renderer.
val request = updateRequest(input.request, isPreview = true)
previewHandler.handle(input.imageLoader, request)
} else {
// Else, execute the request as normal.
val request = updateRequest(input.request, isPreview = false)
input.imageLoader.execute(request).toState()
}
updateState(state)
}
또한 preview가 아닌 실제에서 어떻게 가져오는가가 목적이기에 바로 imageLoader의 excute 함수를 확인하자.
ImageLoader의 실제 구현체는 RealIamgeLoader이다.
RealImageLoader의 코드는 다음과 같은데 전체 코드중 살펴볼 코드만 일부 작성한 것이다.
internal class RealImageLoader(
val options: Options,
) : ImageLoader {
...
override val components = options.componentRegistry.newBuilder()
.addServiceLoaderComponents(options)
.addAndroidComponents(options)
.addJvmComponents(options)
.addAppleComponents(options)
.addCommonComponents()
.add(EngineInterceptor(this, systemCallbacks, requestService, options.logger))
.build()
...
override suspend fun execute(request: ImageRequest): ImageResult {
if (!needsExecuteOnMainDispatcher(request)) {
// Fast path: skip dispatching.
return execute(request, REQUEST_TYPE_EXECUTE)
} else {
// Slow path: dispatch to the main thread.
return coroutineScope {
// Start executing the request on the main thread.
val job = async(options.mainCoroutineContextLazy.value) {
execute(request, REQUEST_TYPE_EXECUTE)
}
// Update the current request attached to the view and await the result.
getDisposable(request, job).job.await()
}
}
}
private suspend fun execute(initialRequest: ImageRequest, type: Int): ImageResult {
// Wrap the request to manage its lifecycle.
val requestDelegate = requestService.requestDelegate(
request = initialRequest,
job = coroutineContext.job,
findLifecycle = type == REQUEST_TYPE_ENQUEUE,
).apply { assertActive() }
// Apply this image loader's defaults and other configuration to this request.
val request = requestService.updateRequest(initialRequest)
// Create a new event listener.
val eventListener = options.eventListenerFactory.create(request)
try {
// Fail before starting if data is null.
if (request.data == NullRequestData) {
throw NullRequestDataException()
}
// Set up the request's lifecycle observers.
requestDelegate.start()
// Enqueued requests suspend until the lifecycle is started.
if (type == REQUEST_TYPE_ENQUEUE) {
requestDelegate.awaitStarted()
}
// Set the placeholder on the target.
val cachedPlaceholder = request.placeholderMemoryCacheKey?.let { memoryCache?.get(it)?.image }
request.target?.onStart(placeholder = cachedPlaceholder ?: request.placeholder())
eventListener.onStart(request)
request.listener?.onStart(request)
// Resolve the size.
val sizeResolver = request.sizeResolver
eventListener.resolveSizeStart(request, sizeResolver)
val size = sizeResolver.size()
eventListener.resolveSizeEnd(request, size)
// Execute the interceptor chain.
val result = withContext(request.interceptorCoroutineContext) {
RealInterceptorChain(
initialRequest = request,
interceptors = components.interceptors,
index = 0,
request = request,
size = size,
eventListener = eventListener,
isPlaceholderCached = cachedPlaceholder != null,
).proceed()
}
// Set the result on the target.
when (result) {
is SuccessResult -> onSuccess(result, request.target, eventListener)
is ErrorResult -> onError(result, request.target, eventListener)
}
return result
} catch (throwable: Throwable) {
if (throwable is CancellationException) {
onCancel(request, eventListener)
throw throwable
} else {
// Create the default error result if there's an uncaught exception.
val result = ErrorResult(request, throwable)
onError(result, request.target, eventListener)
return result
}
} finally {
requestDelegate.complete()
}
}
...
여기에 excute함수가 다음과 같이 구현되어 있다.
override suspend fun execute(request: ImageRequest): ImageResult {
if (!needsExecuteOnMainDispatcher(request)) {
// Fast path: skip dispatching.
return execute(request, REQUEST_TYPE_EXECUTE)
} else {
// Slow path: dispatch to the main thread.
return coroutineScope {
// Start executing the request on the main thread.
val job = async(options.mainCoroutineContextLazy.value) {
execute(request, REQUEST_TYPE_EXECUTE)
}
// Update the current request attached to the view and await the result.
getDisposable(request, job).job.await()
}
}
}
저 excute 함수 중 살펴볼 부분은 아래와 같다.
private suspend fun execute(initialRequest: ImageRequest, type: Int): ImageResult {
...
====================================================
// Execute the interceptor chain.
val result = withContext(request.interceptorCoroutineContext) {
RealInterceptorChain(
initialRequest = request,
interceptors = components.interceptors,
index = 0,
request = request,
size = size,
eventListener = eventListener,
isPlaceholderCached = cachedPlaceholder != null,
).proceed()
}
====================================================
...
}
이 result가 최종 결과물로 저 결과물이 AsyncImagePainter에서 AsyncImagePainter.State로 변환되고 이것을 그리는 것이다.
proceed함수는 다음과 같다.
override suspend fun proceed(): ImageResult {
val interceptor = interceptors[index]
val next = copy(index = index + 1)
val result = interceptor.intercept(next)
checkRequest(result.request, interceptor)
return result
}
이것만 보면 사실 어떻게 이루어지는지 잘 감이 오지 않는다.
위 이미지는 Pluu Dev님의 Coil 요청 가로채기에서 사용된 이미지를 조금 변형한 것인데 위가 완벽한 내용은 아니지만 대충 이런 느낌으로 이해해주면 좋을 듯 하다.
이 인터셉터들이 순서대로 체크하며 정말 네트워크 통신을 해야하는지 확인 후 네트워크 통신을 하게 된다.
인터셉터는 아래와 같은 코드로 add되었다.
override val components = options.componentRegistry.newBuilder()
.addServiceLoaderComponents(options)
.addAndroidComponents(options)
.addJvmComponents(options)
.addAppleComponents(options)
.addCommonComponents()
.add(EngineInterceptor(this, systemCallbacks, requestService, options.logger))
.build()
그럼 이 EngineInterceptor의 코드를 살펴보자
아래의 코드는 EngineInterceptor에서 이 글에서 살펴볼 부분만 일부 가져온 것이다.
/** The last interceptor in the chain which executes the [ImageRequest]. */
internal class EngineInterceptor(
private val imageLoader: ImageLoader,
private val systemCallbacks: SystemCallbacks,
private val requestService: RequestService,
private val logger: Logger?,
) : Interceptor {
...
private suspend fun fetch(
components: ComponentRegistry,
request: ImageRequest,
mappedData: Any,
options: Options,
eventListener: EventListener,
): FetchResult {
val fetchResult: FetchResult
var searchIndex = 0
while (true) {
val pair = components.newFetcher(mappedData, options, imageLoader, searchIndex)
checkNotNull(pair) { "Unable to create a fetcher that supports: $mappedData" }
val fetcher = pair.first
searchIndex = pair.second + 1
eventListener.fetchStart(request, fetcher, options)
val result = fetcher.fetch()
try {
eventListener.fetchEnd(request, fetcher, options, result)
} catch (throwable: Throwable) {
// Ensure the source is closed if an exception occurs before returning the result.
(result as? SourceFetchResult)?.source?.closeQuietly()
throw throwable
}
if (result != null) {
fetchResult = result
break
}
}
return fetchResult
}
...
}
이 fetch() 함수를 보면
다음의 단계를 거친다.
// 1. 등록된 컴포넌트 중 mappedData(URL 등)를 처리할 수 있는 Fetcher를 찾음
val pair = components.newFetcher(mappedData, options, imageLoader, searchIndex)
val fetcher = pair.first
// 2. 그 Fetcher에게 데이터를 가져오라고 시킴 (실제 네트워크 통신 지점)
val result = fetcher.fetch()
저 fetcher는 Fetcher 인터페이스이고 이 구현체는 NetworkFetcher이다.
https://pluu.github.io/blog/android/2024/09/22/coil-intercept/