안드로이드 앱 개발 공부/jetpack Compose

도통 모르겠어서 기록하면서 공부하는 Jetpack Compose_ glance 코드 샘플 분석 후 적용하기까지 (2)

플래시🦥 2023. 3. 21.
반응형

이번에는 https://github.com/android/user-interface-samples/tree/main/AppWidget/app/src/main/java/com/example/android/appwidget/glance/image 여기 코드를 분석해 볼 것이다!

 

이렇게 이미지를 사이즈에 따라 갱신하도록 구현한 것이다.

고정되지 않은 데이터를 보여주는 것이 아니기 때문에 내가 원하는 방법을 구현하는 것에 감이 올 수 도 있을 것 같아서 분석해 본다.

 

 

이거는 두 개의 파일이 있는데 ImageGlanceWidget.kt는 위젯 ui 전방에 관련된 코드이고,  ImageWorker.kt는 이미지를 가져오는 코드로  추정된다. 

 

ImageGlanceWidget.kt에는 3덩어리로 이루어져 있다. 

1. class ImageGlanceWidget : GlanceAppWidget()

2. class RefreshAction : ActionCallback 

3. class ImageGlanceWidgetReceiver : GlanceAppWidgetReceiver()

 

ImageWorker.kt에는 class ImageWorker( private val context: Context, workerParameters: WorkerParameters)  한 덩어리이다. 

 

 

 

 

 

ImageGlanceWidget.kt먼저 분석해 봐야겠다. 

1.  class ImageGlanceWidget : GlanceAppWidget()의 구성은 

companion obect 하나랑 Content함수랑 override된 onDelete함수 그리고 getImageProvider() 함수로 구성되어 있다.

class ImageGlanceWidget : GlanceAppWidget() {

    companion object {
        val sourceKey = stringPreferencesKey("image_source")
        val sourceUrlKey = stringPreferencesKey("image_source_url")

        fun getImageKey(size: DpSize) = getImageKey(size.width.value.toPx, size.height.value.toPx)

        fun getImageKey(width: Float, height: Float) = stringPreferencesKey(
            "uri-$width-$height"
        )
    }

    override val sizeMode: SizeMode = SizeMode.Exact

    @Composable
    override fun Content() {
        val context = LocalContext.current
        val size = LocalSize.current
        val imagePath = currentState(getImageKey(size))
        GlanceTheme {
            Box(
               //box modifier/alignment
            ) {
                if (imagePath != null) {	//이미지 있을 때 
                    Image(
                     provider = getImageProvider(imagePath),	//마지막 함수 참고
                      //이미지 구성
                    )
                    Text(
                        //text 구성,
                        modifier = GlanceModifier
                            .clickable(
                                actionStartActivity(	//클릭하면 activity실행 action
                                    Intent(
                                        Intent.ACTION_VIEW,
                                        Uri.parse(currentState(sourceUrlKey))
                                    )
                                )
                            )
                    )
                } else {	//이미지 없을 때 
                    CircularProgressIndicator()	//로딩
                    val glanceId = LocalGlanceId.current
                    //**
                    SideEffect {
                        ImageWorker.enqueue(context, size, glanceId)
                    }
                }
            }
        }
    }

//삭제할때 
    override suspend fun onDelete(context: Context, glanceId: GlanceId) {
        super.onDelete(context, glanceId)
        ImageWorker.cancel(context, glanceId) //두번째 ImageWorker의 취소함수 실행 
    }


//uri로 부터 이미지 가져오기
    private fun getImageProvider(path: String): ImageProvider {
        if (path.startsWith("content://")) {
            return ImageProvider(path.toUri())
        }
        val bitmap = BitmapFactory.decodeFile(path)
        return ImageProvider(bitmap)
    }
}

companion pbject를 사용해서 작업에 필요한 싱글톤 객체를 만들어 준다. 

이코드에서 SideEggect라는 것이 있는데 이는 Compose상태를 Compose에서 관리하지 않는 객체와 공유하기 위해 리컴포지션 성공시마다 호출되는 컴포지션이라고 공식문서에서 말해주는데 쉽게 말해 @Composeable 범위 밖에서 발생하는 앱 상태에 대한 변경을 말한다. 

sideEffect 참고 : https://kotlinworld.com/245

 

그러니까 이미지가 null일 때 진행되는 작업으로 ImageWorker.kt 의 enqueue를 호출한다. 

 

2. class RefreshAction : ActionCallback 는 위젯 업데이트하는 class이다. 

