이번에는 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는 지속적인 작업에 권장되는 설루션으로 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가 좀 더 개발이되면? 그때 사용해 봐야 겠다.
'안드로이드 앱 개발 공부 > jetpack Compose' 카테고리의 다른 글
도통 모르겠어서 기록하면서 공부하는 Jetpack Compose_ glance 코드 샘플 분석 후 적용하기까지 (1) (0) | 2023.03.20 |
---|---|
[jetpack compose] 제트팩 컴포즈로 탭 만들기 (1) | 2023.03.07 |
JetPack Compose_Scaffold, TextField, Button, 구조분해, SnackBar, 코루틴 스코프 (0) | 2023.01.09 |
JetPack Compose_Image, Card, 상태 (0) | 2023.01.04 |
JetPack Compose_ 리스트, LazyColumn (0) | 2023.01.04 |
댓글