본문 바로가기
안드로이드

[안드로이드] 컴포즈로 조금씩 바꿔보자. (with Progress bar)

by keel_im 2021. 9. 29.
반응형

서론

안녕하세요. 오늘은 컴포즈에 대해 조심스럽게 얘기를 해보려고 합니다. 

Google I/O에서도 많은 언급이 있었고, 매주 오는 Android Weekly에서도, Kotlin Weekly에서도
정말 많은 내용들이 Compose와 연관된 내용입니다. 

이러니 Compose 를 안쓸래야 안 쓸수 없는 상황입니다.

이러한 점에서 학습의 필요성을 느꼈고 완전히 변화가 아니라
서서히 코드를 바꿔가면서 개념을 이해하는 스타일이라 천천히 바꿔볼까 합니다.

사실 Flutter 를 처음보았을 때 와 비슷한 느낌이였습니다. 

"어떻게 UI 를 저렇게 화면도 안보고 바로 작성을 하느냐? 이것은 xml 에게 미안해야 한다."

하지만 몇달 후 저는 익숙한 레이아웃은 디자인텝을 안보고 작성했다는...

참고로 아래 내용은 저의 경험을 적은 글이며,
언제든지 틀린 내용일 수 있기 때문에 많은 글과 코드를 참고하시는 게 좋을 것 같습니다.

본격

먼저 토이 프로젝트 그림을 보겠습니다.

생각보다 쫀득해보이는 화면

안드로이드 Jetpack Navigation 에서 화면 전환을 보여 주는 페이지입니다.
가운데에 프로그레스 바가 보이시나요?

저는 화면에서 프로그레스 바를 컴포즈로 적용한 이야기를 하려고 합니다.


거대한 서막에도 불구하고 적용하는 방법은 정말 심플합니다. 

(%주의 사항 위 과정은 완전히 Compose View로 코드를 바꾸는 것이 아니라
기존에 있는 Xml에서 ComposeView를 추가하여 해당 부분만 Compose로 작성을 하는 것입니다. )

1. xml에 Compose View를 추가를 해줍니다. 

2. compose View에서 setContent를 통해 Compose로 작성한다. 

정말 심플한 과정입니다. 이를 코드에 적용을 해보겠습니다. 

<?xml version="1.0" encoding="utf-8"?>

<androidx.coordinatorlayout.widget.CoordinatorLayout 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:id="@+id/coordinator_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".ui.main.Main2Activity">

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_gravity="center" />

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
        app:navGraph="@navigation/mobile_navigation" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/search_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:borderWidth="0dp"
        android:backgroundTint="@color/colorPrimary"
        android:src="@drawable/ic_baseline_search_24"
        app:layout_anchor="@id/bottom_app_bar" />

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottom_app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:fabAlignmentMode="center"
        app:fabCradleMargin="@dimen/bottom_app_bar_fab_cradle_margin"
        app:fabCradleRoundedCornerRadius="@dimen/bottom_app_bar_fab_cradle_corner_radius"
        app:menu="@menu/bottom_app_bar_menu"
        app:navigationIcon="@drawable/ic_menu_24dp" />



</androidx.coordinatorlayout.widget.CoordinatorLayout>

먼저 xml를 적어줍니다.
위에 androidx.compose.ui.platform.ComposeView를 확인할 수 있습니다.
그리고 Compose를 작성해줍니다. 


여기서 Compose View은 xml 과 Compose의 중간 과정이라고 생각하시면 쉽습니다. 
Compose 를 그릴 수있도록 따로 공간을 잡아두고 그 안에서 Compose 로 선언형 UI 를 짜신다고
생각하시면 됩니다. 

package com.keelim.nandadiagnosis.compose.ui

import androidx.compose.foundation.layout.padding
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp


@Preview
@Composable
private fun PreviewProgress(){
    CircularIndeterminateProgressBar(
        isDisplayed = true
    )
}


@Composable
fun CircularIndeterminateProgressBar(
    isDisplayed: Boolean,
){
    if(isDisplayed){
        CircularProgressIndicator(
            modifier = Modifier.padding(8.dp),
            color = Color.Yellow,
            strokeWidth = 10.dp
        )
    }
}

저는 compose 모듈을 따로 만들었고요

이렇게 코드를 짰습니다. (정말 Compose View를 짜니까 신기하더라고요.)