class RefreshAction : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        // Clear the state to show loading screen
        updateAppWidgetState(context, glanceId) { prefs ->
            prefs.clear()
        }
        ImageGlanceWidget().update(context, glanceId)

        // Enqueue a job for each size the widget can be shown in the current state
        // (i.e landscape/portrait)
        GlanceAppWidgetManager(context).getAppWidgetSizes(glanceId).forEach { size ->
            ImageWorker.enqueue(context, size, glanceId, force = true)
        }
    }
}

3. 마지막은 Reciever 이다. 

class ImageGlanceWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = ImageGlanceWidget()
}

 

 

그다음은 ImageWorker.kt인데 이 부분이 언뜻 봐도 어려워 보였다.

class ImageWorker(
    private val context: Context,
    workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {	
    //*모두 WorkwrManager 관련 함수들
    companion object {

        private val uniqueWorkName = ImageWorker::class.java.simpleName

        fun enqueue(context: Context, size: DpSize, glanceId: GlanceId, force: Boolean = false) {
            val manager = WorkManager.getInstance(context)
            val requestBuilder = OneTimeWorkRequestBuilder<ImageWorker>().apply {
                addTag(glanceId.toString())	//Tag붙일 수 있음 
                setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)	//JobScheduler
                setInputData(
                    Data.Builder()
                        .putFloat("width", size.width.value.toPx)
                        .putFloat("height", size.height.value.toPx)
                        .putBoolean("force", force)
                        .build()
                )
            }
            val workPolicy = if (force) {	//이전에 LocalGlanceId.current값이 넘어옴(기본false)
                ExistingWorkPolicy.REPLACE
            } else {
                ExistingWorkPolicy.KEEP
            }

            manager.enqueueUniqueWork(
                uniqueWorkName + size.width + size.height,
                workPolicy,
                requestBuilder.build()
            )

            // Temporary workaround to avoid WM provider to disable itself and trigger an
            // app widget update
            manager.enqueueUniqueWork(
                "$uniqueWorkName-workaround",
                ExistingWorkPolicy.KEEP,
                OneTimeWorkRequestBuilder<ImageWorker>().apply {
                    setInitialDelay(Duration.ofDays(365))
                }.build()
            )
        }

        // 진행중인 worker 취소 (이전에 위젯 삭제할 때 호출 했음)
        fun cancel(context: Context, glanceId: GlanceId) {
            WorkManager.getInstance(context).cancelAllWorkByTag(glanceId.toString())
        }
    }

	//이미지 업데이트하는 override함수 , dowork 는 꼭있어야하는 함수
    override suspend fun doWork(): Result {
        return try {
            val width = inputData.getFloat("width", 0f)
            val height = inputData.getFloat("height", 0f)
            val force = inputData.getBoolean("force", false)
            val uri = getRandomImage(width, height, force)
            updateImageWidget(width, height, uri)
            Result.success()
        } catch (e: Exception) {
            Log.e(uniqueWorkName, "Error while loading image", e)
            if (runAttemptCount < 10) {
                // Exponential backoff strategy will avoid the request to repeat
                // too fast in case of failures.
                Result.retry()
            } else {
                Result.failure()
            }
        }
    }

	//위에서 호출한 이미지업데이트 함수 
    private suspend fun updateImageWidget(width: Float, height: Float, uri: String) {
        val manager = GlanceAppWidgetManager(context)
        val glanceIds = manager.getGlanceIds(ImageGlanceWidget::class.java)
        glanceIds.forEach { glanceId ->
            updateAppWidgetState(context, glanceId) { prefs ->
                prefs[ImageGlanceWidget.getImageKey(width, height)] = uri
                prefs[ImageGlanceWidget.sourceKey] = "Picsum Photos"
                prefs[ImageGlanceWidget.sourceUrlKey] = "https://picsum.photos/"
            }
        }
        ImageGlanceWidget().updateAll(context)
    }

   // Coil and Picsum Photos를 사용해서 이미지를 랜덤하게 가져오는 
    @OptIn(ExperimentalCoilApi::class)
    private suspend fun getRandomImage(width: Float, height: Float, force: Boolean): String {
        val url = "https://picsum.photos/${width.roundToInt()}/${height.roundToInt()}"
        val request = ImageRequest.Builder(context)
            .data(url)
            .build()

        // Request the image to be loaded and throw error if it failed
        with(context.imageLoader) {
            if (force) {
                diskCache?.remove(url)
                memoryCache?.remove(MemoryCache.Key(url))
            }
            val result = execute(request)
            if (result is ErrorResult) {
                throw result.throwable
            }
        }

        // Get the path of the loaded image from DiskCache.
        val path = context.imageLoader.diskCache?.get(url)?.use { snapshot ->
            val imageFile = snapshot.data.toFile()

            // Use the FileProvider to create a content URI
            val contentUri = getUriForFile(
                context,
                "com.example.android.appwidget.fileprovider",
                imageFile
            )

            // Find the current launcher everytime to ensure it has read permissions
            val resolveInfo = context.packageManager.resolveActivity(
                Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_HOME) },
                PackageManager.MATCH_DEFAULT_ONLY
            )
            val launcherName = resolveInfo?.activityInfo?.packageName
            if (launcherName != null) {
                context.grantUriPermission(
                    launcherName,
                    contentUri,
                    FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_PERSISTABLE_URI_PERMISSION
                )
            }

            // return the path
            contentUri.toString()
        }
        return requireNotNull(path) {
            "Couldn't find cached file"
        }
    }
}

이 코드를 분석하면서 처음보는 것들이 많아서 너무 어려웠다. 

여기서 가장 처음 핵심이 되는 개념이 WorkManager이다. 

WorkManager 공식문서 링크

 

WorkManager는 지속적인 작업에 권장되는 설루션으로 jetpack 구성요소이다. 

앱이 다시 시작되거나 시스템이 재부팅될 때 작업이 예약된 채로 남아 있으면 그 작업을 유지된다. 대부분의 백그라운드 처리는 지속적인 작업을 통해 가장 잘 처리되므로 WorkManager는 백그라운드 처리에 권장하는 기본 API라고 한다.

 

WorkManager의 지속적인 작업 유형은 세가지가 있는데 

1. 즉시 : 즉시 시작하고 곧 완료하는 작업으로 신속하게 처리

2. 장기실행 : 더 오래(10분이상) 실행되는 작업

3. 지연가능 : 나중에 시작하며 주기적으로 실행될 수 있는 예약된 작업.

이렇다. 

 

위에서 사용된

addTag는 WorkRequest.Builder의 메서드로 작업에 Tag를 붙인다. 

setExpedited(@NonNull OutOfQuotaPolicy policy)는  사용자에게 중요한 것으로 표시하고, 

setInputDate(data)는 작업에 테이터를 추가하는 메서드이다. 

enqueueUniqueWork()는 일회성 작업하는 메서드이다. 

 

 

Companion object로 파라미터로 넘겨받은 size를 바탕으로 데이터를 추가하는 작업을 하는 것 같고,

dowork에서는 추가한 데이터를 가지고 랜덤이미지 url을 생성하고, 이미지를 업데이트를 해 줄 수 있도록 호출하는 코드로 이루어져 있다.

 

공부를 하면 할 수록 모르는 게 많다는 걸 느낀다..ㅠㅠ

 


이걸 바탕으로 어떻게 하면 내가 원하는 작업을 구현할 수 있을지 고민해봐야 한다. 

내가 원하는 것은 위젯에 db에 저장되어 있는 정보를 가지고 와서 보여주는 것이다. 

간단하게 생각해보면 그러려면 위 샘플 코드처럼 우선 ui를 구성해 주고 해당하는 작업을 실행할 수 있도록 처리를 해주면 될 것 같다. 

 

위젯의 UI를 구성하는 부분과 데이터를 가져오는 부분을 가지고 구성을 시작한다. 

첫번째로 ui구성에 있는 LazyColumn에서 아이템을 가져오는데, 리스트가 없으면 가지고 올 수 있게끔 만들어 주면 된다.

 

말로는 쉬운데 코드로 짜는 건 왜 이리 어렵징...ㅜㅜ

 

기존에 viewmodel사용해서 room db 가져오는 방식으로 하려고 했는데 계속 오류가 난다. 

 java.lang.IllegalStateException: CompositionLocal LocalView not present

java.lang.IllegalStateException: CompositionLocal LocalLifecycleOwner not present

등..

이런 오류인데, 무슨 LocalView 나 LocalLifecycleOwner가 없다는 건 알겠는데 어느 부분에서, 왜 일어나는 오류인지 찾아도 잘 모르겠다...

 

아직 사용하기에는 무리인 것 같아서 우선 기존의 방식으로 구현을 시작하는 것이 좋을 것 같다. 

그리고 이런저런 내용을 찾아보다 보니 초기버전은 기능을 완전히 사용하기 어려울 수 있다고 한다. 그래서 중단하고 기존 방식으로 개발을 진행하기로 결정했다. 

계속 여러가지 공부하면서 어느정도 실력이 늘고 glance가 좀 더 개발이되면? 그때 사용해 봐야 겠다. 

728x90
반응형

댓글