저는 이 내용들을 저의 토이 프로젝트에 적용하기로 했습니다.

저는 Jetpack Navigation과 SingleActivity로 구성을 했기 때문에 여러분과 다를 수 있습니다.

(%주의 지금부터는 내용은 저의 프로젝트 상황에 맞게 특수 사항을 해결하기 위한 과정입니다.)

Jetpack Navigation을 이용하면 activity_xml에서 기준 fragment를 잡고 핸들링이 가능합니다.
그렇다는 것은 Activity 에서 적용한 Compsoe View를 모든 Fragment에서 보여주는 것이 가능합니다. 

저는 이 상황때문에 Progress Bar를 컴포즈에서 제일 먼저 사용하기로 했습니다. 

하지만 여기 문제가 있습니다. 바로 상태를 어떻게 공유를 할 것인가 입니다. 
온전히 Compose 를 사용한다면 상태 관리 등을 State로 사용할 수 있습니다. 

https://developer.android.com/jetpack/compose/state?hl=ko

 

상태 및 Jetpack Compose  |  Android Developers

상태 및 Jetpack Compose 앱의 상태는 시간이 지남에 따라 변할 수 있는 값입니다. 이는 매우 광범위한 정의로서 Room 데이터베이스부터 클래스 변수까지 모든 항목이 포함됩니다. 모든 Android 앱에서

developer.android.com

저는 문제를 어떻게 했을까요?
액티비티와 프레그먼트에서 상태를 어떻게 관리를 해야 하는가? => 응 그래 ViewModel

저는 이 과정에서 ViewModel를 통해 해결을 하였습니다.

 

Hilt를 사용하면서 ViewModel 주입이 간편해졌는데.
액티비티와 프레그먼트가 뷰 모델을 공유를 하려면 선언에서 가져오는 방식만 다르게 설정하면 됩니다. 

먼저 ViewModel을 보시면

package com.keelim.nandadiagnosis.ui.main

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.keelim.nandadiagnosis.domain.GetAppThemeUseCase
import com.keelim.nandadiagnosis.domain.SetAppThemeUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

@HiltViewModel
class MainViewModel @Inject constructor(
  getTheme: GetAppThemeUseCase,
  private val setTheme: SetAppThemeUseCase,
) : ViewModel() {
  val theme: LiveData<Int> = getTheme.appTheme.asLiveData()

  fun setAppTheme(theme: Int) = viewModelScope.launch {
    setTheme.invoke(theme)
  }

  private val _loading = MutableLiveData(false)
  val loading:LiveData<Boolean> = _loading

  fun loadingOn() = viewModelScope.launch{
    _loading.value = true
    delay(1000)
    _loading.value = false
  }
}

일반적인 ViewModel입니다. 저는 loading이라는 함수를 통해 내비게이션 될 때,
공유된 ViewModel에서 호출하여 _loading에 상태를 변경하고,
_loading을 옵저빙 하고 있는 Activity에서 상태를 변경해주는 것입니다. 

  private fun observeLoading() = mainViewModel.loading.observe(this){
    when(it){
      binding.composeView.apply {
        bringToFront()

        setContent {
          CircularIndeterminateProgressBar(
            isDisplayed = it
          )
        }
      }
    }
  }

사실 이 부분은 상당히 마음에 들지 않아 조금 더 찾아보고 수정할 계획입니다.
옵저빙을 하기 때문에 상태가 변할 때마다 ComposeView를 다시 그리는 방법으로
ProgressBar를 표시할 수 있습니다. 

ViewModel을 불러오는 방법들

private val mainViewModel:MainViewModel by viewModels() // Activity
private val mainViewModel:MainViewModel by activityViewModels() //Fragment

결론

Compose는 정말 신기합니다.
신기한 만큼 사용해볼 만한 기술인 것 같습니다. 

비록 제가 적은 코드들이 아쉽지만, 뭐 내일의 코드는 더 좋아지지 않을까? 생각합니다.

아래 저장소에서 작업을 하였으니, 참고하시면 좋을 것 같습니다. 
벌써 10월 이네요 ㅎㅎㅎ🎃 오늘도 정말 수고 많으셨습니다. 

질문과 비판은 언제나 환영합니다.  

https://github.com/keelim/nandaDiagnosis

 

GitHub - keelim/nandaDiagnosis

Contribute to keelim/nandaDiagnosis development by creating an account on GitHub.

github.com

 

반응형

댓글